use std::ffi::OsString;
use std::io::Write;
use std::path::Path;
use std::process::ExitCode;
use crate::{Sponge, SpongeBuilder, Target};
const STRICT_USAGE_BANNER: &str =
"sponge [-a] <file>: soak up all input from stdin and write it to <file>\n";
pub fn run(argv: &[OsString]) -> ExitCode {
let parsed = parse_argv(argv);
if parsed.show_usage {
let stdout = std::io::stdout();
let mut out = stdout.lock();
let _ = out.write_all(STRICT_USAGE_BANNER.as_bytes());
return ExitCode::SUCCESS;
}
if let Some(unk) = parsed.unknown_letters.first() {
let stderr = std::io::stderr();
let mut err = stderr.lock();
let _ = writeln!(err, "sponge: invalid option -- '{unk}'");
}
let target = match parsed.target {
Some(path) => Target::File(path.into()),
None => Target::Stdout,
};
if let Target::File(ref path) = target {
if let Ok(meta) = std::fs::symlink_metadata(path) {
if meta.is_dir() {
let stderr = std::io::stderr();
let mut err = stderr.lock();
let _ = writeln!(err, "error opening output file: Is a directory");
return ExitCode::from(1);
}
}
}
let builder = SpongeBuilder::new()
.target(target)
.append(parsed.append)
.compat(crate::CompatibilityMode::Strict);
let mut sponge: Sponge = match builder.build() {
Ok(s) => s,
Err(_e) => {
return ExitCode::SUCCESS;
}
};
let stdin = std::io::stdin();
let locked = stdin.lock();
match sponge.run(locked) {
Ok(()) => ExitCode::SUCCESS,
Err(crate::Error::TargetIsDirectory(_)) => {
let stderr = std::io::stderr();
let mut err = stderr.lock();
let _ = writeln!(err, "error opening output file: Is a directory");
ExitCode::from(1)
}
Err(crate::Error::Io(io_err)) => {
let stderr = std::io::stderr();
let mut err = stderr.lock();
let _ = writeln!(err, "error opening output file: {io_err}");
ExitCode::from(1)
}
Err(_) => ExitCode::from(1),
}
}
struct StrictArgs {
append: bool,
show_usage: bool,
unknown_letters: Vec<char>,
target: Option<OsString>,
}
fn parse_argv(argv: &[OsString]) -> StrictArgs {
let mut out = StrictArgs {
append: false,
show_usage: false,
unknown_letters: Vec::new(),
target: None,
};
let mut iter = argv.iter().skip(1);
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() {
if out.target.is_none() {
out.target = Some(rest.clone());
}
}
break;
}
if s.starts_with('-') && s.len() >= 2 && !s.starts_with("--") {
for c in s.chars().skip(1) {
match c {
'a' => out.append = true,
'h' => out.show_usage = true,
other => {
if out.unknown_letters.is_empty() {
out.unknown_letters.push(other);
}
}
}
}
continue;
}
if s.starts_with("--") {
if out.unknown_letters.is_empty() {
out.unknown_letters.push('-');
}
continue;
}
if out.target.is_none() {
out.target = Some(arg.clone());
}
}
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
}
#[allow(dead_code)]
fn argv0_basename(argv0: &Path) -> Option<&std::ffi::OsStr> {
argv0.file_stem()
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(parts: &[&str]) -> Vec<OsString> {
parts.iter().map(|s| OsString::from(*s)).collect()
}
#[test]
fn parse_no_args_means_stdout_target() {
let p = parse_argv(&argv(&["sponge"]));
assert!(!p.append);
assert!(!p.show_usage);
assert!(p.unknown_letters.is_empty());
assert!(p.target.is_none());
}
#[test]
fn parse_dash_h_sets_show_usage() {
let p = parse_argv(&argv(&["sponge", "-h"]));
assert!(p.show_usage);
}
#[test]
fn parse_dash_a_sets_append() {
let p = parse_argv(&argv(&["sponge", "-a", "file.txt"]));
assert!(p.append);
assert_eq!(p.target.as_deref(), Some(std::ffi::OsStr::new("file.txt")));
}
#[test]
fn parse_dash_x_records_one_unknown_letter() {
let p = parse_argv(&argv(&["sponge", "-x"]));
assert_eq!(p.unknown_letters, vec!['x']);
}
#[test]
fn parse_grouped_unknown_records_only_first() {
let p = parse_argv(&argv(&["sponge", "-xyz"]));
assert_eq!(p.unknown_letters, vec!['x']);
}
#[test]
fn parse_long_unknown_flag_records_dash() {
let p = parse_argv(&argv(&["sponge", "--some-flag"]));
assert_eq!(p.unknown_letters, vec!['-']);
}
#[test]
fn parse_first_positional_wins() {
let p = parse_argv(&argv(&["sponge", "first.txt", "second.txt", "third.txt"]));
assert_eq!(p.target.as_deref(), Some(std::ffi::OsStr::new("first.txt")));
}
#[test]
fn parse_double_dash_consumes_first_positional_only() {
let p = parse_argv(&argv(&["sponge", "--", "-a", "file.txt"]));
assert!(!p.append, "-a after -- is a positional, not a flag");
assert_eq!(p.target.as_deref(), Some(std::ffi::OsStr::new("-a")));
}
#[test]
fn pre_scan_detects_strict() {
assert_eq!(
pre_scan_strict_flag(&argv(&["rusty-sponge", "--strict", "out.txt"])),
Some(true)
);
}
#[test]
fn pre_scan_detects_no_strict() {
assert_eq!(
pre_scan_strict_flag(&argv(&["rusty-sponge", "--no-strict", "out.txt"])),
Some(false)
);
}
#[test]
fn pre_scan_returns_none_when_neither() {
assert_eq!(
pre_scan_strict_flag(&argv(&["rusty-sponge", "out.txt"])),
None
);
}
#[test]
fn pre_scan_last_occurrence_wins() {
assert_eq!(
pre_scan_strict_flag(&argv(&["rusty-sponge", "--strict", "--no-strict"])),
Some(false)
);
}
}