Skip to content

Commit ed2cbe6

Browse files
committed
xargs: Implement -I to replace
1 parent 5588ff6 commit ed2cbe6

2 files changed

Lines changed: 84 additions & 9 deletions

File tree

src/xargs/mod.rs

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ mod options {
2828
pub const NO_RUN_IF_EMPTY: &str = "no-run-if-empty";
2929
pub const NULL: &str = "null";
3030
pub const SIZE: &str = "size";
31+
pub const REPLACE: &str = "replace";
3132
pub const VERBOSE: &str = "verbose";
3233
}
3334

@@ -40,6 +41,7 @@ struct Options {
4041
no_run_if_empty: bool,
4142
null: bool,
4243
size: Option<usize>,
44+
replace: Option<String>,
4345
verbose: bool,
4446
}
4547

@@ -340,13 +342,15 @@ struct CommandBuilderOptions {
340342
limiters: LimiterCollection,
341343
verbose: bool,
342344
close_stdin: bool,
345+
replace: Option<String>,
343346
}
344347

345348
impl CommandBuilderOptions {
346349
fn new(
347350
action: ExecAction,
348351
env: HashMap<OsString, OsString>,
349352
mut limiters: LimiterCollection,
353+
replace: Option<String>,
350354
) -> Result<Self, ExhaustedCommandSpace> {
351355
let initial_args = match &action {
352356
ExecAction::Command(args) => args.iter().map(|arg| arg.as_ref()).collect(),
@@ -366,6 +370,7 @@ impl CommandBuilderOptions {
366370
limiters,
367371
verbose: false,
368372
close_stdin: false,
373+
replace,
369374
})
370375
}
371376
}
@@ -396,16 +401,42 @@ impl CommandBuilder<'_> {
396401
ExecAction::Command(args) => (&args[0], &args[1..]),
397402
ExecAction::Echo => (OsStr::new("echo"), &[]),
398403
};
399-
400404
let mut command = Command::new(entry_point);
401-
command
402-
.args(initial_args)
403-
.args(&self.extra_args)
404-
.env_clear()
405-
.envs(&self.options.env);
405+
406+
if let Some(replace_str) = &self.options.replace {
407+
// we replace the first instance of the replacement string with
408+
// the extra args, and then replace all instances of the replacement
409+
let replacement = self
410+
.extra_args
411+
.iter()
412+
.map(|s| s.to_string_lossy())
413+
.collect::<Vec<_>>()
414+
.join(" ");
415+
let initial_args: Vec<OsString> = initial_args
416+
.iter()
417+
.map(|arg| {
418+
let arg_str = arg.to_string_lossy();
419+
OsString::from(arg_str.replace(replace_str, &replacement))
420+
})
421+
.collect();
422+
423+
command
424+
.args(&initial_args)
425+
.env_clear()
426+
.envs(&self.options.env);
427+
} else {
428+
// don't do any replacement
429+
command
430+
.args(initial_args)
431+
.args(&self.extra_args)
432+
.env_clear()
433+
.envs(&self.options.env);
434+
};
435+
406436
if self.options.close_stdin {
407437
command.stdin(Stdio::null());
408438
}
439+
409440
if self.options.verbose {
410441
eprintln!("{command:?}");
411442
}
@@ -810,6 +841,15 @@ fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
810841
invocation",
811842
),
812843
)
844+
.arg(
845+
Arg::with_name(options::REPLACE)
846+
.long(options::REPLACE)
847+
.short("R")
848+
.takes_value(true)
849+
.value_name("R")
850+
.visible_aliases(&["i", "I"])
851+
.help("Replace R in INITIAL-ARGS with names read from standard input; if R is unspecified, assume {}"),
852+
)
813853
.arg(
814854
Arg::with_name(options::VERBOSE)
815855
.short("t")
@@ -837,6 +877,16 @@ fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
837877
size: matches
838878
.value_of(options::SIZE)
839879
.map(|value| value.parse().unwrap()),
880+
replace: if matches.is_present(options::REPLACE) {
881+
Some(
882+
matches
883+
.value_of(options::REPLACE)
884+
.unwrap_or("{}")
885+
.to_string(),
886+
)
887+
} else {
888+
None
889+
},
840890
verbose: matches.is_present(options::VERBOSE),
841891
};
842892

@@ -891,9 +941,10 @@ fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
891941

892942
limiters.add(MaxCharsCommandSizeLimiter::new_system(&env));
893943

894-
let mut builder_options = CommandBuilderOptions::new(action, env, limiters).map_err(|_| {
895-
"Base command and environment are too large to fit into one command execution"
896-
})?;
944+
let mut builder_options =
945+
CommandBuilderOptions::new(action, env, limiters, options.replace.clone()).map_err(
946+
|_| "Base command and environment are too large to fit into one command execution",
947+
)?;
897948

898949
builder_options.verbose = options.verbose;
899950
builder_options.close_stdin = options.arg_file.is_none();

tests/xargs_tests.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,27 @@ fn xargs_zero_lines() {
384384
.stderr(predicate::str::contains("Value must be > 0, not: 0"))
385385
.stdout(predicate::str::is_empty());
386386
}
387+
388+
#[test]
389+
fn xargs_replace() {
390+
Command::cargo_bin("xargs")
391+
.expect("found binary")
392+
.args(["-R", "{}", "echo", "{} bar"])
393+
.write_stdin("foo")
394+
.assert()
395+
.stdout(predicate::str::contains("foo bar"));
396+
397+
Command::cargo_bin("xargs")
398+
.expect("found binary")
399+
.args(["-R", "_", "echo", "_ bar"])
400+
.write_stdin("foo")
401+
.assert()
402+
.stdout(predicate::str::contains("foo bar"));
403+
404+
Command::cargo_bin("xargs")
405+
.expect("found binary")
406+
.args(["-R", "_", "echo", "_ _ bar"])
407+
.write_stdin("foo")
408+
.assert()
409+
.stdout(predicate::str::contains("foo foo bar"));
410+
}

0 commit comments

Comments
 (0)