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) } }