use super::*;
impl<'a> Parser<'a> {
pub(super) fn apply_word_command_effects(&mut self, name: &Word, args: &[Word]) {
let Some(name) = self.literal_word_text(name) else {
return;
};
match name.as_str() {
"shopt" => {
let mut toggle = None;
for arg in args {
let Some(arg) = self.literal_word_text(arg) else {
continue;
};
match arg.as_str() {
"-s" => toggle = Some(true),
"-u" => toggle = Some(false),
"expand_aliases" => {
if let Some(toggle) = toggle {
self.expand_aliases = toggle;
}
}
_ => {}
}
}
}
"alias" => {
for arg in args {
let Some(arg) = self.literal_word_text(arg) else {
continue;
};
if arg == "--" {
continue;
}
let Some((alias_name, value)) = arg.split_once('=') else {
continue;
};
self.aliases
.insert(alias_name.to_string(), self.compile_alias_definition(value));
}
}
"unalias" => {
for arg in args {
let Some(arg) = self.literal_word_text(arg) else {
continue;
};
match arg.as_str() {
"--" => {}
"-a" => self.aliases.clear(),
_ => {
self.aliases.remove(arg.as_str());
}
}
}
}
_ => {}
}
}
pub(super) fn apply_stmt_effects(&mut self, stmt: &Stmt) {
match &stmt.command {
AstCommand::Simple(simple) => {
self.apply_word_command_effects(&simple.name, &simple.args)
}
AstCommand::Binary(binary) if matches!(binary.op, BinaryOp::And | BinaryOp::Or) => {
self.apply_stmt_effects(&binary.left);
self.apply_stmt_effects(&binary.right);
}
_ => {}
}
}
pub(in crate::parser) fn apply_stmt_list_effects(&mut self, stmts: &[Stmt]) {
for stmt in stmts {
self.apply_stmt_effects(stmt);
}
}
pub(in crate::parser) fn parse_command_list_required(&mut self) -> Result<Vec<Stmt>> {
self.parse_command_list()?
.ok_or_else(|| self.error("expected command"))
}
pub(super) fn skip_command_separators(&mut self) -> Result<()> {
loop {
self.skip_newlines()?;
if self.at(TokenKind::Semicolon) {
self.advance();
continue;
}
break;
}
Ok(())
}
pub fn parse(mut self) -> ParseResult {
self.parse_impl()
}
#[cfg(feature = "benchmarking")]
#[doc(hidden)]
pub fn parse_with_benchmark_counters(self) -> (ParseResult, ParserBenchmarkCounters) {
let mut parser = self.rebuild_with_benchmark_counters();
let output = parser.parse_impl();
(output, parser.finish_benchmark_counters())
}
pub(super) fn parse_command_list(&mut self) -> Result<Option<Vec<Stmt>>> {
self.tick()?;
let mut current = match self.parse_pipeline()? {
Some(stmt) => stmt,
None => return Ok(None),
};
let mut stmts = Vec::with_capacity(2);
loop {
let (op, terminator, allow_empty_tail) = match self.current_token_kind {
Some(TokenKind::And) => (Some(BinaryOp::And), None, false),
Some(TokenKind::Or) => (Some(BinaryOp::Or), None, false),
Some(TokenKind::Semicolon) => (None, Some(StmtTerminator::Semicolon), true),
Some(TokenKind::Background) => (
None,
Some(StmtTerminator::Background(BackgroundOperator::Plain)),
true,
),
Some(TokenKind::BackgroundPipe) => (
None,
Some(StmtTerminator::Background(BackgroundOperator::Pipe)),
true,
),
Some(TokenKind::BackgroundBang) => (
None,
Some(StmtTerminator::Background(BackgroundOperator::Bang)),
true,
),
_ => break,
};
let operator_span = self.current_span;
self.advance();
self.skip_newlines()?;
if allow_empty_tail && self.current_token.is_none() {
current.terminator = terminator;
current.terminator_span = Some(operator_span);
stmts.push(current);
return Ok(Some(stmts));
}
if let Some(binary_op) = op {
if let Some(right) = self.parse_pipeline()? {
current = Self::binary_stmt(current, binary_op, operator_span, right);
} else {
break;
}
continue;
}
let Some(terminator) = terminator else {
unreachable!("list terminator should be present");
};
if let Some(next) = self.parse_pipeline()? {
current.terminator = Some(terminator);
current.terminator_span = Some(operator_span);
stmts.push(current);
current = next;
} else if allow_empty_tail {
if self
.current_keyword()
.is_some_and(Self::is_non_command_keyword)
{
if matches!(terminator, StmtTerminator::Background(_)) {
current.terminator = Some(terminator);
current.terminator_span = Some(operator_span);
stmts.push(current);
return Ok(Some(stmts));
}
break;
}
if matches!(
self.current_token_kind,
Some(TokenKind::Semicolon | TokenKind::Newline)
) {
self.advance();
}
current.terminator = Some(terminator);
current.terminator_span = Some(operator_span);
stmts.push(current);
return Ok(Some(stmts));
} else {
break;
}
}
stmts.push(current);
Ok(Some(stmts))
}
pub(super) fn parse_pipeline(&mut self) -> Result<Option<Stmt>> {
let start_span = self.current_span;
let negated = self.at(TokenKind::Word) && self.current_word_str() == Some("!");
if negated {
self.advance();
}
let mut stmt = match self.parse_command()? {
Some(cmd) => Self::lower_non_sequence_command_to_stmt(cmd),
None => {
if negated {
return Err(self.error("expected command after !"));
}
return Ok(None);
}
};
let mut saw_pipe = false;
while self.at_in_set(PIPE_OPERATOR_TOKENS) {
saw_pipe = true;
let op = if self.at(TokenKind::PipeBoth) {
BinaryOp::PipeAll
} else {
BinaryOp::Pipe
};
let operator_span = self.current_span;
self.advance();
self.skip_newlines()?;
if let Some(cmd) = self.parse_command()? {
let right = Self::lower_non_sequence_command_to_stmt(cmd);
stmt = Self::binary_stmt(stmt, op, operator_span, right);
} else {
return Err(self.error("expected command after |"));
}
}
if negated || saw_pipe {
stmt.negated = negated;
stmt.span = start_span.merge(self.current_span);
}
Ok(Some(stmt))
}
pub(super) fn parse_compound_with_redirects(
&mut self,
parser: impl FnOnce(&mut Self) -> Result<CompoundCommand>,
) -> Result<Option<Command>> {
let compound = parser(self)?;
let redirects = self.parse_trailing_redirects();
Ok(Some(Command::Compound(Box::new(compound), redirects)))
}
pub(super) fn current_starts_prefix_redirect_compound(&self) -> bool {
match self.current_keyword() {
Some(Keyword::If)
| Some(Keyword::While)
| Some(Keyword::Until)
| Some(Keyword::Case)
| Some(Keyword::Select)
| Some(Keyword::Time)
| Some(Keyword::Coproc) => true,
Some(Keyword::For) => self.dialect == ShellDialect::Zsh,
Some(Keyword::Repeat) => self.zsh_short_repeat_enabled(),
Some(Keyword::Foreach) => self.zsh_short_loops_enabled(),
Some(Keyword::Function) => false,
None => matches!(
self.current_token_kind,
Some(TokenKind::DoubleLeftParen | TokenKind::LeftParen | TokenKind::LeftBrace)
),
_ => false,
}
}
pub(super) fn parse_prefix_redirected_compound_command(&mut self) -> Result<Option<Command>> {
if !self.current_token_kind.is_some_and(Self::is_redirect_kind) {
return Ok(None);
}
let checkpoint = self.checkpoint();
let mut redirects = self.parse_trailing_redirects();
if redirects.is_empty() || !self.current_starts_prefix_redirect_compound() {
self.restore(checkpoint);
return Ok(None);
}
let Some(mut command) = self.parse_command()? else {
self.restore(checkpoint);
return Ok(None);
};
match &mut command {
Command::Compound(_, trailing) => {
redirects.append(trailing);
*trailing = redirects;
Ok(Some(command))
}
_ => {
self.restore(checkpoint);
Ok(None)
}
}
}
pub(super) fn classify_flow_control_name(&self, word: &Word) -> Option<FlowControlBuiltinKind> {
let name = self.single_literal_word_text(word)?;
match name {
"break" => Some(FlowControlBuiltinKind::Break),
"continue" => Some(FlowControlBuiltinKind::Continue),
"return" => Some(FlowControlBuiltinKind::Return),
"exit" => Some(FlowControlBuiltinKind::Exit),
_ => None,
}
}
pub(super) fn classify_decl_variant_name(&self, word: &Word) -> Option<Name> {
let name = self.single_literal_word_text(word)?;
match name {
"declare" | "local" | "export" | "readonly" | "typeset" => Some(Name::from(name)),
"integer" if self.dialect == ShellDialect::Zsh => Some(Name::from(name)),
_ => None,
}
}
pub(super) fn classify_simple_command(&mut self, command: SimpleCommand) -> Command {
let kind = self.classify_flow_control_name(&command.name);
if let Some(kind) = kind {
let SimpleCommand {
args,
redirects,
assignments,
span,
..
} = command;
let mut args = args.into_iter();
return match kind {
FlowControlBuiltinKind::Break => {
Command::Builtin(BuiltinCommand::Break(BreakCommand {
depth: args.next(),
extra_args: args.collect(),
redirects,
assignments,
span,
}))
}
FlowControlBuiltinKind::Continue => {
Command::Builtin(BuiltinCommand::Continue(ContinueCommand {
depth: args.next(),
extra_args: args.collect(),
redirects,
assignments,
span,
}))
}
FlowControlBuiltinKind::Return => {
Command::Builtin(BuiltinCommand::Return(ReturnCommand {
code: args.next(),
extra_args: args.collect(),
redirects,
assignments,
span,
}))
}
FlowControlBuiltinKind::Exit => {
Command::Builtin(BuiltinCommand::Exit(ExitCommand {
code: args.next(),
extra_args: args.collect(),
redirects,
assignments,
span,
}))
}
};
}
if let Some(variant) = self.classify_decl_variant_name(&command.name) {
let SimpleCommand {
name,
args,
redirects,
assignments,
span,
} = command;
return Command::Decl(Box::new(DeclClause {
variant,
variant_span: name.span,
operands: self.classify_decl_operands(args),
redirects,
assignments,
span,
}));
}
Command::Simple(command)
}
pub(super) fn is_operand_like_double_paren_token(token: &LexedToken<'_>) -> bool {
match token.kind {
TokenKind::LiteralWord | TokenKind::QuotedWord => true,
TokenKind::Word => token.word_string().is_some_and(|text| {
!text.chars().all(|ch| ch.is_ascii_punctuation())
&& !Self::word_contains_obvious_arithmetic_punctuation(&text)
}),
_ => false,
}
}
pub(super) fn word_contains_obvious_arithmetic_punctuation(text: &str) -> bool {
text.chars().any(|ch| {
matches!(
ch,
',' | '='
| '+'
| '*'
| '/'
| '%'
| '<'
| '>'
| '&'
| '|'
| '^'
| '!'
| '?'
| ':'
| '['
| ']'
)
})
}
pub(super) fn suspicious_double_paren_is_command_style(
&mut self,
checkpoint: &ParserCheckpoint<'a>,
) -> bool {
self.restore(checkpoint.clone());
let parses_as_arithmetic = self.parse_arithmetic_command().is_ok();
self.restore(checkpoint.clone());
!parses_as_arithmetic
}
pub(super) fn looks_like_command_style_double_paren(&mut self) -> bool {
if self.current_token_kind != Some(TokenKind::DoubleLeftParen) {
return false;
}
let checkpoint = self.checkpoint();
self.advance();
let mut paren_depth = 0_i32;
let mut previous_top_level_operand = false;
loop {
match self.current_token_kind {
Some(TokenKind::DoubleLeftParen) => {
paren_depth += 2;
previous_top_level_operand = false;
self.advance();
}
Some(TokenKind::LeftParen) => {
paren_depth += 1;
previous_top_level_operand = false;
self.advance();
}
Some(TokenKind::DoubleRightParen) => {
if paren_depth == 0 {
self.restore(checkpoint);
return false;
}
if paren_depth == 1 {
self.restore(checkpoint);
return false;
}
paren_depth -= 2;
previous_top_level_operand = false;
self.advance();
}
Some(TokenKind::RightParen) => {
if paren_depth == 0 {
return self.suspicious_double_paren_is_command_style(&checkpoint);
}
paren_depth -= 1;
previous_top_level_operand = false;
self.advance();
}
Some(TokenKind::Newline) | Some(TokenKind::Semicolon) if paren_depth == 0 => {
previous_top_level_operand = false;
self.advance();
}
Some(TokenKind::Comment) if self.dialect == ShellDialect::Zsh => {
self.restore(checkpoint);
return false;
}
Some(_)
if paren_depth == 0
&& self
.current_token
.as_ref()
.is_some_and(Self::is_operand_like_double_paren_token) =>
{
if previous_top_level_operand {
return self.suspicious_double_paren_is_command_style(&checkpoint);
}
previous_top_level_operand = true;
self.advance();
}
Some(_) => {
previous_top_level_operand = false;
self.advance();
}
None => {
self.restore(checkpoint);
return false;
}
}
}
}
pub(super) fn split_current_double_left_paren(&mut self) {
let (left_span, right_span) = Self::split_double_left_paren(self.current_span);
self.set_current_kind(TokenKind::LeftParen, left_span);
self.synthetic_tokens
.push_front(SyntheticToken::punctuation(
TokenKind::LeftParen,
right_span,
));
}
pub(in crate::parser) fn split_current_double_right_paren(&mut self) {
let (left_span, right_span) = Self::split_double_right_paren(self.current_span);
self.set_current_kind(TokenKind::RightParen, left_span);
self.synthetic_tokens
.push_front(SyntheticToken::punctuation(
TokenKind::RightParen,
right_span,
));
}
pub(super) fn parse_command(&mut self) -> Result<Option<Command>> {
self.skip_newlines()?;
self.check_error_token()?;
self.maybe_expand_current_alias_chain();
self.check_error_token()?;
if !self.zsh_short_repeat_enabled() && self.looks_like_disabled_repeat_loop()? {
self.ensure_repeat_loop()?;
}
if !self.zsh_short_loops_enabled() && self.looks_like_disabled_foreach_loop()? {
self.ensure_foreach_loop()?;
}
if let Some(command) = self.parse_prefix_redirected_compound_command()? {
return Ok(Some(command));
}
if let Some(command) = self.try_parse_zsh_attached_parens_function()? {
return Ok(Some(command));
}
match self.current_keyword() {
Some(Keyword::If) => return self.parse_compound_with_redirects(|s| s.parse_if()),
Some(Keyword::For) => return self.parse_compound_with_redirects(|s| s.parse_for()),
Some(Keyword::Repeat) if self.zsh_short_repeat_enabled() => {
return self.parse_compound_with_redirects(|s| s.parse_repeat());
}
Some(Keyword::Foreach) if self.zsh_short_loops_enabled() => {
return self.parse_compound_with_redirects(|s| s.parse_foreach());
}
Some(Keyword::While) => {
return self.parse_compound_with_redirects(|s| s.parse_while());
}
Some(Keyword::Until) => {
return self.parse_compound_with_redirects(|s| s.parse_until());
}
Some(Keyword::Case) => return self.parse_compound_with_redirects(|s| s.parse_case()),
Some(Keyword::Select) => {
return self.parse_compound_with_redirects(|s| s.parse_select());
}
Some(Keyword::Time) => return self.parse_compound_with_redirects(|s| s.parse_time()),
Some(Keyword::Coproc) => {
return self.parse_compound_with_redirects(|s| s.parse_coproc());
}
Some(Keyword::Function) => return self.parse_function_keyword().map(Some),
_ => {}
}
if self.at(TokenKind::Word)
&& let Some(word) = self.current_source_like_word_text()
&& self.peek_next_is(TokenKind::LeftParen)
{
let checkpoint = self.checkpoint();
self.advance();
self.advance();
let is_right_paren = self.at(TokenKind::RightParen);
self.restore(checkpoint);
if is_right_paren {
if !word.contains('=') && !word.contains('[') {
return self.parse_function_posix().map(Some);
}
} else if word.contains('$') && !word.contains('=') {
return Err(self.error("unexpected '(' after command word"));
}
}
if self.at(TokenKind::DoubleLeftBracket) {
return self.parse_compound_with_redirects(|s| s.parse_conditional());
}
if self.at(TokenKind::DoubleLeftParen) {
if self.looks_like_command_style_double_paren() {
self.split_current_double_left_paren();
return self.parse_compound_with_redirects(|s| s.parse_subshell());
}
let checkpoint = self.checkpoint();
if let Ok(compound) = self.parse_arithmetic_command() {
let redirects = self.parse_trailing_redirects();
return Ok(Some(Command::Compound(Box::new(compound), redirects)));
}
self.restore(checkpoint);
self.split_current_double_left_paren();
return self.parse_compound_with_redirects(|s| s.parse_subshell());
}
if self.dialect == ShellDialect::Zsh && self.at(TokenKind::LeftParen) {
let checkpoint = self.checkpoint();
self.advance();
let is_right_paren = self.at(TokenKind::RightParen);
self.restore(checkpoint);
if is_right_paren {
return self.parse_anonymous_paren_function().map(Some);
}
}
if self.at(TokenKind::LeftParen) {
return self.parse_compound_with_redirects(|s| s.parse_subshell());
}
if self.at(TokenKind::LeftBrace) {
return self.parse_compound_with_redirects(|s| {
s.parse_brace_group(BraceBodyContext::Ordinary)
});
}
match self.parse_simple_command()? {
Some(cmd) => Ok(Some(self.classify_simple_command(cmd))),
None => Ok(None),
}
}
}