Skip to content
Draft
73 changes: 48 additions & 25 deletions src/bin/coreutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,32 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");

include!(concat!(env!("OUT_DIR"), "/uutils_map.rs"));

fn usage<T>(utils: &UtilityMap<T>, name: &str) {
println!("{name} {VERSION} (multi-call binary)\n");
println!("Usage: {name} [function [arguments...]]");
println!(" {name} --list");
println!();
fn usage<T>(utils: &UtilityMap<T>, name: &str) -> bool {
let mut out = io::stdout();
let ok = writeln!(out, "{name} {VERSION} (multi-call binary)\n").is_ok()
&& writeln!(out, "Usage: {name} [function [arguments...]]").is_ok()
&& writeln!(out, " {name} --list").is_ok()
&& writeln!(out).is_ok();
#[cfg(feature = "feat_common_core")]
{
println!("Functions:");
println!(" '<uutils>' [arguments...]");
println!();
}
println!("Options:");
println!(" --list lists all defined functions, one per row\n");
println!("Currently defined functions:\n");
let ok = ok
&& writeln!(out, "Functions:").is_ok()
&& writeln!(out, " '<uutils>' [arguments...]").is_ok()
&& writeln!(out).is_ok();
let display_list = utils.keys().copied().join(", ");
let width = cmp::min(textwrap::termwidth(), 100) - 4 * 2; // (opinion/heuristic) max 100 chars wide with 4 character side indentions
println!(
"{}",
textwrap::indent(&textwrap::fill(&display_list, width), " ")
);
let width = cmp::min(textwrap::termwidth(), 100) - 4 * 2;
ok && writeln!(out, "Options:").is_ok()
&& writeln!(
out,
" --list lists all defined functions, one per row\n"
)
.is_ok()
&& writeln!(out, "Currently defined functions:\n").is_ok()
&& writeln!(
out,
"{}",
textwrap::indent(&textwrap::fill(&display_list, width), " ")
)
.is_ok()
}

#[allow(clippy::cognitive_complexity)]
Expand All @@ -47,7 +53,9 @@ fn main() {

let binary = validation::binary_path(&mut args);
let binary_as_util = validation::name(&binary).unwrap_or_else(|| {
usage(&utils, "<unknown binary name>");
if !usage(&utils, "<unknown binary name>") {
process::exit(1);
}
process::exit(0);
});

Expand Down Expand Up @@ -78,17 +86,28 @@ fn main() {
"--list" => {
// If --help is also present, show usage instead of list
if args.any(|arg| arg == "--help" || arg == "-h") {
usage(&utils, binary_as_util);
if !usage(&utils, binary_as_util) {
process::exit(1);
}
process::exit(0);
}
let utils: Vec<_> = utils.keys().collect();
for util in utils {
println!("{util}");
if writeln!(io::stdout(), "{util}").is_err() {
process::exit(1);
}
}
process::exit(0);
}
"--version" | "-V" => {
println!("{binary_as_util} {VERSION} (multi-call binary)");
if writeln!(
io::stdout(),
"{binary_as_util} {VERSION} (multi-call binary)"
)
.is_err()
{
process::exit(1);
}
process::exit(0);
}
// Not a special command: fallthrough to calling a util
Expand Down Expand Up @@ -120,13 +139,15 @@ fn main() {
.into_iter()
.chain(args),
);
io::stdout().flush().expect("could not flush stdout");
let _ = io::stdout().flush();
process::exit(code);
}
None => validation::not_found(&util_os),
}
}
usage(&utils, binary_as_util);
if !usage(&utils, binary_as_util) {
process::exit(1);
}
process::exit(0);
} else if util.starts_with('-') {
// Argument looks like an option but wasn't recognized
Expand All @@ -138,7 +159,9 @@ fn main() {
}
} else {
// no arguments provided
usage(&utils, binary_as_util);
if !usage(&utils, binary_as_util) {
process::exit(1);
}
process::exit(0);
}
}
2 changes: 1 addition & 1 deletion src/uu/csplit/src/patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ pub fn get_patterns(args: &[&str]) -> Result<Vec<Pattern>, CsplitError> {
fn extract_patterns(args: &[&str]) -> Result<Vec<Pattern>, CsplitError> {
let mut patterns = Vec::with_capacity(args.len());
let to_match_reg =
Regex::new(r"^(/(?P<UPTO>.+)/|%(?P<SKIPTO>.+)%)(?P<OFFSET>[\+-]?[0-9]+)?$").unwrap();
Regex::new(r"^(/(?P<UPTO>.*)/|%(?P<SKIPTO>.*)%)(?P<OFFSET>[\+-]?[0-9]+)?$").unwrap();
let execute_ntimes_reg = Regex::new(r"^\{(?P<TIMES>[0-9]+)|\*\}$").unwrap();
let mut iter = args.iter().copied().peekable();

Expand Down
6 changes: 4 additions & 2 deletions src/uu/env/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use std::io::stderr;
use std::os::unix::ffi::OsStrExt;

use uucore::display::{Quotable, print_all_env_vars};
use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError};
use uucore::error::{ExitCode, UClapError, UError, UResult, USimpleError, UUsageError};
use uucore::line_ending::LineEnding;
#[cfg(unix)]
use uucore::signals::{signal_by_name_or_value, signal_name_by_value, signal_number_upper_bound};
Expand Down Expand Up @@ -654,7 +654,9 @@ impl EnvAppData {
Err(e) => {
match e.kind() {
clap::error::ErrorKind::DisplayHelp
| clap::error::ErrorKind::DisplayVersion => return Err(e.into()),
| clap::error::ErrorKind::DisplayVersion => {
return Err(e.with_exit_code(125).into());
}
_ => {
// Use ErrorFormatter directly to handle error with shebang message callback
let formatter =
Expand Down
12 changes: 9 additions & 3 deletions src/uu/expr/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
.collect::<Result<Vec<_>, _>>()?;

if args.len() == 1 && args[0] == b"--help" {
uu_app().print_help()?;
if uu_app().print_help().is_err() {
std::process::exit(3);
}
} else if args.len() == 1 && args[0] == b"--version" {
writeln!(
if writeln!(
stdout(),
"{} {}",
uucore::util_name(),
uucore::crate_version!()
)?;
)
.is_err()
{
std::process::exit(3);
}
} else {
// The first argument may be "--" and should be be ignored.
let args = if !args.is_empty() && args[0] == b"--" {
Expand Down
3 changes: 2 additions & 1 deletion src/uu/test/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> {
if binary_name.ends_with('[') {
// If invoked as [ we should recognize --help and --version (but not -h or -v)
if args.len() == 1 && (args[0] == "--help" || args[0] == "--version") {
uucore::clap_localization::handle_clap_result(
uucore::clap_localization::handle_clap_result_with_exit_code(
uu_app(),
std::iter::once(program).chain(args.into_iter()),
2,
)?;
return Ok(());
}
Expand Down
5 changes: 4 additions & 1 deletion src/uu/tty/src/tty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ mod options {

#[uucore::main(no_signals)]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 2)?;
// tty exits 2 on argument parse errors but 3 on write errors (consistent with its
// normal write-error behavior on line output failures).
let matches =
uucore::clap_localization::handle_clap_result_with_exit_codes(uu_app(), args, 2, 3)?;

// Disable SIGPIPE so we can handle broken pipe errors gracefully
// and exit with code 3 instead of being killed by the signal.
Expand Down
39 changes: 37 additions & 2 deletions src/uucore/src/lib/mods/clap_localization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
//! instead of parsing error strings, providing a more robust solution.
//!

use crate::error::{UResult, USimpleError};
use crate::error::{UClapError, UResult, USimpleError};
use crate::locale::translate;

use clap::error::{ContextKind, ErrorKind};
Expand Down Expand Up @@ -442,7 +442,10 @@ where
{
cmd.try_get_matches_from(itr).map_err(|e| {
if e.exit_code() == 0 {
e.into() // Preserve help/version
// For help/version display, use exit_code as the write failure code so that
// if stdout is full (e.g., /dev/full), the program exits with the utility's
// expected error code rather than the default 1.
e.with_exit_code(exit_code).into()
} else {
let formatter = ErrorFormatter::new(crate::util_name());
let code = formatter.print_error(&e, exit_code);
Expand All @@ -451,6 +454,38 @@ where
})
}

/// Like [`handle_clap_result_with_exit_code`], but allows specifying separate exit codes for
/// argument parse errors and for write failures when printing help/version output.
///
/// This is useful for utilities that use different exit codes for I/O errors vs. argument
/// parse errors (e.g., `tty` exits 3 on write errors but 2 on argument parse errors).
pub fn handle_clap_result_with_exit_codes<I, T>(
cmd: Command,
itr: I,
parse_error_code: i32,
write_failure_code: i32,
) -> UResult<ArgMatches>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
cmd.try_get_matches_from(itr).map_err(|e| {
if e.exit_code() == 0 {
// For DisplayHelp/DisplayVersion, ClapErrorWrapper::code() ignores the `code` field
// and returns either 0 (success) or `write_failure_code` (on stdout write failure).
// We pass `parse_error_code` to `with_exit_code` only to satisfy the constructor;
// the actual success/failure distinction is driven by `write_failure_code`.
e.with_exit_code(parse_error_code)
.with_write_failure_code(write_failure_code)
.into()
} else {
let formatter = ErrorFormatter::new(crate::util_name());
let code = formatter.print_error(&e, parse_error_code);
USimpleError::new(code, "")
}
})
}

/// Handles a clap error directly with a custom exit code.
///
/// This function processes a clap error and exits the program with the specified
Expand Down
26 changes: 21 additions & 5 deletions src/uucore/src/lib/mods/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -701,10 +701,22 @@ impl From<i32> for Box<dyn UError> {
#[derive(Debug)]
pub struct ClapErrorWrapper {
code: i32,
write_failure_code: i32,
error: clap::Error,
print_failed: Cell<bool>,
}

impl ClapErrorWrapper {
/// Override the exit code to use when writing help/version output fails (e.g., /dev/full).
///
/// By default this matches `code`, but some utilities use different exit codes for I/O errors
/// vs. argument parse errors (e.g., `tty` exits 3 on write errors, 2 on parse errors).
pub fn with_write_failure_code(mut self, code: i32) -> Self {
self.write_failure_code = code;
self
}
}

/// Extension trait for `clap::Error` to adjust the exit code.
pub trait UClapError<T> {
/// Set the exit code for the program if `uumain` returns `Ok(())`.
Expand All @@ -715,6 +727,7 @@ impl From<clap::Error> for Box<dyn UError> {
fn from(e: clap::Error) -> Self {
Box::new(ClapErrorWrapper {
code: 1,
write_failure_code: 1,
error: e,
print_failed: Cell::new(false),
})
Expand All @@ -725,6 +738,7 @@ impl UClapError<ClapErrorWrapper> for clap::Error {
fn with_exit_code(self, code: i32) -> ClapErrorWrapper {
ClapErrorWrapper {
code,
write_failure_code: code,
error: self,
print_failed: Cell::new(false),
}
Expand All @@ -742,11 +756,16 @@ impl UClapError<Result<clap::ArgMatches, ClapErrorWrapper>>
impl UError for ClapErrorWrapper {
fn code(&self) -> i32 {
// If the error is a DisplayHelp or DisplayVersion variant,
// check if printing failed. If it did, return 1, otherwise 0.
// check if printing failed. If it did, return the utility-specific write failure code,
// otherwise 0 (success).
if let clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion =
self.error.kind()
{
i32::from(self.print_failed.get())
if self.print_failed.get() {
self.write_failure_code
} else {
0
}
} else {
self.code
}
Expand All @@ -767,9 +786,6 @@ impl Display for ClapErrorWrapper {
// Try to display this error to stderr, but ignore if that fails too
// since we're already in an error state.
let _ = writeln!(std::io::stderr(), "{}: {print_fail}", crate::util_name());
// Mirror GNU behavior: when failing to print help or version, exit with error code.
// This avoids silent failures when stdout is full or closed.
set_exit_code(1);
}
// Always return Ok(()) to satisfy Display's contract and prevent panic
Ok(())
Expand Down
8 changes: 8 additions & 0 deletions tests/by-util/test_csplit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1583,3 +1583,11 @@ fn test_write_error_dev_full_keep_files() {
assert!(at.file_exists("xx00"));
assert_eq!(at.read("xx00"), "1\n");
}

#[test]
fn test_empty_regex_pattern() {
// '//' uses the empty regex (matches beginning of each line)
let (at, mut ucmd) = at_and_ucmd!();
at.write("input", "hello\nworld\n");
ucmd.args(&["input", "//"]).succeeds();
}
14 changes: 14 additions & 0 deletions tests/by-util/test_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2009,3 +2009,17 @@ fn test_ignore_signal_pipe_broken_pipe_regression() {
"With --ignore-signal=PIPE, process should exit gracefully (0 or 1), got: {ignore_signal_exit_code}"
);
}

#[test]
#[cfg(target_os = "linux")]
fn test_help_version_dev_full_exit_code() {
use std::fs::OpenOptions;
use uutests::new_ucmd;
for arg in ["--help", "--version"] {
let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap();
new_ucmd!()
.arg(arg)
.set_stdout(dev_full)
.fails_with_code(125);
}
}
11 changes: 11 additions & 0 deletions tests/by-util/test_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1975,3 +1975,14 @@ fn test_emoji_operations() {
.succeeds()
.stdout_only("1\n");
}

#[test]
#[cfg(target_os = "linux")]
fn test_help_version_dev_full_exit_code() {
use std::fs::OpenOptions;
use uutests::new_ucmd;
for arg in ["--help", "--version"] {
let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap();
new_ucmd!().arg(arg).set_stdout(dev_full).fails_with_code(3);
}
}
11 changes: 11 additions & 0 deletions tests/by-util/test_ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7119,3 +7119,14 @@ fn test_ls_non_utf8_hidden() {

scene.ucmd().succeeds().stdout_does_not_contain(".hidden");
}

#[test]
#[cfg(target_os = "linux")]
fn test_help_version_dev_full_exit_code() {
use std::fs::OpenOptions;
use uutests::new_ucmd;
for arg in ["--help", "--version"] {
let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap();
new_ucmd!().arg(arg).set_stdout(dev_full).fails_with_code(2);
}
}
Loading
Loading