add wildcards (*, ?, [...])
- unquoted * ? and [...] (including [!...]) are expanded to the matching file names, sorted, per path component - hidden files only match if the pattern starts with a dot, a trailing / only matches directories, a pattern without match is kept as is - quoted or escaped characters and values of variables are never expanded - redirections to a pattern must match exactly one file - completion escapes * ? [ and $ in file names Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c46903e3d2
commit
1182d8d146
3 files changed
+304
-15
No files matched your search
+110
@@ -1514,6 +1514,10 @@ func TestTokenize_Operators(t *testing.T) {
|
||||
t.Errorf("tokenize(%q) returned error: %v", tc.input, err)
|
||||
continue
|
||||
}
|
||||
// Patterns are tested in TestTokenize_Patterns
|
||||
for i := range got {
|
||||
got[i].pattern, got[i].glob = "", false
|
||||
}
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("tokenize(%q) = %+v, want %+v", tc.input, got, tc.want)
|
||||
}
|
||||
@@ -1620,3 +1624,109 @@ func TestExecInput_RedirectionErrors(t *testing.T) {
|
||||
t.Error("file f must not be created for a bad file descriptor")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenize_Patterns(t *testing.T) {
|
||||
t.Setenv("GOSH_GLOB", "*.go")
|
||||
tests := []struct {
|
||||
input string
|
||||
pattern string
|
||||
glob bool
|
||||
}{
|
||||
{"*.go", "*.go", true},
|
||||
{"a?c", "a?c", true},
|
||||
{"[ab].go", "[ab].go", true},
|
||||
{"[!a].go", "[^a].go", true},
|
||||
{"plain", "plain", false},
|
||||
// Quoted, escaped and expanded characters are literal
|
||||
{`"*.go"`, `\*.go`, false},
|
||||
{`'*'.go`, `\*.go`, false},
|
||||
{`\*.go`, `\*.go`, false},
|
||||
{`"x"*`, `x*`, true},
|
||||
{"$GOSH_GLOB", `\*.go`, false},
|
||||
{`'['!a].go`, `\[!a].go`, false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tokens, err := tokenize(tc.input)
|
||||
if err != nil || len(tokens) != 1 {
|
||||
t.Errorf("tokenize(%q) = %+v, %v", tc.input, tokens, err)
|
||||
continue
|
||||
}
|
||||
if tokens[0].pattern != tc.pattern || tokens[0].glob != tc.glob {
|
||||
t.Errorf("tokenize(%q): pattern %q, glob %v; want %q, %v",
|
||||
tc.input, tokens[0].pattern, tokens[0].glob, tc.pattern, tc.glob)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandGlob(t *testing.T) {
|
||||
origDir, _ := os.Getwd()
|
||||
defer os.Chdir(origDir)
|
||||
dir := t.TempDir()
|
||||
os.Chdir(dir)
|
||||
for _, d := range []string{"sub/deep", "other"} {
|
||||
os.MkdirAll(d, 0755)
|
||||
}
|
||||
for _, f := range []string{"a.go", "b.go", "c.txt", ".hidden.go", "with space.go", "[x].go", "sub/s1.go", "sub/deep/d.go", "other/o.go"} {
|
||||
os.WriteFile(f, nil, 0644)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
pattern string
|
||||
want []string
|
||||
}{
|
||||
{"*.go", []string{"[x].go", "a.go", "b.go", "with space.go"}},
|
||||
{".*.go", []string{".hidden.go"}},
|
||||
{"?.go", []string{"a.go", "b.go"}},
|
||||
{"[ab].go", []string{"a.go", "b.go"}},
|
||||
{"[^a].go", []string{"b.go"}},
|
||||
{"*/*.go", []string{"other/o.go", "sub/s1.go"}},
|
||||
{"*/", []string{"other/", "sub/"}},
|
||||
{"sub/*/*.go", []string{"sub/deep/d.go"}},
|
||||
{"./c*", []string{"./c.txt"}},
|
||||
{dir + "/c*", []string{dir + "/c.txt"}},
|
||||
{`\[x\].go`, []string{"[x].go"}},
|
||||
{"*.xyz", nil},
|
||||
{"[", nil},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := expandGlob(tc.pattern); !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("expandGlob(%q) = %q, want %q", tc.pattern, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecInput_Wildcards(t *testing.T) {
|
||||
origDir, _ := os.Getwd()
|
||||
defer os.Chdir(origDir)
|
||||
dir := t.TempDir()
|
||||
os.Chdir(dir)
|
||||
for _, f := range []string{"a.go", "b.go", "c.txt"} {
|
||||
os.WriteFile(f, []byte(f+"\n"), 0644)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"echo *.go > out", "a.go b.go\n"},
|
||||
{"cat *.go > out", "a.go\nb.go\n"},
|
||||
// No match: the word is passed on unchanged, like in sh
|
||||
{"echo *.xyz > out", "*.xyz\n"},
|
||||
{`echo "*.go" '?.go' \*.go > out`, "*.go ?.go *.go\n"},
|
||||
// A redirection to a pattern with a single match uses that file
|
||||
{"echo hi > *.txt; cat c.txt > out", "hi\n"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if err := execInput(tc.input); err != nil {
|
||||
t.Errorf("execInput(%q) returned error: %v", tc.input, err)
|
||||
}
|
||||
if got, _ := os.ReadFile("out"); string(got) != tc.want {
|
||||
t.Errorf("%q: got %q, want %q", tc.input, got, tc.want)
|
||||
}
|
||||
}
|
||||
|
||||
err := execInput("echo hi > *.go")
|
||||
if err == nil || err.Error() != "*.go: ambiguous redirect" {
|
||||
t.Errorf("expected ambiguous redirect error, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user