Skip to content

Commit dd40547

Browse files
committed
uucore: return utility-specific exit code when --help/--version write fails
1 parent e6f91fa commit dd40547

2 files changed

Lines changed: 58 additions & 7 deletions

File tree

src/uucore/src/lib/mods/clap_localization.rs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
//! instead of parsing error strings, providing a more robust solution.
1212
//!
1313
14-
use crate::error::{UResult, USimpleError};
14+
use crate::error::{UClapError, UResult, USimpleError};
1515
use crate::locale::translate;
1616

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

457+
/// Like [`handle_clap_result_with_exit_code`], but allows specifying separate exit codes for
458+
/// argument parse errors and for write failures when printing help/version output.
459+
///
460+
/// This is useful for utilities that use different exit codes for I/O errors vs. argument
461+
/// parse errors (e.g., `tty` exits 3 on write errors but 2 on argument parse errors).
462+
pub fn handle_clap_result_with_exit_codes<I, T>(
463+
cmd: Command,
464+
itr: I,
465+
parse_error_code: i32,
466+
write_failure_code: i32,
467+
) -> UResult<ArgMatches>
468+
where
469+
I: IntoIterator<Item = T>,
470+
T: Into<OsString> + Clone,
471+
{
472+
cmd.try_get_matches_from(itr).map_err(|e| {
473+
if e.exit_code() == 0 {
474+
// For DisplayHelp/DisplayVersion, ClapErrorWrapper::code() ignores the `code` field
475+
// and returns either 0 (success) or `write_failure_code` (on stdout write failure).
476+
// We pass `parse_error_code` to `with_exit_code` only to satisfy the constructor;
477+
// the actual success/failure distinction is driven by `write_failure_code`.
478+
e.with_exit_code(parse_error_code)
479+
.with_write_failure_code(write_failure_code)
480+
.into()
481+
} else {
482+
let formatter = ErrorFormatter::new(crate::util_name());
483+
let code = formatter.print_error(&e, parse_error_code);
484+
USimpleError::new(code, "")
485+
}
486+
})
487+
}
488+
454489
/// Handles a clap error directly with a custom exit code.
455490
///
456491
/// This function processes a clap error and exits the program with the specified

src/uucore/src/lib/mods/error.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -701,10 +701,22 @@ impl From<i32> for Box<dyn UError> {
701701
#[derive(Debug)]
702702
pub struct ClapErrorWrapper {
703703
code: i32,
704+
write_failure_code: i32,
704705
error: clap::Error,
705706
print_failed: Cell<bool>,
706707
}
707708

709+
impl ClapErrorWrapper {
710+
/// Override the exit code to use when writing help/version output fails (e.g., /dev/full).
711+
///
712+
/// By default this matches `code`, but some utilities use different exit codes for I/O errors
713+
/// vs. argument parse errors (e.g., `tty` exits 3 on write errors, 2 on parse errors).
714+
pub fn with_write_failure_code(mut self, code: i32) -> Self {
715+
self.write_failure_code = code;
716+
self
717+
}
718+
}
719+
708720
/// Extension trait for `clap::Error` to adjust the exit code.
709721
pub trait UClapError<T> {
710722
/// Set the exit code for the program if `uumain` returns `Ok(())`.
@@ -715,6 +727,7 @@ impl From<clap::Error> for Box<dyn UError> {
715727
fn from(e: clap::Error) -> Self {
716728
Box::new(ClapErrorWrapper {
717729
code: 1,
730+
write_failure_code: 1,
718731
error: e,
719732
print_failed: Cell::new(false),
720733
})
@@ -725,6 +738,7 @@ impl UClapError<ClapErrorWrapper> for clap::Error {
725738
fn with_exit_code(self, code: i32) -> ClapErrorWrapper {
726739
ClapErrorWrapper {
727740
code,
741+
write_failure_code: code,
728742
error: self,
729743
print_failed: Cell::new(false),
730744
}
@@ -742,11 +756,16 @@ impl UClapError<Result<clap::ArgMatches, ClapErrorWrapper>>
742756
impl UError for ClapErrorWrapper {
743757
fn code(&self) -> i32 {
744758
// If the error is a DisplayHelp or DisplayVersion variant,
745-
// check if printing failed. If it did, return 1, otherwise 0.
759+
// check if printing failed. If it did, return the utility-specific write failure code,
760+
// otherwise 0 (success).
746761
if let clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion =
747762
self.error.kind()
748763
{
749-
i32::from(self.print_failed.get())
764+
if self.print_failed.get() {
765+
self.write_failure_code
766+
} else {
767+
0
768+
}
750769
} else {
751770
self.code
752771
}
@@ -767,9 +786,6 @@ impl Display for ClapErrorWrapper {
767786
// Try to display this error to stderr, but ignore if that fails too
768787
// since we're already in an error state.
769788
let _ = writeln!(std::io::stderr(), "{}: {print_fail}", crate::util_name());
770-
// Mirror GNU behavior: when failing to print help or version, exit with error code.
771-
// This avoids silent failures when stdout is full or closed.
772-
set_exit_code(1);
773789
}
774790
// Always return Ok(()) to satisfy Display's contract and prevent panic
775791
Ok(())

0 commit comments

Comments
 (0)