123 lines
3.7 KiB
Rust
123 lines
3.7 KiB
Rust
//! Build sequential child commands from quoted or NUL-delimited input.
|
|
use sausage_spawn::{Command, Stdio};
|
|
use std::io::{self, Read};
|
|
fn run() -> Result<i32, Box<dyn std::error::Error>> {
|
|
let args: Vec<_> = std::env::args().skip(1).collect();
|
|
let mut i = 0;
|
|
let mut count = usize::MAX;
|
|
let mut null = false;
|
|
let mut no_empty = false;
|
|
let mut replace = None;
|
|
while i < args.len() {
|
|
match args[i].as_str() {
|
|
"-I" => {
|
|
i += 1;
|
|
replace = Some(args.get(i).ok_or("missing -I argument")?.clone());
|
|
count = 1;
|
|
}
|
|
arg if arg.starts_with("-I") => {
|
|
replace = Some(arg[2..].to_owned());
|
|
count = 1;
|
|
}
|
|
"-0" => null = true,
|
|
"-r" => no_empty = true,
|
|
"-n" => {
|
|
i += 1;
|
|
count = args.get(i).ok_or("missing -n argument")?.parse()?;
|
|
}
|
|
"--" => {
|
|
i += 1;
|
|
break;
|
|
}
|
|
arg if arg.starts_with("-n") => count = arg[2..].parse()?,
|
|
arg if arg.starts_with('-') => return Err(format!("unsupported option: {arg}").into()),
|
|
_ => break,
|
|
}
|
|
i += 1;
|
|
}
|
|
if count == 0 {
|
|
return Err("-n must be positive".into());
|
|
}
|
|
let argv = if i == args.len() {
|
|
vec!["echo".into()]
|
|
} else {
|
|
args[i..].to_vec()
|
|
};
|
|
let mut input = String::new();
|
|
io::stdin().read_to_string(&mut input)?;
|
|
let values = if null {
|
|
input.split_terminator('\0').map(str::to_owned).collect()
|
|
} else if replace.is_some() {
|
|
input
|
|
.split_terminator('\n')
|
|
.map(|line| line.trim_start_matches([' ', '\t']))
|
|
.filter(|line| !line.is_empty())
|
|
.map(replacement)
|
|
.collect::<Result<Vec<_>, _>>()?
|
|
} else {
|
|
shlex::split(&input).ok_or("unmatched quote in input")?
|
|
};
|
|
if values.is_empty() && (no_empty || replace.is_some()) {
|
|
return Ok(0);
|
|
}
|
|
let mut result = 0;
|
|
let batches = if values.is_empty() {
|
|
vec![&[][..]]
|
|
} else {
|
|
values.chunks(count).collect()
|
|
};
|
|
for batch in batches {
|
|
let command = if let Some(ref token) = replace {
|
|
argv.iter()
|
|
.map(|arg| arg.replace(token, &batch.join(" ")))
|
|
.collect::<Vec<_>>()
|
|
} else {
|
|
argv.iter().cloned().chain(batch.iter().cloned()).collect()
|
|
};
|
|
match Command::new(&command[0])
|
|
.args(&command[1..])
|
|
.stdin(Stdio::Null)
|
|
.status()
|
|
{
|
|
Ok(0) => (),
|
|
Ok(255) => return Ok(124),
|
|
Ok(_) => result = 123,
|
|
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(127),
|
|
Err(_) => return Ok(126),
|
|
}
|
|
}
|
|
Ok(result)
|
|
}
|
|
fn replacement(line: &str) -> Result<String, Box<dyn std::error::Error>> {
|
|
let mut result = String::new();
|
|
let mut quote = None;
|
|
let mut escaped = false;
|
|
for ch in line.chars() {
|
|
if escaped {
|
|
result.push(ch);
|
|
escaped = false;
|
|
} else if ch == '\\' && quote != Some('\'') {
|
|
escaped = true;
|
|
} else if quote == Some(ch) {
|
|
quote = None;
|
|
} else if quote.is_none() && (ch == '\'' || ch == '"') {
|
|
quote = Some(ch);
|
|
} else {
|
|
result.push(ch);
|
|
}
|
|
}
|
|
if quote.is_some() || escaped {
|
|
return Err("unmatched quote or escape in input".into());
|
|
}
|
|
Ok(result)
|
|
}
|
|
fn main() {
|
|
std::process::exit(match run() {
|
|
Ok(code) => code,
|
|
Err(e) => {
|
|
eprintln!("xargs: {e}");
|
|
1
|
|
}
|
|
});
|
|
}
|