diff --git a/src/xargs/mod.rs b/src/xargs/mod.rs index 44c9a19..b8f6ea1 100644 --- a/src/xargs/mod.rs +++ b/src/xargs/mod.rs @@ -21,6 +21,8 @@ mod options { pub const ARG_FILE: &str = "arg-file"; pub const DELIMITER: &str = "delimiter"; + pub const EOF: &str = "eof"; + pub const EOF_E: &str = "eof-E"; pub const EXIT: &str = "exit"; pub const MAX_ARGS: &str = "max-args"; pub const MAX_CHARS: &str = "max-chars"; @@ -44,6 +46,7 @@ struct Options { null: bool, replace: Option, verbose: bool, + eof_delimiter: Option, } #[derive(Debug, PartialEq, Eq)] @@ -637,6 +640,39 @@ where } } +struct EofArgumentReader { + reader: Box, + eof_delimiter: OsString, + eof_found: bool, +} + +impl EofArgumentReader { + fn new(reader: Box, eof_delimiter: &String) -> Self { + Self { + reader, + eof_delimiter: eof_delimiter.into(), + eof_found: false, + } + } +} + +impl ArgumentReader for EofArgumentReader { + fn next(&mut self) -> io::Result> { + Ok(if self.eof_found { + None + } else { + self.reader.next()?.and_then(|arg| { + if arg.arg == self.eof_delimiter { + self.eof_found = true; + None + } else { + Some(arg) + } + }) + }) + } +} + #[derive(Debug)] enum XargsError { ArgumentTooLarge, @@ -952,6 +988,27 @@ fn do_xargs(args: &[&str]) -> Result { .overrides_with(options::REPLACE) .value_parser(clap::value_parser!(String)), ) + .arg( + Arg::new(options::EOF) + .short('E') + .num_args(1) + .value_name("eof-string") + .help( + "If specified, stop processing the input upon reaching an input \ + item that matches eof-string", + ) + .value_parser(clap::value_parser!(String)), + ) + .arg( + Arg::new(options::EOF_E) + .short('e') + .long(options::EOF) + .num_args(0..=1) + .value_name("eof-string") + .help("Alias for -E") + .overrides_with(options::EOF) + .value_parser(clap::value_parser!(String)), + ) .try_get_matches_from(args); let matches = match matches { @@ -988,6 +1045,13 @@ fn do_xargs(args: &[&str]) -> Result { }) }), verbose: matches.get_flag(options::VERBOSE), + eof_delimiter: [options::EOF_E, options::EOF].iter().find_map(|&option| { + matches.contains_id(option).then(|| { + matches + .get_one::(option) + .map_or_else(|| "{}".to_string(), std::borrow::ToOwned::to_owned) + }) + }), }; let (max_args, max_lines, replace, delimiter) = normalize_options(&options, &matches); @@ -1026,12 +1090,16 @@ fn do_xargs(args: &[&str]) -> Result { Box::new(io::stdin()) }; - let args: Box = if let Some(delimiter) = delimiter { + let mut args: Box = if let Some(delimiter) = delimiter { Box::new(ByteDelimitedArgumentReader::new(args_file, delimiter)) } else { Box::new(WhitespaceDelimitedArgumentReader::new(args_file)) }; + if let Some(eof_delimiter) = options.eof_delimiter { + args = Box::new(EofArgumentReader::new(args, &eof_delimiter)); + } + let result = process_input( &builder_options, args, @@ -1293,6 +1361,47 @@ mod tests { assert_eq!(reader.next().unwrap(), None); } + #[test] + fn test_eof_argument_reader() { + let filter = String::from("def"); + + let reader = WhitespaceDelimitedArgumentReader::new(ChunkReader::new(vec![Chunk::Data( + b"abc def ghi", + )])); + let mut wrapper = EofArgumentReader::new(Box::new(reader), &filter); + assert_eq!(wrapper.next().unwrap().unwrap(), make_arg_soft("abc")); + assert_eq!(wrapper.next().unwrap(), None); + assert_eq!(wrapper.next().unwrap(), None); + + let reader = WhitespaceDelimitedArgumentReader::new(ChunkReader::new(vec![Chunk::Data( + b"abc define undef undefined ghi", + )])); + let mut wrapper = EofArgumentReader::new(Box::new(reader), &filter); + assert_eq!(wrapper.next().unwrap().unwrap(), make_arg_soft("abc")); + assert_eq!(wrapper.next().unwrap().unwrap(), make_arg_soft("define")); + assert_eq!(wrapper.next().unwrap().unwrap(), make_arg_soft("undef")); + assert_eq!(wrapper.next().unwrap().unwrap(), make_arg_soft("undefined")); + assert_eq!(wrapper.next().unwrap().unwrap(), make_arg_soft("ghi")); + assert_eq!(wrapper.next().unwrap(), None); + + let reader = WhitespaceDelimitedArgumentReader::new(ChunkReader::new(vec![ + Chunk::Data(b"abc "), + Chunk::Error(io::ErrorKind::Interrupted), + Chunk::Data(b"deF "), + Chunk::Error(io::ErrorKind::BrokenPipe), + Chunk::Data(b"ghi "), + Chunk::Data(b"def "), + Chunk::Error(io::ErrorKind::BrokenPipe), + ])); + let mut wrapper = EofArgumentReader::new(Box::new(reader), &filter); + assert_eq!(wrapper.next().unwrap().unwrap(), make_arg_soft("abc")); + assert_eq!(wrapper.next().unwrap().unwrap(), make_arg_soft("deF")); + assert!(wrapper.next().err().unwrap().kind() == io::ErrorKind::BrokenPipe); + assert_eq!(wrapper.next().unwrap().unwrap(), make_arg_soft("ghi")); + assert_eq!(wrapper.next().unwrap(), None); + assert_eq!(wrapper.next().unwrap(), None); + } + #[test] fn test_byte_delimited_reader() { let mut reader = ByteDelimitedArgumentReader::new( diff --git a/tests/xargs_tests.rs b/tests/xargs_tests.rs index ad0a355..fbddf58 100644 --- a/tests/xargs_tests.rs +++ b/tests/xargs_tests.rs @@ -552,3 +552,16 @@ fn xargs_version() { .stdout(predicate::str::starts_with("xargs ")); } } + +#[test] +fn xargs_eof() { + for option_style in [vec!["-ecd"], vec!["-E", "cd"], vec!["--eof", "cd"]] { + cargo_bin_cmd!("xargs") + .args(option_style.as_slice()) + .write_stdin("ab cd ef") + .assert() + .success() + .stderr(predicate::str::is_empty()) + .stdout(predicate::str::diff("ab\n")); + } +}