use super::{Arg, SimpleCommand, Statement};
const MAX_UNWRAP_DEPTH: usize = 16;
#[derive(Debug, Clone, Copy)]
enum ArgSkip {
Positional(usize),
Assignments,
None,
}
struct WrapperDef {
name: &'static str,
subcommand: Option<&'static str>,
value_flags: &'static [&'static str],
bool_flags: &'static [&'static str],
skip: ArgSkip,
}
static WRAPPERS: &[WrapperDef] = &[
WrapperDef {
name: "timeout",
subcommand: None,
value_flags: &["-s", "--signal", "-k", "--kill-after"],
bool_flags: &["--preserve-status", "--foreground", "-v", "--verbose"],
skip: ArgSkip::Positional(1),
},
WrapperDef {
name: "nice",
subcommand: None,
value_flags: &["-n", "--adjustment"],
bool_flags: &[],
skip: ArgSkip::None,
},
WrapperDef {
name: "env",
subcommand: None,
value_flags: &["-u", "--unset"],
bool_flags: &["-i", "-0", "--null", "--ignore-environment"],
skip: ArgSkip::Assignments,
},
WrapperDef {
name: "nohup",
subcommand: None,
value_flags: &[],
bool_flags: &[],
skip: ArgSkip::None,
},
WrapperDef {
name: "strace",
subcommand: None,
value_flags: &["-e", "-o", "-p", "-s", "-P", "-I"],
bool_flags: &[
"-f", "-ff", "-c", "-C", "-t", "-tt", "-ttt", "-T", "-v", "-V", "-x", "-xx", "-y",
"-yy",
],
skip: ArgSkip::None,
},
WrapperDef {
name: "time",
subcommand: None,
value_flags: &[],
bool_flags: &["-p"],
skip: ArgSkip::None,
},
WrapperDef {
name: "uv",
subcommand: Some("run"),
value_flags: &["--python", "-p", "--directory", "--project"],
bool_flags: &[
"--no-project",
"--isolated",
"--no-dev",
"--locked",
"--frozen",
],
skip: ArgSkip::None,
},
WrapperDef {
name: "command",
subcommand: None,
value_flags: &[],
bool_flags: &["-v", "-V", "-p"],
skip: ArgSkip::None,
},
WrapperDef {
name: "builtin",
subcommand: None,
value_flags: &[],
bool_flags: &[],
skip: ArgSkip::None,
},
];
fn wrapper_basename(name: &str) -> &str {
name.rsplit('/').next().unwrap_or(name)
}
fn find_wrapper(name: &str) -> Option<&'static WrapperDef> {
let basename = wrapper_basename(name);
WRAPPERS.iter().find(|w| w.name == basename)
}
pub fn value_flags_for(cmd_name: &str, first_arg: Option<&str>) -> &'static [&'static str] {
let Some(wrapper) = find_wrapper(cmd_name) else {
return &[];
};
if let Some(sub) = wrapper.subcommand {
if first_arg != Some(sub) {
return &[];
}
}
wrapper.value_flags
}
fn is_env_assignment(token: &str) -> bool {
let Some(eq_pos) = token.find('=') else {
return false;
};
let name_part = &token[..eq_pos];
if name_part.is_empty() {
return false;
}
let mut chars = name_part.chars();
let first = chars.next().unwrap();
if !first.is_ascii_alphabetic() && first != '_' {
return false;
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
pub fn unwrap_transparent(cmd: &SimpleCommand) -> Option<SimpleCommand> {
let cmd_name = cmd.name.as_deref()?;
let wrapper = find_wrapper(cmd_name)?;
let argv = &cmd.argv;
let start_idx = if let Some(sub) = wrapper.subcommand {
if argv.first().map(|a| a.text.as_str()) != Some(sub) {
return None;
}
1 } else {
0
};
let mut i = start_idx;
while i < argv.len() {
let token = argv[i].text.as_str();
if token == "--" {
i += 1;
break;
}
if wrapper.value_flags.contains(&token) {
i += 2; continue;
}
if wrapper
.value_flags
.iter()
.any(|f| token.starts_with(f) && token.as_bytes().get(f.len()) == Some(&b'='))
{
i += 1;
continue;
}
if wrapper.value_flags.iter().any(|f| {
f.starts_with('-')
&& !f.starts_with("--")
&& token.starts_with(f)
&& token.len() > f.len()
}) {
i += 1;
continue;
}
if wrapper.bool_flags.contains(&token) {
i += 1;
continue;
}
break;
}
match wrapper.skip {
ArgSkip::Positional(n) => {
i += n;
}
ArgSkip::Assignments => {
while i < argv.len() && is_env_assignment(argv[i].text.as_str()) {
i += 1;
}
}
ArgSkip::None => {}
}
if i >= argv.len() {
return None;
}
let inner_name = argv[i].text.clone();
let inner_argv: Vec<Arg> = argv[i + 1..].to_vec();
Some(SimpleCommand {
name: Some(inner_name),
argv: inner_argv,
redirects: cmd.redirects.clone(),
assignments: vec![],
embedded_substitutions: cmd.embedded_substitutions.clone(),
})
}
fn extract_find_exec(cmd: &SimpleCommand) -> Vec<SimpleCommand> {
let mut results = Vec::new();
let argv = &cmd.argv;
let mut i = 0;
while i < argv.len() {
if argv[i].text == "-exec" || argv[i].text == "-execdir" {
i += 1; let start = i;
while i < argv.len() && argv[i].text != ";" && argv[i].text != "+" {
i += 1;
}
if i > start {
let exec_name = argv[start].text.clone();
let exec_argv: Vec<Arg> = argv[start + 1..i]
.iter()
.filter(|a| a.text != "{}")
.cloned()
.collect();
results.push(SimpleCommand {
name: Some(exec_name),
argv: exec_argv,
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
});
}
if i < argv.len() {
i += 1; }
} else {
i += 1;
}
}
results
}
fn extract_xargs_command(cmd: &SimpleCommand) -> Option<SimpleCommand> {
let argv = &cmd.argv;
let mut i = 0;
while i < argv.len() {
let token = argv[i].text.as_str();
if token.starts_with('-') {
if ["-I", "-L", "-n", "-P", "-s", "-E", "-d"].contains(&token) {
i += 2; } else {
i += 1; }
} else {
break;
}
}
if i < argv.len() {
let xargs_cmd_name = argv[i].text.clone();
let xargs_cmd_argv: Vec<Arg> = argv[i + 1..].to_vec();
Some(SimpleCommand {
name: Some(xargs_cmd_name),
argv: xargs_cmd_argv,
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
})
} else {
None
}
}
pub fn extract_inner_commands(stmt: &Statement) -> Vec<Statement> {
let mut results = Vec::new();
collect_inner_commands(stmt, &mut results);
results
}
fn collect_inner_commands(stmt: &Statement, out: &mut Vec<Statement>) {
match stmt {
Statement::SimpleCommand(cmd) => {
let before_unwrap = out.len();
unwrap_recursive(cmd, out, 0);
let unwrap_end = out.len();
for sub in &cmd.embedded_substitutions {
collect_inner_commands(sub, out);
}
if let Some(ref name) = cmd.name {
let basename = wrapper_basename(name);
if basename == "find" {
for inner in extract_find_exec(cmd) {
out.push(Statement::SimpleCommand(inner.clone()));
unwrap_recursive(&inner, out, 0);
}
} else if basename == "xargs" {
if let Some(inner) = extract_xargs_command(cmd) {
out.push(Statement::SimpleCommand(inner.clone()));
unwrap_recursive(&inner, out, 0);
}
}
}
let cmds_to_shell_c: Vec<SimpleCommand> = {
let mut v = vec![cmd.clone()];
for st in &out[before_unwrap..unwrap_end] {
if let Statement::SimpleCommand(sc) = st {
v.push(sc.clone());
}
}
v
};
for sc in cmds_to_shell_c {
if let Some(inner) = crate::parser::shell_c::unwrap_shell_c(&sc) {
collect_shell_c_recursive(&inner, out, 0);
}
}
}
Statement::Pipeline(p) => {
for stage in &p.stages {
collect_inner_commands(stage, out);
}
}
Statement::List(l) => {
collect_inner_commands(&l.first, out);
for (_, s) in &l.rest {
collect_inner_commands(s, out);
}
}
Statement::Subshell(inner) | Statement::CommandSubstitution(inner) => {
collect_inner_commands(inner, out);
}
Statement::Opaque(_) | Statement::Empty => {}
}
}
fn collect_shell_c_recursive(stmt: &Statement, out: &mut Vec<Statement>, depth: usize) {
if depth >= MAX_UNWRAP_DEPTH {
out.push(Statement::Opaque(
"wrapper depth limit exceeded".to_string(),
));
return;
}
out.push(stmt.clone());
match stmt {
Statement::SimpleCommand(inner_cmd) => {
unwrap_recursive(inner_cmd, out, 0);
for sub in &inner_cmd.embedded_substitutions {
collect_inner_commands(sub, out);
}
if let Some(nested) = crate::parser::shell_c::unwrap_shell_c(inner_cmd) {
collect_shell_c_recursive(&nested, out, depth + 1);
}
}
Statement::Pipeline(p) => {
for stage in &p.stages {
collect_inner_commands(stage, out);
}
}
Statement::List(l) => {
collect_inner_commands(&l.first, out);
for (_, s) in &l.rest {
collect_inner_commands(s, out);
}
}
Statement::Subshell(inner) | Statement::CommandSubstitution(inner) => {
collect_inner_commands(inner, out);
}
Statement::Opaque(_) | Statement::Empty => {}
}
}
fn unwrap_recursive(cmd: &SimpleCommand, out: &mut Vec<Statement>, depth: usize) {
if let Some(inner) = unwrap_transparent(cmd) {
if depth >= MAX_UNWRAP_DEPTH {
out.push(Statement::Opaque(
"wrapper depth limit exceeded".to_string(),
));
return;
}
out.push(Statement::SimpleCommand(inner.clone()));
unwrap_recursive(&inner, out, depth + 1);
}
}
#[cfg(test)]
mod tests {
use super::super::Arg;
use super::*;
fn make_cmd(name: &str, argv: &[&str]) -> SimpleCommand {
SimpleCommand {
name: Some(name.to_string()),
argv: argv.iter().map(|s| Arg::plain(*s)).collect(),
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
}
}
#[test]
fn test_wrapper_basename_plain() {
assert_eq!(wrapper_basename("timeout"), "timeout");
}
#[test]
fn test_wrapper_basename_absolute() {
assert_eq!(wrapper_basename("/usr/bin/timeout"), "timeout");
}
#[test]
fn test_wrapper_basename_relative() {
assert_eq!(wrapper_basename("./env"), "env");
}
#[test]
fn test_wrapper_basename_nested() {
assert_eq!(wrapper_basename("/usr/local/bin/nice"), "nice");
}
#[test]
fn test_find_wrapper_known() {
assert!(find_wrapper("timeout").is_some());
assert!(find_wrapper("nice").is_some());
assert!(find_wrapper("env").is_some());
assert!(find_wrapper("nohup").is_some());
assert!(find_wrapper("strace").is_some());
}
#[test]
fn test_find_wrapper_unknown() {
assert!(find_wrapper("ls").is_none());
assert!(find_wrapper("rm").is_none());
assert!(find_wrapper("cargo").is_none());
}
#[test]
fn test_find_wrapper_with_path() {
assert!(find_wrapper("/usr/bin/env").is_some());
assert!(find_wrapper("./timeout").is_some());
}
#[test]
fn test_env_assignment_valid() {
assert!(is_env_assignment("FOO=bar"));
assert!(is_env_assignment("_FOO=bar"));
assert!(is_env_assignment("FOO123=bar"));
assert!(is_env_assignment("PATH=/usr/bin:/usr/local/bin"));
assert!(is_env_assignment("FOO=bar=baz"));
assert!(is_env_assignment("FOO="));
assert!(is_env_assignment("A=1"));
}
#[test]
fn test_env_assignment_invalid() {
assert!(!is_env_assignment("1FOO=bar"));
assert!(!is_env_assignment("=bar"));
assert!(!is_env_assignment("FOO"));
assert!(!is_env_assignment("--foo=bar"));
assert!(!is_env_assignment("-f=bar"));
assert!(!is_env_assignment(""));
assert!(!is_env_assignment("FOO-BAR=baz"));
}
#[test]
fn test_timeout_basic() {
let cmd = make_cmd("timeout", &["30", "ls", "-la"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
assert_eq!(inner.argv, vec![Arg::plain("-la")]);
}
#[test]
fn test_timeout_with_signal_flag() {
let cmd = make_cmd("timeout", &["-s", "KILL", "30", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
assert!(inner.argv.is_empty());
}
#[test]
fn test_timeout_with_signal_eq() {
let cmd = make_cmd("timeout", &["--signal=TERM", "10", "echo", "hi"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("echo"));
assert_eq!(inner.argv, vec![Arg::plain("hi")]);
}
#[test]
fn test_timeout_with_kill_after() {
let cmd = make_cmd("timeout", &["-k", "5", "30", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_timeout_with_kill_after_eq() {
let cmd = make_cmd("timeout", &["--kill-after=5", "30", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_timeout_with_bool_flags() {
let cmd = make_cmd(
"timeout",
&["--preserve-status", "--foreground", "--verbose", "30", "ls"],
);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_timeout_all_flags() {
let cmd = make_cmd(
"timeout",
&[
"-s",
"KILL",
"-k",
"5",
"--verbose",
"--preserve-status",
"--foreground",
"30",
"ls",
],
);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_timeout_no_inner_command() {
let cmd = make_cmd("timeout", &["30"]);
assert!(unwrap_transparent(&cmd).is_none());
}
#[test]
fn test_timeout_empty_argv() {
let cmd = make_cmd("timeout", &[]);
assert!(unwrap_transparent(&cmd).is_none());
}
#[test]
fn test_timeout_preserves_inner_argv() {
let cmd = make_cmd("timeout", &["30", "rm", "-rf", "/"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("rm"));
assert_eq!(inner.argv, vec![Arg::plain("-rf"), Arg::plain("/")]);
}
#[test]
fn test_timeout_double_dash() {
let cmd = make_cmd("timeout", &["--", "30", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_nice_basic() {
let cmd = make_cmd("nice", &["ls", "-la"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
assert_eq!(inner.argv, vec![Arg::plain("-la")]);
}
#[test]
fn test_nice_with_n_flag() {
let cmd = make_cmd("nice", &["-n", "10", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_nice_with_n_combined() {
let cmd = make_cmd("nice", &["-n10", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_nice_with_adjustment_eq() {
let cmd = make_cmd("nice", &["--adjustment=5", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_nice_negative_priority() {
let cmd = make_cmd("nice", &["-n", "-5", "echo", "hello"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("echo"));
assert_eq!(inner.argv, vec![Arg::plain("hello")]);
}
#[test]
fn test_nice_bare() {
let cmd = make_cmd("nice", &[]);
assert!(unwrap_transparent(&cmd).is_none());
}
#[test]
fn test_env_with_assignment() {
let cmd = make_cmd("env", &["FOO=bar", "echo", "hello"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("echo"));
assert_eq!(inner.argv, vec![Arg::plain("hello")]);
}
#[test]
fn test_env_multi_assignments() {
let cmd = make_cmd("env", &["FOO=1", "BAR=2", "BAZ=three", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
assert!(inner.argv.is_empty());
}
#[test]
fn test_env_with_i_flag() {
let cmd = make_cmd("env", &["-i", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_env_with_u_flag() {
let cmd = make_cmd("env", &["-u", "HOME", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_env_with_unset_eq() {
let cmd = make_cmd("env", &["--unset=HOME", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_env_multiple_u_flags() {
let cmd = make_cmd("env", &["-u", "HOME", "-u", "USER", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_env_flags_then_assignments() {
let cmd = make_cmd(
"env",
&["-i", "-u", "HOME", "PATH=/usr/bin", "FOO=bar", "ls"],
);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_env_val_with_equals() {
let cmd = make_cmd("env", &["FOO=bar=baz", "echo", "hello"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("echo"));
}
#[test]
fn test_env_empty_val() {
let cmd = make_cmd("env", &["FOO=", "echo", "hello"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("echo"));
}
#[test]
fn test_env_invalid_var_name_digit() {
let cmd = make_cmd("env", &["1FOO=bar"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("1FOO=bar"));
}
#[test]
fn test_env_only_assignments_no_inner() {
let cmd = make_cmd("env", &["FOO=bar"]);
assert!(unwrap_transparent(&cmd).is_none());
}
#[test]
fn test_env_only_assignments_multi_no_inner() {
let cmd = make_cmd("env", &["FOO=bar", "BAZ=1"]);
assert!(unwrap_transparent(&cmd).is_none());
}
#[test]
fn test_env_bare() {
let cmd = make_cmd("env", &[]);
assert!(unwrap_transparent(&cmd).is_none());
}
#[test]
fn test_env_only_flags_no_inner() {
let cmd = make_cmd("env", &["-i"]);
assert!(unwrap_transparent(&cmd).is_none());
}
#[test]
fn test_env_absolute_path() {
let cmd = make_cmd("/usr/bin/env", &["FOO=bar", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_env_relative_path() {
let cmd = make_cmd("./env", &["FOO=bar", "ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_nohup_basic() {
let cmd = make_cmd("nohup", &["echo", "hello"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("echo"));
assert_eq!(inner.argv, vec![Arg::plain("hello")]);
}
#[test]
fn test_nohup_bare() {
let cmd = make_cmd("nohup", &[]);
assert!(unwrap_transparent(&cmd).is_none());
}
#[test]
fn test_strace_basic() {
let cmd = make_cmd("strace", &["ls"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_strace_with_f() {
let cmd = make_cmd("strace", &["-f", "ls", "-la"]);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
assert_eq!(inner.argv, vec![Arg::plain("-la")]);
}
#[test]
fn test_strace_with_value_flags() {
let cmd = make_cmd(
"strace",
&["-e", "trace=open", "-o", "/tmp/trace.log", "ls"],
);
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
}
#[test]
fn test_strace_p_pid_no_inner() {
let cmd = make_cmd("strace", &["-p", "1234"]);
assert!(unwrap_transparent(&cmd).is_none());
}
#[test]
fn test_strace_bare() {
let cmd = make_cmd("strace", &[]);
assert!(unwrap_transparent(&cmd).is_none());
}
#[test]
fn test_non_wrapper_returns_none() {
let cmd = make_cmd("ls", &["-la"]);
assert!(unwrap_transparent(&cmd).is_none());
}
#[test]
fn test_no_name_returns_none() {
let cmd = SimpleCommand {
name: None,
argv: vec![Arg::plain("foo")],
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
};
assert!(unwrap_transparent(&cmd).is_none());
}
#[test]
fn test_redirects_propagated() {
use crate::parser::{Redirect, RedirectOp};
let cmd = SimpleCommand {
name: Some("timeout".to_string()),
argv: vec![Arg::plain("30"), Arg::plain("ls")],
redirects: vec![Redirect {
fd: None,
op: RedirectOp::Write,
target: "/tmp/out".to_string(),
}],
assignments: vec![],
embedded_substitutions: vec![],
};
let inner = unwrap_transparent(&cmd).unwrap();
assert_eq!(inner.name.as_deref(), Some("ls"));
assert_eq!(inner.redirects.len(), 1);
assert_eq!(inner.redirects[0].target, "/tmp/out");
}
#[test]
fn test_extract_from_simple_wrapper() {
let stmt = Statement::SimpleCommand(make_cmd("timeout", &["30", "ls"]));
let inners = extract_inner_commands(&stmt);
assert_eq!(inners.len(), 1);
if let Statement::SimpleCommand(ref cmd) = inners[0] {
assert_eq!(cmd.name.as_deref(), Some("ls"));
} else {
panic!("Expected SimpleCommand");
}
}
#[test]
fn test_extract_from_non_wrapper() {
let stmt = Statement::SimpleCommand(make_cmd("ls", &["-la"]));
let inners = extract_inner_commands(&stmt);
assert!(inners.is_empty());
}
#[test]
fn test_extract_chained_two_deep() {
let stmt = Statement::SimpleCommand(make_cmd("env", &["VAR=1", "timeout", "30", "ls"]));
let inners = extract_inner_commands(&stmt);
assert_eq!(inners.len(), 2);
if let Statement::SimpleCommand(ref cmd) = inners[0] {
assert_eq!(cmd.name.as_deref(), Some("timeout"));
} else {
panic!("Expected SimpleCommand at [0]");
}
if let Statement::SimpleCommand(ref cmd) = inners[1] {
assert_eq!(cmd.name.as_deref(), Some("ls"));
} else {
panic!("Expected SimpleCommand at [1]");
}
}
#[test]
fn test_extract_chained_three_deep() {
let stmt = Statement::SimpleCommand(make_cmd(
"env",
&["VAR=1", "timeout", "30", "nice", "-n", "5", "ls"],
));
let inners = extract_inner_commands(&stmt);
assert_eq!(inners.len(), 3);
if let Statement::SimpleCommand(ref cmd) = inners[2] {
assert_eq!(cmd.name.as_deref(), Some("ls"));
} else {
panic!("Expected SimpleCommand at [2]");
}
}
#[test]
fn test_extract_from_pipeline() {
use crate::parser::Pipeline;
let stmt = Statement::Pipeline(Pipeline {
stages: vec![
Statement::SimpleCommand(make_cmd("timeout", &["30", "cat", "file.txt"])),
Statement::SimpleCommand(make_cmd("grep", &["pattern"])),
],
negated: false,
});
let inners = extract_inner_commands(&stmt);
assert_eq!(inners.len(), 1);
if let Statement::SimpleCommand(ref cmd) = inners[0] {
assert_eq!(cmd.name.as_deref(), Some("cat"));
} else {
panic!("Expected SimpleCommand");
}
}
#[test]
fn test_extract_from_list() {
use crate::parser::{List, ListOp};
let stmt = Statement::List(List {
first: Box::new(Statement::SimpleCommand(make_cmd("timeout", &["30", "ls"]))),
rest: vec![(
ListOp::And,
Statement::SimpleCommand(make_cmd("nice", &["echo", "done"])),
)],
});
let inners = extract_inner_commands(&stmt);
assert_eq!(inners.len(), 2); }
#[test]
fn test_extract_depth_limit() {
let mut argv: Vec<&str> = Vec::new();
for i in 0..18 {
if i % 2 == 0 {
argv.push("nice");
} else {
argv.push("nohup");
}
}
argv.extend_from_slice(&["rm", "-rf", "/"]);
let stmt = Statement::SimpleCommand(make_cmd("nice", &argv[1..]));
let inners = extract_inner_commands(&stmt);
let last = inners.last().unwrap();
assert!(
matches!(last, Statement::Opaque(_)),
"Depth limit should produce Opaque, got: {:?}",
last
);
}
#[test]
fn test_extract_within_depth_limit() {
let stmt = Statement::SimpleCommand(make_cmd("nice", &["nice", "nice", "ls"]));
let inners = extract_inner_commands(&stmt);
assert_eq!(inners.len(), 3);
if let Statement::SimpleCommand(ref cmd) = inners[2] {
assert_eq!(cmd.name.as_deref(), Some("ls"));
} else {
panic!("Expected SimpleCommand(ls)");
}
}
#[test]
fn test_unwrap_preserves_arg_meta() {
use super::super::{Arg, ArgMeta};
let cmd = SimpleCommand {
name: Some("timeout".to_string()),
argv: vec![
Arg::plain("30"),
Arg::plain("bash"),
Arg::plain("-c"),
Arg {
text: "$VAR".to_string(),
meta: ArgMeta::UnsafeString,
},
],
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
};
let inner = unwrap_transparent(&cmd).expect("unwrap");
assert_eq!(inner.name.as_deref(), Some("bash"));
assert_eq!(inner.argv.len(), 2);
assert_eq!(inner.argv[0].text, "-c");
assert_eq!(inner.argv[0].meta, ArgMeta::PlainWord);
assert_eq!(inner.argv[1].text, "$VAR");
assert_eq!(inner.argv[1].meta, ArgMeta::UnsafeString);
}
#[test]
fn test_find_exec_preserves_arg_meta() {
use super::super::ArgMeta;
let cmd = SimpleCommand {
name: Some("find".to_string()),
argv: vec![
Arg::plain("."),
Arg::plain("-exec"),
Arg::plain("cat"),
Arg {
text: "$VAR".to_string(),
meta: ArgMeta::UnsafeString,
},
Arg::plain(";"),
],
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
};
let inners = extract_find_exec(&cmd);
assert_eq!(inners.len(), 1);
let inner = &inners[0];
assert_eq!(inner.name.as_deref(), Some("cat"));
assert_eq!(inner.argv.len(), 1);
assert_eq!(inner.argv[0].text, "$VAR");
assert_eq!(inner.argv[0].meta, ArgMeta::UnsafeString);
}
#[test]
fn test_xargs_preserves_arg_meta() {
use super::super::ArgMeta;
let cmd = SimpleCommand {
name: Some("xargs".to_string()),
argv: vec![
Arg::plain("cat"),
Arg {
text: "$VAR".to_string(),
meta: ArgMeta::UnsafeString,
},
],
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
};
let inner = extract_xargs_command(&cmd).expect("extract");
assert_eq!(inner.name.as_deref(), Some("cat"));
assert_eq!(inner.argv.len(), 1);
assert_eq!(inner.argv[0].text, "$VAR");
assert_eq!(inner.argv[0].meta, ArgMeta::UnsafeString);
}
}