Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ guessing.
`--no-same-permissions`, `--format`, `--posix`, `--owner`, `--group`,
`--numeric-owner`, `--mtime`, `--sort`, `--no-recursion`, `--exclude`,
`--delay-directory-restore`, `--force-local`. The old-style first argument
(`xf`, `cvzf`) and clustered short options work as in GNU tar; a read
(`xf`, `cvzf`) and clustered short options work as in GNU tar, and a long
option may be given as any unambiguous prefix of its name (`--strip 1`); a read
without a compression flag picks the decoder by magic, and a known
compressor given to `-I` (`zstd`, `zstdmt`, `unzstd`, `gzip`, `xz`, …) is
the built-in codec, any other a filter run with `-d` to read. Extraction
Expand Down
3 changes: 2 additions & 1 deletion internal/tarcmd/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
// --help, --version. The
// first argument may be an old-style option bundle (xf, cvzf); short options
// cluster (-xzf); long options take their argument after '=' or as the next
// word. Anything else is a usage error. Members named for --extract or
// word and may be given as any unambiguous prefix of their name, as GNU tar
// and bsdtar take them. Anything else is a usage error. Members named for --extract or
// --list, on the command line or in -T files, select the entries taken: a
// name as the archive carries it, or a directory with everything under it;
// one no entry carries is reported and the status is 2.
Expand Down
65 changes: 58 additions & 7 deletions internal/tarcmd/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os/user"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -238,10 +239,19 @@ func (o *options) shortWithValue(letter rune, value string) error {
}

// long applies one long option, given without its dashes, with its value
// after '=' or in the next word. It returns how many following words it
// consumed.
// after '=' or in the next word; the name may be any unambiguous prefix of
// an option's, as getopt_long takes it for GNU tar and as bsdtar takes it.
// It returns how many following words it consumed.
func (o *options) long(spec string, following []string) (int, error) {
name, value, hasValue := strings.Cut(spec, "=")
given, value, hasValue := strings.Cut(spec, "=")
if given == "" {
return 0, usage("unrecognized option '--%s'", spec)
}

name, err := resolveLong(given)
if err != nil {
return 0, err
}

flag, isFlag := longFlags()[name]
if isFlag {
Expand All @@ -252,10 +262,7 @@ func (o *options) long(spec string, following []string) (int, error) {
return 0, flag(o)
}

withValue, takesValue := longWithValue()[name]
if !takesValue {
return 0, usage("unrecognized option '--%s'", name)
}
withValue := longWithValue()[name]

if hasValue {
return 0, withValue(o, value)
Expand All @@ -268,6 +275,50 @@ func (o *options) long(spec string, following []string) (int, error) {
return 1, withValue(o, following[0])
}

// resolveLong is the long option the given text names: itself when it is
// an option's whole name, else the one option it is a prefix of. No option
// is unrecognized; several are ambiguous, listed as GNU tar lists them.
func resolveLong(given string) (string, error) {
flags, withValue := longFlags(), longWithValue()

if _, ok := flags[given]; ok {
return given, nil
}

if _, ok := withValue[given]; ok {
return given, nil
}

var found []string

for name := range flags {
if strings.HasPrefix(name, given) {
found = append(found, name)
}
}

for name := range withValue {
if strings.HasPrefix(name, given) {
found = append(found, name)
}
}

switch len(found) {
case 0:
return "", usage("unrecognized option '--%s'", given)
case 1:
return found[0], nil
}

slices.Sort(found)

for i, name := range found {
found[i] = "'--" + name + "'"
}

return "", usage("option '--%s' is ambiguous; possibilities: %s", given, strings.Join(found, " "))
}

// setFlag is a long option without a value: it sets one field.
func setFlag(set func(*options)) func(*options) error {
return func(opts *options) error {
Expand Down
35 changes: 35 additions & 0 deletions internal/tarcmd/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@ func TestUsage(t *testing.T) {
want string
}{
{[]string{"-cf", "a.tar", "--bogus"}, "unrecognized option '--bogus'"},
{[]string{"-cf", "a.tar", "--strp", "y"}, "unrecognized option '--strp'"},
{[]string{"-cf", "a.tar", "--ver", "y"}, "option '--ver' is ambiguous; possibilities: '--verbose' '--version'"},
{[]string{"-cf", "a.tar", "--same", "y"}, "option '--same' is ambiguous; possibilities: '--same-owner' '--same-permissions'"},
{[]string{"-cf", "a.tar", "--=x", "y"}, "unrecognized option '--=x'"},
{[]string{"-q"}, "invalid option -- 'q'"},
{[]string{"-cf"}, "option requires an argument -- 'f'"},
{[]string{"-c"}, "Cowardly refusing to create an empty archive"},
Expand Down Expand Up @@ -1036,3 +1040,34 @@ func TestCompressProgram(t *testing.T) {
t.Errorf("a failing filter: exit %d, %q", status, stderr)
}
}

// TestLongOptionAbbreviation: a long option may be given as any unambiguous
// prefix of its name, as getopt_long takes it for GNU tar and as bsdtar
// takes it, with or without its value after '='; a whole name wins over
// the options it is a prefix of, which is --file against --files-from, the
// one such pair in the tables, each taken as itself. farcloser/tarmac's
// init piped a tarball into "tar xz --strip 1" and was refused.
func TestLongOptionAbbreviation(t *testing.T) {
t.Parallel()

src := tree(t)
list := filepath.Join(t.TempDir(), "list")
writeFile(t, list, "sub\n", 0o644)

stdout, _ := mustRun(t, nil, "--cre", "--file", "-", "--dir", src, "--verb", "--xz", "--files-from", list)
if !strings.HasPrefix(stdout, "\xfd7zXZ") {
t.Fatalf("--xz archive starts %q", stdout[:min(6, len(stdout))])
}

for _, args := range [][]string{
{"--extr", "--strip", "1", "--xz", "-C"},
{"--ext", "--strip=1", "-J", "--directory"},
} {
dst := t.TempDir()
mustRun(t, []byte(stdout), append(args, dst)...)

if got := readFile(t, filepath.Join(dst, "run.sh")); !strings.Contains(got, "#!") {
t.Errorf("tar %v: run.sh after --strip 1 reads %q", args, got)
}
}
}
Loading