add brace expansion
- a{b,c}d, nested braces, empty alternatives (file{,.bak})
- sequences {1..5}, {5..1}, {1..10..3}, zero-padded {01..10}, letters
{a..e}
- happens first on the command text, the results go through ~,
variable and wildcard expansion
- quoted/escaped braces, ${...} and braces without comma or valid
sequence are kept; \, protects a comma
- a redirection target must expand to one word (ambiguous redirect),
a word may expand to at most 100000 words
- completion escapes { in file names
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
da281ca4ab
commit
b3311cbf94
4 files changed
+424
-1
No files matched your search
+118
@@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExpandBraces(t *testing.T) {
|
||||
tests := []struct {
|
||||
word string
|
||||
want []string
|
||||
}{
|
||||
{"a{b,c}d", []string{"abd", "acd"}},
|
||||
{"{a,b}{1,2}", []string{"a1", "a2", "b1", "b2"}},
|
||||
{"x{a,{b,c}}y", []string{"xay", "xby", "xcy"}},
|
||||
{"file{,.bak}", []string{"file", "file.bak"}},
|
||||
{"{1..5}", []string{"1", "2", "3", "4", "5"}},
|
||||
{"{3..1}", []string{"3", "2", "1"}},
|
||||
{"{1..10..3}", []string{"1", "4", "7", "10"}},
|
||||
{"{10..1..-4}", []string{"10", "6", "2"}},
|
||||
{"{01..10..4}", []string{"01", "05", "09"}},
|
||||
{"{8..010..1}", []string{"008", "009", "010"}},
|
||||
{"{-2..2}", []string{"-2", "-1", "0", "1", "2"}},
|
||||
{"{-01..1}", []string{"-01", "000", "001"}},
|
||||
{"{a..e}", []string{"a", "b", "c", "d", "e"}},
|
||||
{"{C..A}", []string{"C", "B", "A"}},
|
||||
{"*.{go,md}", []string{"*.go", "*.md"}},
|
||||
{`{"a b",c}`, []string{`"a b"`, "c"}},
|
||||
{`{a\,b,c}`, []string{`a\,b`, "c"}},
|
||||
{"{$HOME,~}/x", []string{"$HOME/x", "~/x"}},
|
||||
// Not expanded: quoted, escaped, no comma, ${...}, invalid sequences
|
||||
{`"{a,b}"`, []string{`"{a,b}"`}},
|
||||
{`'{a,b}'`, []string{`'{a,b}'`}},
|
||||
{`\{a,b\}`, []string{`\{a,b\}`}},
|
||||
{"{a}", []string{"{a}"}},
|
||||
{"{}", []string{"{}"}},
|
||||
{"{a,b", []string{"{a,b"}},
|
||||
{"a,b}", []string{"a,b}"}},
|
||||
{"${HOME}", []string{"${HOME}"}},
|
||||
{"${X:-a,b}", []string{"${X:-a,b}"}},
|
||||
{"{a..1}", []string{"{a..1}"}},
|
||||
{"{1..2..x}", []string{"{1..2..x}"}},
|
||||
{"{ab..cd}", []string{"{ab..cd}"}},
|
||||
// An invalid brace before a valid one
|
||||
{"{a}{b,c}", []string{"{a}b", "{a}c"}},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got, err := expandBraces(tc.word)
|
||||
if err != nil {
|
||||
t.Errorf("expandBraces(%q) returned error: %v", tc.word, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("expandBraces(%q) = %q, want %q", tc.word, got, tc.want)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := expandBraces("{1..1000000}"); err == nil {
|
||||
t.Error("expected an error for a too large sequence")
|
||||
}
|
||||
if _, err := expandBraces("{1..11}{1..100}{1..100}"); err == nil {
|
||||
t.Error("expected an error for too many words")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandBraceWords(t *testing.T) {
|
||||
tests := []struct {
|
||||
input, want string
|
||||
}{
|
||||
{"echo a{b,c} d", "echo ab ac d"},
|
||||
{"echo {1..3}|wc -w", "echo 1 2 3|wc -w"},
|
||||
{"echo a > {f}", "echo a > {f}"},
|
||||
{"echo {a,b} 2>&1", "echo a b 2>&1"},
|
||||
{"echo x # {a,b}", "echo x # {a,b}"},
|
||||
{`echo "{a,b} c" {d,e}`, `echo "{a,b} c" d e`},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got, err := expandBraceWords(tc.input)
|
||||
if err != nil || got != tc.want {
|
||||
t.Errorf("expandBraceWords(%q) = %q, %v; want %q", tc.input, got, err, tc.want)
|
||||
}
|
||||
}
|
||||
|
||||
for _, input := range []string{"echo a > {f,g}", "cat < x{1..2}", "echo a 2>> {a,b}"} {
|
||||
if _, err := expandBraceWords(input); err == nil || !strings.Contains(err.Error(), "ambiguous redirect") {
|
||||
t.Errorf("expandBraceWords(%q): %v, want ambiguous redirect", input, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecInput_BraceExpansion(t *testing.T) {
|
||||
origDir, _ := os.Getwd()
|
||||
defer os.Chdir(origDir)
|
||||
dir := t.TempDir()
|
||||
os.Chdir(dir)
|
||||
|
||||
if err := execInput("mkdir -p d/{x,y}/{1..2} && touch d/x/1/f{a,b}.txt"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, path := range []string{"d/x/1", "d/x/2", "d/y/1", "d/y/2", "d/x/1/fa.txt", "d/x/1/fb.txt"} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Errorf("%s was not created", path)
|
||||
}
|
||||
}
|
||||
|
||||
// Brace expansion comes before wildcards and variables
|
||||
t.Setenv("GOSH_BRACE", "v")
|
||||
execInput("echo d/x/1/*.{txt,go} {${GOSH_BRACE},w}1 > out")
|
||||
if got, _ := os.ReadFile("out"); string(got) != "d/x/1/fa.txt d/x/1/fb.txt d/x/1/*.go v1 w1\n" {
|
||||
t.Errorf("out = %q", got)
|
||||
}
|
||||
|
||||
if err := execInput("echo a > {f,g}"); err == nil || lastStatus != 1 {
|
||||
t.Errorf("ambiguous redirect: %v, $? = %d", err, lastStatus)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user