use std::ffi::OsString;
use std::io::Write;
use std::process::ExitCode;
use crate::{aggregate, fanout, spawner};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UnknownFlag {
Short(char),
Long(String),
}
pub fn format_unknown_flag(flag: &UnknownFlag) -> String {
match flag {
UnknownFlag::Short(c) => format!("rusty-pee: invalid option -- '{c}'"),
UnknownFlag::Long(name) => format!("rusty-pee: unknown option -- '{name}'"),
}
}
pub fn format_spawn_failure(cmd: &str) -> String {
format!("pee: Can not open pipe to '{cmd}'")
}
pub fn run(argv: &[OsString]) -> ExitCode {
let parsed = match parse_argv(argv) {
Ok(p) => p,
Err(unk) => {
let msg = format_unknown_flag(&unk);
let _ = writeln!(std::io::stderr().lock(), "{msg}");
return ExitCode::from(2);
}
};
if parsed.commands.is_empty() {
use std::io::Read;
let stdin = std::io::stdin();
let mut handle = stdin.lock();
let mut buf = vec![0u8; crate::BUFSIZ];
loop {
match handle.read(&mut buf) {
Ok(0) => break,
Ok(_) => {}
Err(_) => return ExitCode::from(1),
}
}
return ExitCode::SUCCESS;
}
let mut children = Vec::with_capacity(parsed.commands.len());
for cmd in &parsed.commands {
match spawner::spawn_one(cmd) {
Ok(c) => children.push(c),
Err(_) => {
let _ = writeln!(std::io::stderr().lock(), "{}", format_spawn_failure(cmd));
for mut c in children.into_iter() {
let _ = c.kill();
let _ = c.wait();
}
return ExitCode::from(1);
}
}
}
let stdin = std::io::stdin();
let statuses = match fanout::run(stdin.lock(), children) {
Ok(s) => s,
Err(_) => return ExitCode::from(1),
};
let codes: Vec<i32> = statuses.iter().map(|s| s.code().unwrap_or(1)).collect();
let aggregated = aggregate::strict_or(&codes);
let byte = if (0..=255).contains(&aggregated) {
aggregated as u8
} else {
1u8
};
ExitCode::from(byte)
}
#[derive(Debug, Default)]
struct StrictArgs {
commands: Vec<String>,
}
fn parse_argv(argv: &[OsString]) -> Result<StrictArgs, UnknownFlag> {
let mut out = StrictArgs::default();
let mut iter = argv.iter().skip(1);
let mut first_positional = true;
while let Some(arg) = iter.next() {
let s = arg.to_string_lossy();
if s == "--strict" || s == "--no-strict" {
continue;
}
if s == "--" {
for rest in iter.by_ref() {
out.commands.push(rest.to_string_lossy().into_owned());
}
break;
}
if first_positional && s == "completions" {
return Err(UnknownFlag::Long(String::from("completions")));
}
if let Some(rest) = s.strip_prefix("--") {
let flag_name = rest.split('=').next().unwrap_or(rest).to_string();
return Err(UnknownFlag::Long(flag_name));
}
if let Some(rest) = s.strip_prefix('-') {
if !rest.is_empty() {
let first = rest.chars().next().expect("non-empty after strip_prefix");
return Err(UnknownFlag::Short(first));
}
}
first_positional = false;
out.commands.push(s.into_owned());
}
Ok(out)
}
pub fn pre_scan_strict_flag(argv: &[OsString]) -> Option<bool> {
let mut chosen: Option<bool> = None;
for arg in argv.iter().skip(1) {
let s = arg.to_string_lossy();
if s == "--strict" {
chosen = Some(true);
} else if s == "--no-strict" {
chosen = Some(false);
} else if s == "--" {
break;
}
}
chosen
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(parts: &[&str]) -> Vec<OsString> {
parts.iter().map(|s| OsString::from(*s)).collect()
}
#[test]
fn parse_no_flags_yields_no_commands() {
let r = parse_argv(&argv(&["pee"])).unwrap();
assert!(r.commands.is_empty());
}
#[test]
fn parse_two_commands() {
let r = parse_argv(&argv(&["pee", "wc -l", "grep foo"])).unwrap();
assert_eq!(r.commands, vec!["wc -l", "grep foo"]);
}
#[test]
fn parse_rejects_help() {
let err = parse_argv(&argv(&["pee", "--help"])).unwrap_err();
assert_eq!(err, UnknownFlag::Long(String::from("help")));
}
#[test]
fn parse_rejects_version() {
let err = parse_argv(&argv(&["pee", "--version"])).unwrap_err();
assert_eq!(err, UnknownFlag::Long(String::from("version")));
}
#[test]
fn parse_rejects_capture() {
let err = parse_argv(&argv(&["pee", "--capture"])).unwrap_err();
assert_eq!(err, UnknownFlag::Long(String::from("capture")));
}
#[test]
fn parse_rejects_completions_subcommand() {
let err = parse_argv(&argv(&["pee", "completions", "bash"])).unwrap_err();
assert_eq!(err, UnknownFlag::Long(String::from("completions")));
}
#[test]
fn parse_completions_after_other_positional_treated_as_command() {
let r = parse_argv(&argv(&["pee", "wc -l", "completions"])).unwrap();
assert_eq!(r.commands, vec!["wc -l", "completions"]);
}
#[test]
fn parse_rejects_unknown_long_flag() {
let err = parse_argv(&argv(&["pee", "--foo"])).unwrap_err();
assert_eq!(err, UnknownFlag::Long(String::from("foo")));
}
#[test]
fn parse_rejects_unknown_short_flag() {
let err = parse_argv(&argv(&["pee", "-x"])).unwrap_err();
assert_eq!(err, UnknownFlag::Short('x'));
}
#[test]
fn parse_first_unknown_wins() {
let err = parse_argv(&argv(&["pee", "-x", "--foo"])).unwrap_err();
assert_eq!(err, UnknownFlag::Short('x'));
let err = parse_argv(&argv(&["pee", "--foo", "-x"])).unwrap_err();
assert_eq!(err, UnknownFlag::Long(String::from("foo")));
}
#[test]
fn parse_double_dash_treats_rest_as_commands() {
let r = parse_argv(&argv(&["pee", "--", "--help", "-x"])).unwrap();
assert_eq!(
r.commands,
vec!["--help", "-x"],
"after `--` everything is a command"
);
}
#[test]
fn pre_scan_detects_strict() {
assert_eq!(
pre_scan_strict_flag(&argv(&["rusty-pee", "--strict"])),
Some(true)
);
}
#[test]
fn pre_scan_stops_at_double_dash() {
assert_eq!(
pre_scan_strict_flag(&argv(&["rusty-pee", "--", "--strict"])),
None
);
}
#[test]
fn format_unknown_short_matches_spec() {
assert_eq!(
format_unknown_flag(&UnknownFlag::Short('x')),
"rusty-pee: invalid option -- 'x'"
);
}
#[test]
fn format_unknown_long_matches_spec() {
assert_eq!(
format_unknown_flag(&UnknownFlag::Long(String::from("foo"))),
"rusty-pee: unknown option -- 'foo'"
);
}
#[test]
fn format_spawn_failure_uses_two_word_can_not() {
assert_eq!(
format_spawn_failure("nonexistent"),
"pee: Can not open pipe to 'nonexistent'"
);
}
}