@@ -12,7 +12,75 @@ use uucore::error::UResult;
1212use uucore:: format_usage;
1313
1414const 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]
1886pub 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+ }
0 commit comments