Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 44 additions & 27 deletions src/parser/conditionals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,40 +127,57 @@ impl Parser {
Ok(left)
}

#[allow(clippy::too_many_lines)]
fn parse_cond_primary(&mut self) -> Result<Node> {
let start = self.peek_pos()?;
let tok = self.lexer.peek_token()?;
let kind = self.lexer.peek_token()?.kind;

// Handle ! (negation) — Parable drops it in S-expression output,
// but we keep it in the AST so the reformatter can preserve it.
if tok.kind == TokenType::Bang {
self.lexer.next_token()?;
let inner = self.parse_cond_primary()?;
return Ok(self.spanned(
start,
NodeKind::CondNot {
operand: Box::new(inner),
},
));
if let Some(node) = self.try_parse_cond_negation(start, kind)? {
return Ok(node);
}

// Handle ( grouped expression )
if tok.kind == TokenType::LeftParen {
self.lexer.next_token()?;
let inner = self.parse_cond_or()?;
self.expect(TokenType::RightParen)?;
return Ok(self.spanned(
start,
NodeKind::CondParen {
inner: Box::new(inner),
},
));
if let Some(node) = self.try_parse_cond_group(start, kind)? {
return Ok(node);
}

let first = self.lexer.next_token()?;
self.parse_cond_operand(start, first)
}

/// `! expr` — Parable drops the negation in S-expression output, but we
/// keep it in the AST so the reformatter can preserve it.
fn try_parse_cond_negation(&mut self, start: usize, kind: TokenType) -> Result<Option<Node>> {
if kind != TokenType::Bang {
return Ok(None);
}
self.lexer.next_token()?;
let inner = self.parse_cond_primary()?;
Ok(Some(self.spanned(
start,
NodeKind::CondNot {
operand: Box::new(inner),
},
)))
}

/// `( expr )` — a grouped expression inside `[[ … ]]`.
fn try_parse_cond_group(&mut self, start: usize, kind: TokenType) -> Result<Option<Node>> {
if kind != TokenType::LeftParen {
return Ok(None);
}
self.lexer.next_token()?;
let inner = self.parse_cond_or()?;
self.expect(TokenType::RightParen)?;
Ok(Some(self.spanned(
start,
NodeKind::CondParen {
inner: Box::new(inner),
},
)))
}

// Check for unary operators: -f, -d, -z, -n, etc.
/// Parse `-f EXPR` (unary), `EXPR OP EXPR` (binary), or a bare word
/// (`[-n] EXPR`). `first` is the already-consumed leading token.
fn parse_cond_operand(&mut self, start: usize, first: Token) -> Result<Node> {
// Unary operators: -f, -d, -z, -n, etc.
if first.value.starts_with('-')
&& first.value.len() <= 3
&& self.peek_cond_term()?.is_some()
Expand All @@ -175,7 +192,7 @@ impl Parser {
));
}

// Check for binary operators
// Binary operators: ==, !=, =~, <, >, -eq, -ne, ...
if !self.is_cond_close()?
&& !self.peek_is(TokenType::And)?
&& !self.peek_is(TokenType::Or)?
Expand Down
165 changes: 79 additions & 86 deletions src/parser/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::error::Result;
use crate::token::{Token, TokenType};

use super::Parser;
use super::helpers::{is_fd_number, word_node_from_token};
use super::helpers::{is_fd_number, is_redirect_op_kind, word_node_from_token};

impl Parser {
pub(super) fn parse_subshell(&mut self) -> Result<Node> {
Expand Down Expand Up @@ -107,84 +107,58 @@ impl Parser {
))
}

#[allow(clippy::too_many_lines)]
pub(super) fn parse_coproc(&mut self) -> Result<Node> {
let start = self.peek_pos()?;
self.expect(TokenType::Coproc)?;

let tok = self.lexer.peek_token()?;
if tok.kind.starts_command()
&& !matches!(
tok.kind,
TokenType::Coproc | TokenType::Time | TokenType::Bang
)
{
let command = self.parse_command()?;
return Ok(self.spanned(
start,
NodeKind::Coproc {
name: None,
command: Box::new(command),
},
));
// Path A: `coproc CMD` — no name, body is whatever starts a command.
if coproc_starts_command(self.lexer.peek_token()?.kind) {
return self.build_coproc_with_command(start, None);
}

let first_tok = self.lexer.next_token()?;
self.lexer.set_command_start();

// If first token after coproc is a redirect operator, parse as
// a command with redirects (no name, no command word)
if matches!(
first_tok.kind,
TokenType::Less
| TokenType::Greater
| TokenType::DoubleGreater
| TokenType::LessAnd
| TokenType::GreaterAnd
| TokenType::LessGreater
| TokenType::GreaterPipe
| TokenType::AndGreater
| TokenType::AndDoubleGreater
| TokenType::DoubleLess
| TokenType::DoubleLessDash
| TokenType::TripleLess
) {
let mut redirects = vec![self.build_redirect(first_tok, -1, None)?];
redirects.extend(self.parse_trailing_redirects()?);
return Ok(self.spanned(
start,
NodeKind::Coproc {
name: None,
command: Box::new(self.spanned(
start,
NodeKind::Command {
assignments: Vec::new(),
words: Vec::new(),
redirects,
},
)),
},
));
// Path B: `coproc <redir ...` — synthetic command with only redirects.
if is_redirect_op_kind(first_tok.kind) {
return self.parse_coproc_redirect_only(start, first_tok);
}

let next = self.lexer.peek_token()?;
let name = if next.kind.starts_command()
&& !matches!(
next.kind,
TokenType::Coproc | TokenType::Time | TokenType::Bang
) {
let n = Some(first_tok.value);
let command = self.parse_command()?;
return Ok(self.spanned(
start,
NodeKind::Coproc {
name: n,
command: Box::new(command),
},
));
} else {
None
};
// Path C: `coproc NAME CMD` — named coproc, `first_tok` is the name.
if coproc_starts_command(self.lexer.peek_token()?.kind) {
return self.build_coproc_with_command(start, Some(first_tok.value));
}

// Path D: `coproc WORD WORD... [redirs]` — synthetic command with
// `first_tok` as the first word.
let (words, redirects) = self.parse_coproc_word_loop(first_tok)?;
Ok(self.build_coproc_synthetic_command(start, None, words, redirects))
}

/// Parses the body of a `coproc [NAME] CMD` form by delegating to
/// `parse_command`. Returns the wrapped `Coproc` node.
fn build_coproc_with_command(&mut self, start: usize, name: Option<String>) -> Result<Node> {
let command = self.parse_command()?;
Ok(self.spanned(
start,
NodeKind::Coproc {
name,
command: Box::new(command),
},
))
}

/// Path B: first token after `coproc` is a redirect operator. Build a
/// synthetic `Command { redirects }` wrapped in a nameless `Coproc`.
fn parse_coproc_redirect_only(&mut self, start: usize, first_tok: Token) -> Result<Node> {
let mut redirects = vec![self.build_redirect(first_tok, -1, None)?];
redirects.extend(self.parse_trailing_redirects()?);
Ok(self.build_coproc_synthetic_command(start, None, Vec::new(), redirects))
}

/// Path D: loop over words and redirects after `coproc WORD` to collect
/// the synthetic command's contents.
fn parse_coproc_word_loop(&mut self, first_tok: Token) -> Result<(Vec<Node>, Vec<Node>)> {
let mut words = vec![word_node_from_token(first_tok)];
let mut redirects = Vec::new();
loop {
Expand All @@ -196,31 +170,43 @@ impl Parser {
continue;
}
let tok = self.lexer.peek_token()?;
if matches!(tok.kind, TokenType::Word | TokenType::Number) {
let tok = self.lexer.next_token()?;
if is_fd_number(&tok.value) && self.is_redirect_operator()? {
redirects.push(self.parse_redirect_with_fd(&tok)?);
} else {
words.push(word_node_from_token(tok));
}
} else {
if !matches!(tok.kind, TokenType::Word | TokenType::Number) {
break;
}
let tok = self.lexer.next_token()?;
if is_fd_number(&tok.value) && self.is_redirect_operator()? {
redirects.push(self.parse_redirect_with_fd(&tok)?);
} else {
words.push(word_node_from_token(tok));
}
}
Ok(self.spanned(
Ok((words, redirects))
}

/// Wraps a synthetic `Command { assignments: [], words, redirects }` in
/// a `Coproc { name, command }` at `start`.
fn build_coproc_synthetic_command(
&self,
start: usize,
name: Option<String>,
words: Vec<Node>,
redirects: Vec<Node>,
) -> Node {
let command = self.spanned(
start,
NodeKind::Command {
assignments: Vec::new(),
words,
redirects,
},
);
self.spanned(
start,
NodeKind::Coproc {
name,
command: Box::new(self.spanned(
start,
NodeKind::Command {
assignments: Vec::new(),
words,
redirects,
},
)),
command: Box::new(command),
},
))
)
}

pub(super) fn parse_arith_command(&mut self) -> Result<Node> {
Expand All @@ -243,3 +229,10 @@ impl Parser {
self.expect_closing(TokenType::RightBrace, "}")
}
}

/// Returns true when `kind` can start a command at the body position of a
/// `coproc` clause. Excludes `coproc`, `time`, and `!` since they would
/// cause re-entry into `parse_coproc` or ambiguous negation.
const fn coproc_starts_command(kind: TokenType) -> bool {
kind.starts_command() && !matches!(kind, TokenType::Coproc | TokenType::Time | TokenType::Bang)
}
21 changes: 20 additions & 1 deletion src/parser/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
//! Helper functions for the parser.

use crate::ast::{Node, NodeKind};
use crate::token::Token;
use crate::token::{Token, TokenType};

/// Returns true for the 12 token kinds that start a redirect operator.
pub(super) const fn is_redirect_op_kind(kind: TokenType) -> bool {
matches!(
kind,
TokenType::Less
| TokenType::Greater
| TokenType::DoubleGreater
| TokenType::LessAnd
| TokenType::GreaterAnd
| TokenType::LessGreater
| TokenType::GreaterPipe
| TokenType::AndGreater
| TokenType::AndDoubleGreater
| TokenType::DoubleLess
| TokenType::DoubleLessDash
| TokenType::TripleLess
)
}

/// Creates a `Word` node from a lexer token, moving value and spans.
pub fn word_node_from_token(tok: Token) -> Node {
Expand Down
20 changes: 4 additions & 16 deletions src/parser/redirects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use crate::lexer::heredoc::parse_heredoc_delimiter;
use crate::token::{Token, TokenType};

use super::Parser;
use super::helpers::{is_fd_number, is_varfd, word_node, word_node_from_token};
use super::helpers::{
is_fd_number, is_redirect_op_kind, is_varfd, word_node, word_node_from_token,
};

impl Parser {
pub(super) fn parse_redirect(&mut self) -> Result<Node> {
Expand Down Expand Up @@ -165,20 +167,6 @@ impl Parser {

pub(super) fn is_redirect_operator(&mut self) -> Result<bool> {
let tok = self.lexer.peek_token()?;
Ok(matches!(
tok.kind,
TokenType::Less
| TokenType::Greater
| TokenType::DoubleGreater
| TokenType::LessAnd
| TokenType::GreaterAnd
| TokenType::LessGreater
| TokenType::GreaterPipe
| TokenType::AndGreater
| TokenType::AndDoubleGreater
| TokenType::DoubleLess
| TokenType::DoubleLessDash
| TokenType::TripleLess
))
Ok(is_redirect_op_kind(tok.kind))
}
}
Loading
Loading