Files
gosh/brace_test.go
Florian WaltherandClaude Opus 5.5 b3311cbf94 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>
2026-10-03 23:42:25 +02:00

119 lines
3.7 KiB
Go

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