1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
use crate::error;
use super::token::{SpannedToken, Token};
use super::Lexer;
impl Lexer {
pub fn next_token(&mut self) -> error::Result<SpannedToken> {
// Return queued tokens from alias expansion first
if let Some(tok) = self.alias_token_queue.first().cloned() {
self.alias_token_queue.remove(0);
// Update check_alias based on this queued token
self.update_check_alias_after(&tok.token);
return Ok(tok);
}
let tok = self.next_token_raw()?;
// Check for alias expansion on Word tokens when check_alias is set
if self.check_alias
&& let Token::Word(ref word) = tok.token
&& let Some(word_text) = word.as_literal()
&& !self.expanding_aliases.contains(word_text)
&& let Some(alias_value) = self.aliases.get(word_text).cloned()
{
let word_text = word_text.to_string();
// Mark as expanding to prevent recursion
self.expanding_aliases.insert(word_text);
// Check if alias value ends with whitespace —
// if so, the next word after the expansion should also be alias-checked
let trailing_space = alias_value.ends_with(' ')
|| alias_value.ends_with('\t');
// Tokenize the alias value into a separate token stream
let mut alias_lexer = Lexer::new(&alias_value);
// Copy aliases and expanding_aliases to the sub-lexer
alias_lexer.aliases = self.aliases.clone();
alias_lexer.expanding_aliases = self.expanding_aliases.clone();
alias_lexer.check_alias = true;
let mut tokens = Vec::new();
loop {
let t = alias_lexer.next_token()?;
if t.token == Token::Eof {
break;
}
tokens.push(t);
}
// Merge back any recursion-prevention state
self.expanding_aliases = alias_lexer.expanding_aliases;
if tokens.is_empty() {
// Alias expanded to nothing, get next token
if trailing_space {
self.check_alias = true;
}
return self.next_token();
}
// Return the first token, queue the rest
let first = tokens.remove(0);
self.alias_token_queue = tokens;
self.update_check_alias_after(&first.token);
// If alias value ends with space/tab, force next
// word to be alias-checked (overrides normal behavior)
if trailing_space {
self.check_alias = true;
}
return Ok(first);
}
// After producing a token, decide if next word should be alias-checked
self.update_check_alias_after(&tok.token);
Ok(tok)
}
pub(crate) fn update_check_alias_after(&mut self, token: &Token) {
match token {
Token::Semi | Token::Newline | Token::Pipe | Token::AndIf | Token::OrIf
| Token::LParen | Token::Amp | Token::DSemi => {
self.check_alias = true;
}
Token::Word(_) => {
// After first word in command position, stop alias-expanding
// (unless a previous alias ended with space, already handled above)
if self.check_alias {
self.check_alias = false;
}
}
_ => {
// For other tokens (redirects, etc.), don't change check_alias
}
}
}
}