From 96e957c69083c6fb77a222f524652d28c7f7b33c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 07:35:06 +0000 Subject: [PATCH] Add POSIX keystring parsing and argv pre-expansion Support traditional key syntax (for example `tar cvf archive.tar file`) by rewriting a leading key operand into clap-compatible short options before argument parsing. - detect valid keystrings in argv[1] - expand key letters into `-` arguments - consume operands for `f` and `b` in key order - forward `-b` (currently unsupported) so key/dash forms fail consistently - update usage text to document key syntax - add unit tests for key detection/expansion - add integration tests for create/extract parity and `-b` failure parity --- src/uu/tar/src/tar.rs | 187 +++++++++++++++++++++++++++++++++++++- tests/by-util/test_tar.rs | 83 +++++++++++++++++ 2 files changed, 269 insertions(+), 1 deletion(-) diff --git a/src/uu/tar/src/tar.rs b/src/uu/tar/src/tar.rs index 32d35d9..79bc01e 100644 --- a/src/uu/tar/src/tar.rs +++ b/src/uu/tar/src/tar.rs @@ -12,7 +12,75 @@ use uucore::error::UResult; use uucore::format_usage; const ABOUT: &str = "an archiving utility"; -const USAGE: &str = "tar {c|x}[v] -f ARCHIVE [FILE...]"; +const USAGE: &str = "tar key [FILE...]\n tar {-c|-x} [-v] -f ARCHIVE [FILE...]"; + +/// Determines whether a string looks like a POSIX tar keystring. +/// +/// A valid keystring must not start with '-', must contain at least one +/// function letter (c, x, t, u, r), and every character must be a +/// recognised key character. +fn is_posix_keystring(s: &str) -> bool { + if s.is_empty() || s.starts_with('-') { + return false; + } + let valid_chars = "cxturvwfblmo"; + // function letters: c=create, x=extract, t=list, u=update, r=append + // modifier letters: v=verbose, w=interactive, f=file, b=blocking-factor, + // l=one-file-system, m=modification-time, o=no-same-owner + s.chars().all(|c| valid_chars.contains(c)) && s.chars().any(|c| "cxtur".contains(c)) +} + +/// Expands a POSIX tar keystring at `args[1]` into flag-style arguments +/// suitable for clap. +/// +/// Per the POSIX spec the key operand is a function letter optionally +/// followed by modifier letters. Modifier letters `f` and `b` consume +/// the leading file operands (in the order they appear in the key). +/// GNU tar is more permissive and accepts non-standard ordering (for +/// example `fcv`/`vcf`), so we intentionally accept that compatibility mode. +// Keep argv as `OsString` so non-UTF-8/path-native arguments are preserved. +fn expand_posix_keystring(args: Vec) -> Vec { + // Only expand when args[1] is valid UTF-8 and looks like a keystring + let key = match args.get(1).and_then(|s| s.to_str()) { + Some(s) if is_posix_keystring(s) => s.to_string(), + _ => return args, + }; + + // args[2..] are the raw file operands (archive name, blocking factor, files) + let file_operands = &args[2..]; + let mut result: Vec = vec![args[0].clone()]; + let mut file_idx = 0; // how many file operands have been consumed + + for c in key.chars() { + match c { + 'f' => { + // Next file operand is the archive name + result.push(std::ffi::OsString::from("-f")); + if file_idx < file_operands.len() { + result.push(file_operands[file_idx].clone()); + file_idx += 1; + } + } + 'b' => { + // Preserve parity with dash-style parsing by forwarding '-b' + // and its operand (when present). Since '-b' is currently + // unsupported, clap will report it as an unknown argument. + result.push(std::ffi::OsString::from("-b")); + if file_idx < file_operands.len() { + result.push(file_operands[file_idx].clone()); + file_idx += 1; + } + } + other => { + result.push(std::ffi::OsString::from(format!("-{other}"))); + } + } + } + + // Any remaining file operands are the files to archive/extract + result.extend_from_slice(&file_operands[file_idx..]); + result +} #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -31,6 +99,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { args_vec }; + // Support POSIX keystring syntax: `tar cvf archive.tar files…` + // where the first operand is a key rather than a flag-prefixed option. + let args_to_parse = expand_posix_keystring(args_to_parse); + let matches = match uu_app().try_get_matches_from(args_to_parse) { Ok(matches) => matches, Err(err) => { @@ -135,3 +207,116 @@ pub fn uu_app() -> Command { .value_parser(clap::value_parser!(PathBuf)), ]) } + +#[cfg(test)] +mod tests { + use super::*; + + // --- is_posix_keystring --- + + #[test] + fn test_keystring_create() { + assert!(is_posix_keystring("c")); + assert!(is_posix_keystring("cf")); + assert!(is_posix_keystring("cvf")); + assert!(is_posix_keystring("cv")); + } + + #[test] + fn test_keystring_extract() { + assert!(is_posix_keystring("x")); + assert!(is_posix_keystring("xf")); + assert!(is_posix_keystring("xvf")); + } + + #[test] + fn test_keystring_rejects_dash_prefix() { + assert!(!is_posix_keystring("-c")); + assert!(!is_posix_keystring("-cf")); + assert!(!is_posix_keystring("-xvf")); + } + + #[test] + fn test_keystring_rejects_no_function_letter() { + // modifier-only strings are not valid keystrings + assert!(!is_posix_keystring("f")); + assert!(!is_posix_keystring("vf")); + assert!(!is_posix_keystring("v")); + } + + #[test] + fn test_keystring_rejects_invalid_chars() { + assert!(!is_posix_keystring("cz")); // 'z' is not a key char + assert!(!is_posix_keystring("c1")); // digits not allowed + assert!(!is_posix_keystring("archive.tar")); // typical filename + } + + #[test] + fn test_keystring_rejects_empty() { + assert!(!is_posix_keystring("")); + } + + // --- expand_posix_keystring --- + + fn osvec(v: &[&str]) -> Vec { + v.iter().map(std::ffi::OsString::from).collect() + } + + #[test] + fn test_expand_cf() { + let input = osvec(&["tar", "cf", "archive.tar", "file.txt"]); + let expected = osvec(&["tar", "-c", "-f", "archive.tar", "file.txt"]); + assert_eq!(expand_posix_keystring(input), expected); + } + + #[test] + fn test_expand_cvf() { + let input = osvec(&["tar", "cvf", "archive.tar", "file.txt"]); + let expected = osvec(&["tar", "-c", "-v", "-f", "archive.tar", "file.txt"]); + assert_eq!(expand_posix_keystring(input), expected); + } + + #[test] + fn test_expand_xf() { + let input = osvec(&["tar", "xf", "archive.tar"]); + let expected = osvec(&["tar", "-x", "-f", "archive.tar"]); + assert_eq!(expand_posix_keystring(input), expected); + } + + #[test] + fn test_expand_xvf() { + let input = osvec(&["tar", "xvf", "archive.tar"]); + let expected = osvec(&["tar", "-x", "-v", "-f", "archive.tar"]); + assert_eq!(expand_posix_keystring(input), expected); + } + + #[test] + fn test_expand_preserves_dash_prefix_args() { + // When args already use '-' prefixes, no expansion should occur + let input = osvec(&["tar", "-cvf", "archive.tar", "file.txt"]); + assert_eq!(expand_posix_keystring(input.clone()), input); + } + + #[test] + fn test_expand_f_before_files() { + // 'f' consumes only the archive name; remaining args are files + let input = osvec(&["tar", "cf", "archive.tar", "a.txt", "b.txt"]); + let expected = osvec(&["tar", "-c", "-f", "archive.tar", "a.txt", "b.txt"]); + assert_eq!(expand_posix_keystring(input), expected); + } + + #[test] + fn test_expand_function_letter_only() { + // No 'f' modifier: no archive consumed from file operands + let input = osvec(&["tar", "c", "file.txt"]); + let expected = osvec(&["tar", "-c", "file.txt"]); + assert_eq!(expand_posix_keystring(input), expected); + } + + #[test] + fn test_expand_cbf() { + let input = osvec(&["tar", "cbf", "20", "archive.tar", "file.txt"]); + let expected = osvec(&["tar", "-c", "-b", "20", "-f", "archive.tar", "file.txt"]); + assert_eq!(expand_posix_keystring(input), expected); + } +} diff --git a/tests/by-util/test_tar.rs b/tests/by-util/test_tar.rs index 15ecc39..49df72e 100644 --- a/tests/by-util/test_tar.rs +++ b/tests/by-util/test_tar.rs @@ -574,3 +574,86 @@ fn test_extract_created_from_absolute_path() { assert!(at.file_exists(expected_path)); } + +// POSIX keystring tests (no leading '-' on the key operand) + +#[test] +fn test_posix_create_verbose() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.write("file1.txt", "content"); + + ucmd.args(&["cvf", "archive.tar", "file1.txt"]) + .succeeds() + .stdout_contains("file1.txt"); + + assert!(at.file_exists("archive.tar")); +} + +#[test] +fn test_posix_extract_verbose() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.write("file1.txt", "content1"); + at.write("file2.txt", "content2"); + ucmd.args(&["cf", "archive.tar", "file1.txt", "file2.txt"]) + .succeeds(); + + at.remove("file1.txt"); + at.remove("file2.txt"); + + let result = new_ucmd!() + .args(&["xvf", &at.plus_as_string("archive.tar")]) + .current_dir(at.as_string()) + .succeeds(); + + let stdout = result.stdout_str(); + assert!(stdout.contains("file1.txt")); + assert!(stdout.contains("file2.txt")); + + assert!(at.file_exists("file1.txt")); + assert!(at.file_exists("file2.txt")); +} + +#[test] +fn test_posix_and_dash_prefix_both_work() { + // Confirm that POSIX-style and dash-prefixed styles produce identical results. + let (at, mut ucmd) = at_and_ucmd!(); + + at.write("file.txt", "hello"); + + // POSIX style + ucmd.args(&["cf", "posix.tar", "file.txt"]).succeeds(); + + // Dash-prefix style + new_ucmd!() + .args(&["-cf", "dash.tar", "file.txt"]) + .current_dir(at.as_string()) + .succeeds(); + + assert_eq!( + at.read_bytes("posix.tar").len(), + at.read_bytes("dash.tar").len() + ); +} + +#[test] +fn test_posix_b_matches_dash_prefix_failure() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.write("file.txt", "hello"); + + // Dash-prefixed form currently rejects '-b'. + new_ucmd!() + .args(&["-cbf", "20", "dash.tar", "file.txt"]) + .current_dir(at.as_string()) + .fails() + .code_is(64) + .stderr_contains("unexpected argument '-b'"); + + // POSIX keystring form should fail with the same unsupported option. + ucmd.args(&["cbf", "20", "posix.tar", "file.txt"]) + .fails() + .code_is(64) + .stderr_contains("unexpected argument '-b'"); +}