Skip to content

Commit 96e957c

Browse files
Copilotkaladron
authored andcommitted
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 `-<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
1 parent c66ad49 commit 96e957c

File tree

2 files changed

+269
-1
lines changed

2 files changed

+269
-1
lines changed

src/uu/tar/src/tar.rs

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,75 @@ use uucore::error::UResult;
1212
use uucore::format_usage;
1313

1414
const ABOUT: &str = "an archiving utility";
15-
const USAGE: &str = "tar {c|x}[v] -f ARCHIVE [FILE...]";
15+
const USAGE: &str = "tar key [FILE...]\n tar {-c|-x} [-v] -f ARCHIVE [FILE...]";
16+
17+
/// Determines whether a string looks like a POSIX tar keystring.
18+
///
19+
/// A valid keystring must not start with '-', must contain at least one
20+
/// function letter (c, x, t, u, r), and every character must be a
21+
/// recognised key character.
22+
fn is_posix_keystring(s: &str) -> bool {
23+
if s.is_empty() || s.starts_with('-') {
24+
return false;
25+
}
26+
let valid_chars = "cxturvwfblmo";
27+
// function letters: c=create, x=extract, t=list, u=update, r=append
28+
// modifier letters: v=verbose, w=interactive, f=file, b=blocking-factor,
29+
// l=one-file-system, m=modification-time, o=no-same-owner
30+
s.chars().all(|c| valid_chars.contains(c)) && s.chars().any(|c| "cxtur".contains(c))
31+
}
32+
33+
/// Expands a POSIX tar keystring at `args[1]` into flag-style arguments
34+
/// suitable for clap.
35+
///
36+
/// Per the POSIX spec the key operand is a function letter optionally
37+
/// followed by modifier letters. Modifier letters `f` and `b` consume
38+
/// the leading file operands (in the order they appear in the key).
39+
/// GNU tar is more permissive and accepts non-standard ordering (for
40+
/// example `fcv`/`vcf`), so we intentionally accept that compatibility mode.
41+
// Keep argv as `OsString` so non-UTF-8/path-native arguments are preserved.
42+
fn expand_posix_keystring(args: Vec<std::ffi::OsString>) -> Vec<std::ffi::OsString> {
43+
// Only expand when args[1] is valid UTF-8 and looks like a keystring
44+
let key = match args.get(1).and_then(|s| s.to_str()) {
45+
Some(s) if is_posix_keystring(s) => s.to_string(),
46+
_ => return args,
47+
};
48+
49+
// args[2..] are the raw file operands (archive name, blocking factor, files)
50+
let file_operands = &args[2..];
51+
let mut result: Vec<std::ffi::OsString> = vec![args[0].clone()];
52+
let mut file_idx = 0; // how many file operands have been consumed
53+
54+
for c in key.chars() {
55+
match c {
56+
'f' => {
57+
// Next file operand is the archive name
58+
result.push(std::ffi::OsString::from("-f"));
59+
if file_idx < file_operands.len() {
60+
result.push(file_operands[file_idx].clone());
61+
file_idx += 1;
62+
}
63+
}
64+
'b' => {
65+
// Preserve parity with dash-style parsing by forwarding '-b'
66+
// and its operand (when present). Since '-b' is currently
67+
// unsupported, clap will report it as an unknown argument.
68+
result.push(std::ffi::OsString::from("-b"));
69+
if file_idx < file_operands.len() {
70+
result.push(file_operands[file_idx].clone());
71+
file_idx += 1;
72+
}
73+
}
74+
other => {
75+
result.push(std::ffi::OsString::from(format!("-{other}")));
76+
}
77+
}
78+
}
79+
80+
// Any remaining file operands are the files to archive/extract
81+
result.extend_from_slice(&file_operands[file_idx..]);
82+
result
83+
}
1684

1785
#[uucore::main]
1886
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
@@ -31,6 +99,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
3199
args_vec
32100
};
33101

102+
// Support POSIX keystring syntax: `tar cvf archive.tar files…`
103+
// where the first operand is a key rather than a flag-prefixed option.
104+
let args_to_parse = expand_posix_keystring(args_to_parse);
105+
34106
let matches = match uu_app().try_get_matches_from(args_to_parse) {
35107
Ok(matches) => matches,
36108
Err(err) => {
@@ -135,3 +207,116 @@ pub fn uu_app() -> Command {
135207
.value_parser(clap::value_parser!(PathBuf)),
136208
])
137209
}
210+
211+
#[cfg(test)]
212+
mod tests {
213+
use super::*;
214+
215+
// --- is_posix_keystring ---
216+
217+
#[test]
218+
fn test_keystring_create() {
219+
assert!(is_posix_keystring("c"));
220+
assert!(is_posix_keystring("cf"));
221+
assert!(is_posix_keystring("cvf"));
222+
assert!(is_posix_keystring("cv"));
223+
}
224+
225+
#[test]
226+
fn test_keystring_extract() {
227+
assert!(is_posix_keystring("x"));
228+
assert!(is_posix_keystring("xf"));
229+
assert!(is_posix_keystring("xvf"));
230+
}
231+
232+
#[test]
233+
fn test_keystring_rejects_dash_prefix() {
234+
assert!(!is_posix_keystring("-c"));
235+
assert!(!is_posix_keystring("-cf"));
236+
assert!(!is_posix_keystring("-xvf"));
237+
}
238+
239+
#[test]
240+
fn test_keystring_rejects_no_function_letter() {
241+
// modifier-only strings are not valid keystrings
242+
assert!(!is_posix_keystring("f"));
243+
assert!(!is_posix_keystring("vf"));
244+
assert!(!is_posix_keystring("v"));
245+
}
246+
247+
#[test]
248+
fn test_keystring_rejects_invalid_chars() {
249+
assert!(!is_posix_keystring("cz")); // 'z' is not a key char
250+
assert!(!is_posix_keystring("c1")); // digits not allowed
251+
assert!(!is_posix_keystring("archive.tar")); // typical filename
252+
}
253+
254+
#[test]
255+
fn test_keystring_rejects_empty() {
256+
assert!(!is_posix_keystring(""));
257+
}
258+
259+
// --- expand_posix_keystring ---
260+
261+
fn osvec(v: &[&str]) -> Vec<std::ffi::OsString> {
262+
v.iter().map(std::ffi::OsString::from).collect()
263+
}
264+
265+
#[test]
266+
fn test_expand_cf() {
267+
let input = osvec(&["tar", "cf", "archive.tar", "file.txt"]);
268+
let expected = osvec(&["tar", "-c", "-f", "archive.tar", "file.txt"]);
269+
assert_eq!(expand_posix_keystring(input), expected);
270+
}
271+
272+
#[test]
273+
fn test_expand_cvf() {
274+
let input = osvec(&["tar", "cvf", "archive.tar", "file.txt"]);
275+
let expected = osvec(&["tar", "-c", "-v", "-f", "archive.tar", "file.txt"]);
276+
assert_eq!(expand_posix_keystring(input), expected);
277+
}
278+
279+
#[test]
280+
fn test_expand_xf() {
281+
let input = osvec(&["tar", "xf", "archive.tar"]);
282+
let expected = osvec(&["tar", "-x", "-f", "archive.tar"]);
283+
assert_eq!(expand_posix_keystring(input), expected);
284+
}
285+
286+
#[test]
287+
fn test_expand_xvf() {
288+
let input = osvec(&["tar", "xvf", "archive.tar"]);
289+
let expected = osvec(&["tar", "-x", "-v", "-f", "archive.tar"]);
290+
assert_eq!(expand_posix_keystring(input), expected);
291+
}
292+
293+
#[test]
294+
fn test_expand_preserves_dash_prefix_args() {
295+
// When args already use '-' prefixes, no expansion should occur
296+
let input = osvec(&["tar", "-cvf", "archive.tar", "file.txt"]);
297+
assert_eq!(expand_posix_keystring(input.clone()), input);
298+
}
299+
300+
#[test]
301+
fn test_expand_f_before_files() {
302+
// 'f' consumes only the archive name; remaining args are files
303+
let input = osvec(&["tar", "cf", "archive.tar", "a.txt", "b.txt"]);
304+
let expected = osvec(&["tar", "-c", "-f", "archive.tar", "a.txt", "b.txt"]);
305+
assert_eq!(expand_posix_keystring(input), expected);
306+
}
307+
308+
#[test]
309+
fn test_expand_function_letter_only() {
310+
// No 'f' modifier: no archive consumed from file operands
311+
let input = osvec(&["tar", "c", "file.txt"]);
312+
let expected = osvec(&["tar", "-c", "file.txt"]);
313+
assert_eq!(expand_posix_keystring(input), expected);
314+
}
315+
316+
#[test]
317+
fn test_expand_cbf() {
318+
let input = osvec(&["tar", "cbf", "20", "archive.tar", "file.txt"]);
319+
let expected = osvec(&["tar", "-c", "-b", "20", "-f", "archive.tar", "file.txt"]);
320+
assert_eq!(expand_posix_keystring(input), expected);
321+
}
322+
}

tests/by-util/test_tar.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,3 +574,86 @@ fn test_extract_created_from_absolute_path() {
574574

575575
assert!(at.file_exists(expected_path));
576576
}
577+
578+
// POSIX keystring tests (no leading '-' on the key operand)
579+
580+
#[test]
581+
fn test_posix_create_verbose() {
582+
let (at, mut ucmd) = at_and_ucmd!();
583+
584+
at.write("file1.txt", "content");
585+
586+
ucmd.args(&["cvf", "archive.tar", "file1.txt"])
587+
.succeeds()
588+
.stdout_contains("file1.txt");
589+
590+
assert!(at.file_exists("archive.tar"));
591+
}
592+
593+
#[test]
594+
fn test_posix_extract_verbose() {
595+
let (at, mut ucmd) = at_and_ucmd!();
596+
597+
at.write("file1.txt", "content1");
598+
at.write("file2.txt", "content2");
599+
ucmd.args(&["cf", "archive.tar", "file1.txt", "file2.txt"])
600+
.succeeds();
601+
602+
at.remove("file1.txt");
603+
at.remove("file2.txt");
604+
605+
let result = new_ucmd!()
606+
.args(&["xvf", &at.plus_as_string("archive.tar")])
607+
.current_dir(at.as_string())
608+
.succeeds();
609+
610+
let stdout = result.stdout_str();
611+
assert!(stdout.contains("file1.txt"));
612+
assert!(stdout.contains("file2.txt"));
613+
614+
assert!(at.file_exists("file1.txt"));
615+
assert!(at.file_exists("file2.txt"));
616+
}
617+
618+
#[test]
619+
fn test_posix_and_dash_prefix_both_work() {
620+
// Confirm that POSIX-style and dash-prefixed styles produce identical results.
621+
let (at, mut ucmd) = at_and_ucmd!();
622+
623+
at.write("file.txt", "hello");
624+
625+
// POSIX style
626+
ucmd.args(&["cf", "posix.tar", "file.txt"]).succeeds();
627+
628+
// Dash-prefix style
629+
new_ucmd!()
630+
.args(&["-cf", "dash.tar", "file.txt"])
631+
.current_dir(at.as_string())
632+
.succeeds();
633+
634+
assert_eq!(
635+
at.read_bytes("posix.tar").len(),
636+
at.read_bytes("dash.tar").len()
637+
);
638+
}
639+
640+
#[test]
641+
fn test_posix_b_matches_dash_prefix_failure() {
642+
let (at, mut ucmd) = at_and_ucmd!();
643+
644+
at.write("file.txt", "hello");
645+
646+
// Dash-prefixed form currently rejects '-b'.
647+
new_ucmd!()
648+
.args(&["-cbf", "20", "dash.tar", "file.txt"])
649+
.current_dir(at.as_string())
650+
.fails()
651+
.code_is(64)
652+
.stderr_contains("unexpected argument '-b'");
653+
654+
// POSIX keystring form should fail with the same unsupported option.
655+
ucmd.args(&["cbf", "20", "posix.tar", "file.txt"])
656+
.fails()
657+
.code_is(64)
658+
.stderr_contains("unexpected argument '-b'");
659+
}

0 commit comments

Comments
 (0)