From fb0bae993d15efd88b73794cc9eeadfb78123482 Mon Sep 17 00:00:00 2001 From: arbelonson-source <269032023+arbelonson-source@users.noreply.github.com> Date: Mon, 31 Aug 2026 00:57:14 +0300 Subject: [PATCH] expand, unexpand: read a hyphen-leading tab list as a tab list `-1` is the obsolete spelling of `--tabs=1`, and `expand_shortcuts` rewrote it wherever it appeared, including where it was already the value of `-t`: $ expand -t -1 file error: a value is required for '--tabs ' but none was supplied GNU reads it as a (bad) tab list and reports it as one: `tab size contains invalid character(s): '-1'`. The check that produces that message already exists here -- `expand -t abc` and `expand -t 1,-2` match GNU today -- the value just never reached it. Rewrite the obsolete form only where an option is expected: not as the value of -t/--tabs, and not past a `--` terminator, which GNU treats as a file name. `allow_hyphen_values` lets clap pass the value through. --- src/uu/expand/src/expand.rs | 37 ++++++++++++++++++++++++++++++++- src/uu/unexpand/src/unexpand.rs | 36 +++++++++++++++++++++++++++++++- tests/by-util/test_expand.rs | 16 ++++++++++++++ tests/by-util/test_unexpand.rs | 14 +++++++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index a9bf25be282..6014216fe46 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -199,11 +199,42 @@ impl Options { /// Preprocess command line arguments and expand shortcuts. For example, "-7" is expanded to /// "--tabs=7" and "-1,3" to "--tabs=1 --tabs=3". +/// Whether `arg` is a `-t`/`--tabs` spelling that takes the *next* argument as +/// its value, so that argument is a tab list rather than an obsolete `-N`. +fn takes_tabs_value(arg: &str) -> bool { + if let Some(long) = arg.strip_prefix("--") { + !long.is_empty() && !long.contains('=') && options::TABS.starts_with(long) + } else if let Some(short) = arg.strip_prefix('-') { + // Only a trailing `t` takes the next argument; in `-t8` the value is + // attached. A bare `-` is stdin and leaves nothing to index. + !short.is_empty() && short.find('t') == Some(short.len() - 1) + } else { + false + } +} + fn expand_shortcuts(args: Vec) -> Vec { let mut processed_args = Vec::with_capacity(args.len()); + let mut expecting_tabs = false; + let mut end_of_options = false; + for arg in args { - if let Some(arg) = arg.to_str() + // `-1` is the obsolete spelling of `--tabs=1`, but only where an + // option is expected. As the value of `-t` it is a (bad) tab list that + // has to reach the tab-list check, and past `--` it is a file name. + let is_operand = end_of_options || expecting_tabs; + if let Some(text) = arg.to_str() { + if is_operand { + expecting_tabs = false; + } else if text == "--" { + end_of_options = true; + } else { + expecting_tabs = takes_tabs_value(text); + } + } + if !is_operand + && let Some(arg) = arg.to_str() && arg.starts_with('-') && arg[1..].chars().all(is_digit_or_comma) { @@ -248,6 +279,10 @@ pub fn uu_app() -> Command { .long(options::TABS) .short('t') .value_name("N, LIST") + // A tab list may start with a hyphen. It is not valid, but the + // check below reports it far better than clap does, so it has to + // reach that check rather than be taken for an option. + .allow_hyphen_values(true) .action(ArgAction::Append) .help(translate!("expand-help-tabs")), ) diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index 61a80f9c013..d4b86c79bd8 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -195,14 +195,44 @@ fn is_digit_or_comma(c: char) -> bool { /// Preprocess command line arguments and expand shortcuts. For example, "-7" is expanded to /// "--tabs=7 --first-only" and "-1,3" to "--tabs=1 --tabs=3 --first-only". However, if "-a" or /// "--all" is provided, "--first-only" is omitted. +/// Whether `arg` is a `-t`/`--tabs` spelling that takes the *next* argument as +/// its value, so that argument is a tab list rather than an obsolete `-N`. +fn takes_tabs_value(arg: &str) -> bool { + if let Some(long) = arg.strip_prefix("--") { + !long.is_empty() && !long.contains('=') && options::TABS.starts_with(long) + } else if let Some(short) = arg.strip_prefix('-') { + // Only a trailing `t` takes the next argument; in `-t8` the value is + // attached. A bare `-` is stdin and leaves nothing to index. + !short.is_empty() && short.find('t') == Some(short.len() - 1) + } else { + false + } +} + fn expand_shortcuts(args: Vec) -> Vec { let mut processed_args = Vec::with_capacity(args.len()); let mut is_all_arg_provided = false; let mut has_shortcuts = false; + let mut expecting_tabs = false; + let mut end_of_options = false; + for arg in args { + // `-1` is the obsolete spelling of `--tabs=1`, but only where an + // option is expected. As the value of `-t` it is a (bad) tab list that + // has to reach the tab-list check, and past `--` it is a file name. + let is_operand = end_of_options || expecting_tabs; + if let Some(text) = arg.to_str() { + if is_operand { + expecting_tabs = false; + } else if text == "--" { + end_of_options = true; + } else { + expecting_tabs = takes_tabs_value(text); + } + } if let Some(arg) = arg.to_str() { - if arg.starts_with('-') && arg[1..].chars().all(is_digit_or_comma) { + if !is_operand && arg.starts_with('-') && arg[1..].chars().all(is_digit_or_comma) { arg[1..] .split(',') .filter(|s| !s.is_empty()) @@ -268,6 +298,10 @@ pub fn uu_app() -> Command { Arg::new(options::TABS) .short('t') .long(options::TABS) + // A tab list may start with a hyphen. It is not valid, but the + // check below reports it far better than clap does, so it has + // to reach that check rather than be taken for an option. + .allow_hyphen_values(true) .help(translate!("unexpand-help-tabs")) .action(ArgAction::Append) .value_name("N, LIST"), diff --git a/tests/by-util/test_expand.rs b/tests/by-util/test_expand.rs index 6cc45c682a3..eff0d070b2f 100644 --- a/tests/by-util/test_expand.rs +++ b/tests/by-util/test_expand.rs @@ -500,3 +500,19 @@ fn test_buffered_reads_new_line_no_tabs_in_first_chunk() { .succeeds() .stdout_is_fixture("new_line_in_chunk_expected.txt"); } + +#[test] +fn test_tab_list_starting_with_hyphen() { + // `-1` is the obsolete spelling of `--tabs=1`, but as the value of -t it + // is a tab list. It used to be rewritten to `--tabs=1` before the check, + // which then complained about `'--tabs=1'` rather than what was typed. + for arg in ["-1", "-0"] { + new_ucmd!() + .args(&["-t", arg]) + .fails() + .stderr_contains(format!("tab size contains invalid character(s): '{arg}'")); + } + + // The obsolete spelling still works where an option is expected. + new_ucmd!().arg("-4").pipe_in("\tx\n").succeeds(); +} diff --git a/tests/by-util/test_unexpand.rs b/tests/by-util/test_unexpand.rs index ae9d86aae16..c7cb680d8ad 100644 --- a/tests/by-util/test_unexpand.rs +++ b/tests/by-util/test_unexpand.rs @@ -544,3 +544,17 @@ fn test_buffered_read_edgecase_behaviour() { .stdout_only(String::from_utf8(expected).unwrap()); } } + +#[test] +fn test_tab_list_starting_with_hyphen() { + // See the matching expand test: `-1` after -t is a tab list, not the + // obsolete `--tabs=1` spelling. + for arg in ["-1", "-0"] { + new_ucmd!() + .args(&["-t", arg]) + .fails() + .stderr_contains(format!("tab size contains invalid character(s): '{arg}'")); + } + + new_ucmd!().arg("-4").pipe_in(" x\n").succeeds(); +}