package main // Brace expansion like in bash: a{b,c}d becomes abd acd, {1..5} becomes // 1 2 3 4 5. It happens first, on the text of the command line, so that the // resulting words go through the other expansions (~, variables, wildcards). import ( "errors" "fmt" "strconv" "strings" ) // maxBraceWords limits the number of words a single word may expand to. const maxBraceWords = 100000 // expandBraceWords applies brace expansion to every word of a command (the // text of a pipeline). Quoted or escaped braces and ${...} are left alone. // A redirection target must expand to a single word. func expandBraceWords(input string) (string, error) { var ( out strings.Builder afterRedirect bool // the next word is the target of a redirection ) for i := 0; i < len(input); { ch := input[i] switch { case ch == ' ' || ch == '\t' || ch == '\n': out.WriteByte(ch) i++ case strings.IndexByte(";&|<>", ch) >= 0: if ch == '<' || ch == '>' { afterRedirect = true } out.WriteByte(ch) i++ case ch == '#': // A comment, until the end of the line out.WriteString(input[i:]) return out.String(), nil default: end := wordEnd(input, i) words, err := expandBraces(input[i:end]) if err != nil { return "", err } if afterRedirect && len(words) > 1 { return "", fmt.Errorf("%s: ambiguous redirect", sanitize(input[i:end])) } afterRedirect = false out.WriteString(strings.Join(words, " ")) i = end } } return out.String(), nil } // wordEnd returns the end of the word starting at i: the next unquoted blank // or operator character. func wordEnd(s string, i int) int { var quote byte for ; i < len(s); i++ { ch := s[i] switch { case quote == '\'': if ch == '\'' { quote = 0 } case ch == '\\': i++ case ch == '$' && i+1 < len(s) && s[i+1] == '{': if j := strings.IndexByte(s[i:], '}'); j >= 0 { i += j } case quote == '"': if ch == '"' { quote = 0 } case ch == '"' || ch == '\'': quote = ch case strings.IndexByte(" \t\n;&|<>", ch) >= 0: return i } } return len(s) } // braceScan calls fn for every unquoted, unescaped character of s that is // not part of a ${...} reference, with its index and the brace depth. // Scanning stops when fn returns false. func braceScan(s string, fn func(i, depth int) bool) { var quote byte depth := 0 for i := 0; i < len(s); i++ { ch := s[i] switch { case quote == '\'': if ch == '\'' { quote = 0 } continue case ch == '\\': i++ continue case ch == '$' && i+1 < len(s) && s[i+1] == '{': if j := strings.IndexByte(s[i:], '}'); j >= 0 { i += j } continue case quote == '"': if ch == '"' { quote = 0 } continue case ch == '"' || ch == '\'': quote = ch continue } if ch == '}' { depth-- } if !fn(i, depth) { return } if ch == '{' { depth++ } } } // expandBraces expands the first valid brace expression of a word and then // recursively the results: a{b,c}d -> abd acd, x{1..3} -> x1 x2 x3. A brace // expression is valid if it contains an unquoted comma at its top level or // is a sequence; other braces ({}, {a}, ${x}) are kept as they are. func expandBraces(word string) ([]string, error) { // Find the candidates: unquoted { with their matching } var opens []int pairs := map[int]int{} braceScan(word, func(i, depth int) bool { switch word[i] { case '{': opens = append(opens, i) case '}': if len(opens) > 0 { pairs[opens[len(opens)-1]] = i opens = opens[:len(opens)-1] } } return true }) for start := 0; start < len(word); start++ { end, ok := pairs[start] if !ok { continue } inner := word[start+1 : end] alternatives := splitTopLevelCommas(inner) if len(alternatives) < 2 { seq, ok, err := braceSequence(inner) if err != nil { return nil, err } if !ok { continue } alternatives = seq } prefix, suffix := word[:start], word[end+1:] var words []string for _, alt := range alternatives { expanded, err := expandBraces(prefix + alt + suffix) if err != nil { return nil, err } words = append(words, expanded...) if len(words) > maxBraceWords { return nil, errors.New("brace expansion: too many words") } } return words, nil } return []string{word}, nil } // splitTopLevelCommas splits s at its unquoted commas that are not inside // nested braces. func splitTopLevelCommas(s string) []string { var parts []string last := 0 braceScan(s, func(i, depth int) bool { if s[i] == ',' && depth == 0 { parts = append(parts, s[last:i]) last = i + 1 } return true }) return append(parts, s[last:]) } // braceSequence expands a sequence expression: x..y or x..y..step with // integers (zero-padded to the same width if one of them has a leading zero) // or single letters. ok is false if s is no sequence. func braceSequence(s string) (words []string, ok bool, err error) { parts := strings.Split(s, "..") if len(parts) != 2 && len(parts) != 3 { return nil, false, nil } step := 1 if len(parts) == 3 { step, err = strconv.Atoi(parts[2]) if err != nil { return nil, false, nil } if step < 0 { step = -step } if step == 0 { step = 1 } } from, errFrom := strconv.Atoi(parts[0]) to, errTo := strconv.Atoi(parts[1]) isLetter := func(p string) bool { return len(p) == 1 && (p[0] >= 'a' && p[0] <= 'z' || p[0] >= 'A' && p[0] <= 'Z') } format := func(n int) string { return strconv.Itoa(n) } switch { case errFrom == nil && errTo == nil: // {01..10}: pad to the width of the longer number if hasLeadingZero(parts[0]) || hasLeadingZero(parts[1]) { width := max(len(parts[0]), len(parts[1])) format = func(n int) string { if n < 0 { return fmt.Sprintf("-%0*d", width-1, -n) } return fmt.Sprintf("%0*d", width, n) } } case isLetter(parts[0]) && isLetter(parts[1]): from, to = int(parts[0][0]), int(parts[1][0]) format = func(n int) string { return string(rune(n)) } default: return nil, false, nil } count := (max(from, to)-min(from, to))/step + 1 if count > maxBraceWords { return nil, false, errors.New("brace expansion: too many words") } if to < from { step = -step } for n := from; (step > 0 && n <= to) || (step < 0 && n >= to); n += step { words = append(words, format(n)) } return words, true, nil } // hasLeadingZero reports whether a number is written with a leading zero // (01, -05), which makes a sequence zero-padded. func hasLeadingZero(s string) bool { s = strings.TrimPrefix(s, "-") return len(s) > 1 && s[0] == '0' }