mirror of
https://github.com/uutils/tar.git
synced 2026-06-10 16:14:35 -07:00
Add POSIX keystring parsing and argv pre-expansion (#114)
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 `-<opt>` 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 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
+186
-1
@@ -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<std::ffi::OsString>) -> Vec<std::ffi::OsString> {
|
||||
// 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<std::ffi::OsString> = 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<std::ffi::OsString> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user