Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -662,7 +662,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 = uucore::clap_localization::ErrorFormatter::new("env");
Expand Down
15 changes: 13 additions & 2 deletions src/uu/expr/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +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!(stdout(), "expr {}", uucore::crate_version!())?;
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 @@ -51,9 +51,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 @@ -2032,3 +2032,17 @@ fn test_env_disallow_double_underscore_all() {
.fails()
.stderr_contains("invalid signal");
}

#[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 @@ -2007,3 +2007,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 @@ -7244,3 +7244,14 @@ fn test_ls_a_dotdot_no_error_on_wasi() {
.stdout_contains("..")
.no_stderr();
}

#[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);
}
}
11 changes: 11 additions & 0 deletions tests/by-util/test_sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2961,3 +2961,14 @@ e f 5436 down data path1 path2 path3 path4 path5\n";
}

/* spell-checker: enable */

#[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);
}
}
13 changes: 13 additions & 0 deletions tests/by-util/test_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1073,3 +1073,16 @@ fn test_unary_op_as_literal_in_three_arg_form() {
new_ucmd!().args(&["-f", "=", "a"]).fails_with_code(1);
new_ucmd!().args(&["-f", "=", "a", "-o", "b"]).succeeds();
}

#[test]
#[cfg(target_os = "linux")]
fn test_lbracket_help_dev_full_exit_code() {
use std::fs::OpenOptions;
use uutests::util::TestScenario;
let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap();
TestScenario::new("[")
.ucmd()
.arg("--help")
.set_stdout(dev_full)
.fails_with_code(2);
}
10 changes: 10 additions & 0 deletions tests/by-util/test_tty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,13 @@ fn test_version_pipe_no_stderr() {
child.close_stdout();
child.wait().unwrap().no_stderr();
}

#[test]
#[cfg(target_os = "linux")]
fn test_help_version_dev_full_exit_code() {
use std::fs::OpenOptions;
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);
}
}
1 change: 1 addition & 0 deletions util/gnu-patches/series
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ tests_du_move_dir_while_traversing.patch
test_mkdir_restorecon.patch
error_msg_uniq.diff
tests_numfmt.patch
tests_help_help-version.patch
17 changes: 17 additions & 0 deletions util/gnu-patches/tests_help_help-version.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Index: gnu/tests/help/help-version.sh
===================================================================
--- gnu.orig/tests/help/help-version.sh
+++ gnu/tests/help/help-version.sh
@@ -56,9 +56,9 @@ expected_failure_status_fgrep=2
test "$built_programs" \
|| fail_ "built_programs not specified!?!"

-test "$VERSION" \
- || fail_ "set envvar VERSION; it is required for a PATH sanity-check"
-
+# Extract VERSION dynamically from first program's output for uutils
+for i in $built_programs; do
+ VERSION=$(env $i --version | sed -n '1s/.* //p;q'); break; done
# Extract version from --version output of the first program
for i in $built_programs; do
v=$(env $i --version | sed -n '1s/.* //p;q')
Loading