use super::{ClassifyError, ShellContext, Sink, awk, sed};
use crate::action::AtomicAction;
#[derive(Debug, Default)]
pub(super) struct Operands {
pub data: Vec<usize>,
pub writes: Vec<usize>,
}
pub(super) struct Spec {
pub flags: &'static str,
pub valued: &'static str,
pub optional: &'static str,
pub long_flags: &'static [&'static str],
pub long_valued: &'static [&'static str],
pub long_optional: &'static [&'static str],
pub permute: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Arg<'a> {
Opt {
name: &'a str,
value: Option<&'a str>,
data: Option<usize>,
},
Operand(usize),
Unknown(&'a str),
}
pub(super) fn classify(
argv: &[String],
program: &str,
ctx: &ShellContext<'_>,
sink: &mut Sink,
) -> Result<Operands, ClassifyError> {
let (mut ops, operands) = match program {
"sed" => return sed::classify(argv, ctx, sink),
"awk" => return awk::classify(argv, sink),
"sort" => options(argv, &SORT, SORT_WRITES, ctx, sink)?,
"uniq" => options(argv, &UNIQ, &[], ctx, sink)?,
"cut" => options(argv, &CUT, &[], ctx, sink)?,
_ => return Ok(Operands::default()),
};
if program == "uniq" {
ops.writes.extend(operands.get(1));
}
Ok(ops)
}
const SORT_WRITES: &[&str] = &["o", "T", "output", "temporary-directory"];
const SORT: Spec = Spec {
flags: "bcCdfghiMmnRrsuVz",
valued: "kStTo",
optional: "",
long_flags: &[
"ignore-leading-blanks",
"dictionary-order",
"ignore-case",
"general-numeric-sort",
"ignore-nonprinting",
"month-sort",
"human-numeric-sort",
"numeric-sort",
"random-sort",
"reverse",
"version-sort",
"check",
"merge",
"stable",
"unique",
"zero-terminated",
"debug",
],
long_valued: &[
"key",
"sort",
"buffer-size",
"field-separator",
"parallel",
"output",
"temporary-directory",
],
long_optional: &[],
permute: true,
};
const UNIQ: Spec = Spec {
flags: "cdDiuz",
valued: "fsw",
optional: "",
long_flags: &[
"count",
"repeated",
"ignore-case",
"unique",
"zero-terminated",
],
long_valued: &["skip-fields", "skip-chars", "check-chars"],
long_optional: &["all-repeated", "group"],
permute: true,
};
const CUT: Spec = Spec {
flags: "nsz",
valued: "bcdf",
optional: "",
long_flags: &["complement", "only-delimited", "zero-terminated"],
long_valued: &[
"bytes",
"characters",
"delimiter",
"fields",
"output-delimiter",
],
long_optional: &[],
permute: true,
};
fn options(
argv: &[String],
spec: &Spec,
writes: &[&str],
ctx: &ShellContext<'_>,
sink: &mut Sink,
) -> Result<(Operands, Vec<usize>), ClassifyError> {
let mut ops = Operands::default();
let mut operands = Vec::new();
for arg in parse(argv, spec) {
match arg {
Arg::Opt { name, value, data } => {
ops.data.extend(data);
if let Some(path) = value.filter(|_| writes.contains(&name)) {
sink.write(ctx, path)?;
}
}
Arg::Unknown(word) => unproven(argv, word, sink)?,
Arg::Operand(i) => operands.push(i),
}
}
Ok((ops, operands))
}
pub(super) fn unproven(argv: &[String], text: &str, sink: &mut Sink) -> Result<(), ClassifyError> {
sink.push(AtomicAction::Shell {
argv: vec![argv[0].clone(), format!("@{text}")],
})
}
pub(super) fn parse<'a>(argv: &'a [String], spec: &Spec) -> Vec<Arg<'a>> {
let mut parsed = Vec::new();
let mut at = 1;
let mut options_ended = false;
let mut seen_operand = false;
while let Some(word) = argv.get(at) {
let next = argv.get(at + 1).map(String::as_str);
let separate = (!seen_operand).then_some(at + 1);
if options_ended || word == "-" || !word.starts_with('-') {
parsed.push(Arg::Operand(at));
seen_operand = true;
options_ended |= !spec.permute;
} else if word == "--" {
options_ended = true;
} else {
let (arg, took_next) = match word.strip_prefix("--") {
Some(long) => long_option(word, long, next, spec, at, separate),
None => short_cluster(word, next, spec, at, separate, &mut parsed),
};
parsed.push(arg);
at += usize::from(took_next);
}
at += 1;
}
parsed
}
fn long_option<'a>(
word: &'a str,
long: &'a str,
next: Option<&'a str>,
spec: &Spec,
at: usize,
separate: Option<usize>,
) -> (Arg<'a>, bool) {
let (name, attached) = match long.split_once('=') {
Some((name, value)) => (name, Some(value)),
None => (long, None),
};
let opt = |value, data| Arg::Opt { name, value, data };
if spec.long_flags.contains(&name) && attached.is_none() {
(opt(None, None), false)
} else if spec.long_optional.contains(&name) {
(opt(attached, Some(at)), false)
} else if !spec.long_valued.contains(&name) {
(Arg::Unknown(word), false)
} else if attached.is_some() {
(opt(attached, Some(at)), false)
} else if next.is_some() {
(opt(next, separate), true)
} else {
(Arg::Unknown(word), false)
}
}
fn short_cluster<'a>(
word: &'a str,
next: Option<&'a str>,
spec: &Spec,
at: usize,
separate: Option<usize>,
args: &mut Vec<Arg<'a>>,
) -> (Arg<'a>, bool) {
let cluster = &word[1..];
for (pos, c) in cluster.char_indices() {
let name = &cluster[pos..pos + c.len_utf8()];
let rest = &cluster[pos + c.len_utf8()..];
let opt = |value, data| Arg::Opt { name, value, data };
if spec.flags.contains(c) {
if rest.is_empty() {
return (opt(None, None), false);
}
args.push(opt(None, None));
} else if spec.optional.contains(c) {
return (opt(Some(rest).filter(|r| !r.is_empty()), Some(at)), false);
} else if spec.valued.contains(c) && !rest.is_empty() {
return (opt(Some(rest), Some(at)), false);
} else if spec.valued.contains(c) && next.is_some() {
return (opt(next, separate), true);
} else {
break;
}
}
(Arg::Unknown(word), false)
}
#[cfg(test)]
mod tests {
use super::{Arg, SORT, parse};
fn argv(line: &str) -> Vec<String> {
line.split(' ').map(str::to_owned).collect()
}
#[test]
fn options_values_and_operands() {
let a = argv("sort -rnk 2 data.txt -o out --key=3 -- -x");
let opt = |name, value, data| Arg::Opt { name, value, data };
assert_eq!(
parse(&a, &SORT),
[
opt("r", None, None),
opt("n", None, None),
opt("k", Some("2"), Some(2)),
Arg::Operand(3),
opt("o", Some("out"), None),
opt("key", Some("3"), Some(6)),
Arg::Operand(8),
]
);
}
#[test]
fn unknown_options_and_missing_values() {
for line in [
"sort --compress-program=gzip",
"sort --comp=x",
"sort -y",
"sort -k",
] {
let a = argv(line);
assert!(
matches!(parse(&a, &SORT).as_slice(), [Arg::Unknown(_)]),
"{line}"
);
}
}
}