Merge pull request #323 from sylvestre/replace

xargs: Implement replace / -I (Closes: #310)
This commit is contained in:
Daniel Hofstetter
2024-04-03 14:38:01 +02:00
committed by GitHub
4 changed files with 262 additions and 60 deletions
Generated
+23
View File
@@ -215,6 +215,12 @@ dependencies = [
"parking_lot_core",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "difflib"
version = "0.4.0"
@@ -285,6 +291,7 @@ dependencies = [
"once_cell",
"onig",
"predicates",
"pretty_assertions",
"regex",
"serial_test",
"tempfile",
@@ -591,6 +598,16 @@ dependencies = [
"treeline",
]
[[package]]
name = "pretty_assertions"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro2"
version = "1.0.60"
@@ -1058,3 +1075,9 @@ name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
+1
View File
@@ -26,6 +26,7 @@ nix = { version = "0.28", features = ["fs"] }
predicates = "3"
serial_test = "3.0"
tempfile = "3"
pretty_assertions = "1.4.0"
[[bin]]
name = "find"
+87 -26
View File
@@ -28,6 +28,8 @@ mod options {
pub const MAX_PROCS: &str = "max-procs";
pub const NO_RUN_IF_EMPTY: &str = "no-run-if-empty";
pub const NULL: &str = "null";
pub const REPLACE: &str = "replace";
pub const REPLACE_I: &str = "replace-I";
pub const VERBOSE: &str = "verbose";
}
@@ -40,6 +42,7 @@ struct Options {
max_lines: Option<usize>,
no_run_if_empty: bool,
null: bool,
replace: Option<String>,
verbose: bool,
}
@@ -340,13 +343,14 @@ struct CommandBuilderOptions {
limiters: LimiterCollection,
verbose: bool,
close_stdin: bool,
replace: Option<String>,
}
impl CommandBuilderOptions {
fn new(
action: ExecAction,
env: HashMap<OsString, OsString>,
mut limiters: LimiterCollection,
replace: Option<String>,
) -> Result<Self, ExhaustedCommandSpace> {
let initial_args = match &action {
ExecAction::Command(args) => args.iter().map(|arg| arg.as_ref()).collect(),
@@ -366,6 +370,7 @@ impl CommandBuilderOptions {
limiters,
verbose: false,
close_stdin: false,
replace,
})
}
}
@@ -398,14 +403,41 @@ impl CommandBuilder<'_> {
};
let mut command = Command::new(entry_point);
command
.args(initial_args)
.args(&self.extra_args)
.env_clear()
.envs(&self.options.env);
if let Some(replace_str) = &self.options.replace {
// we replace the first instance of the replacement string with
// the extra args, and then replace all instances of the replacement
let replacement = self
.extra_args
.iter()
.map(|s| s.to_string_lossy())
.collect::<Vec<_>>()
.join(" ");
let initial_args: Vec<OsString> = initial_args
.iter()
.map(|arg| {
let arg_str = arg.to_string_lossy();
OsString::from(arg_str.replace(replace_str, &replacement))
})
.collect();
command
.args(&initial_args)
.env_clear()
.envs(&self.options.env);
} else {
// don't do any replacement
command
.args(initial_args)
.args(&self.extra_args)
.env_clear()
.envs(&self.options.env);
};
if self.options.close_stdin {
command.stdin(Stdio::null());
}
if self.options.verbose {
eprintln!("{command:?}");
}
@@ -692,12 +724,14 @@ fn process_input(
}
fn parse_delimiter(s: &str) -> Result<u8, String> {
if let Some(hex) = s.strip_prefix("\\x") {
u8::from_str_radix(hex, 16).map_err(|e| format!("Invalid hex sequence: {}", e))
} else if let Some(oct) = s.strip_prefix("\\0") {
u8::from_str_radix(oct, 8).map_err(|e| format!("Invalid octal sequence: {}", e))
} else if let Some(special) = s.strip_prefix('\\') {
match special {
match s.strip_prefix('\\') {
Some(hex) if hex.starts_with('x') => {
u8::from_str_radix(&hex[1..], 16).map_err(|e| format!("Invalid hex sequence: {}", e))
}
Some(oct) if oct.starts_with('0') => {
u8::from_str_radix(&oct[1..], 8).map_err(|e| format!("Invalid octal sequence: {}", e))
}
Some(special) => match special {
"a" => Ok(b'\x07'),
"b" => Ok(b'\x08'),
"f" => Ok(b'\x0C'),
@@ -705,17 +739,12 @@ fn parse_delimiter(s: &str) -> Result<u8, String> {
"r" => Ok(b'\r'),
"t" => Ok(b'\t'),
"v" => Ok(b'\x0B'),
"0" => Ok(b'\0'),
"\\" => Ok(b'\\'),
_ => Err(format!("Invalid escape sequence: {s}")),
}
} else {
let bytes = s.as_bytes();
if bytes.len() == 1 {
Ok(bytes[0])
} else {
Err("Delimiter must be one byte".to_owned())
}
"0" => Ok(b'\0'),
_ => Err(format!("Invalid escape sequence: \\{}", special)),
},
None if s.len() == 1 => Ok(s.as_bytes()[0]),
None => Err("Delimiter must be one byte".to_owned()),
}
}
@@ -819,6 +848,28 @@ fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
.help("Be verbose")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::REPLACE)
.long(options::REPLACE)
.short('i')
.num_args(0..=1)
.require_equals(true)
.value_parser(clap::value_parser!(String))
.value_name("R")
.help(
"Replace R in INITIAL-ARGS with names read from standard input; \
if R is unspecified, assume {}",
),
)
.arg(
Arg::new(options::REPLACE_I)
.short('I')
.num_args(1)
.help("same as --replace=R")
.value_name("R")
.overrides_with(options::REPLACE)
.value_parser(clap::value_parser!(String)),
)
.try_get_matches_from(args);
let matches = match matches {
@@ -837,6 +888,16 @@ fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
max_lines: matches.get_one::<usize>(options::MAX_LINES).copied(),
no_run_if_empty: matches.get_flag(options::NO_RUN_IF_EMPTY),
null: matches.get_flag(options::NULL),
replace: [options::REPLACE_I, options::REPLACE]
.iter()
.find_map(|&option| {
matches.contains_id(option).then(|| {
matches
.get_one::<String>(option)
.map(|value| value.to_owned())
.unwrap_or_else(|| "{}".to_string())
})
}),
verbose: matches.get_flag(options::VERBOSE),
};
@@ -861,7 +922,6 @@ fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
}
_ => ExecAction::Echo,
};
let env = std::env::vars_os().collect();
let mut limiters = LimiterCollection::new();
@@ -891,9 +951,10 @@ fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
limiters.add(MaxCharsCommandSizeLimiter::new_system(&env));
let mut builder_options = CommandBuilderOptions::new(action, env, limiters).map_err(|_| {
"Base command and environment are too large to fit into one command execution"
})?;
let mut builder_options =
CommandBuilderOptions::new(action, env, limiters, options.replace.clone()).map_err(
|_| "Base command and environment are too large to fit into one command execution",
)?;
builder_options.verbose = options.verbose;
builder_options.close_stdin = options.arg_file.is_none();
+151 -34
View File
@@ -12,6 +12,7 @@ use assert_cmd::Command;
use predicates::prelude::*;
use common::test_helpers::path_to_testing_commandline;
use pretty_assertions::assert_eq;
mod common;
@@ -208,7 +209,7 @@ fn xargs_exit_on_large() {
#[test]
fn xargs_exec() {
Command::cargo_bin("xargs")
let result = Command::cargo_bin("xargs")
.expect("found binary")
.args([
"-n2",
@@ -218,13 +219,20 @@ fn xargs_exec() {
"--no_print_cwd",
])
.write_stdin("a b c\nd")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff(
"stdin=\nargs=\n--print_stdin\n--no_print_cwd\na\nb\n\
.output();
assert!(result.is_ok(), "xargs failed: {:?}", result);
let result = result.unwrap();
assert_eq!(result.status.code(), Some(0));
assert!(result.stderr.is_empty(), "stderr: {:?}", result);
let stdout_string = String::from_utf8(result.stdout).expect("Found invalid UTF-8");
assert_eq!(
stdout_string,
"stdin=\nargs=\n--print_stdin\n--no_print_cwd\na\nb\n\
stdin=\nargs=\n--print_stdin\n--no_print_cwd\nc\nd\n",
));
);
}
#[test]
@@ -234,7 +242,7 @@ fn xargs_exec_stdin_open() {
write!(temp_file, "a b c").unwrap();
temp_file.seek(SeekFrom::Start(0)).unwrap();
Command::cargo_bin("xargs")
let result = Command::cargo_bin("xargs")
.expect("found binary")
.args([
"-a",
@@ -245,17 +253,25 @@ fn xargs_exec_stdin_open() {
"--no_print_cwd",
])
.write_stdin("test")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff(
"stdin=test\nargs=\n--print_stdin\n--no_print_cwd\na\nb\nc\n",
));
.output();
assert!(result.is_ok(), "xargs failed: {:?}", result);
let result = result.unwrap();
assert_eq!(result.status.code(), Some(0));
assert!(result.stderr.is_empty(), "stderr: {:?}", result);
let stdout_string = String::from_utf8(result.stdout).expect("Found invalid UTF-8");
assert_eq!(
stdout_string,
"stdin=test\nargs=\n--print_stdin\n--no_print_cwd\na\nb\nc\n",
);
}
#[test]
fn xargs_exec_failure() {
Command::cargo_bin("xargs")
let result = Command::cargo_bin("xargs")
.expect("found binary")
.args([
"-n1",
@@ -265,19 +281,26 @@ fn xargs_exec_failure() {
"--exit_with_failure",
])
.write_stdin("a b")
.assert()
.failure()
.code(123)
.stderr(predicate::str::is_empty())
.stdout(
"args=\n--no_print_cwd\n--exit_with_failure\na\n\
.output();
assert!(result.is_ok(), "xargs failed: {:?}", result);
let result = result.unwrap();
assert_eq!(result.status.code(), Some(123));
assert!(result.stderr.is_empty(), "stderr: {:?}", result);
let stdout_string = String::from_utf8(result.stdout).expect("Found invalid UTF-8");
assert_eq!(
stdout_string,
"args=\n--no_print_cwd\n--exit_with_failure\na\n\
args=\n--no_print_cwd\n--exit_with_failure\nb\n",
);
);
}
#[test]
fn xargs_exec_urgent_failure() {
Command::cargo_bin("xargs")
let result = Command::cargo_bin("xargs")
.expect("found binary")
.args([
"-n1",
@@ -287,17 +310,26 @@ fn xargs_exec_urgent_failure() {
"--exit_with_urgent_failure",
])
.write_stdin("a b")
.assert()
.failure()
.code(124)
.stderr(predicate::str::contains("Error:"))
.stdout("args=\n--no_print_cwd\n--exit_with_urgent_failure\na\n");
.output();
assert!(result.is_ok(), "xargs failed: {:?}", result);
let result = result.unwrap();
assert_eq!(result.status.code(), Some(124));
assert!(!result.stderr.is_empty(), "stderr: {:?}", result);
let stdout_string = String::from_utf8(result.stdout).expect("Found invalid UTF-8");
assert_eq!(
stdout_string,
"args=\n--no_print_cwd\n--exit_with_urgent_failure\na\n"
);
}
#[test]
#[cfg(unix)]
fn xargs_exec_with_signal() {
Command::cargo_bin("xargs")
let result = Command::cargo_bin("xargs")
.expect("found binary")
.args([
"-n1",
@@ -307,11 +339,19 @@ fn xargs_exec_with_signal() {
"--exit_with_signal",
])
.write_stdin("a b")
.assert()
.failure()
.code(125)
.stderr(predicate::str::contains("Error:"))
.stdout("args=\n--no_print_cwd\n--exit_with_signal\na\n");
.output();
assert!(result.is_ok(), "xargs failed: {:?}", result);
let result = result.unwrap();
assert_eq!(result.status.code(), Some(125));
assert!(!result.stderr.is_empty(), "stderr: {:?}", result);
let stdout_string = String::from_utf8(result.stdout).expect("Found invalid UTF-8");
assert_eq!(
stdout_string,
"args=\n--no_print_cwd\n--exit_with_signal\na\n"
);
}
#[test]
@@ -385,3 +425,80 @@ fn xargs_zero_lines() {
.stderr(predicate::str::contains("Value must be > 0, not: 0"))
.stdout(predicate::str::is_empty());
}
#[test]
fn xargs_replace() {
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-i={}", "echo", "{} bar"])
.write_stdin("foo")
.assert()
.stdout(predicate::str::contains("foo bar"));
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-i=_", "echo", "_ bar"])
.write_stdin("foo")
.assert()
.stdout(predicate::str::contains("foo bar"));
Command::cargo_bin("xargs")
.expect("found binary")
.args(["--replace=_", "echo", "_ _ bar"])
.write_stdin("foo")
.assert()
.stdout(predicate::str::contains("foo foo bar"));
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-i=_", "echo", "_ _ bar"])
.write_stdin("foo")
.assert()
.stdout(predicate::str::contains("foo foo bar"));
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-i", "echo", "{} {} bar"])
.write_stdin("foo")
.assert()
.stdout(predicate::str::contains("foo foo bar"));
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-I={}", "echo", "{} bar {}"])
.write_stdin("foo")
.assert()
.stdout(predicate::str::contains("foo bar foo"));
// Combine the two options to see which one wins
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-I=_", "-i", "echo", "{} bar {}"])
.write_stdin("foo")
.assert()
.stdout(predicate::str::contains("foo bar foo"));
// other order
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-i", "-I=_", "echo", "{} bar {}"])
.write_stdin("foo")
.assert()
.stdout(predicate::str::contains("{} bar {}"));
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-i", "-I", "_", "echo", "{} bar _"])
.write_stdin("foo")
.assert()
.stdout(predicate::str::contains("{} bar foo"));
// Expected to fail
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-I", "echo", "_ _ bar"])
.write_stdin("foo")
.assert()
.failure()
.stderr(predicate::str::contains("Error: Command not found"));
}