use super::super::SimpleCommand;
use super::super::piped::Piped;
use crate::shell::ClassifyError;
const MAX_RUNS: usize = 256;
#[derive(Default)]
struct Options {
replace: Option<String>,
delimiter: Option<char>,
batches: bool,
from_file: bool,
}
pub(super) fn runs(
cmd: &SimpleCommand,
argv: &[String],
inner: &[String],
) -> Result<Vec<Vec<String>>, ClassifyError> {
let options = parse(argv, inner.len());
if options.from_file || cmd.piped.iter().any(Piped::is_hidden) {
return Ok(Vec::new());
}
let input: Vec<&str> = cmd
.stdin_texts()
.chain(cmd.piped.iter().filter_map(Piped::text))
.collect();
if input.is_empty() {
return Ok(Vec::new());
}
let input = input.join("\n");
if let Some(replace) = &options.replace {
let lines: Vec<&str> = input
.lines()
.map(str::trim_start)
.filter(|l| !l.is_empty())
.collect();
if lines.len() > MAX_RUNS {
return Err(ClassifyError::TooManyActions);
}
return Ok(lines
.iter()
.map(|line| {
inner
.iter()
.map(|a| a.replace(replace.as_str(), line))
.collect()
})
.collect());
}
let items = items(&input, options.delimiter);
let all = inner.iter().cloned().chain(items.iter().cloned()).collect();
let mut runs = vec![all];
if options.batches {
if items.len() > MAX_RUNS {
return Err(ClassifyError::TooManyActions);
}
runs.extend(items.iter().map(|item| {
let mut run = inner.to_vec();
run.push(item.clone());
run
}));
}
Ok(runs)
}
fn parse(argv: &[String], inner_len: usize) -> Options {
let mut options = Options::default();
let end = argv.len().saturating_sub(inner_len);
let mut words = argv.get(1..end).unwrap_or_default().iter();
while let Some(arg) = words.next() {
let mut value = |attached: &str| {
if attached.is_empty() {
words.next().cloned().unwrap_or_default()
} else {
attached.to_owned()
}
};
if let Some(long) = arg.strip_prefix("--") {
let (name, attached) = long.split_once('=').unwrap_or((long, ""));
match name {
"replace" => {
options.replace = Some(
Some(attached)
.filter(|a| !a.is_empty())
.unwrap_or("{}")
.to_owned(),
);
}
"null" => options.delimiter = Some('\0'),
"delimiter" => options.delimiter = delimiter(&value(attached)),
"max-args" | "max-lines" => options.batches = true,
"arg-file" => options.from_file = true,
_ => {}
}
continue;
}
let Some(cluster) = arg.strip_prefix('-') else {
continue;
};
for (pos, c) in cluster.char_indices() {
let rest = &cluster[pos + c.len_utf8()..];
match c {
'0' => options.delimiter = Some('\0'),
'I' | 'J' => options.replace = Some(value(rest)),
'i' => options.replace = Some(if rest.is_empty() { "{}" } else { rest }.to_owned()),
'd' => options.delimiter = delimiter(&value(rest)),
'n' | 'L' | 'l' => options.batches = true,
'a' => options.from_file = true,
_ => continue,
}
if c != '0' {
break;
}
}
}
options
}
fn delimiter(value: &str) -> Option<char> {
match value {
"\\n" => Some('\n'),
"\\t" => Some('\t'),
"\\0" => Some('\0'),
_ => value.chars().next(),
}
}
fn items(input: &str, delimiter: Option<char>) -> Vec<String> {
if let Some(d) = delimiter {
return input
.split(d)
.filter(|i| !i.is_empty())
.map(str::to_owned)
.collect();
}
let mut items = Vec::new();
let mut current = String::new();
let mut quote: Option<char> = None;
let mut chars = input.chars();
while let Some(c) = chars.next() {
match (quote, c) {
(Some(q), c) if c == q => quote = None,
(None, '\'' | '"') => quote = Some(c),
(None, '\\') => current.extend(chars.next()),
(None, c) if c.is_whitespace() => {
if !current.is_empty() {
items.push(std::mem::take(&mut current));
}
}
(_, c) => current.push(c),
}
}
if !current.is_empty() {
items.push(current);
}
items
}
#[cfg(test)]
mod tests {
use super::{items, parse};
fn argv(line: &str) -> Vec<String> {
line.split(' ').map(str::to_owned).collect()
}
#[test]
fn options_are_read_as_xargs_reads_them() {
let o = parse(&argv("xargs -I {} cp {} x"), 3);
assert_eq!(o.replace.as_deref(), Some("{}"));
let o = parse(&argv("xargs -I% cp % x"), 3);
assert_eq!(o.replace.as_deref(), Some("%"));
let o = parse(&argv("xargs -i cp {} x"), 3);
assert_eq!(o.replace.as_deref(), Some("{}"));
let o = parse(&argv("xargs --replace=@ cp @ x"), 3);
assert_eq!(o.replace.as_deref(), Some("@"));
let o = parse(&argv("xargs -0n1 sh -c"), 2);
assert!(o.batches && o.delimiter == Some('\0'));
assert!(parse(&argv("xargs -a list cat"), 1).from_file);
let o = parse(&argv("xargs -t cat"), 1);
assert!(o.replace.is_none() && !o.batches && !o.from_file);
}
#[test]
fn items_split_at_blanks_outside_quotes() {
assert_eq!(
items("a 'b c'\n\"d\" e\\ f", None),
["a", "b c", "d", "e f"]
);
assert_eq!(items("a b\nc d\n", Some('\n')), ["a b", "c d"]);
}
}