Merge nucleic/sleek-thistle-egret-fyej into dev

This commit is contained in:
2026-07-18 05:19:31 -07:00
commit b6be87b72d
677 changed files with 102939 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
[package]
name = "brush-parser"
description = "POSIX/bash shell tokenizer and parsers (used by brush-shell)"
version = "0.4.0"
authors.workspace = true
categories.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[lib]
bench = false
[features]
arbitrary = ["dep:arbitrary"]
debug-tracing = ["peg/trace"]
diagnostics = ["dep:miette"]
serde = ["dep:serde"]
winnow-parser = ["dep:winnow"]
[dependencies]
arbitrary = { version = "1.4.2", optional = true, features = ["derive"] }
bon = "3.9.1"
cached = "0.59.0"
indenter = "0.3.4"
insta = { version = "1.47.1", features = ["redactions"] }
miette = { version = "7.6.0", optional = true, default-features = false, features = [
"derive",
] }
peg = "0.8.5"
serde = { version = "1.0.228", optional = true, features = ["derive", "rc"] }
thiserror = "2.0.18"
tracing = "0.1.44"
utf8-chars = "3.0.6"
winnow = { version = "1.0.0", optional = true }
[target.wasm32-unknown-unknown.dependencies]
getrandom = { version = "0.4.2", features = ["wasm_js"] }
uuid = { version = "1.23.1", features = ["js"] }
[dev-dependencies]
anyhow = "1.0.102"
criterion = { version = "0.8.2", features = ["html_reports"] }
insta = { version = "1.47.1", features = ["glob", "ron", "yaml", "redactions"] }
miette = { version = "7.6.0", features = ["fancy"] }
pretty_assertions = { version = "1.4.1", features = ["unstable"] }
serde = { version = "1.0.228", features = ["derive", "rc"] }
serde_json = "1.0.149"
serde_yaml = "0.9.34"
[[bench]]
name = "parser"
harness = false
[[example]]
name = "miette"
required-features = ["diagnostics"]
[[example]]
name = "serde"
required-features = ["serde"]
+1
View File
@@ -0,0 +1 @@
../LICENSE
+280
View File
@@ -0,0 +1,280 @@
//! Benchmarks for the brush-parser crate.
//!
//! Compares parsing approaches:
//! 1. PEG parser (tokenize + peg parse)
//! 2. `Winnow_str` parser (direct string parse) - when winnow-parser feature enabled
#![allow(missing_docs)]
#![allow(clippy::unwrap_used)]
#[cfg(unix)]
mod unix {
use brush_parser::Token;
use criterion::Criterion;
fn uncached_tokenize(content: &str) -> Vec<brush_parser::Token> {
brush_parser::uncached_tokenize_str(content, &brush_parser::TokenizerOptions::default())
.unwrap()
}
fn cacheable_tokenize(content: &str) -> Vec<brush_parser::Token> {
brush_parser::tokenize_str_with_options(content, &brush_parser::TokenizerOptions::default())
.unwrap()
}
fn parse_peg(tokens: &[Token]) -> brush_parser::ast::Program {
brush_parser::parse_tokens(tokens, &brush_parser::ParserOptions::default()).unwrap()
}
#[cfg(feature = "winnow-parser")]
fn parse_winnow_str(content: &str) -> brush_parser::ast::Program {
use brush_parser::{ParserOptions, SourceInfo, winnow_str};
winnow_str::parse_program(content, &ParserOptions::default(), &SourceInfo::default())
.unwrap()
}
// Combined tokenize + parse functions for full pipeline comparison
fn tokenize_and_parse_peg(content: &str) -> brush_parser::ast::Program {
let tokens = uncached_tokenize(content);
parse_peg(&tokens)
}
const SAMPLE_SCRIPT: &str = r#"
for f in A B C; do
echo "${f@L}" >&2
done
"#;
const SIMPLE_SCRIPT: &str = "echo hello world";
const PIPELINE_SCRIPT: &str = "cat file.txt | grep pattern | wc -l";
const COMPLEX_SCRIPT: &str = r#"
#!/bin/bash
# Complex script with multiple constructs
function process_file() {
local file="$1"
if [[ -f "$file" ]]; then
while read -r line; do
case "$line" in
start*)
echo "Starting: $line"
;;
end*)
echo "Ending: $line"
;;
*)
echo "Processing: $line"
;;
esac
done < "$file"
fi
}
for i in {1..10}; do
if (( i % 2 == 0 )); then
echo "$i is even" | tee -a output.txt
else
echo "$i is odd" >> output.txt
fi
done
process_file "input.txt" && echo "Success" || echo "Failed"
"#;
const NESTED_EXPANSIONS_SCRIPT: &str = r"
# Script with deeply nested expansions (tests balanced delimiter parsing)
result=$(echo $(echo $((1 + (2 * (3 - 4))))))
fallback=${foo:-${bar:-${baz}}}
arithmetic=$((1 + (2 * (3 + (4 - 5)))))
command_subst=$(ls $(pwd))
mixed=$(echo $((1 + 2)) | cat)
backtick=`echo (nested parens)`
";
// Extended test expression benchmarks - various patterns
#[allow(dead_code)]
const EXTENDED_TEST_SIMPLE: &str = "[[ -f file.txt ]]";
#[allow(dead_code)]
const EXTENDED_TEST_BINARY: &str = "[[ $a == $b ]]";
#[allow(dead_code)]
const EXTENDED_TEST_REGEX: &str = "[[ $str =~ ^[0-9]+$ ]]";
#[allow(dead_code)]
const EXTENDED_TEST_COMPLEX_REGEX: &str = "[[ $input =~ ^(foo|bar)[0-9]+(baz|qux)$ ]]";
#[allow(dead_code)]
const EXTENDED_TEST_LOGICAL: &str = "[[ -f file.txt && -r file.txt || -w other.txt ]]";
#[allow(dead_code)]
const EXTENDED_TEST_NESTED: &str = "[[ ( -f $file && -r $file ) || ( -d $dir && -x $dir ) ]]";
#[allow(dead_code)]
const EXTENDED_TEST_COMPLEX: &str =
"[[ ! ( $a -eq 5 && $b -gt 10 ) || ( $c =~ pattern && -f $file ) ]]";
fn benchmark_parsing_script_using_caches(c: &mut Criterion, script_path: &std::path::Path) {
let contents = std::fs::read_to_string(script_path).unwrap();
let filename = script_path.file_name().unwrap().to_string_lossy();
c.bench_function(std::format!("parse_peg_{filename}").as_str(), |b| {
b.iter(|| parse_peg(&cacheable_tokenize(contents.as_str())));
});
}
pub(crate) fn criterion_benchmark(c: &mut Criterion) {
const POSSIBLE_BASH_COMPLETION_SCRIPT_PATH: &str =
"/usr/share/bash-completion/bash_completion";
// Tokenization benchmark (applies to both parsers)
c.bench_function("tokenize_sample_script", |b| {
b.iter(|| uncached_tokenize(SAMPLE_SCRIPT));
});
// Simple script benchmarks
let simple_tokens = uncached_tokenize(SIMPLE_SCRIPT);
c.bench_function("parse_peg_simple", |b| b.iter(|| parse_peg(&simple_tokens)));
#[cfg(feature = "winnow-parser")]
c.bench_function("parse_winnow_str_simple", |b| {
b.iter(|| parse_winnow_str(SIMPLE_SCRIPT));
});
// Pipeline script benchmarks
let pipeline_tokens = uncached_tokenize(PIPELINE_SCRIPT);
c.bench_function("parse_peg_pipeline", |b| {
b.iter(|| parse_peg(&pipeline_tokens));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("parse_winnow_str_pipeline", |b| {
b.iter(|| parse_winnow_str(PIPELINE_SCRIPT));
});
// Sample script (for loop) benchmarks
let sample_tokens = uncached_tokenize(SAMPLE_SCRIPT);
c.bench_function("parse_peg_for_loop", |b| {
b.iter(|| parse_peg(&sample_tokens));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("parse_winnow_str_for_loop", |b| {
b.iter(|| parse_winnow_str(SAMPLE_SCRIPT));
});
// Complex script benchmarks
let complex_tokens = uncached_tokenize(COMPLEX_SCRIPT);
c.bench_function("parse_peg_complex", |b| {
b.iter(|| parse_peg(&complex_tokens));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("parse_winnow_str_complex", |b| {
b.iter(|| parse_winnow_str(COMPLEX_SCRIPT));
});
// Real-world bash completion script (if available)
let well_known_complicated_script =
std::path::PathBuf::from(POSSIBLE_BASH_COMPLETION_SCRIPT_PATH);
if well_known_complicated_script.exists() {
benchmark_parsing_script_using_caches(c, &well_known_complicated_script);
}
// ========================================================================
// FULL PIPELINE BENCHMARKS (tokenize + parse)
// ========================================================================
// These benchmarks measure the complete parsing pipeline from string to AST,
// allowing fair comparison between different approaches:
// - tokenize_and_parse_peg: Legacy tokenizer + PEG parser
// - parse_winnow_str: Direct string parsing (no separate tokenization)
// Simple script full pipeline
c.bench_function("full_peg_simple", |b| {
b.iter(|| tokenize_and_parse_peg(SIMPLE_SCRIPT));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("full_winnow_str_simple", |b| {
b.iter(|| parse_winnow_str(SIMPLE_SCRIPT));
});
// Pipeline script full pipeline
c.bench_function("full_peg_pipeline", |b| {
b.iter(|| tokenize_and_parse_peg(PIPELINE_SCRIPT));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("full_winnow_str_pipeline", |b| {
b.iter(|| parse_winnow_str(PIPELINE_SCRIPT));
});
// For loop full pipeline
c.bench_function("full_peg_for_loop", |b| {
b.iter(|| tokenize_and_parse_peg(SAMPLE_SCRIPT));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("full_winnow_str_for_loop", |b| {
b.iter(|| parse_winnow_str(SAMPLE_SCRIPT));
});
// Complex script full pipeline
c.bench_function("full_peg_complex", |b| {
b.iter(|| tokenize_and_parse_peg(COMPLEX_SCRIPT));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("full_winnow_str_complex", |b| {
b.iter(|| parse_winnow_str(COMPLEX_SCRIPT));
});
// Nested expansions (balanced delimiter parsing stress test)
c.bench_function("full_peg_nested_expansions", |b| {
b.iter(|| tokenize_and_parse_peg(NESTED_EXPANSIONS_SCRIPT));
});
#[cfg(feature = "winnow-parser")]
c.bench_function("full_winnow_str_nested_expansions", |b| {
b.iter(|| parse_winnow_str(NESTED_EXPANSIONS_SCRIPT));
});
// ========================================================================
// EXTENDED TEST EXPRESSION BENCHMARKS
// ========================================================================
// Benchmarks for the refactored extended test ([[ ]]) parser
// Tests various patterns: simple, binary, regex, logical operators, nesting
#[cfg(feature = "winnow-parser")]
{
c.bench_function("extended_test_simple", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_SIMPLE));
});
c.bench_function("extended_test_binary", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_BINARY));
});
c.bench_function("extended_test_regex", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_REGEX));
});
c.bench_function("extended_test_complex_regex", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_COMPLEX_REGEX));
});
c.bench_function("extended_test_logical", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_LOGICAL));
});
c.bench_function("extended_test_nested", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_NESTED));
});
c.bench_function("extended_test_complex", |b| {
b.iter(|| parse_winnow_str(EXTENDED_TEST_COMPLEX));
});
}
}
}
#[cfg(unix)]
criterion::criterion_group! {
name = benches;
config = criterion::Criterion::default();
targets = unix::criterion_benchmark
}
#[cfg(unix)]
criterion::criterion_main!(benches);
#[cfg(not(unix))]
fn main() {}
+24
View File
@@ -0,0 +1,24 @@
//! Simple example of miette usage
use std::io::Cursor;
use brush_parser::Parser;
use miette::{IntoDiagnostic, miette};
fn main() -> miette::Result<()> {
let f = std::env::args()
.nth(1)
.ok_or_else(|| miette!("Please provide a file name"))?;
let source = std::fs::read_to_string(&f).into_diagnostic()?;
let reader = Cursor::new(&source);
let mut parser = Parser::builder().build(reader);
let ast = parser
.parse_program()
.map_err(|e| e.to_pretty_error(&source))?;
println!("{ast:#?}");
Ok(())
}
+31
View File
@@ -0,0 +1,31 @@
//! Example demonstrating AST serialization and deserialization with the `serde` feature.
//!
//! Run with: `cargo run --package brush-parser --example serde --features serde`
use brush_parser::{Parser, ParserOptions};
use std::io::BufReader;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Parse a simple shell command
let input = "echo 'Hello, World!' && ls -la";
let reader = BufReader::new(input.as_bytes());
let options = ParserOptions::default();
let mut parser = Parser::new(reader, &options);
let program = parser.parse_program()?;
// Serialize the AST to JSON
let json = serde_json::to_string_pretty(&program)?;
println!("Parsed AST:");
println!("{json}");
// Demonstrate round-trip: deserialize the JSON back to AST
println!("\nRound-trip deserialization:");
let deserialized: brush_parser::ast::Program = serde_json::from_str(&json)?;
println!(
"Successfully deserialized AST with {} command(s)",
deserialized.complete_commands.len()
);
Ok(())
}
+171
View File
@@ -0,0 +1,171 @@
//! Parser for shell arithmetic expressions.
use crate::ast;
use crate::error;
/// Parses a shell arithmetic expression.
///
/// # Arguments
///
/// * `input` - The arithmetic expression to parse, in string form.
pub fn parse(input: &str) -> Result<ast::ArithmeticExpr, error::WordParseError> {
cacheable_parse(input.to_owned())
}
#[cached::proc_macro::cached(size = 64, result = true)]
fn cacheable_parse(input: String) -> Result<ast::ArithmeticExpr, error::WordParseError> {
tracing::debug!(target: "arithmetic", "parsing arithmetic expression: '{input}'");
arithmetic::full_expression(input.as_str())
.map_err(|e| error::WordParseError::ArithmeticExpression(e.into()))
}
peg::parser! {
grammar arithmetic() for str {
pub(crate) rule full_expression() -> ast::ArithmeticExpr =
![_] { ast::ArithmeticExpr::Literal(0) } /
_ e:expression() _ { e }
pub(crate) rule expression() -> ast::ArithmeticExpr = precedence!{
x:(@) _ "," _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Comma, Box::new(x), Box::new(y)) }
--
x:lvalue() _ "*=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Multiply, x, Box::new(y)) }
x:lvalue() _ "/=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Divide, x, Box::new(y)) }
x:lvalue() _ "%=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Modulo, x, Box::new(y)) }
x:lvalue() _ "+=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Add, x, Box::new(y)) }
x:lvalue() _ "-=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::Subtract, x, Box::new(y)) }
x:lvalue() _ "<<=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::ShiftLeft, x, Box::new(y)) }
x:lvalue() _ ">>=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::ShiftRight, x, Box::new(y)) }
x:lvalue() _ "&=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::BitwiseAnd, x, Box::new(y)) }
x:lvalue() _ "|=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::BitwiseOr, x, Box::new(y)) }
x:lvalue() _ "^=" _ y:(@) { ast::ArithmeticExpr::BinaryAssignment(ast::BinaryOperator::BitwiseXor, x, Box::new(y)) }
x:lvalue() _ "=" _ y:(@) { ast::ArithmeticExpr::Assignment(x, Box::new(y)) }
--
x:@ _ "?" _ y:expression() _ ":" _ z:(@) { ast::ArithmeticExpr::Conditional(Box::new(x), Box::new(y), Box::new(z)) }
--
x:(@) _ "||" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LogicalOr, Box::new(x), Box::new(y)) }
--
x:(@) _ "&&" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LogicalAnd, Box::new(x), Box::new(y)) }
--
x:(@) _ "|" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::BitwiseOr, Box::new(x), Box::new(y)) }
--
x:(@) _ "^" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::BitwiseXor, Box::new(x), Box::new(y)) }
--
x:(@) _ "&" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::BitwiseAnd, Box::new(x), Box::new(y)) }
--
x:(@) _ "==" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Equals, Box::new(x), Box::new(y)) }
x:(@) _ "!=" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::NotEquals, Box::new(x), Box::new(y)) }
--
x:(@) _ "<" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LessThan, Box::new(x), Box::new(y)) }
x:(@) _ ">" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::GreaterThan, Box::new(x), Box::new(y)) }
x:(@) _ "<=" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::LessThanOrEqualTo, Box::new(x), Box::new(y)) }
x:(@) _ ">=" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::GreaterThanOrEqualTo, Box::new(x), Box::new(y)) }
--
x:(@) _ "<<" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::ShiftLeft, Box::new(x), Box::new(y)) }
x:(@) _ ">>" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::ShiftRight, Box::new(x), Box::new(y)) }
--
x:(@) _ "+" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Add, Box::new(x), Box::new(y)) }
x:(@) _ "-" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Subtract, Box::new(x), Box::new(y)) }
--
x:(@) _ "*" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Multiply, Box::new(x), Box::new(y)) }
x:(@) _ "%" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Modulo, Box::new(x), Box::new(y)) }
x:(@) _ "/" _ y:@ { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Divide, Box::new(x), Box::new(y)) }
--
x:@ _ "**" _ y:(@) { ast::ArithmeticExpr::BinaryOp(ast::BinaryOperator::Power, Box::new(x), Box::new(y)) }
--
"!" _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::LogicalNot, Box::new(x)) }
"~" _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::BitwiseNot, Box::new(x)) }
--
// NOTE: We add negative lookahead to avoid ambiguity with the pre-increment/pre-decrement operators.
"+" !['+'] _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::UnaryPlus, Box::new(x)) }
"-" !['-'] _ x:(@) { ast::ArithmeticExpr::UnaryOp(ast::UnaryOperator::UnaryMinus, Box::new(x)) }
--
"++" _ x:lvalue() { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PrefixIncrement, x) }
"--" _ x:lvalue() { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PrefixDecrement, x) }
--
x:lvalue() _ "++" { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PostfixIncrement, x) }
x:lvalue() _ "--" { ast::ArithmeticExpr::UnaryAssignment(ast::UnaryAssignmentOperator::PostfixDecrement, x) }
--
n:literal_number() { ast::ArithmeticExpr::Literal(n) }
l:lvalue() { ast::ArithmeticExpr::Reference(l) }
"(" _ expr:expression() _ ")" { expr }
}
rule lvalue() -> ast::ArithmeticTarget =
name:variable_name() "[" index:expression() "]" {
ast::ArithmeticTarget::ArrayElement(name.to_owned(), Box::new(index))
} /
name:variable_name() {
ast::ArithmeticTarget::Variable(name.to_owned())
}
rule variable_name() -> &'input str =
$(['a'..='z' | 'A'..='Z' | '_'](['a'..='z' | 'A'..='Z' | '_' | '0'..='9']*))
rule _() -> () = quiet!{[' ' | '\t' | '\n' | '\r']*} {}
rule literal_number() -> i64 =
// Literal with explicit radix (format: <base>#<literal>)
radix:decimal_literal() "#" s:$(['0'..='9' | 'a'..='z' | 'A'..='Z' | '@' | '_']+) {?
parse_shell_literal_number(s, radix.cast_unsigned())
} /
// Hex literal
"0" ['x' | 'X'] s:$(['0'..='9' | 'a'..='f' | 'A'..='F']*) {?
i64::from_str_radix(s, 16).or(Err("i64"))
} /
// Octal literal
s:$("0" ['0'..='8']*) {?
i64::from_str_radix(s, 8).or(Err("i64"))
} /
// Decimal literal
decimal_literal()
rule decimal_literal() -> i64 =
s:$(['1'..='9'] ['0'..='9']*) {?
// Parse as u64 first, then cast to i64. This handles values like
// 9223372036854775808 (i64::MAX + 1) which is needed for INT64_MIN
// when preceded by unary minus: -(9223372036854775808) wraps to i64::MIN.
s.parse::<u64>().map(|v| v.cast_signed()).or(Err("i64"))
}
}
}
fn parse_shell_literal_number(s: &str, radix: u64) -> Result<i64, &'static str> {
if !(2..=64).contains(&radix) {
return Err("invalid base");
}
// For bases <= 36: case-insensitive (a-z and A-Z both map to 10-35)
// For bases > 36 (bash extension):
// 0-9 = 0-9, a-z = 10-35, A-Z = 36-61, @ = 62, _ = 63
let mut result: i64 = 0;
for ch in s.chars() {
let digit_val = if radix <= 36 {
match ch {
'0'..='9' => (ch as u64) - ('0' as u64),
'a'..='z' => (ch as u64) - ('a' as u64) + 10,
'A'..='Z' => (ch as u64) - ('A' as u64) + 10,
_ => return Err("invalid digit"),
}
} else {
match ch {
'0'..='9' => (ch as u64) - ('0' as u64),
'a'..='z' => (ch as u64) - ('a' as u64) + 10,
'A'..='Z' => (ch as u64) - ('A' as u64) + 36,
'@' => 62,
'_' => 63,
_ => return Err("invalid digit"),
}
};
if digit_val >= radix {
return Err("value too great for base");
}
result = result
.wrapping_mul(radix.cast_signed())
.wrapping_add(digit_val.cast_signed());
}
Ok(result)
}
File diff suppressed because it is too large Load Diff
+132
View File
@@ -0,0 +1,132 @@
use crate::tokenizer;
/// Represents an error that occurred while parsing tokens.
#[derive(thiserror::Error, Debug)]
pub enum ParseError {
/// A parsing error occurred near the given position.
#[error("syntax error at line {} col {}", .0.line, .0.column)]
ParsingNear(crate::SourcePosition),
/// A parsing error occurred at the end of the input.
#[error("syntax error at end of input")]
ParsingAtEndOfInput,
/// An error occurred while tokenizing the input stream.
#[error("{} (detected near {})", .inner, .position.as_ref().map_or_else(|| String::from("<unknown position>"), |p| std::format!("line {} col {}", p.line, p.column)))]
Tokenizing {
/// The inner error.
inner: tokenizer::TokenizerError,
/// Optionally provides the position of the error.
position: Option<crate::SourcePosition>,
},
}
#[cfg(feature = "diagnostics")]
#[allow(clippy::cast_sign_loss)]
#[allow(unused)] // Workaround unused warnings in nightly versions of the compiler
pub mod miette {
use super::ParseError;
use miette::SourceOffset;
impl ParseError {
/// Convert the original error to one miette can pretty print
pub fn to_pretty_error(self, input: impl Into<String>) -> PrettyError {
let input = input.into();
let location = match self {
Self::ParsingNear(ref pos) => {
Some(SourceOffset::from_location(&input, pos.line, pos.column))
}
Self::Tokenizing { ref position, .. } => position
.as_ref()
.map(|p| SourceOffset::from_location(&input, p.line, p.column)),
Self::ParsingAtEndOfInput => {
Some(SourceOffset::from_location(&input, usize::MAX, usize::MAX))
}
};
PrettyError {
cause: self,
input,
location,
}
}
}
/// Represents an error that occurred while parsing tokens.
#[derive(thiserror::Error, Debug, miette::Diagnostic)]
#[error("Cannot parse the input script")]
pub struct PrettyError {
cause: ParseError,
#[source_code]
input: String,
#[label("{cause}")]
location: Option<SourceOffset>,
}
}
/// Represents a parsing error with its location information
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub struct ParseErrorLocation {
#[from]
inner: peg::error::ParseError<peg::str::LineCol>,
}
/// Represents an error that occurred while parsing a word.
#[derive(Debug, thiserror::Error)]
pub enum WordParseError {
/// An error occurred while parsing an arithmetic expression.
#[error("failed to parse arithmetic expression")]
ArithmeticExpression(ParseErrorLocation),
/// An error occurred while parsing a shell pattern.
#[error("failed to parse pattern")]
Pattern(ParseErrorLocation),
/// An error occurred while parsing a prompt string.
#[error("failed to parse prompt string")]
Prompt(ParseErrorLocation),
/// An error occurred while parsing a parameter.
#[error("failed to parse parameter '{0}'")]
Parameter(String, ParseErrorLocation),
/// An error occurred while parsing for brace expansion.
#[error("failed to parse for brace expansion: '{0}'")]
BraceExpansion(String, ParseErrorLocation),
/// An error occurred while parsing a word.
#[error("failed to parse word '{0}'")]
Word(String, ParseErrorLocation),
}
/// Represents an error that occurred while parsing a (non-extended) test command.
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub struct TestCommandParseError(#[from] peg::error::ParseError<usize>);
/// Represents an error that occurred while parsing a key-binding specification.
#[derive(Debug, thiserror::Error)]
pub enum BindingParseError {
/// An unknown error occurred while parsing a key-binding specification.
#[error("unknown error while parsing key-binding: '{0}'")]
Unknown(String),
/// A key code was missing from the key-binding specification.
#[error("missing key code in key-binding")]
MissingKeyCode,
}
pub(crate) fn convert_peg_parse_error(
err: &peg::error::ParseError<usize>,
tokens: &[crate::Token],
) -> ParseError {
let approx_token_index = err.location;
if approx_token_index < tokens.len() {
let token = &tokens[approx_token_index];
ParseError::ParsingNear((*token.location().start).clone())
} else {
ParseError::ParsingAtEndOfInput
}
}
+37
View File
@@ -0,0 +1,37 @@
//! Implements a tokenizer and parsers for POSIX / bash shell syntax.
// TODO(unwrap): remove or scope this allow attribute
#![allow(clippy::unwrap_used)]
pub mod arithmetic;
pub mod ast;
pub mod pattern;
pub mod prompt;
pub mod readline_binding;
pub mod test_command;
pub mod word;
mod error;
mod parser;
mod source;
mod tokenizer;
#[cfg(test)]
mod snapshot_tests;
pub use error::{
BindingParseError, ParseError, ParseErrorLocation, TestCommandParseError, WordParseError,
};
#[cfg(feature = "diagnostics")]
pub use error::miette::PrettyError;
#[cfg(feature = "winnow-parser")]
pub use parser::winnow_str;
pub use parser::{Parser, ParserBuilder, ParserImpl, ParserOptions, SourceInfo, parse_tokens};
pub use source::{SourcePosition, SourcePositionOffset, SourceSpan};
pub use tokenizer::{
Token, TokenLocation, TokenizerError, TokenizerOptions, tokenize_str,
tokenize_str_with_options, uncached_tokenize_str, unquote_str,
};
+258
View File
@@ -0,0 +1,258 @@
use std::path::PathBuf;
use bon::bon;
use crate::ast;
use crate::tokenizer::{Token, TokenEndReason, Tokenizer, TokenizerOptions, Tokens};
pub mod peg;
#[cfg(feature = "winnow-parser")]
pub mod winnow_str;
/// Parser implementation to use
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Default)]
pub enum ParserImpl {
/// PEG-based parser (token-based)
#[default]
Peg,
/// Winnow-based parser (string-based, direct)
#[cfg(feature = "winnow-parser")]
Winnow,
}
/// Options used to control the behavior of the parser.
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct ParserOptions {
/// Whether or not to enable extended globbing (a.k.a. `extglob`).
pub enable_extended_globbing: bool,
/// Whether or not to enable POSIX compliance mode.
pub posix_mode: bool,
/// Whether or not to enable maximal compatibility with the `sh` shell.
pub sh_mode: bool,
/// Whether or not to perform tilde expansion for tildes at the start of words.
pub tilde_expansion_at_word_start: bool,
/// Whether or not to perform tilde expansion for tildes after colons.
pub tilde_expansion_after_colon: bool,
/// Select the parser internal implementation
pub parser_impl: ParserImpl,
}
impl Default for ParserOptions {
fn default() -> Self {
Self {
enable_extended_globbing: true,
posix_mode: false,
sh_mode: false,
tilde_expansion_at_word_start: true,
tilde_expansion_after_colon: false,
parser_impl: ParserImpl::default(),
}
}
}
impl ParserOptions {
/// Returns the tokenizer options implied by these parser options.
pub const fn tokenizer_options(&self) -> TokenizerOptions {
TokenizerOptions {
enable_extended_globbing: self.enable_extended_globbing,
posix_mode: self.posix_mode,
sh_mode: self.sh_mode,
}
}
}
/// Information about the source of tokens.
#[derive(Clone, Debug, Default)]
#[allow(dead_code)]
pub struct SourceInfo {
/// The source of the tokens.
pub source: String,
}
impl From<PathBuf> for SourceInfo {
fn from(path: PathBuf) -> Self {
Self {
source: path.to_string_lossy().to_string(),
}
}
}
/// Implements parsing for shell programs.
pub struct Parser<R: std::io::BufRead> {
/// The reader to use for input
reader: R,
/// Parsing options
options: ParserOptions,
}
#[bon]
impl<R: std::io::BufRead> Parser<R> {
///
/// # Arguments
///
/// * `reader` - The reader to use for input.
/// * `options` - The options to use when parsing.
pub fn new(reader: R, options: &ParserOptions) -> Self {
Self {
reader,
options: options.clone(),
}
}
/// Create a new parser instance through a builder
#[builder(
finish_fn(doc {
/// Instantiate a parser with the provided reader as input
})
)]
pub const fn builder(
/// The reader to use for input
#[builder(finish_fn)]
reader: R,
#[builder(default = true)]
/// Whether or not to enable extended globbing (a.k.a. `extglob`).
enable_extended_globbing: bool,
#[builder(default = false)]
/// Whether or not to enable POSIX compliance mode.
posix_mode: bool,
#[builder(default = false)]
/// Whether or not to enable maximal compatibility with the `sh` shell.
sh_mode: bool,
#[builder(default = true)]
/// Whether or not to perform tilde expansion for tildes at the start of words.
tilde_expansion_at_word_start: bool,
#[builder(default = false)]
/// Whether or not to perform tilde expansion for tildes after colons.
tilde_expansion_after_colon: bool,
#[builder(default)]
/// Select the parser internal implementation
parser_impl: ParserImpl,
) -> Self {
let options = ParserOptions {
enable_extended_globbing,
posix_mode,
sh_mode,
tilde_expansion_at_word_start,
tilde_expansion_after_colon,
parser_impl,
};
Self { reader, options }
}
/// Parses the input into an abstract syntax tree (AST) of a shell program.
pub fn parse_program(&mut self) -> Result<ast::Program, crate::error::ParseError> {
//
// References:
// * https://www.gnu.org/software/bash/manual/bash.html#Shell-Syntax
// * https://mywiki.wooledge.org/BashParser
// * https://aosabook.org/en/v1/bash.html
// * https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
//
match self.options.parser_impl {
ParserImpl::Peg => {
let tokens = self.tokenize()?;
parse_tokens(&tokens, &self.options)
}
#[cfg(feature = "winnow-parser")]
ParserImpl::Winnow => {
// Read entire input to string for winnow_str parser
let mut input_str = String::new();
std::io::Read::read_to_string(&mut self.reader, &mut input_str).map_err(|e| {
crate::error::ParseError::Tokenizing {
inner: crate::tokenizer::TokenizerError::from(e),
position: None,
}
})?;
winnow_str::parse_program(&input_str, &self.options, &SourceInfo::default())
.map_err(|_e| {
// Convert winnow error to ParseError
// TODO: Extract position information from winnow error
crate::error::ParseError::ParsingAtEndOfInput
})
}
}
}
/// Parses a function definition body from the input. The body is expected to be
/// preceded by "()", but no function name.
pub fn parse_function_parens_and_body(
&mut self,
) -> Result<ast::FunctionBody, crate::error::ParseError> {
let tokens = self.tokenize()?;
let parse_result =
peg::token_parser::function_parens_and_body(&Tokens { tokens: &tokens }, &self.options);
parse_result_to_error(parse_result, &tokens)
}
fn tokenize(&mut self) -> Result<Vec<Token>, crate::error::ParseError> {
// First we tokenize the input, according to the policy implied by provided options.
let mut tokenizer = Tokenizer::new(&mut self.reader, &self.options.tokenizer_options());
tracing::debug!(target: "tokenize", "Tokenizing...");
let mut tokens = vec![];
loop {
let result = match tokenizer.next_token() {
Ok(result) => result,
Err(e) => {
return Err(crate::error::ParseError::Tokenizing {
inner: e,
position: tokenizer.current_location(),
});
}
};
let reason = result.reason;
if let Some(token) = result.token {
tracing::debug!(target: "tokenize", "TOKEN {}: {:?} {reason:?}", tokens.len(), token);
tokens.push(token);
}
if matches!(reason, TokenEndReason::EndOfInput) {
break;
}
}
tracing::debug!(target: "tokenize", " => {} token(s)", tokens.len());
Ok(tokens)
}
}
/// Parses a sequence of tokens into the abstract syntax tree (AST) of a shell program.
///
/// # Arguments
///
/// * `tokens` - The tokens to parse.
/// * `options` - The options to use when parsing.
pub fn parse_tokens(
tokens: &[Token],
options: &ParserOptions,
) -> Result<ast::Program, crate::error::ParseError> {
let parse_result = peg::token_parser::program(&Tokens { tokens }, options);
parse_result_to_error(parse_result, tokens)
}
fn parse_result_to_error<R>(
parse_result: Result<R, ::peg::error::ParseError<usize>>,
tokens: &[Token],
) -> Result<R, crate::error::ParseError>
where
R: std::fmt::Debug,
{
match parse_result {
Ok(program) => {
tracing::debug!(target: "parse", "PROG: {:?}", program);
Ok(program)
}
Err(parse_error) => {
tracing::debug!(target: "parse", "Parse error: {:?}", parse_error);
Err(crate::error::convert_peg_parse_error(&parse_error, tokens))
}
}
}
#[cfg(test)]
mod tests;
+817
View File
@@ -0,0 +1,817 @@
//! PEG-based parser implementation for shell scripts.
use crate::SourceSpan;
use crate::ast::{self, SeparatorOperator, SourceLocation, maybe_location};
use crate::tokenizer::Token;
use crate::word;
use super::{ParserOptions, Tokens};
peg::parser! {
pub grammar token_parser<'a>(parser_options: &ParserOptions) for Tokens<'a> {
pub(crate) rule program() -> ast::Program =
linebreak() c:complete_commands() linebreak() { ast::Program { complete_commands: c } } /
linebreak() { ast::Program { complete_commands: vec![] } }
rule complete_commands() -> Vec<ast::CompleteCommand> =
c:complete_command() ++ newline_list()
rule complete_command() -> ast::CompleteCommand =
first:and_or() remainder:(s:separator_op() l:and_or() { (s, l) })* last_sep:separator_op()? {
let mut and_ors = vec![first];
let mut seps = vec![];
for (sep, ao) in remainder {
seps.push(sep);
and_ors.push(ao);
}
// N.B. We default to synchronous if no separator op is given.
seps.push(last_sep.unwrap_or(SeparatorOperator::Sequence));
let mut items = vec![];
for (i, ao) in and_ors.into_iter().enumerate() {
items.push(ast::CompoundListItem(ao, seps[i].clone()));
}
ast::CompoundList(items)
}
rule and_or() -> ast::AndOrList =
first:pipeline() additional:_and_or_item()* { ast::AndOrList { first, additional } }
rule _and_or_item() -> ast::AndOr =
op:_and_or_op() linebreak() p:pipeline() { op(p) }
rule _and_or_op() -> fn(ast::Pipeline) -> ast::AndOr =
specific_operator("&&") { ast::AndOr::And } /
specific_operator("||") { ast::AndOr::Or }
rule pipeline() -> ast::Pipeline =
timed:pipeline_timed()? bang:bang()* seq:pipe_sequence() {?
if timed.is_none() && bang.is_empty() && seq.is_empty() {
Err("empty pipeline")
} else {
let invert = bang.len() % 2 == 1;
Ok(ast::Pipeline { timed, bang: invert, seq })
}
}
rule pipeline_timed() -> ast::PipelineTimed =
non_posix_extensions_enabled() s:specific_word("time") posix_output:specific_word("-p")? {
let start = s.location();
if let Some(end) = posix_output {
ast::PipelineTimed::TimedWithPosixOutput(SourceSpan::within(start, end.location()))
} else {
ast::PipelineTimed::Timed(start.to_owned())
}
}
rule bang() -> bool = specific_word("!") { true }
pub(crate) rule pipe_sequence() -> Vec<ast::Command> =
c:(c:command() r:&pipe_extension_redirection()? {? // check for `|&` without consuming the stream.
let mut c = c;
if r.is_some() {
add_pipe_extension_redirection(&mut c)?;
}
Ok(c)
}) ** (pipe_operator() linebreak()) {
c
}
rule pipe_operator() =
specific_operator("|") /
pipe_extension_redirection()
rule pipe_extension_redirection() -> &'input Token =
non_posix_extensions_enabled() p:specific_operator("|&") { p }
// N.B. We needed to move the function definition branch up to avoid conflicts with array assignment syntax.
rule command() -> ast::Command =
f:function_definition() { ast::Command::Function(f) } /
c:simple_command() { ast::Command::Simple(c) } /
c:compound_command() r:redirect_list()? { ast::Command::Compound(c, r) } /
// N.B. Extended test commands are bash extensions.
non_posix_extensions_enabled() c:extended_test_command() r:redirect_list()? { ast::Command::ExtendedTest(c, r) } /
expected!("command")
// N.B. The arithmetic command is a non-sh extension.
// N.B. The arithmetic for clause command is a non-sh extension.
pub(crate) rule compound_command() -> ast::CompoundCommand =
non_posix_extensions_enabled() a:arithmetic_command() { ast::CompoundCommand::Arithmetic(a) } /
non_posix_extensions_enabled() c:coproc_clause() { ast::CompoundCommand::Coprocess(c) } /
b:brace_group() { ast::CompoundCommand::BraceGroup(b) } /
s:subshell() { ast::CompoundCommand::Subshell(s) } /
f:for_clause() { ast::CompoundCommand::ForClause(f) } /
c:case_clause() { ast::CompoundCommand::CaseClause(c) } /
i:if_clause() { ast::CompoundCommand::IfClause(i) } /
w:while_clause() { ast::CompoundCommand::WhileClause(w) } /
u:until_clause() { ast::CompoundCommand::UntilClause(u) } /
non_posix_extensions_enabled() c:arithmetic_for_clause() { ast::CompoundCommand::ArithmeticForClause(c) } /
expected!("compound command")
pub(crate) rule arithmetic_command() -> ast::ArithmeticCommand =
start:specific_operator("(") specific_operator("(") expr:arithmetic_expression() specific_operator(")") end:specific_operator(")") {
let loc = SourceSpan::within(
start.location(),
end.location()
);
ast::ArithmeticCommand { expr, loc }
}
pub(crate) rule arithmetic_expression() -> ast::UnexpandedArithmeticExpr =
raw_expr:$(arithmetic_expression_piece()*) { ast::UnexpandedArithmeticExpr { value: raw_expr } }
rule arithmetic_expression_piece() =
// Allow a parenthesized expression (with matching opening and closing parens).
specific_operator("(") (!specific_operator(")") arithmetic_expression_piece())* specific_operator(")") {} /
// Otherwise consume any token that's neither the normal end of the entire arithmetic expression, nor an
// unexpected mismatched closing parenthesis. In the latter case, it may be that this really was never an
// arithmetic expression in the first place and we need to backtrack and instead try parsing as a subshell
// command instead.
!arithmetic_end() !specific_operator(")") [_] {}
// TODO(arithmetic): evaluate arithmetic end; the semicolon is used in arithmetic for loops.
rule arithmetic_end() -> () =
specific_operator(")") specific_operator(")") {} /
specific_operator(";") {}
rule subshell() -> ast::SubshellCommand =
start:specific_operator("(") list:compound_list() end:specific_operator(")") {
let loc = SourceSpan::within(start.location(), end.location());
ast::SubshellCommand { list, loc }
}
rule compound_list() -> ast::CompoundList =
linebreak() first:and_or() remainder:(s:separator() l:and_or() { (s, l) })* last_sep:separator()? {
let mut and_ors = vec![first];
let mut seps = vec![];
for (sep, ao) in remainder {
seps.push(sep.unwrap_or(SeparatorOperator::Sequence));
and_ors.push(ao);
}
// N.B. We default to synchronous if no separator op is given.
let last_sep = last_sep.unwrap_or(None);
seps.push(last_sep.unwrap_or(SeparatorOperator::Sequence));
let mut items = vec![];
for (i, ao) in and_ors.into_iter().enumerate() {
items.push(ast::CompoundListItem(ao, seps[i].clone()));
}
ast::CompoundList(items)
}
rule for_clause() -> ast::ForClauseCommand =
s:specific_word("for") n:name() linebreak() _in() w:wordlist()? sequential_sep() d:do_group() {
let start = s.location();
let end = &d.loc;
let loc = SourceSpan::within(start, end);
ast::ForClauseCommand { variable_name: n.to_owned(), values: w, body: d, loc }
} /
s:specific_word("for") n:name() sequential_sep()? d:do_group() {
let start = s.location();
let end = &d.loc;
let loc = SourceSpan::within(start, end);
ast::ForClauseCommand { variable_name: n.to_owned(), values: None, body: d, loc }
}
// N.B. The arithmetic for loop is a non-sh extension.
rule arithmetic_for_clause() -> ast::ArithmeticForClauseCommand =
s:specific_word("for")
specific_operator("(") specific_operator("(")
initializer:arithmetic_expression()? specific_operator(";")
condition:arithmetic_expression()? specific_operator(";")
updater:arithmetic_expression()?
specific_operator(")") specific_operator(")")
body:arithmetic_for_body() {
let start = s.location();
let end = &body.loc;
let loc = SourceSpan::within(start, end);
ast::ArithmeticForClauseCommand { initializer, condition, updater, body, loc }
}
rule arithmetic_for_body() -> ast::DoGroupCommand =
sequential_sep()? body:do_group() { body } /
body:brace_group() { ast::DoGroupCommand { list: body.list, loc: body.loc } }
rule extended_test_command() -> ast::ExtendedTestExprCommand =
s:specific_word("[[") linebreak() expr:extended_test_expression() linebreak() e:specific_word("]]") {
let start = s.location();
let end = e.location();
let loc = SourceSpan::within(start, end);
ast::ExtendedTestExprCommand { expr, loc }
}
rule extended_test_expression() -> ast::ExtendedTestExpr = precedence! {
left:(@) linebreak() specific_operator("||") linebreak() right:@ { ast::ExtendedTestExpr::Or(Box::from(left), Box::from(right)) }
--
left:(@) linebreak() specific_operator("&&") linebreak() right:@ { ast::ExtendedTestExpr::And(Box::from(left), Box::from(right)) }
--
specific_word("!") e:@ { ast::ExtendedTestExpr::Not(Box::from(e)) }
--
specific_operator("(") e:extended_test_expression() specific_operator(")") { ast::ExtendedTestExpr::Parenthesized(Box::from(e)) }
--
// Arithmetic operators
left:word() specific_word("-eq") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::ArithmeticEqualTo, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-ne") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::ArithmeticNotEqualTo, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-lt") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::ArithmeticLessThan, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-le") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::ArithmeticLessThanOrEqualTo, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-gt") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::ArithmeticGreaterThan, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-ge") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::ArithmeticGreaterThanOrEqualTo, ast::Word::from(left), ast::Word::from(right)) }
// Non-arithmetic binary operators
left:word() specific_word("-ef") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::FilesReferToSameDeviceAndInodeNumbers, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-nt") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::LeftFileIsNewerOrExistsWhenRightDoesNot, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("-ot") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes, ast::Word::from(left), ast::Word::from(right)) }
left:word() (specific_word("==") / specific_word("=")) right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::StringExactlyMatchesPattern, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("!=") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::StringDoesNotExactlyMatchPattern, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_word("=~") right:regex_word() {
if right.value.starts_with(['\'', '\"']) {
// TODO(test): Confirm it ends with that too?
ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::StringContainsSubstring, ast::Word::from(left), right)
} else {
ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::StringMatchesRegex, ast::Word::from(left), right)
}
}
left:word() specific_operator("<") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::LeftSortsBeforeRight, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_operator(">") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::LeftSortsAfterRight, ast::Word::from(left), ast::Word::from(right)) }
--
p:extended_unary_predicate() f:word() { ast::ExtendedTestExpr::UnaryTest(p, ast::Word::from(f)) }
--
w:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::StringHasNonZeroLength, ast::Word::from(w)) }
}
rule extended_unary_predicate() -> ast::UnaryPredicate =
specific_word("-a") { ast::UnaryPredicate::FileExists } /
specific_word("-b") { ast::UnaryPredicate::FileExistsAndIsBlockSpecialFile } /
specific_word("-c") { ast::UnaryPredicate::FileExistsAndIsCharSpecialFile } /
specific_word("-d") { ast::UnaryPredicate::FileExistsAndIsDir } /
specific_word("-e") { ast::UnaryPredicate::FileExists } /
specific_word("-f") { ast::UnaryPredicate::FileExistsAndIsRegularFile } /
specific_word("-g") { ast::UnaryPredicate::FileExistsAndIsSetgid } /
specific_word("-h") { ast::UnaryPredicate::FileExistsAndIsSymlink } /
specific_word("-k") { ast::UnaryPredicate::FileExistsAndHasStickyBit } /
specific_word("-n") { ast::UnaryPredicate::StringHasNonZeroLength } /
specific_word("-o") { ast::UnaryPredicate::ShellOptionEnabled } /
specific_word("-p") { ast::UnaryPredicate::FileExistsAndIsFifo } /
specific_word("-r") { ast::UnaryPredicate::FileExistsAndIsReadable } /
specific_word("-s") { ast::UnaryPredicate::FileExistsAndIsNotZeroLength } /
specific_word("-t") { ast::UnaryPredicate::FdIsOpenTerminal } /
specific_word("-u") { ast::UnaryPredicate::FileExistsAndIsSetuid } /
specific_word("-v") { ast::UnaryPredicate::ShellVariableIsSetAndAssigned } /
specific_word("-w") { ast::UnaryPredicate::FileExistsAndIsWritable } /
specific_word("-x") { ast::UnaryPredicate::FileExistsAndIsExecutable } /
specific_word("-z") { ast::UnaryPredicate::StringHasZeroLength } /
specific_word("-G") { ast::UnaryPredicate::FileExistsAndOwnedByEffectiveGroupId } /
specific_word("-L") { ast::UnaryPredicate::FileExistsAndIsSymlink } /
specific_word("-N") { ast::UnaryPredicate::FileExistsAndModifiedSinceLastRead } /
specific_word("-O") { ast::UnaryPredicate::FileExistsAndOwnedByEffectiveUserId } /
specific_word("-R") { ast::UnaryPredicate::ShellVariableIsSetAndNameRef } /
specific_word("-S") { ast::UnaryPredicate::FileExistsAndIsSocket }
// N.B. For some reason we seem to need to allow a select subset
// of unescaped operators in regex words.
rule regex_word() -> ast::Word =
value:$((!specific_word("]]") regex_word_piece())+) {
ast::Word::from(value)
}
rule regex_word_piece() =
word() {} /
specific_operator("|") {} /
specific_operator("(") parenthesized_regex_word()* specific_operator(")") {}
rule parenthesized_regex_word() =
regex_word_piece() /
!specific_operator(")") !specific_operator("]]") [_]
rule name() -> &'input str =
w:[Token::Word(_, _)] { w.to_str() }
rule _in() -> () =
specific_word("in") { }
rule wordlist() -> Vec<ast::Word> =
(w:word() { ast::Word::from(w) })+
pub(crate) rule case_clause() -> ast::CaseClauseCommand =
start:specific_word("case") w:word() linebreak() _in() linebreak() first_items:case_item()* last_item:case_item_ns()? end:specific_word("esac") {
let mut cases = first_items;
if let Some(last_item) = last_item {
cases.push(last_item);
}
let loc = SourceSpan::within(start.location(), end.location());
ast::CaseClauseCommand { value: ast::Word::from(w), cases, loc }
}
pub(crate) rule case_item_ns() -> ast::CaseItem =
s:specific_operator("(")? p:pattern() specific_operator(")") c:compound_list() {
let start = s.map(Token::location).or_else(|| p.first().and_then(|w| w.loc.as_ref()));
let end = c.location();
let loc = maybe_location(start, end.as_ref());
ast::CaseItem { patterns: p, cmd: Some(c), post_action: ast::CaseItemPostAction::ExitCase, loc }
} /
s:specific_operator("(")? p:pattern() e:specific_operator(")") linebreak() {
let start = s.map(Token::location).or_else(|| p.first().and_then(|w| w.loc.as_ref()));
let end = Some(e.location());
let loc = maybe_location(start, end);
ast::CaseItem { patterns: p, cmd: None, post_action: ast::CaseItemPostAction::ExitCase, loc }
}
pub(crate) rule case_item() -> ast::CaseItem =
s:specific_operator("(")? p:pattern() specific_operator(")") linebreak() post_action:case_item_post_action() linebreak() {
let start = s.map(Token::location).or_else(|| p.first().and_then(|w| w.loc.as_ref()));
let end = Some(post_action.1);
let loc = maybe_location(start, end);
ast::CaseItem { patterns: p, cmd: None, post_action: post_action.0, loc }
} /
s:specific_operator("(")? p:pattern() specific_operator(")") c:compound_list() post_action:case_item_post_action() linebreak() {
let start = s.map(Token::location).or_else(|| p.first().and_then(|w| w.loc.as_ref()));
let end = Some(post_action.1);
let loc = maybe_location(start, end);
ast::CaseItem { patterns: p, cmd: Some(c), post_action: post_action.0, loc }
}
rule case_item_post_action() -> (ast::CaseItemPostAction, &'input SourceSpan) =
s:specific_operator(";;") {
(ast::CaseItemPostAction::ExitCase, s.location())
} /
non_posix_extensions_enabled() s:specific_operator(";;&") {
(ast::CaseItemPostAction::ContinueEvaluatingCases, s.location())
} /
non_posix_extensions_enabled() s:specific_operator(";&") {
(ast::CaseItemPostAction::UnconditionallyExecuteNextCaseItem, s.location())
}
rule pattern() -> Vec<ast::Word> =
(w:word() { ast::Word::from(w) }) ++ specific_operator("|")
rule if_clause() -> ast::IfClauseCommand =
s:specific_word("if") condition:compound_list() specific_word("then") then:compound_list() elses:else_part()? e:specific_word("fi") {
let start = s.location();
let end = s.location();
let loc = SourceSpan::within(start, end);
ast::IfClauseCommand {
condition,
then,
elses,
loc
}
}
rule else_part() -> Vec<ast::ElseClause> =
cs:_conditional_else_part()+ u:_unconditional_else_part()? {
let mut parts = vec![];
for c in cs {
parts.push(c);
}
if let Some(uncond) = u {
parts.push(uncond);
}
parts
} /
e:_unconditional_else_part() { vec![e] }
rule _conditional_else_part() -> ast::ElseClause =
specific_word("elif") condition:compound_list() specific_word("then") body:compound_list() {
ast::ElseClause { condition: Some(condition), body }
}
rule _unconditional_else_part() -> ast::ElseClause =
specific_word("else") body:compound_list() {
ast::ElseClause { condition: None, body }
}
rule while_clause() -> ast::WhileOrUntilClauseCommand =
s:specific_word("while") c:compound_list() d:do_group() {
let start = s.location();
let end = &d.loc;
let loc = SourceSpan::within(start, end);
ast::WhileOrUntilClauseCommand(c, d, loc)
}
rule until_clause() -> ast::WhileOrUntilClauseCommand =
s:specific_word("until") c:compound_list() d:do_group() {
let start = s.location();
let end = &d.loc;
let loc = SourceSpan::within(start, end);
ast::WhileOrUntilClauseCommand(c, d, loc)
}
// N.B. Coproc is a bash extension.
rule coproc_clause() -> ast::CoprocessCommand =
s:specific_word("coproc") linebreak() name:coproc_name()? body:command() {
let start = s.location();
let loc = body.location().unwrap_or_else(|| start.clone());
ast::CoprocessCommand { name, body: Box::new(body), loc: SourceSpan::within(start, &loc) }
}
// N.B. The name must be followed by a compound command start ({ or ()
rule coproc_name() -> ast::Word =
w:fname() linebreak() &(specific_word("{") / specific_operator("(")) {
w
}
// N.B. Non-sh extensions allows use of the 'function' word to indicate a function definition.
// N.B. Without the 'function' keyword, reserved words cannot be used as function names
// (bash rejects e.g. `for (){ :; }`). With the 'function' keyword, reserved words are
// allowed as function names (bash accepts e.g. `function for { :; }`).
rule function_definition() -> ast::FunctionDefinition =
specific_word("function") fname:fname() body:function_parens_and_body() {
ast::FunctionDefinition { fname, body }
} /
fname:non_reserved_fname() body:function_parens_and_body() {
ast::FunctionDefinition { fname, body }
} /
specific_word("function") fname:fname() linebreak() body:function_body() {
ast::FunctionDefinition { fname, body }
} /
expected!("function definition")
pub(crate) rule function_parens_and_body() -> ast::FunctionBody =
specific_operator("(") specific_operator(")") linebreak() body:function_body() { body }
rule function_body() -> ast::FunctionBody =
c:compound_command() r:redirect_list()? { ast::FunctionBody(c, r) }
rule fname() -> ast::Word =
// Special-case: don't allow it to end with an equals sign, to avoid the challenge of
// misinterpreting certain declaration assignments as function definitions.
// TODO(parser): Find a way to make this still work without requiring this targeted exception.
w:[Token::Word(word, l) if !word.ends_with('=')] { ast::Word::with_location(word, l) }
rule non_reserved_fname() -> ast::Word =
!reserved_word() w:fname() { w }
rule brace_group() -> ast::BraceGroupCommand =
start:specific_word("{") list:compound_list() end:specific_word("}") {
let loc = SourceSpan::within(start.location(), end.location());
ast::BraceGroupCommand { list, loc }
}
rule do_group() -> ast::DoGroupCommand =
start:specific_word("do") list:compound_list() end:specific_word("done") {
let loc = SourceSpan::within(start.location(), end.location());
ast::DoGroupCommand { list, loc }
}
rule simple_command() -> ast::SimpleCommand =
prefix:cmd_prefix() word_and_suffix:(word_or_name:cmd_word() suffix:cmd_suffix()? { (word_or_name, suffix) })? {
match word_and_suffix {
Some((word_or_name, suffix)) => {
ast::SimpleCommand { prefix: Some(prefix), word_or_name: Some(ast::Word::from(word_or_name)), suffix }
}
None => {
ast::SimpleCommand { prefix: Some(prefix), word_or_name: None, suffix: None }
}
}
} /
word_or_name:cmd_name() suffix:cmd_suffix()? {
ast::SimpleCommand { prefix: None, word_or_name: Some(ast::Word::from(word_or_name)), suffix } } /
expected!("simple command")
rule cmd_name() -> &'input Token =
non_reserved_word()
rule cmd_word() -> &'input Token =
!assignment_word() w:non_reserved_word() { w }
rule cmd_prefix() -> ast::CommandPrefix =
p:(
i:io_redirect() { ast::CommandPrefixOrSuffixItem::IoRedirect(i) } /
assignment_and_word:assignment_word() {
let (assignment, word) = assignment_and_word;
ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, word)
}
)+ { ast::CommandPrefix(p) }
rule cmd_suffix() -> ast::CommandSuffix =
s:(
non_posix_extensions_enabled() sub:process_substitution() {
let (kind, subshell) = sub;
ast::CommandPrefixOrSuffixItem::ProcessSubstitution(kind, subshell)
} /
i:io_redirect() {
ast::CommandPrefixOrSuffixItem::IoRedirect(i)
} /
assignment_and_word:assignment_word() {
let (assignment, word) = assignment_and_word;
ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, word)
} /
w:word() {
ast::CommandPrefixOrSuffixItem::Word(ast::Word::from(w))
}
)+ { ast::CommandSuffix(s) }
rule redirect_list() -> ast::RedirectList =
r:io_redirect()+ { ast::RedirectList(r) } /
expected!("redirect list")
// N.B. here strings are extensions to the POSIX standard.
rule io_redirect() -> ast::IoRedirect =
n:io_number()? f:io_file() {
let (kind, target) = f;
ast::IoRedirect::File(n, kind, target)
} /
non_posix_extensions_enabled() specific_operator("&>>") target:filename() { ast::IoRedirect::OutputAndError(ast::Word::from(target), true) } /
non_posix_extensions_enabled() specific_operator("&>") target:filename() { ast::IoRedirect::OutputAndError(ast::Word::from(target), false) } /
non_posix_extensions_enabled() n:io_number()? specific_operator("<<<") w:word() { ast::IoRedirect::HereString(n, ast::Word::from(w)) } /
n:io_number()? h:io_here() { ast::IoRedirect::HereDocument(n, h) } /
expected!("I/O redirect")
// N.B. Process substitution forms are extensions to the POSIX standard.
rule io_file() -> (ast::IoFileRedirectKind, ast::IoFileRedirectTarget) =
specific_operator("<") f:io_filename() { (ast::IoFileRedirectKind::Read, f) } /
specific_operator("<&") f:io_fd_duplication_source() { (ast::IoFileRedirectKind::DuplicateInput, f) } /
specific_operator(">") f:io_filename() { (ast::IoFileRedirectKind::Write, f) } /
specific_operator(">&") f:io_fd_duplication_source() { (ast::IoFileRedirectKind::DuplicateOutput, f) } /
specific_operator(">>") f:io_filename() { (ast::IoFileRedirectKind::Append, f) } /
specific_operator("<>") f:io_filename() { (ast::IoFileRedirectKind::ReadAndWrite, f) } /
specific_operator(">|") f:io_filename() { (ast::IoFileRedirectKind::Clobber, f) }
rule io_fd_duplication_source() -> ast::IoFileRedirectTarget =
w:word() { ast::IoFileRedirectTarget::Duplicate(ast::Word::from(w)) }
rule io_fd() -> u32 =
w:[Token::Word(_, _)] {? w.to_str().parse().or(Err("io_fd u32")) }
rule io_filename() -> ast::IoFileRedirectTarget =
non_posix_extensions_enabled() sub:process_substitution() {
let (kind, subshell) = sub;
ast::IoFileRedirectTarget::ProcessSubstitution(kind, subshell)
} /
f:filename() { ast::IoFileRedirectTarget::Filename(ast::Word::from(f)) }
rule filename() -> &'input Token =
word()
pub(crate) rule io_here() -> ast::IoHereDocument =
specific_operator("<<-") here_tag:here_tag() doc:[_] closing_tag:here_tag() {
let requires_expansion = !here_tag.to_str().contains(['\'', '"', '\\']);
ast::IoHereDocument {
remove_tabs: true,
requires_expansion,
here_end: ast::Word::from(here_tag),
doc: ast::Word::from(doc)
}
} /
specific_operator("<<") here_tag:here_tag() doc:[_] closing_tag:here_tag() {
let requires_expansion = !here_tag.to_str().contains(['\'', '"', '\\']);
ast::IoHereDocument {
remove_tabs: false,
requires_expansion,
here_end: ast::Word::from(here_tag),
doc: ast::Word::from(doc)
}
}
rule here_tag() -> &'input Token =
word()
rule process_substitution() -> (ast::ProcessSubstitutionKind, ast::SubshellCommand) =
specific_operator("<") s:subshell() { (ast::ProcessSubstitutionKind::Read, s) } /
specific_operator(">") s:subshell() { (ast::ProcessSubstitutionKind::Write, s) }
rule newline_list() -> () =
newline()+ {}
rule linebreak() -> () =
quiet! {
newline()* {}
}
rule separator_op() -> ast::SeparatorOperator =
specific_operator("&") { ast::SeparatorOperator::Async } /
specific_operator(";") { ast::SeparatorOperator::Sequence }
rule separator() -> Option<ast::SeparatorOperator> =
s:separator_op() linebreak() { Some(s) } /
newline_list() { None }
rule sequential_sep() -> () =
specific_operator(";") linebreak() /
newline_list()
//
// Token interpretation
//
rule non_reserved_word() -> &'input Token =
!reserved_word() w:word() { w }
rule word() -> &'input Token =
[Token::Word(_, _)]
rule reserved_word() -> &'input Token =
[Token::Word(w, _) if matches!(w.as_str(),
"!" |
"{" |
"}" |
"case" |
"do" |
"done" |
"elif" |
"else" |
"esac" |
"fi" |
"for" |
"if" |
"in" |
"then" |
"until" |
"while"
)] /
// N.B. bash also treats the following as reserved.
non_posix_extensions_enabled() token:non_posix_reserved_word_token() { token }
rule non_posix_reserved_word_token() -> &'input Token =
specific_word("[[") /
specific_word("]]") /
specific_word("function") /
specific_word("select") /
specific_word("coproc")
rule newline() -> () = quiet! {
specific_operator("\n") {}
}
pub(crate) rule assignment_word() -> (ast::Assignment, ast::Word) =
non_posix_extensions_enabled() [Token::Word(w, l)] specific_operator("(") elements:array_elements() end:specific_operator(")") {?
let mut parsed = word::parse_array_assignment(w.as_str(), elements.as_slice())?;
let mut all_as_word = w.to_owned();
all_as_word.push('(');
for (i, e) in elements.iter().enumerate() {
if i > 0 {
all_as_word.push(' ');
}
all_as_word.push_str(e);
}
all_as_word.push(')');
let loc = SourceSpan::within(l, end.location());
parsed.loc = loc.clone();
Ok((parsed, ast::Word::with_location(&all_as_word, &loc)))
} /
[Token::Word(w, l)] {?
let mut parsed = word::parse_assignment_word(w.as_str()).map_err(|_| "not assignment word")?;
parsed.loc = l.clone();
Ok((parsed, ast::Word::with_location(w, l)))
}
rule array_elements() -> Vec<&'input String> =
linebreak() e:array_element()* { e }
rule array_element() -> &'input String =
linebreak() [Token::Word(e, _)] linebreak() { e }
// N.B. An I/O number must be a string of only digits, and it must be
// followed by a '<' or '>' character (but not consume them). We also
// need to make sure that there was no space between the number and the
// redirection operator; unfortunately we don't have the space anymore
// but we can infer it by looking at the tokens' locations.
rule io_number() -> ast::IoFd =
[Token::Word(w, num_loc) if w.chars().all(|c: char| c.is_ascii_digit())]
&([Token::Operator(o, redir_loc) if
o.starts_with(['<', '>']) &&
locations_are_contiguous(num_loc, redir_loc)]) {
w.parse().unwrap()
}
//
// Helpers
//
rule specific_operator(expected: &str) -> &'input Token =
[Token::Operator(w, _) if w.as_str() == expected]
rule specific_word(expected: &str) -> &'input Token =
[Token::Word(w, _) if w.as_str() == expected]
rule non_posix_extensions_enabled() -> () =
&[_] {? if !parser_options.sh_mode { Ok(()) } else { Err("posix") } }
}
}
// add `2>&1` to the command if the pipeline is `|&`
fn add_pipe_extension_redirection(c: &mut ast::Command) -> Result<(), &'static str> {
fn add_to_redirect_list(l: &mut Option<ast::RedirectList>, r: ast::IoRedirect) {
if let Some(l) = l {
l.0.push(r);
} else {
let v = vec![r];
*l = Some(ast::RedirectList(v));
}
}
let r = ast::IoRedirect::File(
Some(2),
ast::IoFileRedirectKind::DuplicateOutput,
ast::IoFileRedirectTarget::Fd(1),
);
match c {
ast::Command::Simple(c) => {
let r = ast::CommandPrefixOrSuffixItem::IoRedirect(r);
if let Some(l) = &mut c.suffix {
l.0.push(r);
} else {
c.suffix = Some(ast::CommandSuffix(vec![r]));
}
}
ast::Command::Compound(_, l) => add_to_redirect_list(l, r),
ast::Command::Function(f) => add_to_redirect_list(&mut f.body.1, r),
ast::Command::ExtendedTest(..) => return Err("|& unimplemented for extended tests"),
}
Ok(())
}
#[inline]
fn locations_are_contiguous(loc_left: &crate::SourceSpan, loc_right: &crate::SourceSpan) -> bool {
loc_left.end.index == loc_right.start.index
}
impl peg::Parse for Tokens<'_> {
type PositionRepr = usize;
#[inline]
fn start(&self) -> usize {
0
}
#[inline]
fn is_eof(&self, p: usize) -> bool {
p >= self.tokens.len()
}
#[inline]
fn position_repr(&self, p: usize) -> Self::PositionRepr {
p
}
}
impl<'a> peg::ParseElem<'a> for Tokens<'a> {
type Element = &'a Token;
#[inline]
fn parse_elem(&'a self, pos: usize) -> peg::RuleResult<Self::Element> {
match self.tokens.get(pos) {
Some(c) => peg::RuleResult::Matched(pos + 1, c),
None => peg::RuleResult::Failed,
}
}
}
impl<'a> peg::ParseSlice<'a> for Tokens<'a> {
type Slice = String;
/// Reconstructs a source string from a slice of tokens.
///
/// Uses each token's source position to detect whether whitespace existed
/// between adjacent tokens in the original source, preserving it as a
/// single space. This matters for constructs like `[[ x =~ (a| *) ]]`
/// where the space inside the regex group is significant.
///
/// N.B. This relies on tokens having accurate, contiguous source
/// positions. All tokens produced by the normal tokenizer path satisfy
/// this. If positions are missing or non-monotonic (as can happen with
/// synthetic here-document end-tag tokens), the gap check evaluates
/// false and no space is inserted — a safe degradation to the previous
/// behavior, which also omitted spaces between non-word tokens.
fn parse_slice(&'a self, start: usize, end: usize) -> Self::Slice {
let mut result = String::new();
let mut prev_end_index: Option<usize> = None;
for token in &self.tokens[start..end] {
let loc = token.location();
if let Some(prev_end) = prev_end_index {
if loc.start.index > prev_end {
result.push(' ');
}
}
result.push_str(token.to_str());
prev_end_index = Some(loc.end.index);
}
result
}
}
@@ -0,0 +1,126 @@
---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "( : && ( (( 0 )) || : ) )",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Subshell(SubshellCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: ":",
loc: Some(SourceSpan(
start: SourcePosition(
index: 2,
line: 1,
column: 3,
),
end: SourcePosition(
index: 3,
line: 1,
column: 4,
),
)),
)),
)),
],
),
additional: [
And(Pipeline(
seq: [
Compound(Subshell(SubshellCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Arithmetic(ArithmeticCommand(
expr: UnexpandedArithmeticExpr(
value: "0",
),
loc: SourceSpan(
start: SourcePosition(
index: 9,
line: 1,
column: 10,
),
end: SourcePosition(
index: 16,
line: 1,
column: 17,
),
),
)), None),
],
),
additional: [
Or(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: ":",
loc: Some(SourceSpan(
start: SourcePosition(
index: 20,
line: 1,
column: 21,
),
end: SourcePosition(
index: 21,
line: 1,
column: 22,
),
)),
)),
)),
],
)),
],
), Sequence),
]),
loc: SourceSpan(
start: SourcePosition(
index: 7,
line: 1,
column: 8,
),
end: SourcePosition(
index: 23,
line: 1,
column: 24,
),
),
)), None),
],
)),
],
), Sequence),
]),
loc: SourceSpan(
start: SourcePosition(
index: 0,
line: 1,
column: 1,
),
end: SourcePosition(
index: 25,
line: 1,
column: 26,
),
),
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,124 @@
---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "\\\ncase x in\nx)\n echo y;;\nesac\\\n",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(CaseClause(CaseClauseCommand(
value: Word(
value: "x",
loc: Some(SourceSpan(
start: SourcePosition(
index: 7,
line: 2,
column: 6,
),
end: SourcePosition(
index: 8,
line: 2,
column: 7,
),
)),
),
cases: [
CaseItem(
patterns: [
Word(
value: "x",
loc: Some(SourceSpan(
start: SourcePosition(
index: 12,
line: 3,
column: 1,
),
end: SourcePosition(
index: 13,
line: 3,
column: 2,
),
)),
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 19,
line: 4,
column: 5,
),
end: SourcePosition(
index: 23,
line: 4,
column: 9,
),
)),
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "y",
loc: Some(SourceSpan(
start: SourcePosition(
index: 24,
line: 4,
column: 10,
),
end: SourcePosition(
index: 25,
line: 4,
column: 11,
),
)),
)),
])),
)),
],
),
), Sequence),
])),
post_action: ExitCase,
loc: Some(SourceSpan(
start: SourcePosition(
index: 12,
line: 3,
column: 1,
),
end: SourcePosition(
index: 27,
line: 4,
column: 13,
),
)),
),
],
loc: SourceSpan(
start: SourcePosition(
index: 0,
line: 1,
column: 1,
),
end: SourcePosition(
index: 34,
line: 6,
column: 1,
),
),
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,124 @@
---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "\\\ncase x in\nx)\n echo y\nesac\\\n",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(CaseClause(CaseClauseCommand(
value: Word(
value: "x",
loc: Some(SourceSpan(
start: SourcePosition(
index: 7,
line: 2,
column: 6,
),
end: SourcePosition(
index: 8,
line: 2,
column: 7,
),
)),
),
cases: [
CaseItem(
patterns: [
Word(
value: "x",
loc: Some(SourceSpan(
start: SourcePosition(
index: 12,
line: 3,
column: 1,
),
end: SourcePosition(
index: 13,
line: 3,
column: 2,
),
)),
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 19,
line: 4,
column: 5,
),
end: SourcePosition(
index: 23,
line: 4,
column: 9,
),
)),
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "y",
loc: Some(SourceSpan(
start: SourcePosition(
index: 24,
line: 4,
column: 10,
),
end: SourcePosition(
index: 25,
line: 4,
column: 11,
),
)),
)),
])),
)),
],
),
), Sequence),
])),
post_action: ExitCase,
loc: Some(SourceSpan(
start: SourcePosition(
index: 12,
line: 3,
column: 1,
),
end: SourcePosition(
index: 25,
line: 4,
column: 11,
),
)),
),
],
loc: SourceSpan(
start: SourcePosition(
index: 0,
line: 1,
column: 1,
),
end: SourcePosition(
index: 32,
line: 6,
column: 1,
),
),
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,111 @@
---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "foo() { echo 1; } |& cat",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Function(FunctionDefinition(
fname: Word(
value: "foo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 0,
line: 1,
column: 1,
),
end: SourcePosition(
index: 3,
line: 1,
column: 4,
),
)),
),
body: FunctionBody(BraceGroup(BraceGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 8,
line: 1,
column: 9,
),
end: SourcePosition(
index: 12,
line: 1,
column: 13,
),
)),
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "1",
loc: Some(SourceSpan(
start: SourcePosition(
index: 13,
line: 1,
column: 14,
),
end: SourcePosition(
index: 14,
line: 1,
column: 15,
),
)),
)),
])),
)),
],
),
), Sequence),
]),
loc: SourceSpan(
start: SourcePosition(
index: 6,
line: 1,
column: 7,
),
end: SourcePosition(
index: 17,
line: 1,
column: 18,
),
),
)), Some(RedirectList([
File(Some(2), DuplicateOutput, Fd(1)),
]))),
)),
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cat",
loc: Some(SourceSpan(
start: SourcePosition(
index: 21,
line: 1,
column: 22,
),
end: SourcePosition(
index: 24,
line: 1,
column: 25,
),
)),
)),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,125 @@
---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "foo() { echo 1; } 2>&1 | cat",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Function(FunctionDefinition(
fname: Word(
value: "foo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 0,
line: 1,
column: 1,
),
end: SourcePosition(
index: 3,
line: 1,
column: 4,
),
)),
),
body: FunctionBody(BraceGroup(BraceGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 8,
line: 1,
column: 9,
),
end: SourcePosition(
index: 12,
line: 1,
column: 13,
),
)),
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "1",
loc: Some(SourceSpan(
start: SourcePosition(
index: 13,
line: 1,
column: 14,
),
end: SourcePosition(
index: 14,
line: 1,
column: 15,
),
)),
)),
])),
)),
],
),
), Sequence),
]),
loc: SourceSpan(
start: SourcePosition(
index: 6,
line: 1,
column: 7,
),
end: SourcePosition(
index: 17,
line: 1,
column: 18,
),
),
)), Some(RedirectList([
File(Some(2), DuplicateOutput, Duplicate(Word(
value: "1",
loc: Some(SourceSpan(
start: SourcePosition(
index: 21,
line: 1,
column: 22,
),
end: SourcePosition(
index: 22,
line: 1,
column: 23,
),
)),
))),
]))),
)),
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cat",
loc: Some(SourceSpan(
start: SourcePosition(
index: 25,
line: 1,
column: 26,
),
end: SourcePosition(
index: 28,
line: 1,
column: 29,
),
)),
)),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,71 @@
---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "cat <<EOF\nSomething\nEOF",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cat",
loc: Some(SourceSpan(
start: SourcePosition(
index: 0,
line: 1,
column: 1,
),
end: SourcePosition(
index: 3,
line: 1,
column: 4,
),
)),
)),
suffix: Some(CommandSuffix([
IoRedirect(HereDocument(None, IoHereDocument(
requires_expansion: true,
here_end: Word(
value: "EOF",
loc: Some(SourceSpan(
start: SourcePosition(
index: 6,
line: 1,
column: 7,
),
end: SourcePosition(
index: 9,
line: 1,
column: 10,
),
)),
),
doc: Word(
value: "Something\n",
loc: Some(SourceSpan(
start: SourcePosition(
index: 10,
line: 2,
column: 1,
),
end: SourcePosition(
index: 23,
line: 3,
column: 4,
),
)),
),
))),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,152 @@
---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "\n\n#!/usr/bin/env bash\n\nfor f in A B C; do\n\n # sdfsdf\n echo \"${f@L}\" >&2\n\n done\n\n",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(ForClause(ForClauseCommand(
variable_name: "f",
values: Some([
Word(
value: "A",
loc: Some(SourceSpan(
start: SourcePosition(
index: 32,
line: 5,
column: 10,
),
end: SourcePosition(
index: 33,
line: 5,
column: 11,
),
)),
),
Word(
value: "B",
loc: Some(SourceSpan(
start: SourcePosition(
index: 34,
line: 5,
column: 12,
),
end: SourcePosition(
index: 35,
line: 5,
column: 13,
),
)),
),
Word(
value: "C",
loc: Some(SourceSpan(
start: SourcePosition(
index: 36,
line: 5,
column: 14,
),
end: SourcePosition(
index: 37,
line: 5,
column: 15,
),
)),
),
]),
body: DoGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 60,
line: 8,
column: 5,
),
end: SourcePosition(
index: 64,
line: 8,
column: 9,
),
)),
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "\"${f@L}\"",
loc: Some(SourceSpan(
start: SourcePosition(
index: 65,
line: 8,
column: 10,
),
end: SourcePosition(
index: 73,
line: 8,
column: 18,
),
)),
)),
IoRedirect(File(None, DuplicateOutput, Duplicate(Word(
value: "2",
loc: Some(SourceSpan(
start: SourcePosition(
index: 76,
line: 8,
column: 21,
),
end: SourcePosition(
index: 77,
line: 8,
column: 22,
),
)),
)))),
])),
)),
],
),
), Sequence),
]),
loc: SourceSpan(
start: SourcePosition(
index: 39,
line: 5,
column: 17,
),
end: SourcePosition(
index: 86,
line: 10,
column: 8,
),
),
),
loc: SourceSpan(
start: SourcePosition(
index: 23,
line: 5,
column: 1,
),
end: SourcePosition(
index: 86,
line: 10,
column: 8,
),
),
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,56 @@
---
source: brush-parser/src/parser/mod.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "echo |& wc",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: Some(SourceSpan(
start: SourcePosition(
index: 0,
line: 1,
column: 1,
),
end: SourcePosition(
index: 4,
line: 1,
column: 5,
),
)),
)),
suffix: Some(CommandSuffix([
IoRedirect(File(Some(2), DuplicateOutput, Fd(1))),
])),
)),
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "wc",
loc: Some(SourceSpan(
start: SourcePosition(
index: 8,
line: 1,
column: 9,
),
end: SourcePosition(
index: 10,
line: 1,
column: 11,
),
)),
)),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,82 @@
//! Tests for and/or list parsing.
use super::{ParseResult, test_with_snapshot};
use crate::assert_snapshot_redacted;
use anyhow::Result;
#[test]
fn parse_simple_and() -> Result<()> {
let input = "true && echo yes";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_simple_or() -> Result<()> {
let input = "false || echo no";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_chained_and() -> Result<()> {
let input = "cmd1 && cmd2 && cmd3";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_chained_or() -> Result<()> {
let input = "cmd1 || cmd2 || cmd3";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_mixed_and_or() -> Result<()> {
let input = "cmd1 && cmd2 || cmd3";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_and_or_with_pipes() -> Result<()> {
let input = "cmd1 | cmd2 && cmd3 | cmd4";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_and_or_in_sequence() -> Result<()> {
let input = "cmd1 && cmd2; cmd3 || cmd4";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
@@ -0,0 +1,248 @@
//! Tests for assignment parsing.
use super::{ParseResult, test_with_snapshot};
use crate::assert_snapshot_redacted;
use anyhow::Result;
// Scalar assignments
#[test]
fn parse_assignment_simple() -> Result<()> {
let input = "x=value";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_assignment_empty() -> Result<()> {
let input = "x=";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_assignment_quoted() -> Result<()> {
let input = r#"x="hello world""#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_assignment_single_quoted() -> Result<()> {
let input = "x='hello world'";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_assignment_with_expansion() -> Result<()> {
let input = "x=$HOME/bin";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_assignment_with_command_substitution() -> Result<()> {
let input = "x=$(pwd)";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Append assignments
#[test]
fn parse_assignment_append() -> Result<()> {
let input = "x+=more";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_assignment_append_quoted() -> Result<()> {
let input = r#"x+=" more text""#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Array assignments
#[test]
fn parse_assignment_array() -> Result<()> {
let input = "arr=(a b c)";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_assignment_array_empty() -> Result<()> {
let input = "arr=()";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_assignment_array_with_indices() -> Result<()> {
let input = "arr=([0]=a [1]=b [2]=c)";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_assignment_array_mixed() -> Result<()> {
let input = "arr=(a [5]=b c)";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_assignment_array_quoted_elements() -> Result<()> {
let input = r#"arr=("hello world" "foo bar")"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Array element assignment
#[test]
fn parse_assignment_array_element() -> Result<()> {
let input = "arr[0]=value";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_assignment_array_element_expression() -> Result<()> {
let input = "arr[i+1]=value";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Multiple assignments
#[test]
fn parse_multiple_assignments() -> Result<()> {
let input = "x=1 y=2 z=3";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_assignment_with_command() -> Result<()> {
let input = "VAR=value command arg1 arg2";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_multiple_assignments_with_command() -> Result<()> {
let input = "A=1 B=2 command";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Export/local with assignment
#[test]
fn parse_export_assignment() -> Result<()> {
let input = "export VAR=value";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_local_assignment() -> Result<()> {
let input = "local x=5";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_declare_assignment() -> Result<()> {
let input = "declare -i x=5";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
+255
View File
@@ -0,0 +1,255 @@
//! Complex and integration tests that combine multiple parser features.
use super::{ParseResult, test_with_snapshot};
use crate::assert_snapshot_redacted;
use anyhow::Result;
#[test]
fn parse_shebang_and_program() -> Result<()> {
let input = r#"#!/usr/bin/env bash
for f in A B C; do
# sdfsdf
echo "${f@L}" >&2
done
"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_case_with_newlines() -> Result<()> {
let input = r"case x in
x)
echo y;;
esac";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_case_no_semicolon() -> Result<()> {
let input = r"case x in
x)
echo y
esac";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_nested_if_while() -> Result<()> {
let input = r#"if true; then
while read line; do
echo "$line"
done < file.txt
fi"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_function_with_if() -> Result<()> {
let input = r#"myfunc() {
if [[ -z "$1" ]]; then
echo "No argument"
return 1
fi
echo "Got: $1"
}"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_pipeline_with_redirections() -> Result<()> {
let input = "cat < input.txt | grep pattern | tee output.txt > /dev/null 2>&1";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_command_substitution_nested() -> Result<()> {
let input = "echo \"$(echo $(pwd))\"";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_for_with_command_substitution() -> Result<()> {
let input = "for f in $(ls *.txt); do cat \"$f\"; done";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_arithmetic_in_condition() -> Result<()> {
let input = "if (( x > 5 )); then echo big; else echo small; fi";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_brace_expansion_context() -> Result<()> {
let input = "echo {a,b,c}";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
#[allow(clippy::literal_string_with_formatting_args)]
fn parse_parameter_expansion_complex() -> Result<()> {
let input = r#"echo "${var:-default}" "${var:+alt}" "${var:=assign}""#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_subshell_with_assignments() -> Result<()> {
let input = "( x=1; y=2; echo $((x + y)) )";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_coprocess() -> Result<()> {
let input = "coproc cat";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_multiple_here_docs() -> Result<()> {
let input = r"cmd <<EOF1 <<EOF2
first
EOF1
second
EOF2
";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_backgrounded_commands() -> Result<()> {
let input = "cmd1 & cmd2 & cmd3";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_mixed_sequences() -> Result<()> {
let input = "cmd1; cmd2 && cmd3 || cmd4; cmd5";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_complex_array_operations() -> Result<()> {
let input = r#"arr=($(seq 1 10)); echo "${arr[@]}"; echo "${#arr[@]}""#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_glob_patterns() -> Result<()> {
let input = "ls *.txt **/foo.* file?.txt";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extglob_patterns() -> Result<()> {
let input = "ls !(*.txt) +(foo|bar) ?(a|b)";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_tilde_expansion() -> Result<()> {
let input = "cd ~/projects; ls ~user/home";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
@@ -0,0 +1,317 @@
//! Tests for compound command parsing.
use super::{ParseResult, test_with_snapshot};
use crate::assert_snapshot_redacted;
use anyhow::Result;
// Arithmetic commands
#[test]
fn parse_arithmetic_simple() -> Result<()> {
let input = "(( 1 + 2 ))";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_arithmetic_increment() -> Result<()> {
let input = "(( x++ ))";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_arithmetic_complex() -> Result<()> {
let input = "(( x = 5 + 3 * 2 ))";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Arithmetic for clause
#[test]
fn parse_arithmetic_for() -> Result<()> {
let input = "for (( i = 0; i < 10; i++ )); do echo $i; done";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_arithmetic_for_empty_parts() -> Result<()> {
let input = "for (( ; ; )); do echo loop; done";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Brace group
#[test]
fn parse_brace_group() -> Result<()> {
let input = "{ echo hello; }";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_brace_group_multiline() -> Result<()> {
let input = r"{
echo hello
echo world
}";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Subshell
#[test]
fn parse_subshell() -> Result<()> {
let input = "( echo hello )";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_subshell_multiple_commands() -> Result<()> {
let input = "( echo hello; echo world )";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_nested_subshell() -> Result<()> {
let input = "( ( echo nested ) )";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// For clause
#[test]
fn parse_for_in() -> Result<()> {
let input = "for x in a b c; do echo $x; done";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_for_in_multiline() -> Result<()> {
let input = r"for x in a b c
do
echo $x
done";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_for_no_in() -> Result<()> {
let input = "for x; do echo $x; done";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Case clause
#[test]
fn parse_case_simple() -> Result<()> {
let input = "case x in a) echo a;; esac";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_case_multiple_patterns() -> Result<()> {
let input = r"case x in
a|b) echo ab;;
c) echo c;;
*) echo default;;
esac";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_case_fallthrough() -> Result<()> {
let input = "case x in a) echo a;& b) echo b;; esac";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_case_continue() -> Result<()> {
let input = "case x in a) echo a;;& b) echo b;; esac";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// If clause
#[test]
fn parse_if_simple() -> Result<()> {
let input = "if true; then echo yes; fi";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_if_else() -> Result<()> {
let input = "if true; then echo yes; else echo no; fi";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_if_elif() -> Result<()> {
let input = "if false; then echo one; elif true; then echo two; else echo three; fi";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_if_multiline() -> Result<()> {
let input = r"if true
then
echo yes
else
echo no
fi";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// While/Until
#[test]
fn parse_while() -> Result<()> {
let input = "while true; do echo loop; done";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_until() -> Result<()> {
let input = "until false; do echo loop; done";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_while_multiline() -> Result<()> {
let input = r"while true
do
echo loop
break
done";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Mixed/nested
#[test]
fn parse_arith_and_non_arith_parens() -> Result<()> {
let input = "( : && ( (( 0 )) || : ) )";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
@@ -0,0 +1,268 @@
//! Tests for extended test expression [[ ]] parsing.
use super::{ParseResult, test_with_snapshot};
use crate::assert_snapshot_redacted;
use anyhow::Result;
// File tests
#[test]
fn parse_extended_test_file_exists() -> Result<()> {
let input = "[[ -f file ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_directory() -> Result<()> {
let input = "[[ -d /path/to/dir ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_readable() -> Result<()> {
let input = "[[ -r file ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_writable() -> Result<()> {
let input = "[[ -w file ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_executable() -> Result<()> {
let input = "[[ -x file ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// String tests
#[test]
fn parse_extended_test_string_zero_length() -> Result<()> {
let input = r#"[[ -z "$var" ]]"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_string_non_zero() -> Result<()> {
let input = r#"[[ -n "$var" ]]"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_string_equal() -> Result<()> {
let input = r#"[[ "$a" == "$b" ]]"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_string_not_equal() -> Result<()> {
let input = r#"[[ "$a" != "$b" ]]"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_string_pattern() -> Result<()> {
let input = r#"[[ "$str" == *pattern* ]]"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_regex() -> Result<()> {
let input = r#"[[ "$str" =~ ^[0-9]+$ ]]"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_regex_with_spaces() -> Result<()> {
let input = r#"[[ "x" =~ (a| *) ]]"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Logical operators
#[test]
fn parse_extended_test_and() -> Result<()> {
let input = "[[ -f file && -r file ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_or() -> Result<()> {
let input = "[[ -f file || -d file ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_not() -> Result<()> {
let input = "[[ ! -f file ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_parenthesized() -> Result<()> {
let input = "[[ ( -f file ) ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_complex() -> Result<()> {
let input = "[[ ( -f file && -r file ) || -d file ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Comparison operators
#[test]
fn parse_extended_test_less_than() -> Result<()> {
let input = r#"[[ "$a" < "$b" ]]"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_greater_than() -> Result<()> {
let input = r#"[[ "$a" > "$b" ]]"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Arithmetic comparison
#[test]
fn parse_extended_test_arith_equal() -> Result<()> {
let input = "[[ 5 -eq 5 ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_arith_not_equal() -> Result<()> {
let input = "[[ 5 -ne 3 ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_arith_less_than() -> Result<()> {
let input = "[[ 3 -lt 5 ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_extended_test_arith_greater_than() -> Result<()> {
let input = "[[ 5 -gt 3 ]]";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
@@ -0,0 +1,99 @@
//! Tests for function definition parsing.
use super::{ParseResult, test_with_snapshot};
use crate::assert_snapshot_redacted;
use anyhow::Result;
#[test]
fn parse_function_basic() -> Result<()> {
let input = "foo() { echo hello; }";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_function_keyword() -> Result<()> {
let input = "function foo { echo hello; }";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_function_keyword_with_parens() -> Result<()> {
let input = "function foo() { echo hello; }";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_function_with_redirect() -> Result<()> {
let input = "foo() { echo 1; } 2>&1 | cat";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_function_with_stderr_redirect() -> Result<()> {
let input = "foo() { echo 1; } |& cat";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_function_multiline() -> Result<()> {
let input = r"foo() {
echo hello
echo world
}";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_function_with_subshell_body() -> Result<()> {
let input = "foo() ( echo subshell )";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_function_with_local_vars() -> Result<()> {
let input = r"foo() {
local x=1
echo $x
}";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
+129
View File
@@ -0,0 +1,129 @@
//! Tests for here-document parsing.
use super::{ParseResult, test_with_snapshot};
use crate::assert_snapshot_redacted;
use anyhow::Result;
#[test]
fn parse_here_doc_basic() -> Result<()> {
let input = r"cat <<EOF
content line 1
content line 2
EOF
";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_here_doc_no_trailing_newline() -> Result<()> {
let input = r"cat <<EOF
Something
EOF";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_here_doc_tab_removal() -> Result<()> {
let input = "cat <<-EOF\n\tcontent with tab\nEOF\n";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_here_doc_quoted_delimiter() -> Result<()> {
let input = r"cat <<'EOF'
$variable should not expand
EOF
";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_here_doc_double_quoted_delimiter() -> Result<()> {
let input = r#"cat <<"EOF"
$variable should not expand
EOF
"#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_here_doc_with_expansion() -> Result<()> {
let input = r"cat <<EOF
Hello $USER
Your home is $HOME
EOF
";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_here_doc_empty() -> Result<()> {
let input = r"cat <<EOF
EOF
";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_here_doc_with_command_after() -> Result<()> {
let input = r"cat <<EOF | grep hello
hello world
goodbye world
EOF
";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_here_doc_with_fd() -> Result<()> {
let input = r"command 3<<EOF
content for fd 3
EOF
";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
+267
View File
@@ -0,0 +1,267 @@
//! Test harness for comparing Peg and Winnow parser implementations.
//!
//! This module provides utilities to test both parser implementations
//! and verify they produce equivalent results.
mod and_or_lists;
mod assignments;
mod complex;
mod compound_commands;
mod extended_test;
mod functions;
mod here_docs;
mod pipelines;
mod redirections;
mod simple_commands;
use crate::ast::Program;
use crate::error::ParseError;
use crate::parser::{Parser, ParserImpl, ParserOptions};
use anyhow::Result;
#[cfg(feature = "winnow-parser")]
use serde_json::Value;
/// Wrapper struct for serializing parse results with input context
#[derive(serde::Serialize)]
pub struct ParseResult<'a, T> {
pub input: &'a str,
pub result: &'a T,
}
/// Macro to assert snapshots with location information redacted.
/// This makes snapshots stable across parser changes that only affect source locations.
#[macro_export]
macro_rules! assert_snapshot_redacted {
($value:expr) => {{
let mut settings = insta::Settings::clone_current();
settings.add_redaction(".**.loc", "[location]");
settings.bind(|| {
insta::assert_ron_snapshot!($value);
});
}};
}
/// Recursively redact location fields from a JSON value.
/// This normalizes AST representations for comparison by removing source location info.
#[cfg(feature = "winnow-parser")]
fn redact_locations(value: &mut Value) {
match value {
Value::Object(map) => {
// Remove location-related fields
map.remove("loc");
// Also handle tuple structs that store SourceSpan as positional element
// These appear as arrays with SourceSpan objects
// Recursively process remaining fields
for (_, v) in map.iter_mut() {
// If this value is a SourceSpan object, normalize it
if is_source_span(v) {
normalize_source_span(v);
} else {
redact_locations(v);
}
}
}
Value::Array(arr) => {
// Check if this array looks like a tuple struct containing SourceSpan at the end
// SourceSpan has: start: SourcePosition, end: SourcePosition
// SourcePosition has: index, line, column
if let Some(last) = arr.last() {
if is_source_span(last) {
arr.pop();
}
}
for item in arr.iter_mut() {
redact_locations(item);
}
}
_ => {}
}
}
/// Check if a value looks like a `SourceSpan` object
#[cfg(feature = "winnow-parser")]
fn is_source_span(value: &Value) -> bool {
if let Value::Object(map) = value {
map.contains_key("start") && map.contains_key("end") && map.len() == 2
} else {
false
}
}
/// Normalize a `SourceSpan` object by replacing its positions with placeholder values.
/// This allows comparing ASTs without position differences.
#[cfg(feature = "winnow-parser")]
fn normalize_source_span(value: &mut Value) {
if let Value::Object(map) = value {
let placeholder_pos = serde_json::json!({
"index": 0,
"line": 0,
"column": 0
});
map.insert("start".to_string(), placeholder_pos.clone());
map.insert("end".to_string(), placeholder_pos);
}
}
/// Convert a Program to a normalized JSON value with locations redacted
#[cfg(feature = "winnow-parser")]
#[allow(clippy::expect_used)]
fn normalize_ast(program: &Program) -> Value {
let mut value = serde_json::to_value(program).expect("Failed to serialize Program to JSON");
redact_locations(&mut value);
value
}
/// A named parser configuration for test output clarity
#[derive(Debug, Clone)]
pub struct ParserConfig {
pub name: &'static str,
pub parser_impl: ParserImpl,
}
/// Returns all available parser implementations for testing.
/// - Without `winnow-parser`: returns only Peg
/// - With `winnow-parser`: returns both Peg and Winnow
pub fn parser_configs() -> Vec<ParserConfig> {
#[allow(unused_mut)]
let mut configs = vec![ParserConfig {
name: "peg",
parser_impl: ParserImpl::Peg,
}];
#[cfg(feature = "winnow-parser")]
configs.push(ParserConfig {
name: "winnow",
parser_impl: ParserImpl::Winnow,
});
configs
}
/// Helper to parse input with a specific parser configuration
pub fn parse_with_config(input: &str, config: &ParserConfig) -> Result<Program, ParseError> {
let options = ParserOptions {
parser_impl: config.parser_impl,
..Default::default()
};
let mut parser = Parser::new(std::io::Cursor::new(input), &options);
parser.parse_program()
}
/// Verify all parser implementations produce the same result.
///
/// This function parses the input with each available parser and verifies
/// they all produce structurally equivalent ASTs. Returns an error if
/// parsing fails or if the results differ.
#[cfg(feature = "winnow-parser")]
#[allow(dead_code)]
pub fn test_all_parsers_match(input: &str) -> Result<()> {
let configs = parser_configs();
// Parse with each configuration
let mut results: Vec<(&str, Program)> = Vec::new();
for config in &configs {
let result = parse_with_config(input, config).map_err(|e| {
anyhow::anyhow!(
"Parser '{}' failed to parse input: {}\nInput: {}",
config.name,
e,
input
)
})?;
results.push((config.name, result));
}
// Compare all results against the first (peg) implementation
// Normalize ASTs by redacting location info before comparison
if results.len() > 1 {
let (base_name, base_result) = &results[0];
let base_normalized = normalize_ast(base_result);
for (name, result) in results.iter().skip(1) {
let result_normalized = normalize_ast(result);
pretty_assertions::assert_eq!(
base_normalized,
result_normalized,
"Parser outputs differ between '{}' and '{}'.\nInput: {}",
base_name,
name,
input
);
}
}
Ok(())
}
/// Run a test and create snapshot for peg parser (canonical implementation).
///
/// This function parses the input with the peg parser and returns the result
/// for snapshot testing. When the winnow-parser feature is enabled, it also
/// verifies that winnow produces the same result.
pub fn test_with_snapshot(input: &str) -> Result<Program> {
// Always parse with peg (canonical implementation)
let peg_config = ParserConfig {
name: "peg",
parser_impl: ParserImpl::Peg,
};
let peg_result = parse_with_config(input, &peg_config)
.map_err(|e| anyhow::anyhow!("Peg parser failed: {e}\nInput: {input}"))?;
// When winnow is enabled, verify it matches (ignoring location differences)
#[cfg(feature = "winnow-parser")]
{
let winnow_config = ParserConfig {
name: "winnow",
parser_impl: ParserImpl::Winnow,
};
let winnow_result = parse_with_config(input, &winnow_config)
.map_err(|e| anyhow::anyhow!("Winnow parser failed: {e}\nInput: {input}"))?;
// Normalize both ASTs by redacting location info before comparison
let peg_normalized = normalize_ast(&peg_result);
let winnow_normalized = normalize_ast(&winnow_result);
pretty_assertions::assert_eq!(
peg_normalized,
winnow_normalized,
"Parser outputs differ between 'peg' and 'winnow'.\nInput: {}",
input
);
}
Ok(peg_result)
}
#[cfg(test)]
mod harness_tests {
use super::*;
#[test]
fn test_parser_configs_includes_peg() {
let configs = parser_configs();
assert!(!configs.is_empty());
assert_eq!(configs[0].name, "peg");
}
#[test]
#[cfg(feature = "winnow-parser")]
fn test_parser_configs_includes_winnow() {
let configs = parser_configs();
assert!(configs.len() >= 2);
assert!(configs.iter().any(|c| c.name == "winnow"));
}
#[test]
fn test_parse_with_config_basic() {
let config = ParserConfig {
name: "peg",
parser_impl: ParserImpl::Peg,
};
let result = parse_with_config("echo hello", &config);
assert!(result.is_ok());
}
}
@@ -0,0 +1,93 @@
//! Tests for pipeline parsing.
use super::{ParseResult, test_with_snapshot};
use crate::assert_snapshot_redacted;
use anyhow::Result;
#[test]
fn parse_simple_pipe() -> Result<()> {
let input = "echo hello | grep world";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_multi_stage_pipe() -> Result<()> {
let input = "cat file | grep pattern | wc -l";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_pipe_with_stderr() -> Result<()> {
let input = "echo |& wc";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_timed_pipeline() -> Result<()> {
let input = "time echo hello";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_timed_pipeline_posix() -> Result<()> {
let input = "time -p echo hello";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_negated_pipeline() -> Result<()> {
let input = "! echo hello";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_negated_timed_pipeline() -> Result<()> {
let input = "time ! echo hello";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_pipe_with_multiple_commands() -> Result<()> {
let input = "ls -la | head -10 | tail -5";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
@@ -0,0 +1,191 @@
//! Tests for redirection parsing.
use super::{ParseResult, test_with_snapshot};
use crate::assert_snapshot_redacted;
use anyhow::Result;
// File redirections
#[test]
fn parse_redirect_output() -> Result<()> {
let input = "echo hello > file.txt";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_redirect_input() -> Result<()> {
let input = "cat < input.txt";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_redirect_append() -> Result<()> {
let input = "echo hello >> file.txt";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_redirect_clobber() -> Result<()> {
let input = "echo hello >| file.txt";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_redirect_read_write() -> Result<()> {
let input = "cat <> file.txt";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// FD operations
#[test]
fn parse_redirect_stderr_to_stdout() -> Result<()> {
let input = "command 2>&1";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_redirect_stdout_to_stderr() -> Result<()> {
let input = "command 1>&2";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_redirect_fd_close() -> Result<()> {
let input = "command 2>&-";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_redirect_stdin_dup() -> Result<()> {
let input = "command <&3";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Combined redirections
#[test]
fn parse_redirect_output_and_error() -> Result<()> {
let input = "command &> file.txt";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_redirect_output_and_error_append() -> Result<()> {
let input = "command &>> file.txt";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_redirect_multiple() -> Result<()> {
let input = "command < input.txt > output.txt 2>&1";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Process substitution
#[test]
fn parse_process_substitution_read() -> Result<()> {
let input = "diff <(sort file1) <(sort file2)";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_process_substitution_write() -> Result<()> {
let input = "tee >(grep error > errors.txt)";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
// Here string
#[test]
fn parse_here_string() -> Result<()> {
let input = "cat <<< 'hello world'";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_here_string_with_variable() -> Result<()> {
let input = "cat <<< $variable";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
@@ -0,0 +1,137 @@
//! Tests for simple command parsing.
use super::{ParseResult, test_with_snapshot};
use crate::assert_snapshot_redacted;
use anyhow::Result;
#[test]
fn parse_echo_hello() -> Result<()> {
let input = "echo hello";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_ls() -> Result<()> {
let input = "ls";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_colon() -> Result<()> {
let input = ":";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_true() -> Result<()> {
let input = "true";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_echo_hello_world() -> Result<()> {
let input = "echo hello world";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_ls_la_home() -> Result<()> {
let input = "ls -la /home";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_command_with_quoted_args() -> Result<()> {
let input = r#"echo "hello world""#;
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_command_with_single_quotes() -> Result<()> {
let input = "echo 'hello world'";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_command_with_backslash_escape() -> Result<()> {
let input = r"echo hello\ world";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_command_with_variable() -> Result<()> {
let input = "echo $HOME";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_command_with_command_substitution() -> Result<()> {
let input = "echo $(whoami)";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
#[test]
fn parse_command_with_backtick_substitution() -> Result<()> {
let input = "echo `whoami`";
let result = test_with_snapshot(input)?;
assert_snapshot_redacted!(ParseResult {
input,
result: &result
});
Ok(())
}
@@ -0,0 +1,61 @@
---
source: brush-parser/src/parser/tests/and_or_lists.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "cmd1 && cmd2; cmd3 || cmd4",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd1",
loc: "[location]",
)),
)),
],
),
additional: [
And(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd2",
loc: "[location]",
)),
)),
],
)),
],
), Sequence),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd3",
loc: "[location]",
)),
)),
],
),
additional: [
Or(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd4",
loc: "[location]",
)),
)),
],
)),
],
), Sequence),
]),
],
),
)
@@ -0,0 +1,49 @@
---
source: brush-parser/src/parser/tests/and_or_lists.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "cmd1 | cmd2 && cmd3 | cmd4",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd1",
loc: "[location]",
)),
)),
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd2",
loc: "[location]",
)),
)),
],
),
additional: [
And(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd3",
loc: "[location]",
)),
)),
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd4",
loc: "[location]",
)),
)),
],
)),
],
), Sequence),
]),
],
),
)
@@ -0,0 +1,47 @@
---
source: brush-parser/src/parser/tests/and_or_lists.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "cmd1 && cmd2 && cmd3",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd1",
loc: "[location]",
)),
)),
],
),
additional: [
And(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd2",
loc: "[location]",
)),
)),
],
)),
And(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd3",
loc: "[location]",
)),
)),
],
)),
],
), Sequence),
]),
],
),
)
@@ -0,0 +1,47 @@
---
source: brush-parser/src/parser/tests/and_or_lists.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "cmd1 || cmd2 || cmd3",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd1",
loc: "[location]",
)),
)),
],
),
additional: [
Or(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd2",
loc: "[location]",
)),
)),
],
)),
Or(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd3",
loc: "[location]",
)),
)),
],
)),
],
), Sequence),
]),
],
),
)
@@ -0,0 +1,47 @@
---
source: brush-parser/src/parser/tests/and_or_lists.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "cmd1 && cmd2 || cmd3",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd1",
loc: "[location]",
)),
)),
],
),
additional: [
And(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd2",
loc: "[location]",
)),
)),
],
)),
Or(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd3",
loc: "[location]",
)),
)),
],
)),
],
), Sequence),
]),
],
),
)
@@ -0,0 +1,43 @@
---
source: brush-parser/src/parser/tests/and_or_lists.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "true && echo yes",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "true",
loc: "[location]",
)),
)),
],
),
additional: [
And(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "yes",
loc: "[location]",
)),
])),
)),
],
)),
],
), Sequence),
]),
],
),
)
@@ -0,0 +1,43 @@
---
source: brush-parser/src/parser/tests/and_or_lists.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "false || echo no",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "false",
loc: "[location]",
)),
)),
],
),
additional: [
Or(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "no",
loc: "[location]",
)),
])),
)),
],
)),
],
), Sequence),
]),
],
),
)
@@ -0,0 +1,35 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "x+=more",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("x"),
value: Scalar(Word(
value: "more",
loc: "[location]",
)),
append: true,
loc: "[location]",
), Word(
value: "x+=more",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,35 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "x+=\" more text\"",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("x"),
value: Scalar(Word(
value: "\" more text\"",
loc: "[location]",
)),
append: true,
loc: "[location]",
), Word(
value: "x+=\" more text\"",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,44 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "arr=(a b c)",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("arr"),
value: Array([
(None, Word(
value: "a",
loc: "[location]",
)),
(None, Word(
value: "b",
loc: "[location]",
)),
(None, Word(
value: "c",
loc: "[location]",
)),
]),
loc: "[location]",
), Word(
value: "arr=(a b c)",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,34 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "arr[0]=value",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: ArrayElementName("arr", "0"),
value: Scalar(Word(
value: "value",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "arr[0]=value",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,34 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "arr[i+1]=value",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: ArrayElementName("arr", "i+1"),
value: Scalar(Word(
value: "value",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "arr[i+1]=value",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,31 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "arr=()",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("arr"),
value: Array([]),
loc: "[location]",
), Word(
value: "arr=()",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,47 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "arr=(a [5]=b c)",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("arr"),
value: Array([
(None, Word(
value: "a",
loc: "[location]",
)),
(Some(Word(
value: "5",
loc: "[location]",
)), Word(
value: "b",
loc: "[location]",
)),
(None, Word(
value: "c",
loc: "[location]",
)),
]),
loc: "[location]",
), Word(
value: "arr=(a [5]=b c)",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,40 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "arr=(\"hello world\" \"foo bar\")",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("arr"),
value: Array([
(None, Word(
value: "\"hello world\"",
loc: "[location]",
)),
(None, Word(
value: "\"foo bar\"",
loc: "[location]",
)),
]),
loc: "[location]",
), Word(
value: "arr=(\"hello world\" \"foo bar\")",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,53 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "arr=([0]=a [1]=b [2]=c)",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("arr"),
value: Array([
(Some(Word(
value: "0",
loc: "[location]",
)), Word(
value: "a",
loc: "[location]",
)),
(Some(Word(
value: "1",
loc: "[location]",
)), Word(
value: "b",
loc: "[location]",
)),
(Some(Word(
value: "2",
loc: "[location]",
)), Word(
value: "c",
loc: "[location]",
)),
]),
loc: "[location]",
), Word(
value: "arr=([0]=a [1]=b [2]=c)",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,34 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "x=",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("x"),
value: Scalar(Word(
value: "",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "x=",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,34 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "x=\"hello world\"",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("x"),
value: Scalar(Word(
value: "\"hello world\"",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "x=\"hello world\"",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,34 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "x=value",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("x"),
value: Scalar(Word(
value: "value",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "x=value",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,34 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "x=\'hello world\'",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("x"),
value: Scalar(Word(
value: "\'hello world\'",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "x=\'hello world\'",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,48 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "VAR=value command arg1 arg2",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("VAR"),
value: Scalar(Word(
value: "value",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "VAR=value",
loc: "[location]",
)),
])),
word_or_name: Some(Word(
value: "command",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "arg1",
loc: "[location]",
)),
Word(Word(
value: "arg2",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,34 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "x=$(pwd)",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("x"),
value: Scalar(Word(
value: "$(pwd)",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "x=$(pwd)",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,34 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "x=$HOME/bin",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("x"),
value: Scalar(Word(
value: "$HOME/bin",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "x=$HOME/bin",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,42 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "declare -i x=5",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "declare",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "-i",
loc: "[location]",
)),
AssignmentWord(Assignment(
name: VariableName("x"),
value: Scalar(Word(
value: "5",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "x=5",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,38 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "export VAR=value",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "export",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
AssignmentWord(Assignment(
name: VariableName("VAR"),
value: Scalar(Word(
value: "value",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "VAR=value",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,38 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "local x=5",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "local",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
AssignmentWord(Assignment(
name: VariableName("x"),
value: Scalar(Word(
value: "5",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "x=5",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,56 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "x=1 y=2 z=3",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("x"),
value: Scalar(Word(
value: "1",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "x=1",
loc: "[location]",
)),
AssignmentWord(Assignment(
name: VariableName("y"),
value: Scalar(Word(
value: "2",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "y=2",
loc: "[location]",
)),
AssignmentWord(Assignment(
name: VariableName("z"),
value: Scalar(Word(
value: "3",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "z=3",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,49 @@
---
source: brush-parser/src/parser/tests/assignments.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "A=1 B=2 command",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("A"),
value: Scalar(Word(
value: "1",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "A=1",
loc: "[location]",
)),
AssignmentWord(Assignment(
name: VariableName("B"),
value: Scalar(Word(
value: "2",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "B=2",
loc: "[location]",
)),
])),
word_or_name: Some(Word(
value: "command",
loc: "[location]",
)),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,80 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "if (( x > 5 )); then echo big; else echo small; fi",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(IfClause(IfClauseCommand(
condition: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Arithmetic(ArithmeticCommand(
expr: UnexpandedArithmeticExpr(
value: "x > 5",
),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
then: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "big",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
elses: Some([
ElseClause(
body: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "small",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
),
]),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,49 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "cmd1 & cmd2 & cmd3",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd1",
loc: "[location]",
)),
)),
],
),
), Async),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd2",
loc: "[location]",
)),
)),
],
),
), Async),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd3",
loc: "[location]",
)),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,31 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "echo {a,b,c}",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "{a,b,c}",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,58 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "case x in\nx)\n echo y\nesac",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(CaseClause(CaseClauseCommand(
value: Word(
value: "x",
loc: "[location]",
),
cases: [
CaseItem(
patterns: [
Word(
value: "x",
loc: "[location]",
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "y",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
])),
post_action: ExitCase,
loc: "[location]",
),
],
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,58 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "case x in\nx)\n echo y;;\nesac",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(CaseClause(CaseClauseCommand(
value: Word(
value: "x",
loc: "[location]",
),
cases: [
CaseItem(
patterns: [
Word(
value: "x",
loc: "[location]",
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "y",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
])),
post_action: ExitCase,
loc: "[location]",
),
],
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,31 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "echo \"$(echo $(pwd))\"",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "\"$(echo $(pwd))\"",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,72 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "arr=($(seq 1 10)); echo \"${arr[@]}\"; echo \"${#arr[@]}\"",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("arr"),
value: Array([
(None, Word(
value: "$(seq 1 10)",
loc: "[location]",
)),
]),
loc: "[location]",
), Word(
value: "arr=($(seq 1 10))",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "\"${arr[@]}\"",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "\"${#arr[@]}\"",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,27 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "coproc cat",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Coprocess(CoprocessCommand(
body: Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cat",
loc: "[location]",
)),
)),
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,39 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "ls !(*.txt) +(foo|bar) ?(a|b)",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "ls",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "!(*.txt)",
loc: "[location]",
)),
Word(Word(
value: "+(foo|bar)",
loc: "[location]",
)),
Word(Word(
value: "?(a|b)",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,52 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "for f in $(ls *.txt); do cat \"$f\"; done",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(ForClause(ForClauseCommand(
variable_name: "f",
values: Some([
Word(
value: "$(ls *.txt)",
loc: "[location]",
),
]),
body: DoGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cat",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "\"$f\"",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,110 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "myfunc() {\n if [[ -z \"$1\" ]]; then\n echo \"No argument\"\n return 1\n fi\n echo \"Got: $1\"\n}",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Function(FunctionDefinition(
fname: Word(
value: "myfunc",
loc: "[location]",
),
body: FunctionBody(BraceGroup(BraceGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(IfClause(IfClauseCommand(
condition: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
ExtendedTest(ExtendedTestExprCommand(
expr: UnaryTest(StringHasZeroLength, Word(
value: "\"$1\"",
loc: "[location]",
)),
loc: "[location]",
), None),
],
),
), Sequence),
]),
then: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "\"No argument\"",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "return",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "1",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
)), None),
],
),
), Sequence),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "\"Got: $1\"",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
)), None),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,39 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "ls *.txt **/foo.* file?.txt",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "ls",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "*.txt",
loc: "[location]",
)),
Word(Word(
value: "**/foo.*",
loc: "[location]",
)),
Word(Word(
value: "file?.txt",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,71 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "cmd1; cmd2 && cmd3 || cmd4; cmd5",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd1",
loc: "[location]",
)),
)),
],
),
), Sequence),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd2",
loc: "[location]",
)),
)),
],
),
additional: [
And(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd3",
loc: "[location]",
)),
)),
],
)),
Or(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd4",
loc: "[location]",
)),
)),
],
)),
],
), Sequence),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd5",
loc: "[location]",
)),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,49 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "cmd <<EOF1 <<EOF2\nfirst\nEOF1\nsecond\nEOF2\n",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cmd",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
IoRedirect(HereDocument(None, IoHereDocument(
requires_expansion: true,
here_end: Word(
value: "EOF1",
loc: "[location]",
),
doc: Word(
value: "first\n",
loc: "[location]",
),
))),
IoRedirect(HereDocument(None, IoHereDocument(
requires_expansion: true,
here_end: Word(
value: "EOF2",
loc: "[location]",
),
doc: Word(
value: "second\n",
loc: "[location]",
),
))),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,102 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "if true; then\n while read line; do\n echo \"$line\"\n done < file.txt\nfi",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(IfClause(IfClauseCommand(
condition: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "true",
loc: "[location]",
)),
)),
],
),
), Sequence),
]),
then: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(WhileClause(WhileOrUntilClauseCommand(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "read",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "line",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]), DoGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "\"$line\"",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
), SourceSpan(
start: SourcePosition(
index: 18,
line: 2,
column: 5,
),
end: SourcePosition(
index: 67,
line: 4,
column: 9,
),
))), Some(RedirectList([
File(None, Read, Filename(Word(
value: "file.txt",
loc: "[location]",
))),
]))),
],
),
), Sequence),
]),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,39 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "echo \"${var:-default}\" \"${var:+alt}\" \"${var:=assign}\"",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "\"${var:-default}\"",
loc: "[location]",
)),
Word(Word(
value: "\"${var:+alt}\"",
loc: "[location]",
)),
Word(Word(
value: "\"${var:=assign}\"",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,63 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "cat < input.txt | grep pattern | tee output.txt > /dev/null 2>&1",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cat",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
IoRedirect(File(None, Read, Filename(Word(
value: "input.txt",
loc: "[location]",
)))),
])),
)),
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "grep",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "pattern",
loc: "[location]",
)),
])),
)),
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "tee",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "output.txt",
loc: "[location]",
)),
IoRedirect(File(None, Write, Filename(Word(
value: "/dev/null",
loc: "[location]",
)))),
IoRedirect(File(Some(2), DuplicateOutput, Duplicate(Word(
value: "1",
loc: "[location]",
)))),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,64 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "#!/usr/bin/env bash\n\nfor f in A B C; do\n\n # sdfsdf\n echo \"${f@L}\" >&2\n\n done\n",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(ForClause(ForClauseCommand(
variable_name: "f",
values: Some([
Word(
value: "A",
loc: "[location]",
),
Word(
value: "B",
loc: "[location]",
),
Word(
value: "C",
loc: "[location]",
),
]),
body: DoGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "\"${f@L}\"",
loc: "[location]",
)),
IoRedirect(File(None, DuplicateOutput, Duplicate(Word(
value: "2",
loc: "[location]",
)))),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,84 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "( x=1; y=2; echo $((x + y)) )",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Subshell(SubshellCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("x"),
value: Scalar(Word(
value: "1",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "x=1",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
prefix: Some(CommandPrefix([
AssignmentWord(Assignment(
name: VariableName("y"),
value: Scalar(Word(
value: "2",
loc: "[location]",
)),
loc: "[location]",
), Word(
value: "y=2",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "$((x + y))",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,49 @@
---
source: brush-parser/src/parser/tests/complex.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "cd ~/projects; ls ~user/home",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "cd",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "~/projects",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "ls",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "~user/home",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,71 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "( : && ( (( 0 )) || : ) )",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Subshell(SubshellCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: ":",
loc: "[location]",
)),
)),
],
),
additional: [
And(Pipeline(
seq: [
Compound(Subshell(SubshellCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Arithmetic(ArithmeticCommand(
expr: UnexpandedArithmeticExpr(
value: "0",
),
loc: "[location]",
)), None),
],
),
additional: [
Or(Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: ":",
loc: "[location]",
)),
)),
],
)),
],
), Sequence),
]),
loc: "[location]",
)), None),
],
)),
],
), Sequence),
]),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,25 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "(( x = 5 + 3 * 2 ))",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Arithmetic(ArithmeticCommand(
expr: UnexpandedArithmeticExpr(
value: "x = 5 + 3 * 2",
),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,54 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "for (( i = 0; i < 10; i++ )); do echo $i; done",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(ArithmeticForClause(ArithmeticForClauseCommand(
initializer: Some(UnexpandedArithmeticExpr(
value: "i = 0",
)),
condition: Some(UnexpandedArithmeticExpr(
value: "i < 10",
)),
updater: Some(UnexpandedArithmeticExpr(
value: "i++",
)),
body: DoGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "$i",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,54 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "for (( ; ; )); do echo loop; done",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(ArithmeticForClause(ArithmeticForClauseCommand(
initializer: Some(UnexpandedArithmeticExpr(
value: "",
)),
condition: Some(UnexpandedArithmeticExpr(
value: "",
)),
updater: Some(UnexpandedArithmeticExpr(
value: "",
)),
body: DoGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "loop",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,25 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "(( x++ ))",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Arithmetic(ArithmeticCommand(
expr: UnexpandedArithmeticExpr(
value: "x++",
),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,25 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "(( 1 + 2 ))",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Arithmetic(ArithmeticCommand(
expr: UnexpandedArithmeticExpr(
value: "1 + 2",
),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,42 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "{ echo hello; }",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(BraceGroup(BraceGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "hello",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,60 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "{\n echo hello\n echo world\n}",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(BraceGroup(BraceGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "hello",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "world",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,88 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "case x in a) echo a;;& b) echo b;; esac",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(CaseClause(CaseClauseCommand(
value: Word(
value: "x",
loc: "[location]",
),
cases: [
CaseItem(
patterns: [
Word(
value: "a",
loc: "[location]",
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "a",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
])),
post_action: ContinueEvaluatingCases,
loc: "[location]",
),
CaseItem(
patterns: [
Word(
value: "b",
loc: "[location]",
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "b",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
])),
post_action: ExitCase,
loc: "[location]",
),
],
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,88 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "case x in a) echo a;& b) echo b;; esac",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(CaseClause(CaseClauseCommand(
value: Word(
value: "x",
loc: "[location]",
),
cases: [
CaseItem(
patterns: [
Word(
value: "a",
loc: "[location]",
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "a",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
])),
post_action: UnconditionallyExecuteNextCaseItem,
loc: "[location]",
),
CaseItem(
patterns: [
Word(
value: "b",
loc: "[location]",
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "b",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
])),
post_action: ExitCase,
loc: "[location]",
),
],
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,122 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "case x in\n a|b) echo ab;;\n c) echo c;;\n *) echo default;;\nesac",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(CaseClause(CaseClauseCommand(
value: Word(
value: "x",
loc: "[location]",
),
cases: [
CaseItem(
patterns: [
Word(
value: "a",
loc: "[location]",
),
Word(
value: "b",
loc: "[location]",
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "ab",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
])),
post_action: ExitCase,
loc: "[location]",
),
CaseItem(
patterns: [
Word(
value: "c",
loc: "[location]",
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "c",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
])),
post_action: ExitCase,
loc: "[location]",
),
CaseItem(
patterns: [
Word(
value: "*",
loc: "[location]",
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "default",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
])),
post_action: ExitCase,
loc: "[location]",
),
],
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,58 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "case x in a) echo a;; esac",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(CaseClause(CaseClauseCommand(
value: Word(
value: "x",
loc: "[location]",
),
cases: [
CaseItem(
patterns: [
Word(
value: "a",
loc: "[location]",
),
],
cmd: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "a",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
])),
post_action: ExitCase,
loc: "[location]",
),
],
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,60 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "for x in a b c; do echo $x; done",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(ForClause(ForClauseCommand(
variable_name: "x",
values: Some([
Word(
value: "a",
loc: "[location]",
),
Word(
value: "b",
loc: "[location]",
),
Word(
value: "c",
loc: "[location]",
),
]),
body: DoGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "$x",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,60 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "for x in a b c\ndo\n echo $x\ndone",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(ForClause(ForClauseCommand(
variable_name: "x",
values: Some([
Word(
value: "a",
loc: "[location]",
),
Word(
value: "b",
loc: "[location]",
),
Word(
value: "c",
loc: "[location]",
),
]),
body: DoGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "$x",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,47 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "for x; do echo $x; done",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(ForClause(ForClauseCommand(
variable_name: "x",
values: None,
body: DoGroupCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "$x",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,116 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "if false; then echo one; elif true; then echo two; else echo three; fi",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(IfClause(IfClauseCommand(
condition: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "false",
loc: "[location]",
)),
)),
],
),
), Sequence),
]),
then: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "one",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
elses: Some([
ElseClause(
condition: Some(CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "true",
loc: "[location]",
)),
)),
],
),
), Sequence),
])),
body: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "two",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
),
ElseClause(
body: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "three",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
),
]),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,80 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "if true; then echo yes; else echo no; fi",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(IfClause(IfClauseCommand(
condition: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "true",
loc: "[location]",
)),
)),
],
),
), Sequence),
]),
then: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "yes",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
elses: Some([
ElseClause(
body: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "no",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
),
]),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,80 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "if true\nthen\n echo yes\nelse\n echo no\nfi",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(IfClause(IfClauseCommand(
condition: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "true",
loc: "[location]",
)),
)),
],
),
), Sequence),
]),
then: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "yes",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
elses: Some([
ElseClause(
body: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "no",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
),
]),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,56 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "if true; then echo yes; fi",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(IfClause(IfClauseCommand(
condition: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "true",
loc: "[location]",
)),
)),
],
),
), Sequence),
]),
then: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "yes",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,25 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "( ( echo nested ) )",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Arithmetic(ArithmeticCommand(
expr: UnexpandedArithmeticExpr(
value: "echo nested",
),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,42 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "( echo hello )",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Subshell(SubshellCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "hello",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)
@@ -0,0 +1,60 @@
---
source: brush-parser/src/parser/tests/compound_commands.rs
expression: "ParseResult { input, result: &result }"
---
ParseResult(
input: "( echo hello; echo world )",
result: Program(
complete_commands: [
CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Compound(Subshell(SubshellCommand(
list: CompoundList([
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "hello",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
CompoundListItem(AndOrList(
first: Pipeline(
seq: [
Simple(SimpleCommand(
word_or_name: Some(Word(
value: "echo",
loc: "[location]",
)),
suffix: Some(CommandSuffix([
Word(Word(
value: "world",
loc: "[location]",
)),
])),
)),
],
),
), Sequence),
]),
loc: "[location]",
)), None),
],
),
), Sequence),
]),
],
),
)

Some files were not shown because too many files have changed in this diff Show More