use super::*;
#[test]
fn test_parse_simple_command() {
let input = "echo hello";
let parser = Parser::new(input);
let parsed = parser.parse().unwrap();
assert_eq!(parsed.status, ParseStatus::Clean);
assert!(parsed.diagnostics.is_empty());
assert!(parsed.terminal_error.is_none());
let script = parsed.file;
assert_eq!(script.body.len(), 1);
if let AstCommand::Simple(cmd) = &script.body[0].command {
assert_eq!(cmd.name.render(input), "echo");
assert_eq!(cmd.args.len(), 1);
assert_eq!(cmd.args[0].render(input), "hello");
} else {
panic!("expected simple command");
}
}
#[test]
fn test_parse_break_as_typed_builtin() {
let input = "break 2";
let parser = Parser::new(input);
let script = parser.parse().unwrap().file;
let AstCommand::Builtin(AstBuiltinCommand::Break(command)) = &script.body[0].command else {
panic!("expected break builtin");
};
assert_eq!(command.depth.as_ref().unwrap().render(input), "2");
assert!(command.extra_args.is_empty());
}
#[test]
fn test_parse_continue_preserves_extra_args() {
let input = "continue 1 extra";
let parser = Parser::new(input);
let script = parser.parse().unwrap().file;
let AstCommand::Builtin(AstBuiltinCommand::Continue(command)) = &script.body[0].command else {
panic!("expected continue builtin");
};
assert_eq!(command.depth.as_ref().unwrap().render(input), "1");
assert_eq!(command.extra_args.len(), 1);
assert_eq!(command.extra_args[0].render(input), "extra");
}
#[test]
fn test_parse_exit_as_typed_builtin() {
let input = "exit 1";
let parser = Parser::new(input);
let script = parser.parse().unwrap().file;
let AstCommand::Builtin(AstBuiltinCommand::Exit(command)) = &script.body[0].command else {
panic!("expected exit builtin");
};
assert_eq!(command.code.as_ref().unwrap().render(input), "1");
assert!(command.extra_args.is_empty());
}
#[test]
fn test_parse_multiple_args() {
let input = "echo hello world";
let parser = Parser::new(input);
let script = parser.parse().unwrap().file;
if let AstCommand::Simple(cmd) = &script.body[0].command {
assert_eq!(cmd.name.render(input), "echo");
assert_eq!(cmd.args.len(), 2);
assert_eq!(cmd.args[0].render(input), "hello");
assert_eq!(cmd.args[1].render(input), "world");
} else {
panic!("expected simple command");
}
}
#[test]
fn test_simple_command_treats_conditional_brackets_as_arguments_after_name() {
let cases = [
(
"eval ! [[ \"$env_var\" =~ ^[[:digit:]]+$ ]]",
vec!["eval", "!", "[[", "$env_var", "=~", "^[[:digit:]]+$", "]]"],
),
("\"eval\" [[ 1 ]]", vec!["eval", "[[", "1", "]]"]),
("${cmd} [[ 1 ]]", vec!["${cmd}", "[[", "1", "]]"]),
(
"$dbracket foo == foo ]]",
vec!["$dbracket", "foo", "==", "foo", "]]"],
),
];
for (input, expected_words) in cases {
let parsed = Parser::new(input).parse().unwrap();
assert_eq!(parsed.status, ParseStatus::Clean, "{input}");
assert_eq!(parsed.file.body.len(), 1, "{input}");
let AstCommand::Simple(command) = &parsed.file.body[0].command else {
panic!("expected simple command for {input}");
};
let words = std::iter::once(&command.name)
.chain(command.args.iter())
.map(|arg| arg.render(input))
.collect::<Vec<_>>();
assert_eq!(words, expected_words, "{input}");
}
}
#[test]
fn test_runtime_constructed_conditional_open_keeps_close_as_argument() {
let input = "dbracket=[[\n$dbracket foo == foo ]]";
let parsed = Parser::new(input).parse().unwrap();
assert_eq!(parsed.status, ParseStatus::Clean);
assert_eq!(parsed.file.body.len(), 2);
let AstCommand::Simple(command) = &parsed.file.body[1].command else {
panic!("expected simple command");
};
assert_eq!(command.name.render(input), "$dbracket");
assert_eq!(
command
.args
.iter()
.map(|arg| arg.render(input))
.collect::<Vec<_>>(),
vec!["foo", "==", "foo", "]]"]
);
}
#[test]
fn test_simple_command_treats_conditional_open_as_name_after_prefixes() {
let cases = [
("VAR=1 [[ 1 ]]", "[[", vec!["1", "]]"], 1, 0),
(">out [[ 1 ]]", "[[", vec!["1", "]]"], 0, 1),
("VAR=1 ]]", "]]", Vec::new(), 1, 0),
(">out ]]", "]]", Vec::new(), 0, 1),
];
for (input, expected_name, expected_args, expected_assignments, expected_redirects) in cases {
let parsed = Parser::new(input).parse().unwrap();
assert_eq!(parsed.status, ParseStatus::Clean, "{input}");
assert_eq!(parsed.file.body.len(), 1, "{input}");
let stmt = &parsed.file.body[0];
let AstCommand::Simple(command) = &stmt.command else {
panic!("expected simple command for {input}");
};
assert_eq!(command.assignments.len(), expected_assignments, "{input}");
assert_eq!(stmt.redirects.len(), expected_redirects, "{input}");
assert_eq!(command.name.render(input), expected_name, "{input}");
assert_eq!(
command
.args
.iter()
.map(|arg| arg.render(input))
.collect::<Vec<_>>(),
expected_args,
"{input}"
);
}
}
#[test]
fn test_simple_command_allows_nft_brace_literals_inside_and_block() {
let input = "\
start_nftables() {
[ \"$tun_statu\" = true ] && {
nft add chain inet fw4 forward { type filter hook forward priority filter \\; } 2>/dev/null
nft add chain inet fw4 input { type filter hook input priority filter \\; } 2>/dev/null
}
}
";
let parsed = Parser::new(input).parse().unwrap();
assert_eq!(parsed.file.body.len(), 1);
let function = expect_function(&parsed.file.body[0]);
assert!(matches!(function.body.command, AstCommand::Compound(..)));
}
#[test]
fn test_parse_nft_literal_braces_inside_nested_and_groups() {
let input = r#"
start_nftables() {
[ "$redir_mod" = "Tproxy" ] && (modprobe nft_tproxy >/dev/null 2>&1 || lsmod 2>/dev/null | grep -q nft_tproxy) && {
[ "$local_proxy" = true ] && {
nft add chain inet shellcrash mark_out { type filter hook prerouting priority -100 \; }
}
}
[ "$tun_statu" = true ] && {
[ "$lan_proxy" = true ] && {
nft list chain inet fw4 forward >/dev/null 2>&1 || nft add chain inet fw4 forward { type filter hook forward priority filter \; } 2>/dev/null
nft list chain inet fw4 input >/dev/null 2>&1 || nft add chain inet fw4 input { type filter hook input priority filter \; } 2>/dev/null
}
[ "$local_proxy" = true ] && start_nft_route output output route -150
}
}
"#;
let parsed = Parser::with_dialect(input, ShellDialect::Bash).parse();
assert_eq!(
parsed.status,
ParseStatus::Clean,
"{}",
parsed.strict_error()
);
}
#[test]
fn test_brace_group_command_can_use_right_brace_as_literal_argument() {
let source = "rbrace() { echo }; }; rbrace\n";
let output = Parser::new(source).parse().unwrap();
let function = expect_function(&output.file.body[0]);
let (compound, redirects) = expect_compound(function.body.as_ref());
let AstCompoundCommand::BraceGroup(body) = compound else {
panic!("expected brace-group function body");
};
assert!(redirects.is_empty());
assert_eq!(body.len(), 1);
let command = expect_simple(&body[0]);
assert_eq!(command.name.render(source), "echo");
assert_eq!(command.args.len(), 1);
assert_eq!(command.args[0].render(source), "}");
}
#[test]
fn test_parse_coproc_with_non_plain_name_candidate_defaults_to_coproc_name() {
let source = "coproc 'roc' cat\n";
let output = Parser::new(source).parse().unwrap().file;
let (compound, _) = expect_compound(&output.body[0]);
let AstCompoundCommand::Coproc(command) = compound else {
panic!("expected coproc command");
};
assert_eq!(command.name.as_str(), "COPROC");
let simple = expect_simple(command.body.as_ref());
assert_eq!(simple.name.render_syntax(source), "'roc'");
assert_eq!(simple.args[0].render(source), "cat");
}