Skip to content

Commit

Permalink
xargs: support args with conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
YDX-2147483647 authored and sylvestre committed Jun 1, 2024
1 parent fdde66f commit 8beab49
Showing 1 changed file with 104 additions and 56 deletions.
160 changes: 104 additions & 56 deletions src/xargs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -682,10 +682,33 @@ impl From<io::Error> for XargsError {
}
}

struct InputProcessOptions {
exit_if_pass_char_limit: bool,
max_args: Option<usize>,
max_lines: Option<usize>,
no_run_if_empty: bool,
}

impl InputProcessOptions {
fn new(
exit_if_pass_char_limit: bool,
max_args: Option<usize>,
max_lines: Option<usize>,
no_run_if_empty: bool,
) -> Self {
InputProcessOptions {
exit_if_pass_char_limit,
max_args,
max_lines,
no_run_if_empty,
}
}
}

fn process_input(
builder_options: CommandBuilderOptions,
mut args: Box<dyn ArgumentReader>,
options: &Options,
options: &InputProcessOptions,
) -> Result<CommandResult, XargsError> {
let mut current_builder = CommandBuilder::new(&builder_options);
let mut have_pending_command = false;
Expand Down Expand Up @@ -752,6 +775,66 @@ fn validate_positive_usize(s: &str) -> Result<usize, String> {
}
}

fn normalize_options<'a>(
options: &'a Options,
matches: &'a clap::ArgMatches,
) -> (Option<usize>, Option<usize>, &'a Option<String>, Option<u8>) {
let (max_args, max_lines, replace) =
match (options.max_args, options.max_lines, &options.replace) {
// These 3 options are mutually exclusive.
// But `max_args=1` and `replace` do not actually conflict, so no warning.
(None | Some(1), None, Some(_)) => {
// If `replace`, all matches in initial args should be replaced with extra args read from stdin.
// It is possible to have multiple matches and multiple extra args, and the Cartesian product is desired.
// To be specific, we process extra args one by one, and replace all matches with the same extra arg in each time.
(Some(1), None, &options.replace)
}
(Some(_), None, None) | (None, Some(_), None) | (None, None, None) => {
(options.max_args, options.max_lines, &None)
}
_ => {
eprintln!(
"WARNING: -L, -n and -I/-i are mutually exclusive, but more than one were given; \
only the last option will be used"
);
let lines_index = matches
.indices_of(options::MAX_LINES)
.and_then(|v| v.last());
let args_index = matches.indices_of(options::MAX_ARGS).and_then(|v| v.last());
let replace_index = [options::REPLACE, options::REPLACE_I]
.iter()
.flat_map(|o| matches.indices_of(o).and_then(|v| v.last()))
.max();
if lines_index > args_index && lines_index > replace_index {
(None, options.max_lines, &None)
} else if args_index > lines_index && args_index > replace_index {
(options.max_args, None, &None)
} else {
(Some(1), None, &options.replace)
}
}
};

let delimiter = match (options.delimiter, options.null) {
(Some(delimiter), true) => {
if matches.indices_of(options::NULL).unwrap().last()
> matches.indices_of(options::DELIMITER).unwrap().last()
{
Some(b'\0')
} else {
Some(delimiter)

Check warning on line 825 in src/xargs/mod.rs

View check run for this annotation

Codecov / codecov/patch

src/xargs/mod.rs#L825

Added line #L825 was not covered by tests
}
}
(Some(delimiter), false) => Some(delimiter),
(None, true) => Some(b'\0'),
// If `replace` and no delimiter specified, each line of stdin turns into a line of stdout,
// so the input should be split at newlines only.
(None, false) => replace.as_ref().map(|_| b'\n'),
};

(max_args, max_lines, replace, delimiter)
}

fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
let matches = clap::Command::new("xargs")
.version(crate_version!())
Expand Down Expand Up @@ -897,22 +980,7 @@ fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
verbose: matches.get_flag(options::VERBOSE),
};

let delimiter = match (options.delimiter, options.null) {
(Some(delimiter), true) => {
if matches.indices_of(options::NULL).unwrap().last()
> matches.indices_of(options::DELIMITER).unwrap().last()
{
Some(b'\0')
} else {
Some(delimiter)
}
}
(Some(delimiter), false) => Some(delimiter),
(None, true) => Some(b'\0'),
// If `replace` and no delimiter specified, each line of stdin turns into a line of stdout,
// so the input should be split at newlines only.
(None, false) => options.replace.as_ref().map(|_| b'\n'),
};
let (max_args, max_lines, replace, delimiter) = normalize_options(&options, &matches);

let action = match matches.get_many::<OsString>(options::COMMAND) {
Some(args) if args.len() > 0 => {
Expand All @@ -923,50 +991,21 @@ fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
let env = std::env::vars_os().collect();

let mut limiters = LimiterCollection::new();

match (options.max_args, options.max_lines, &options.replace) {
// These 3 options are mutually exclusive.
// But `max_args=1` and `replace` do not actually conflict, so no warning.
(None | Some(1), None, Some(_)) => {
// If `replace`, all matches in initial args should be replaced with extra args read from stdin.
// It is possible to have multiple matches and multiple extra args, and the Cartesian product is desired.
// To be specific, we process extra args one by one, and replace all matches with the same extra arg in each time.
limiters.add(MaxArgsCommandSizeLimiter::new(1))
}
(Some(max_args), None, None) => limiters.add(MaxArgsCommandSizeLimiter::new(max_args)),
(None, Some(max_lines), None) => limiters.add(MaxLinesCommandSizeLimiter::new(max_lines)),
(None, None, None) => (),
_ => {
eprintln!(
"WARNING: -L, -n and -I/-i are mutually exclusive, but more than one were given; \
only the last option will be used"
);
let lines_index = matches
.indices_of(options::MAX_LINES)
.and_then(|v| v.last());
let args_index = matches.indices_of(options::MAX_ARGS).and_then(|v| v.last());
let replace_index = [options::REPLACE, options::REPLACE_I]
.iter()
.flat_map(|o| matches.indices_of(o).and_then(|v| v.last()))
.max();
if lines_index > args_index && lines_index > replace_index {
limiters.add(MaxLinesCommandSizeLimiter::new(options.max_lines.unwrap()));
} else if args_index > lines_index && args_index > replace_index {
limiters.add(MaxArgsCommandSizeLimiter::new(options.max_args.unwrap()));
}
}
if let Some(max_args) = max_args {
limiters.add(MaxArgsCommandSizeLimiter::new(max_args));
}
if let Some(max_lines) = max_lines {
limiters.add(MaxLinesCommandSizeLimiter::new(max_lines));
}

if let Some(max_chars) = options.max_chars {
limiters.add(MaxCharsCommandSizeLimiter::new(max_chars));
}

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

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",
)?;
let mut builder_options = CommandBuilderOptions::new(action, env, limiters, replace.clone())
.map_err(|_| {

Check warning on line 1006 in src/xargs/mod.rs

View check run for this annotation

Codecov / codecov/patch

src/xargs/mod.rs#L1006

Added line #L1006 was not covered by tests
"Base command and environment are too large to fit into one command execution"
})?;

Check warning on line 1008 in src/xargs/mod.rs

View check run for this annotation

Codecov / codecov/patch

src/xargs/mod.rs#L1008

Added line #L1008 was not covered by tests

builder_options.verbose = options.verbose;
builder_options.close_stdin = options.arg_file.is_none();
Expand All @@ -983,7 +1022,16 @@ fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
Box::new(WhitespaceDelimitedArgumentReader::new(args_file))
};

let result = process_input(builder_options, args, &options)?;
let result = process_input(
builder_options,
args,
&InputProcessOptions::new(
options.exit_if_pass_char_limit,
max_args,
max_lines,
options.no_run_if_empty,
),
)?;
Ok(result)
}

Expand Down

0 comments on commit 8beab49

Please sign in to comment.