struct Spec {
code_flags: &'static [&'static str],
file_flags: &'static [&'static str],
one_value: &'static [&'static str],
two_values: &'static [&'static str],
safe_form: &'static str,
}
fn spec(stem: &str) -> Option<Spec> {
Some(match stem {
"awk" | "gawk" | "mawk" | "nawk" => Spec {
code_flags: &["-e", "--source"],
file_flags: &["-f", "--file"],
one_value: &[
"-v",
"--assign",
"-F",
"--field-separator",
"-i",
"--include",
],
two_values: &[],
safe_form: "pass it with -v name=value and read `name` in the program",
},
"sed" | "gsed" => Spec {
code_flags: &["-e", "--expression"],
file_flags: &["-f", "--file"],
one_value: &["-l", "--line-length"],
two_values: &[],
safe_form: "sed can't take a value safely; use awk -v, or a script reading \"$1\"",
},
"jq" | "gojq" | "jaq" => Spec {
code_flags: &[],
file_flags: &["-f", "--from-file"],
one_value: &["--indent", "--tab"],
two_values: &["--arg", "--argjson", "--slurpfile", "--rawfile"],
safe_form: "pass it with --arg name value and read $name in the filter",
},
_ => return None,
})
}
pub(crate) fn program_text_problems(command: &str, stem: &str, args: &[&str]) -> Vec<String> {
let Some(spec) = spec(stem) else {
return Vec::new();
};
program_texts(&spec, args)
.into_iter()
.filter_map(|text| {
let found = bnto_core::placeholders::data_placeholders(text);
let first = found.first()?;
Some(format!(
"templates can't go inside {command} program text: '{first}' would run data \
as code. {}",
spec.safe_form
))
})
.collect()
}
fn program_texts<'a>(spec: &Spec, args: &[&'a str]) -> Vec<&'a str> {
let mut texts = Vec::new();
let (mut i, mut given, mut options) = (0, false, true);
while let Some(arg) = args.get(i).copied() {
let next = args.get(i + 1).copied();
if options && arg == "--" {
options = false;
} else if options && arg.starts_with('-') && arg.len() > 1 {
let codes: Vec<_> = spec
.code_flags
.iter()
.filter_map(|flag| crate::interpreter_code::code_after(arg, flag, next))
.collect();
given |= !codes.is_empty() || flag_matches(spec.file_flags, arg);
texts.extend(codes);
i += skipped_values(spec, arg);
} else if !given {
texts.push(arg);
break;
} else {
break;
}
i += 1;
}
texts
}
fn skipped_values(spec: &Spec, arg: &str) -> usize {
if spec.two_values.contains(&arg) {
2
} else if spec.code_flags.contains(&arg)
|| spec.file_flags.contains(&arg)
|| spec.one_value.contains(&arg)
{
1
} else {
0
}
}
fn flag_matches(flags: &[&str], arg: &str) -> bool {
flags.iter().any(|flag| {
arg == *flag
|| arg.strip_prefix(flag).is_some_and(|rest| {
if flag.starts_with("--") {
rest.starts_with('=')
} else {
!rest.is_empty()
}
})
})
}
#[cfg(test)]
mod tests;