Merge nucleic/sleek-thistle-egret-fyej into dev
This commit is contained in:
@@ -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"]
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
@@ -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() {}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+126
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+111
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+125
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+71
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
+61
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+49
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+47
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+47
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+47
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+43
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+43
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+35
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+35
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+44
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+34
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+34
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+31
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+47
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+40
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+53
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+34
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+34
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+34
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+34
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+48
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+34
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+34
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+42
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+38
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+38
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+56
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+49
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+80
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+49
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+31
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+58
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+58
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+31
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+72
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+27
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+39
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+52
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+110
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+39
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+71
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+49
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+102
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+39
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+63
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+64
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+84
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+49
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+71
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+25
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+54
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+54
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+25
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+25
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+42
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+60
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+88
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+88
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+122
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+58
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+60
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+60
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+47
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+116
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+80
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+80
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+56
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+25
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+42
@@ -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),
|
||||
]),
|
||||
],
|
||||
),
|
||||
)
|
||||
+60
@@ -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
Reference in New Issue
Block a user