Skip to content

Commit 0ef2c9c

Browse files
committed
xargs: add --show-limits diagnostics
Among other things, Debian maintainers use the output of xargs --show-limits to set --max-chars appropriately for subsequent commands. Accept GNU-compatible --show-limits and print command-line size diagnostics before reading input or building a command. The output includes the Maximum length line parsed by Debian maintainer scripts. The regression test links to Debian python3.13's libPVER-minimal.prerm.in, where 3.13.5-2+deb13u2 parses xargs --show-limits, and to GNU findutils 4.10.0's xargs/xargs.c, where the diagnostic text is emitted. Reuse the existing system command-size calculation for both the default limiter and the diagnostic output, and add an integration test for the parsed limit.
1 parent c944cf7 commit 0ef2c9c

2 files changed

Lines changed: 100 additions & 9 deletions

File tree

src/xargs/mod.rs

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ mod options {
3333
pub const NULL: &str = "null";
3434
pub const REPLACE: &str = "replace";
3535
pub const REPLACE_I: &str = "replace-I";
36+
pub const SHOW_LIMITS: &str = "show-limits";
3637
pub const VERBOSE: &str = "verbose";
3738
}
3839

@@ -46,6 +47,7 @@ struct Options {
4647
no_run_if_empty: bool,
4748
null: bool,
4849
replace: Option<String>,
50+
show_limits: bool,
4951
verbose: bool,
5052
eof_delimiter: Option<String>,
5153
}
@@ -175,20 +177,76 @@ impl MaxCharsCommandSizeLimiter {
175177

176178
#[cfg(unix)]
177179
fn new_system(env: &HashMap<OsString, OsString>) -> Self {
178-
// POSIX requires that we leave 2048 bytes of space so that the child processes
179-
// can have room to set their own environment variables.
180-
const ARG_HEADROOM: usize = 2048;
181-
let arg_max = unsafe { uucore::libc::sysconf(uucore::libc::_SC_ARG_MAX) } as usize;
180+
Self::new(system_command_size_limit(env))
181+
}
182+
}
182183

183-
let env_size: usize = env
184-
.iter()
185-
.map(|(var, value)| count_osstr_chars_for_exec(var) + count_osstr_chars_for_exec(value))
186-
.sum();
184+
const POSIX_MIN_ARG_MAX: usize = 4096;
187185

188-
Self::new(arg_max - ARG_HEADROOM - env_size)
186+
#[cfg(unix)]
187+
const ARG_HEADROOM: usize = 2048;
188+
189+
#[cfg(unix)]
190+
fn system_arg_max() -> usize {
191+
let arg_max = unsafe { uucore::libc::sysconf(uucore::libc::_SC_ARG_MAX) };
192+
if arg_max > 0 {
193+
arg_max as usize
194+
} else {
195+
POSIX_MIN_ARG_MAX
189196
}
190197
}
191198

199+
#[cfg(windows)]
200+
fn system_arg_max() -> usize {
201+
// Taken from the CreateProcess docs.
202+
32767
203+
}
204+
205+
fn environment_size(env: &HashMap<OsString, OsString>) -> usize {
206+
env.iter()
207+
.map(|(var, value)| count_osstr_chars_for_exec(var) + count_osstr_chars_for_exec(value))
208+
.sum()
209+
}
210+
211+
#[cfg(unix)]
212+
fn system_command_size_limit(env: &HashMap<OsString, OsString>) -> usize {
213+
// POSIX requires that we leave 2048 bytes of space so that the child
214+
// processes can have room to set their own environment variables.
215+
system_arg_max()
216+
.saturating_sub(ARG_HEADROOM)
217+
.saturating_sub(environment_size(env))
218+
}
219+
220+
#[cfg(windows)]
221+
fn system_command_size_limit(_env: &HashMap<OsString, OsString>) -> usize {
222+
system_arg_max()
223+
}
224+
225+
fn show_limits(env: &HashMap<OsString, OsString>, max_chars: Option<usize>) {
226+
// Match the GNU xargs diagnostics that downstream scripts parse:
227+
// https://git.savannah.gnu.org/cgit/findutils.git/tree/xargs/xargs.c?h=v4.10.0#n795
228+
let system_limit = system_command_size_limit(env);
229+
let buffer_size = max_chars.unwrap_or(system_limit).min(system_limit);
230+
231+
eprintln!(
232+
"Your environment variables take up {} bytes",
233+
environment_size(env)
234+
);
235+
eprintln!(
236+
"POSIX upper limit on argument length (this system): {}",
237+
system_arg_max()
238+
);
239+
eprintln!(
240+
"POSIX smallest allowable upper limit on argument length (all systems): {POSIX_MIN_ARG_MAX}"
241+
);
242+
eprintln!("Maximum length of command we could actually use: {system_limit}");
243+
eprintln!("Size of command buffer we are actually using: {buffer_size}");
244+
eprintln!(
245+
"Maximum parallelism (--max-procs must be no greater): {}",
246+
i32::MAX
247+
);
248+
}
249+
192250
impl CommandSizeLimiter for MaxCharsCommandSizeLimiter {
193251
fn try_arg(
194252
&mut self,
@@ -884,6 +942,7 @@ fn normalize_options(options: Options, matches: &clap::ArgMatches) -> Options {
884942
no_run_if_empty: options.no_run_if_empty,
885943
null: options.null,
886944
replace,
945+
show_limits: options.show_limits,
887946
verbose: options.verbose,
888947
eof_delimiter,
889948
}
@@ -985,6 +1044,12 @@ fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
9851044
)
9861045
.value_parser(validate_positive_usize),
9871046
)
1047+
.arg(
1048+
Arg::new(options::SHOW_LIMITS)
1049+
.long(options::SHOW_LIMITS)
1050+
.help("Display the command-line length limits and exit")
1051+
.action(ArgAction::SetTrue),
1052+
)
9881053
.arg(
9891054
Arg::new(options::VERBOSE)
9901055
.short('t')
@@ -1082,6 +1147,7 @@ fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
10821147
.map_or_else(|| "{}".to_string(), std::borrow::ToOwned::to_owned)
10831148
})
10841149
}),
1150+
show_limits: matches.get_flag(options::SHOW_LIMITS),
10851151
verbose: matches.get_flag(options::VERBOSE),
10861152
eof_delimiter: [options::EOF_E, options::EOF].iter().find_map(|&option| {
10871153
matches.contains_id(option).then(|| {
@@ -1102,6 +1168,11 @@ fn do_xargs(args: &[&str]) -> Result<CommandResult, XargsError> {
11021168
};
11031169
let env = std::env::vars_os().collect();
11041170

1171+
if options.show_limits {
1172+
show_limits(&env, options.max_chars);
1173+
return Ok(CommandResult::Success);
1174+
}
1175+
11051176
let mut limiters = LimiterCollection::new();
11061177
if let Some(max_args) = options.max_args {
11071178
limiters.add(MaxArgsCommandSizeLimiter::new(max_args));

tests/test_xargs.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,26 @@ fn xargs_max_chars() {
169169
.no_stdout();
170170
}
171171

172+
#[test]
173+
fn xargs_show_limits() {
174+
// Debian's python3.13 maintainer scripts parse this GNU xargs diagnostic
175+
// to choose a safe --max-chars value:
176+
// https://sources.debian.org/src/python3.13/3.13.5-2%2Bdeb13u2/debian/libPVER-minimal.prerm.in/#L8
177+
// GNU findutils emits the compatibility target here:
178+
// https://git.savannah.gnu.org/cgit/findutils.git/tree/xargs/xargs.c?h=v4.10.0#n795
179+
let output = ucmd().args(&["--show-limits"]).pipe_in("").succeeds();
180+
let result = output.no_stdout();
181+
182+
let max = result
183+
.stderr_str()
184+
.lines()
185+
.find_map(|line| line.strip_prefix("Maximum length of command we could actually use: "))
186+
.expect("expected GNU-compatible maximum length diagnostic")
187+
.parse::<usize>()
188+
.expect("expected maximum length diagnostic to end in an integer");
189+
assert!(max > 0, "expected a positive maximum length, got {max}");
190+
}
191+
172192
#[test]
173193
fn xargs_exit_on_large() {
174194
ucmd()

0 commit comments

Comments
 (0)