use bash_ast::{parse_to_ast, ParseError};
fn must_parse(src: &str) -> bash_ast::ast::Program {
parse_to_ast(src).unwrap_or_else(|e| panic!("parse failed for `{src}`: {e}"))
}
#[test]
fn simple_command() {
let p = must_parse("echo hi");
assert_eq!(p.statements.len(), 1);
}
#[test]
fn pipeline() {
must_parse("echo hi | grep foo | wc -l");
}
#[test]
fn list_and_or() {
must_parse("true && false || echo done");
}
#[test]
fn redirections() {
must_parse("cmd > out.txt 2>&1");
must_parse("cmd < in.txt > out.txt");
must_parse("cmd >> log");
must_parse("cmd <<< 'inline'");
}
#[test]
fn heredoc() {
must_parse("cat <<EOF\nhello\nEOF\n");
}
#[test]
fn command_substitution() {
must_parse("echo $(date)");
must_parse("echo `date`");
}
#[test]
fn arithmetic() {
must_parse("echo $((1 + 2 * 3))");
must_parse("(( x = 1 + 2 ))");
}
#[test]
fn process_substitution() {
must_parse("diff <(ls a) <(ls b)");
}
#[test]
fn parameter_expansion() {
must_parse("echo ${foo:-default} ${arr[2]} ${var//x/y}");
}
#[test]
fn test_brackets() {
must_parse("[[ -f /etc/passwd && $x = foo ]]");
must_parse("[ -z \"$x\" ]");
}
#[test]
fn if_while_for() {
must_parse(
r#"
if [ -f x ]; then
echo a
elif [ -d y ]; then
echo b
else
echo c
fi
"#,
);
must_parse(
r#"
while read line; do
echo "$line"
done < input.txt
"#,
);
must_parse(
r#"
for f in *.txt; do
cat "$f"
done
"#,
);
}
#[test]
fn c_style_for() {
must_parse("for ((i=0; i<10; i++)); do echo $i; done");
}
#[test]
fn case_statement() {
must_parse(
r#"
case "$1" in
foo|bar) echo matched ;;
*) echo other ;;
esac
"#,
);
}
#[test]
fn function_definition() {
must_parse("greet() { echo hello $1; }");
must_parse("function greet { echo hello; }");
}
#[test]
fn variable_assignments_and_arrays() {
must_parse("FOO=bar BAZ=qux cmd");
must_parse("arr=(a b c)");
must_parse("arr[0]=first");
}
#[test]
fn declaration_commands() {
must_parse("export PATH=/usr/local/bin:$PATH");
must_parse("declare -A map=([k]=v)");
must_parse("local x=1");
must_parse("readonly Y=2");
must_parse("unset foo");
}
#[test]
fn comments_preserved_on_program() {
let p = must_parse("# top-level\necho hi # trailing\n");
assert!(
!p.comments.is_empty(),
"expected at least one top-level comment, got {:?}",
p.comments
);
}
#[test]
fn realistic_script() {
must_parse(
r#"#!/usr/bin/env bash
set -euo pipefail
main() {
local target="${1:-build}"
case "$target" in
build) cargo build --release ;;
test) cargo test --workspace ;;
*)
echo "unknown target: $target" >&2
return 1
;;
esac
}
main "$@"
"#,
);
}
#[test]
fn unknown_node_kind_is_an_error() {
let _e = ParseError::UnknownNode {
kind: "fictional".into(),
range: tree_sitter::Range {
start_byte: 0,
end_byte: 0,
start_point: tree_sitter::Point { row: 0, column: 0 },
end_point: tree_sitter::Point { row: 0, column: 0 },
},
};
}