#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
use super::ast::{BashExpr, BashStmt};
use super::parser::BashParser;
fn parse_ok(input: &str) -> super::ast::BashAst {
let mut p = BashParser::new(input).unwrap();
p.parse().unwrap()
}
fn parse_no_panic(input: &str) {
let _ = BashParser::new(input).and_then(|mut p| p.parse());
}
#[test]
fn test_var_expansion_all_operators() {
let cases = [
"echo ${#PATH}", "echo ${HOME:-/tmp}", "echo ${TMPDIR:=/tmp}", "echo ${DEBUG:+enabled}", "echo ${CFG:?required}", "echo ${PATH##*/}", "echo ${FILE#*/}", "echo ${FILE%%.*}", "echo ${FILE%.*}", "echo ${HOME}", ];
for input in cases {
let ast = parse_ok(input);
assert!(!ast.statements.is_empty(), "failed for: {input}");
}
}
#[test]
fn test_expr_number_token() {
let ast = parse_ok("echo 42");
if let BashStmt::Command { args, .. } = &ast.statements[0] {
assert!(matches!(args[0], BashExpr::Literal(ref s) if s == "42"));
}
}
#[test]
fn test_expr_arithmetic_expansion() {
let ast = parse_ok("X=$((2 + 3))");
if let BashStmt::Assignment { value, .. } = &ast.statements[0] {
assert!(matches!(value, BashExpr::Arithmetic(_)));
}
}
#[test]
fn test_expr_command_substitution() {
let ast = parse_ok("DIR=$(pwd)");
if let BashStmt::Assignment { value, .. } = &ast.statements[0] {
assert!(matches!(value, BashExpr::CommandSubst(_)));
}
}
#[test]
fn test_expr_heredoc_token() {
parse_no_panic("cat <<EOF\nhello world\nEOF");
}
#[test]
fn test_expr_glob_identifier() {
let ast = parse_ok("ls *.txt");
if let BashStmt::Command { args, .. } = &ast.statements[0] {
assert!(matches!(&args[0], BashExpr::Glob(ref s) if s.contains('*')));
}
}
#[test]
fn test_expr_brace_literal() {
parse_no_panic("find . -name test -exec rm {} ;");
}
#[test]
fn test_expr_keywords_as_arguments() {
let keywords = [
"done", "fi", "then", "while", "for", "case", "in", "if", "elif", "else", "until", "do",
"esac", "function", "return", "export", "local", "coproc", "select",
];
for kw in keywords {
let input = format!("echo {kw}");
let result = BashParser::new(&input).and_then(|mut p| p.parse());
let _ = result;
}
}
#[test]
fn test_array_literal_basic() {
let ast = parse_ok("ARR=(one two three)");
if let BashStmt::Assignment { value, .. } = &ast.statements[0] {
assert!(matches!(value, BashExpr::Array(_)));
}
}
#[test]
fn test_array_literal_sparse() {
let ast = parse_ok("ARR=([0]=first [3]=fourth)");
if let BashStmt::Assignment { value, .. } = &ast.statements[0] {
assert!(matches!(value, BashExpr::Array(_)));
}
}
#[test]
fn test_array_with_newlines() {
let ast = parse_ok("ARR=(\nalpha\nbeta\ngamma\n)");
assert!(!ast.statements.is_empty());
}
#[test]
fn test_sparse_array_edge_cases() {
parse_no_panic("ARR=([0]=$HOME)");
parse_no_panic("ARR=([0]=)");
}
#[test]
fn test_glob_bracket_patterns() {
parse_no_panic("ls [0-9]"); parse_no_panic("ls [a-z]"); parse_no_panic("ls [!abc]"); parse_no_panic("ls [0-9]*.sql"); }
#[test]
fn test_single_bracket_string_operators() {
assert!(BashParser::new("if [ \"$x\" = \"y\" ]; then echo m; fi")
.and_then(|mut p| p.parse())
.is_ok());
assert!(BashParser::new("if [ \"$x\" != \"y\" ]; then echo m; fi")
.and_then(|mut p| p.parse())
.is_ok());
}
#[test]
fn test_single_bracket_int_operators() {
let ops = ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"];
for op in ops {
let input = format!("if [ $x {op} 5 ]; then echo ok; fi");
let result = BashParser::new(&input).and_then(|mut p| p.parse());
assert!(result.is_ok(), "failed for operator: {op}");
}
}
#[test]
fn test_single_bracket_and_or() {
assert!(BashParser::new("if [ -f /a -a -f /b ]; then echo x; fi")
.and_then(|mut p| p.parse())
.is_ok());
assert!(BashParser::new("if [ -f /a -o -f /b ]; then echo x; fi")
.and_then(|mut p| p.parse())
.is_ok());
}
#[test]
fn test_single_bracket_lt_gt_operators() {
parse_no_panic("if [ \"$a\" < \"$b\" ]; then echo lt; fi");
parse_no_panic("if [ \"$a\" > \"$b\" ]; then echo gt; fi");
}
#[test]
fn test_double_bracket_combinators() {
assert!(BashParser::new("if [[ -f /a && -d /b ]]; then echo x; fi")
.and_then(|mut p| p.parse())
.is_ok());
assert!(BashParser::new("if [[ -f /a || -d /b ]]; then echo x; fi")
.and_then(|mut p| p.parse())
.is_ok());
assert!(BashParser::new("if [[ ! -f /tmp/no ]]; then echo x; fi")
.and_then(|mut p| p.parse())
.is_ok());
assert!(BashParser::new("if [[ $x == yes ]]; then echo x; fi")
.and_then(|mut p| p.parse())
.is_ok());
}
#[test]
fn test_unary_test_operators() {
let ops = ["-f", "-e", "-s", "-d", "-r", "-w", "-x", "-L", "-n", "-z"];
for op in ops {
let input = format!("if [ {op} /tmp/test ]; then echo ok; fi");
let result = BashParser::new(&input).and_then(|mut p| p.parse());
assert!(result.is_ok(), "failed for unary op: {op}");
}
}
#[test]
fn test_negated_conditions() {
parse_no_panic("if ! grep -q pattern file; then echo no; fi");
parse_no_panic("if ! [ -f /tmp/x ]; then echo no; fi");
}
#[test]
fn test_compound_test_and_or() {
assert!(
BashParser::new("if [ -f /a ] && [ -f /b ]; then echo x; fi")
.and_then(|mut p| p.parse())
.is_ok()
);
assert!(
BashParser::new("if [ -f /a ] || [ -f /b ]; then echo x; fi")
.and_then(|mut p| p.parse())
.is_ok()
);
}
#[test]
fn test_condition_command_variants() {
assert!(BashParser::new("if grep -q pat f; then echo y; fi")
.and_then(|mut p| p.parse())
.is_ok());
parse_no_panic("if echo t | grep -q t; then echo p; fi"); parse_no_panic("if pid=$(pgrep sshd); then echo r; fi"); parse_no_panic("if ( cd /tmp && ls ); then echo ok; fi"); parse_no_panic("if $CMD; then echo ran; fi"); }
#[test]
fn test_condition_env_prefixes() {
parse_no_panic("while IFS= read -r line; do echo $line; done");
parse_no_panic("if LC_ALL=C sort --check f; then echo ok; fi");
}
#[test]
fn test_condition_redirects() {
let redirects = [
"if cmd > /dev/null; then echo ok; fi", "if cmd >> /tmp/log; then echo ok; fi", "if cmd < /tmp/in; then echo ok; fi", "if cmd 2>/dev/null; then echo ok; fi", "if cmd 2>&1; then echo ok; fi", "if cmd &>/dev/null; then echo ok; fi", "if cmd >&2; then echo ok; fi", ];
for input in redirects {
parse_no_panic(input);
}
}
#[test]
fn test_test_condition_bare_values() {
assert!(BashParser::new("if [ hello ]; then echo x; fi")
.and_then(|mut p| p.parse())
.is_ok());
assert!(BashParser::new("if [ $VAR ]; then echo x; fi")
.and_then(|mut p| p.parse())
.is_ok());
}
#[test]
fn test_condition_boundary_tokens() {
parse_no_panic("if cmd arg1 & then echo ok; fi"); parse_no_panic("if cmd arg1 # comment\nthen echo ok; fi"); parse_no_panic("if (cmd arg); then echo ok; fi"); }