use super::*;
fn v(words: &[&str]) -> Vec<String> {
words.iter().map(|w| (*w).to_string()).collect()
}
#[test]
fn the_verb_is_basename_normalised_for_two_names_and_no_others() {
assert_eq!(normalise_verb("/bin/rm"), "rm");
assert_eq!(normalise_verb("/usr/bin/rmdir"), "rmdir");
assert_eq!(normalise_verb("rm"), "rm");
assert_eq!(normalise_verb("rmdir"), "rmdir");
assert_eq!(normalise_verb("/bin/ls"), "/bin/ls");
assert_eq!(normalise_verb("/bin/rmx"), "/bin/rmx");
assert_eq!(normalise_verb("xargs"), "xargs");
assert_eq!(normalise_verb(""), "");
}
#[test]
fn the_prefix_peel_consumes_each_prefix_commands_own_tokens() {
assert_eq!(
strip_prefix_commands(&v(&["nohup", "rm", "-rf", "x"])),
v(&["rm", "-rf", "x"])
);
assert_eq!(
strip_prefix_commands(&v(&["nice", "-n", "rm", "y"])),
v(&["rm", "y"])
);
assert_eq!(
strip_prefix_commands(&v(&["env", "--", "rm", "y"])),
v(&["rm", "y"])
);
assert_eq!(
strip_prefix_commands(&v(&["env", "A=1", "rm", "y"])),
v(&["rm", "y"])
);
assert_eq!(
strip_prefix_commands(&v(&["time", "A=1", "rm"])),
v(&["A=1", "rm"])
);
assert_eq!(
strip_prefix_commands(&v(&["timeout", "30s", "rm", "y"])),
v(&["rm", "y"])
);
assert_eq!(
strip_prefix_commands(&v(&["timeout", "later", "rm"])),
v(&["later", "rm"])
);
assert_eq!(
strip_prefix_commands(&v(&["nohup", "nice", "rm"])),
v(&["rm"])
);
assert_eq!(
strip_prefix_commands(&v(&["rm", "-rf", "x"])),
v(&["rm", "-rf", "x"])
);
assert!(strip_prefix_commands(&[]).is_empty());
assert!(strip_prefix_commands(&v(&["nohup"])).is_empty());
assert!(strip_prefix_commands(&v(&["env", "A=1"])).is_empty());
}
#[test]
fn a_duration_is_digits_with_an_optional_unit_suffix() {
assert!(is_duration("30"));
assert!(is_duration("30s"));
assert!(is_duration("1.5h"));
assert!(is_duration("2d"));
assert!(!is_duration(""));
assert!(!is_duration("s"), "a bare unit has no number left");
assert!(!is_duration("later"));
assert!(!is_duration("30x"));
}
#[test]
fn the_positional_walk_stops_at_the_first_operand_or_a_double_dash() {
assert_eq!(
positional_operands(&v(&["-rf", "a", "-b"])),
v(&["a", "-b"])
);
assert_eq!(positional_operands(&v(&["-", "a"])), v(&["-", "a"]));
assert_eq!(
positional_operands(&v(&["--", "-rf", "a"])),
v(&["-rf", "a"])
);
assert!(positional_operands(&v(&["-r", "-f"])).is_empty());
assert!(positional_operands(&[]).is_empty());
}
#[test]
fn the_cd_flag_is_true_for_any_statement_that_is_a_cd() {
assert!(any_statement_is_cd("cd /tmp"));
assert!(any_statement_is_cd("echo hi; cd /tmp && rm -rf x"));
assert!(
any_statement_is_cd("nohup cd /tmp"),
"through the prefix peel"
);
assert!(!any_statement_is_cd("rm -rf /tmp/build"));
assert!(!any_statement_is_cd("cdx /tmp"), "cd is a whole word");
assert!(!any_statement_is_cd("echo cd"), "only as the verb");
}
#[test]
fn a_statements_argv_drops_its_assignments_and_its_redirections() {
assert_eq!(command_argv("A=1 B=2 rm -rf x"), v(&["rm", "-rf", "x"]));
assert_eq!(command_argv("rm -rf x > out"), v(&["rm", "-rf", "x"]));
assert_eq!(command_argv("rm -rf 2>/dev/null x"), v(&["rm", "-rf", "x"]));
assert_eq!(command_argv("rm A=1"), v(&["rm", "A=1"]));
}
#[test]
fn the_substitution_walk_extracts_each_body_exactly() {
assert_eq!(substitution_bodies("echo $(a b)"), v(&["a b"]));
assert_eq!(substitution_bodies("x `a b` y"), v(&["a b"]));
assert_eq!(substitution_bodies("diff <(a) >(b)"), v(&["a", "b"]));
assert_eq!(substitution_bodies("$(a $(b) c)"), v(&["a $(b) c"]));
assert_eq!(substitution_bodies("$(a)$(b)"), v(&["a", "b"]));
assert_eq!(substitution_bodies("`a``b`"), v(&["a", "b"]));
assert_eq!(substitution_bodies("$()"), v(&[""]));
assert_eq!(substitution_bodies("\\$(a) $(b)"), v(&["b"]));
assert!(substitution_bodies("echo $(a").is_empty());
assert!(substitution_bodies("echo `a").is_empty());
assert_eq!(substitution_bodies("a < b $(c)"), v(&["c"]));
assert!(substitution_bodies("echo plain").is_empty());
}
#[test]
fn decomposition_reaches_a_removal_inside_a_substitution() {
let out = simple_commands("echo $(rm x); ls");
assert!(out.iter().any(|s| s == "rm x"), "{out:?}");
assert!(out.iter().any(|s| s == "ls"), "{out:?}");
assert!(
out.iter().any(|s| s.contains("__CMDSUB_OUTPUT__")),
"{out:?}"
);
assert_eq!(out.len(), 3, "{out:?}");
}
#[test]
fn a_statement_the_split_cannot_balance_is_decomposed_whole() {
assert_eq!(simple_commands("echo \"a"), v(&["echo \"a"]));
assert_eq!(simple_commands("rm -rf $D/* )"), v(&["rm -rf $D/* )"]));
}
#[test]
fn the_decomposition_queue_stops_at_its_own_bound() {
let subs: String = (0..300).map(|i| format!("$(x{i})")).collect();
let out = simple_commands(&format!("echo {subs}"));
assert_eq!(out.len(), 256, "the bound is a count, not an approximation");
let small: String = (0..10).map(|i| format!("$(y{i})")).collect();
let ten = simple_commands(&format!("echo {small}"));
assert_eq!(ten.len(), 11, "{ten:?}");
for i in 0..10 {
assert!(ten.iter().any(|s| s == &format!("y{i}")), "{ten:?}");
}
}
#[test]
fn the_substitution_splice_replaces_each_body_with_the_sentinel() {
assert_eq!(strip_substitutions("echo $(a b)"), "echo __CMDSUB_OUTPUT__");
assert_eq!(strip_substitutions("x `a` y"), "x __CMDSUB_OUTPUT__ y");
assert_eq!(
strip_substitutions("diff <(a) >(b)"),
"diff __CMDSUB_OUTPUT__ __CMDSUB_OUTPUT__"
);
assert_eq!(strip_substitutions("$(a $(b) c)"), "__CMDSUB_OUTPUT__");
assert_eq!(
strip_substitutions("$(a)$(b)"),
"__CMDSUB_OUTPUT____CMDSUB_OUTPUT__"
);
for unchanged in [
"\\$(a)",
"echo $(a",
"echo `a",
"echo $",
"a < b",
"rm -rf /tmp",
] {
assert_eq!(strip_substitutions(unchanged), unchanged, "{unchanged:?}");
}
}
#[test]
fn a_trailing_delimiter_is_not_an_opener() {
for tail in ["echo $", "a <", "a >", "$", "<", ">"] {
assert!(substitution_bodies(tail).is_empty(), "{tail:?}");
assert_eq!(strip_substitutions(tail), tail, "{tail:?}");
}
assert!(substitution_bodies("$(a").is_empty());
assert_eq!(strip_substitutions("$(a"), "$(a");
}
#[test]
fn a_dollar_is_an_opener_only_when_a_paren_follows_it() {
assert_eq!(strip_substitutions("$x (y)"), "$x (y)");
assert!(substitution_bodies("$x (y)").is_empty());
}
#[test]
fn a_backslash_with_nothing_after_it_escapes_nothing() {
assert_eq!(strip_substitutions("a\\"), "a\\");
assert_eq!(argv_words("rm a\\"), v(&["rm", "a\\"]));
assert_eq!(argv_words("rm \"a\\"), v(&["rm", "a\\"]));
}
#[test]
fn the_substitution_walk_resumes_past_the_closing_paren() {
assert_eq!(substitution_bodies("$(a`)`"), v(&["a`"]));
}
#[test]
fn argv_words_strips_quotes_and_sentinels_every_expansion() {
assert_eq!(argv_words("rm -rf x"), v(&["rm", "-rf", "x"]));
assert_eq!(argv_words("rm \"a b\""), v(&["rm", "a b"]));
assert_eq!(argv_words("rm 'a b'"), v(&["rm", "a b"]));
assert_eq!(argv_words("rm \"\""), v(&["rm", ""]));
assert_eq!(argv_words("rm \"$D/x\""), v(&["rm", "__TRACKED_VAR__/x"]));
assert_eq!(argv_words("rm '$D/x'"), v(&["rm", "$D/x"]));
assert_eq!(argv_words("rm ${D}/x"), v(&["rm", "__TRACKED_VAR__/x"]));
assert_eq!(
argv_words("rm $A$B"),
v(&["rm", "__TRACKED_VAR____TRACKED_VAR__"])
);
assert_eq!(argv_words("rm $"), v(&["rm", "$"]));
assert_eq!(
argv_words("rm $HOME/x"),
v(&["rm", "__CSIFT_ENV_VALUE__/x"])
);
assert_eq!(argv_words("rm a\\ b"), v(&["rm", "a b"]));
assert_eq!(argv_words("rm \"a\\\"b\" c"), v(&["rm", "a\"b", "c"]));
assert_eq!(argv_words("rm \"a b\" c"), v(&["rm", "a b", "c"]));
assert_eq!(argv_words("rm x "), v(&["rm", "x"]));
assert!(argv_words(" ").is_empty());
assert!(argv_words("").is_empty());
}
#[test]
fn an_expansion_reports_the_characters_it_consumed() {
let at = |s: &str| {
let chars: Vec<char> = s.chars().collect();
expansion_at(&chars, 0)
};
assert_eq!(at("$D/x"), ("__TRACKED_VAR__".to_string(), 2));
assert_eq!(at("${D}/x"), ("__TRACKED_VAR__".to_string(), 4));
assert_eq!(at("$HOME/x"), ("__CSIFT_ENV_VALUE__".to_string(), 5));
assert_eq!(at("$"), ("$".to_string(), 1));
assert_eq!(at("$/x"), ("$".to_string(), 1));
assert_eq!(at("${D"), ("__TRACKED_VAR__".to_string(), 3));
assert_eq!(at("${HOME}/x"), ("__CSIFT_ENV_VALUE__".to_string(), 7));
assert_eq!(
at("${BASH_VERSION}"),
("__CSIFT_ENV_VALUE__".to_string(), 15)
);
}