743 lines
17 KiB
Go
743 lines
17 KiB
Go
package shell
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func isVarChar(c byte) bool {
|
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
|
|
}
|
|
|
|
// expandWord expands $VAR, ${VAR}, $(...), $((...)) in a single token.
|
|
// Quote characters (single/double) are interpreted here and stripped from output.
|
|
func (s *Shell) expandWord(word string) string {
|
|
// Tilde expansion (only when not quoted)
|
|
if strings.HasPrefix(word, "~") {
|
|
home := s.GetVar("HOME")
|
|
if home == "" {
|
|
home = os.Getenv("USERPROFILE")
|
|
}
|
|
if len(word) == 1 {
|
|
return home
|
|
}
|
|
if word[1] == '/' || word[1] == '\\' {
|
|
return home + word[1:]
|
|
}
|
|
}
|
|
|
|
var result strings.Builder
|
|
inSingle := false
|
|
inDouble := false
|
|
i := 0
|
|
|
|
for i < len(word) {
|
|
ch := word[i]
|
|
|
|
switch {
|
|
case ch == '\'' && !inDouble:
|
|
inSingle = !inSingle
|
|
i++
|
|
case ch == '"' && !inSingle:
|
|
inDouble = !inDouble
|
|
i++
|
|
case ch == '\\' && !inSingle:
|
|
if i+1 < len(word) {
|
|
next := word[i+1]
|
|
if inDouble {
|
|
// In double quotes, only certain chars are escaped
|
|
switch next {
|
|
case '$', '`', '"', '\\', '\n':
|
|
result.WriteByte(next)
|
|
i += 2
|
|
default:
|
|
result.WriteByte('\\')
|
|
result.WriteByte(next)
|
|
i += 2
|
|
}
|
|
} else {
|
|
// Only consume the backslash when escaping a shell
|
|
// metacharacter. Before regular path characters (letters,
|
|
// digits, etc.) keep it literal so Windows paths like
|
|
// C:\workspace work unquoted.
|
|
const metachars = " \t\n$*?[\"'\\|&;()<>{}!#~`"
|
|
if strings.ContainsRune(metachars, rune(next)) {
|
|
result.WriteByte(next)
|
|
} else {
|
|
result.WriteByte('\\')
|
|
result.WriteByte(next)
|
|
}
|
|
i += 2
|
|
}
|
|
} else {
|
|
i++
|
|
}
|
|
case ch == '$' && !inSingle:
|
|
i++ // skip $
|
|
if i >= len(word) {
|
|
result.WriteByte('$')
|
|
break
|
|
}
|
|
// $'...' ANSI C string
|
|
if word[i] == '\'' {
|
|
i++ // skip opening '
|
|
for i < len(word) && word[i] != '\'' {
|
|
if word[i] == '\\' && i+1 < len(word) {
|
|
i++ // skip backslash, now at escape char
|
|
switch word[i] {
|
|
case 'n':
|
|
result.WriteByte('\n')
|
|
case 't':
|
|
result.WriteByte('\t')
|
|
case 'r':
|
|
result.WriteByte('\r')
|
|
case '\\':
|
|
result.WriteByte('\\')
|
|
case '\'':
|
|
result.WriteByte('\'')
|
|
case '"':
|
|
result.WriteByte('"')
|
|
case 'a':
|
|
result.WriteByte('\a')
|
|
case 'b':
|
|
result.WriteByte('\b')
|
|
case 'f':
|
|
result.WriteByte('\f')
|
|
case 'v':
|
|
result.WriteByte('\v')
|
|
case 'e', 'E':
|
|
result.WriteByte(0x1b)
|
|
case '0', '1', '2', '3', '4', '5', '6', '7':
|
|
// Octal \NNN — up to 3 digits
|
|
oct := 0
|
|
for k := 0; k < 3 && i < len(word) && word[i] >= '0' && word[i] <= '7'; k++ {
|
|
oct = oct*8 + int(word[i]-'0')
|
|
i++
|
|
}
|
|
result.WriteByte(byte(oct))
|
|
continue
|
|
case 'x':
|
|
// Hex \xNN — up to 2 digits
|
|
i++ // skip 'x'
|
|
hexv := 0
|
|
for k := 0; k < 2 && i < len(word); k++ {
|
|
d := word[i]
|
|
if d >= '0' && d <= '9' {
|
|
hexv = hexv*16 + int(d-'0')
|
|
i++
|
|
} else if d >= 'a' && d <= 'f' {
|
|
hexv = hexv*16 + int(d-'a'+10)
|
|
i++
|
|
} else if d >= 'A' && d <= 'F' {
|
|
hexv = hexv*16 + int(d-'A'+10)
|
|
i++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
result.WriteByte(byte(hexv))
|
|
continue
|
|
default:
|
|
result.WriteByte('\\')
|
|
result.WriteByte(word[i])
|
|
}
|
|
i++
|
|
} else {
|
|
result.WriteByte(word[i])
|
|
i++
|
|
}
|
|
}
|
|
if i < len(word) {
|
|
i++ // skip closing '
|
|
}
|
|
continue
|
|
}
|
|
|
|
switch word[i] {
|
|
case '(':
|
|
if i+1 < len(word) && word[i+1] == '(' {
|
|
// $(( arithmetic ))
|
|
j := i + 2
|
|
depth := 2
|
|
for j < len(word) {
|
|
if word[j] == '(' {
|
|
depth++
|
|
}
|
|
if word[j] == ')' {
|
|
depth--
|
|
if depth == 0 {
|
|
j++
|
|
break
|
|
}
|
|
}
|
|
j++
|
|
}
|
|
expr := word[i+2 : j-2]
|
|
result.WriteString(strconv.Itoa(s.evalArith(expr)))
|
|
i = j
|
|
} else {
|
|
// $( command substitution )
|
|
j := i + 1
|
|
depth := 1
|
|
for j < len(word) {
|
|
if word[j] == '(' {
|
|
depth++
|
|
}
|
|
if word[j] == ')' {
|
|
depth--
|
|
if depth == 0 {
|
|
break
|
|
}
|
|
}
|
|
j++
|
|
}
|
|
cmd := word[i+1 : j]
|
|
out := s.captureCommand(cmd)
|
|
result.WriteString(strings.TrimRight(out, "\n"))
|
|
i = j + 1
|
|
}
|
|
case '{':
|
|
j := i + 1
|
|
depth := 1
|
|
for j < len(word) {
|
|
if word[j] == '{' {
|
|
depth++
|
|
}
|
|
if word[j] == '}' {
|
|
depth--
|
|
if depth == 0 {
|
|
break
|
|
}
|
|
}
|
|
j++
|
|
}
|
|
varExpr := word[i+1 : j]
|
|
result.WriteString(s.evalVarExpr(varExpr))
|
|
i = j + 1
|
|
case '?':
|
|
result.WriteString(s.vars["?"])
|
|
i++
|
|
case '$':
|
|
result.WriteString(fmt.Sprintf("%d", os.Getpid()))
|
|
i++
|
|
case '!':
|
|
result.WriteString(s.vars["!"])
|
|
i++
|
|
case '#':
|
|
result.WriteString(s.vars["#"])
|
|
i++
|
|
case '@':
|
|
if len(s.args) == 0 {
|
|
result.WriteString("\x01") // empty-expansion sentinel → 0 tokens
|
|
} else {
|
|
result.WriteString(strings.Join(s.args, "\x01"))
|
|
}
|
|
i++
|
|
case '*':
|
|
result.WriteString(s.vars["*"])
|
|
i++
|
|
default:
|
|
j := i
|
|
for j < len(word) && isVarChar(word[j]) {
|
|
j++
|
|
}
|
|
if j == i {
|
|
result.WriteByte('$')
|
|
} else {
|
|
result.WriteString(s.getVar(word[i:j]))
|
|
i = j
|
|
}
|
|
}
|
|
default:
|
|
result.WriteByte(ch)
|
|
i++
|
|
}
|
|
}
|
|
return result.String()
|
|
}
|
|
|
|
func (s *Shell) getVar(name string) string {
|
|
// Resolve nameref chain
|
|
resolved := s.resolveNR(name)
|
|
if v, ok := s.vars[resolved]; ok {
|
|
return v
|
|
}
|
|
return os.Getenv(resolved)
|
|
}
|
|
|
|
func (s *Shell) evalVarExpr(expr string) string {
|
|
// ${#arr[@]} or ${#arr[*]} → length of array
|
|
if strings.HasPrefix(expr, "#") {
|
|
rest := expr[1:]
|
|
if strings.HasSuffix(rest, "[@]") || strings.HasSuffix(rest, "[*]") {
|
|
arrName := rest[:len(rest)-3]
|
|
return strconv.Itoa(len(s.getArray(arrName)))
|
|
}
|
|
// ${#VAR} — string length
|
|
return strconv.Itoa(len(s.getVar(rest)))
|
|
}
|
|
|
|
// Array indexing: ${arr[@]}, ${arr[*]}, ${arr[N]}, ${arr[N]:-default}, etc.
|
|
// Find the bracket pair and evaluate the array access even when an operator
|
|
// (:- := :+) follows the closing ].
|
|
if bracketIdx := strings.Index(expr, "["); bracketIdx >= 0 {
|
|
closeIdx := -1
|
|
// Find matching ] — must balance nested $((…)) parens inside the index
|
|
depth := 0
|
|
for k := bracketIdx + 1; k < len(expr); k++ {
|
|
if expr[k] == '(' {
|
|
depth++
|
|
} else if expr[k] == ')' {
|
|
depth--
|
|
} else if expr[k] == ']' && depth == 0 {
|
|
closeIdx = k
|
|
break
|
|
}
|
|
}
|
|
if closeIdx > bracketIdx {
|
|
prefix := expr[:bracketIdx]
|
|
// No operator before the bracket
|
|
if !strings.ContainsAny(prefix, ":=+%#") {
|
|
arrName := prefix
|
|
idx := expr[bracketIdx+1 : closeIdx]
|
|
rest := expr[closeIdx+1:] // may be empty or ":-…" / ":+…" / ":=…"
|
|
|
|
arrVal := ""
|
|
isMulti := false
|
|
multiVal := ""
|
|
|
|
switch idx {
|
|
case "@":
|
|
arr := s.getArray(arrName)
|
|
if len(arr) == 0 {
|
|
arrVal = ""
|
|
} else {
|
|
isMulti = true
|
|
multiVal = strings.Join(arr, "\x01")
|
|
}
|
|
case "*":
|
|
arr := s.getArray(arrName)
|
|
arrVal = strings.Join(arr, " ")
|
|
default:
|
|
n := s.evalArith(idx)
|
|
arr := s.getArray(arrName)
|
|
if n >= 0 && n < len(arr) {
|
|
arrVal = arr[n]
|
|
}
|
|
}
|
|
|
|
// Apply trailing operator (:- := :+) if present
|
|
switch {
|
|
case rest == "":
|
|
if isMulti {
|
|
if multiVal == "" {
|
|
return "\x01"
|
|
}
|
|
return multiVal
|
|
}
|
|
return arrVal
|
|
case strings.HasPrefix(rest, ":-"):
|
|
if isMulti {
|
|
return multiVal
|
|
}
|
|
if arrVal != "" {
|
|
return arrVal
|
|
}
|
|
return s.expandWord(rest[2:])
|
|
case strings.HasPrefix(rest, ":="):
|
|
if arrVal != "" {
|
|
return arrVal
|
|
}
|
|
expanded := s.expandWord(rest[2:])
|
|
s.vars[arrName] = expanded
|
|
return expanded
|
|
case strings.HasPrefix(rest, ":+"):
|
|
if isMulti {
|
|
return s.expandWord(rest[2:])
|
|
}
|
|
if arrVal != "" {
|
|
return s.expandWord(rest[2:])
|
|
}
|
|
return ""
|
|
default:
|
|
if isMulti {
|
|
return multiVal
|
|
}
|
|
return arrVal
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ${VAR:-default}
|
|
if idx := strings.Index(expr, ":-"); idx >= 0 {
|
|
varName := expr[:idx]
|
|
if v := s.getVar(varName); v != "" {
|
|
return v
|
|
}
|
|
return s.expandWord(expr[idx+2:])
|
|
}
|
|
// ${VAR:=default}
|
|
if idx := strings.Index(expr, ":="); idx >= 0 {
|
|
varName := expr[:idx]
|
|
if v := s.getVar(varName); v != "" {
|
|
return v
|
|
}
|
|
expanded := s.expandWord(expr[idx+2:])
|
|
s.vars[varName] = expanded
|
|
return expanded
|
|
}
|
|
// ${VAR:+alt}
|
|
if idx := strings.Index(expr, ":+"); idx >= 0 {
|
|
varName := expr[:idx]
|
|
if v := s.getVar(varName); v != "" {
|
|
return s.expandWord(expr[idx+2:])
|
|
}
|
|
return ""
|
|
}
|
|
// ${VAR%pattern} — strip shortest suffix
|
|
if idx := strings.Index(expr, "%"); idx >= 0 {
|
|
varName := expr[:idx]
|
|
pattern := expr[idx+1:]
|
|
v := s.getVar(varName)
|
|
if strings.HasSuffix(v, pattern) {
|
|
return v[:len(v)-len(pattern)]
|
|
}
|
|
return v
|
|
}
|
|
// ${VAR#pattern} — strip shortest prefix
|
|
if idx := strings.Index(expr, "#"); idx >= 0 {
|
|
varName := expr[:idx]
|
|
pattern := expr[idx+1:]
|
|
v := s.getVar(varName)
|
|
if strings.HasPrefix(v, pattern) {
|
|
return v[len(pattern):]
|
|
}
|
|
return v
|
|
}
|
|
return s.getVar(expr)
|
|
}
|
|
|
|
// captureCommand runs a command and returns its stdout as a string.
|
|
func (s *Shell) captureCommand(cmd string) string {
|
|
var buf bytes.Buffer
|
|
s.withIO(nil, &buf, nil, func() error {
|
|
return s.Execute(cmd)
|
|
})
|
|
return buf.String()
|
|
}
|
|
|
|
// evalArith evaluates a shell arithmetic expression.
|
|
func (s *Shell) evalArith(expr string) int {
|
|
expr = strings.TrimSpace(s.expandWord(expr))
|
|
// Expand bare variable names (e.g. i+1 → value_of_i + 1)
|
|
expr = s.expandArithVars(expr)
|
|
return evalArithExpr(expr)
|
|
}
|
|
|
|
// expandArithVars replaces bare identifier names with their shell variable values.
|
|
func (s *Shell) expandArithVars(expr string) string {
|
|
var result strings.Builder
|
|
i := 0
|
|
for i < len(expr) {
|
|
c := expr[i]
|
|
// Identifier start (letter or _), but not a digit
|
|
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' {
|
|
j := i
|
|
for j < len(expr) && isVarChar(expr[j]) {
|
|
j++
|
|
}
|
|
varName := expr[i:j]
|
|
val := s.getVar(varName)
|
|
if val == "" {
|
|
val = "0"
|
|
}
|
|
result.WriteString(val)
|
|
i = j
|
|
} else {
|
|
result.WriteByte(c)
|
|
i++
|
|
}
|
|
}
|
|
return result.String()
|
|
}
|
|
|
|
func evalArithExpr(expr string) int {
|
|
expr = strings.TrimSpace(expr)
|
|
if n, err := strconv.Atoi(expr); err == nil {
|
|
return n
|
|
}
|
|
// Strip outer parens
|
|
if strings.HasPrefix(expr, "(") && strings.HasSuffix(expr, ")") {
|
|
return evalArithExpr(expr[1 : len(expr)-1])
|
|
}
|
|
|
|
// Comparison operators (lowest precedence) — multi-char first
|
|
for _, op := range []string{"<=", ">=", "==", "!=", "<", ">"} {
|
|
if idx := findBinaryOpStr(expr, op); idx >= 0 {
|
|
left := evalArithExpr(expr[:idx])
|
|
right := evalArithExpr(expr[idx+len(op):])
|
|
switch op {
|
|
case "<":
|
|
if left < right {
|
|
return 1
|
|
}
|
|
return 0
|
|
case ">":
|
|
if left > right {
|
|
return 1
|
|
}
|
|
return 0
|
|
case "<=":
|
|
if left <= right {
|
|
return 1
|
|
}
|
|
return 0
|
|
case ">=":
|
|
if left >= right {
|
|
return 1
|
|
}
|
|
return 0
|
|
case "==":
|
|
if left == right {
|
|
return 1
|
|
}
|
|
return 0
|
|
case "!=":
|
|
if left != right {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// Arithmetic operators in precedence order (lowest first so we split on last occurrence)
|
|
for _, op := range []string{"+", "-", "*", "/", "%"} {
|
|
if idx := findBinaryOp(expr, op); idx >= 0 {
|
|
left := evalArithExpr(expr[:idx])
|
|
right := evalArithExpr(expr[idx+1:])
|
|
switch op {
|
|
case "+":
|
|
return left + right
|
|
case "-":
|
|
return left - right
|
|
case "*":
|
|
return left * right
|
|
case "/":
|
|
if right == 0 {
|
|
return 0
|
|
}
|
|
return left / right
|
|
case "%":
|
|
if right == 0 {
|
|
return 0
|
|
}
|
|
return left % right
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// findBinaryOpStr finds the rightmost occurrence of a multi-character binary operator.
|
|
func findBinaryOpStr(expr, op string) int {
|
|
depth := 0
|
|
// Search right-to-left
|
|
for i := len(expr) - len(op); i >= 0; i-- {
|
|
switch expr[i] {
|
|
case ')':
|
|
depth++
|
|
case '(':
|
|
depth--
|
|
}
|
|
if depth != 0 {
|
|
continue
|
|
}
|
|
if expr[i:i+len(op)] == op {
|
|
// Make sure it's not part of a longer operator
|
|
// e.g. don't match < in <=
|
|
if len(op) == 1 {
|
|
// For < and >, make sure next char is not =
|
|
if i+1 < len(expr) && (expr[i+1] == '=' || (op == "<" && expr[i+1] == '<') || (op == ">" && expr[i+1] == '>')) {
|
|
continue
|
|
}
|
|
}
|
|
// For single char ops, make sure previous char is not the same op (e.g. << or >>)
|
|
if len(op) == 1 && i > 0 && expr[i-1] == expr[i] {
|
|
continue
|
|
}
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func findBinaryOp(expr, op string) int {
|
|
depth := 0
|
|
for i := len(expr) - 1; i >= 0; i-- {
|
|
switch expr[i] {
|
|
case ')':
|
|
depth++
|
|
case '(':
|
|
depth--
|
|
}
|
|
if depth != 0 {
|
|
continue
|
|
}
|
|
if expr[i:i+1] == op {
|
|
if (op == "-" || op == "+") && i == 0 {
|
|
continue
|
|
}
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// expandGlob expands glob patterns; returns original if no match.
|
|
func (s *Shell) expandGlob(word string) []string {
|
|
if !strings.ContainsAny(word, "*?[") {
|
|
return []string{word}
|
|
}
|
|
matches, err := filepath.Glob(word)
|
|
if err != nil || len(matches) == 0 {
|
|
return []string{word}
|
|
}
|
|
return matches
|
|
}
|
|
|
|
// tokenize splits input into tokens, expands variables, handles quotes and globs.
|
|
func (s *Shell) tokenize(input string) []string {
|
|
var rawTokens []string
|
|
current := strings.Builder{}
|
|
inSingle := false
|
|
inDouble := false
|
|
parenDepth := 0 // nesting depth inside $(...) or $((...))
|
|
pendingDollar := false // true after $ when next char is (
|
|
wasQuoted := false
|
|
|
|
flush := func() {
|
|
if current.Len() > 0 {
|
|
tok := current.String()
|
|
if wasQuoted {
|
|
tok = "\x00q" + tok
|
|
}
|
|
rawTokens = append(rawTokens, tok)
|
|
current.Reset()
|
|
wasQuoted = false
|
|
pendingDollar = false
|
|
}
|
|
}
|
|
|
|
for i := 0; i < len(input); i++ {
|
|
c := input[i]
|
|
switch {
|
|
case c == '\'' && !inDouble && parenDepth == 0:
|
|
inSingle = !inSingle
|
|
wasQuoted = true
|
|
current.WriteByte(c)
|
|
case c == '"' && !inSingle && parenDepth == 0:
|
|
inDouble = !inDouble
|
|
wasQuoted = true
|
|
current.WriteByte(c)
|
|
// Process substitution <(...): when '<' is immediately followed by '('
|
|
// (no space between), keep the entire <(cmd args...) as one token so
|
|
// extractRedirects can run the command and redirect stdin to its output.
|
|
// The standalone '<' redirect operator always has a space after it and
|
|
// therefore becomes its own token before this case is reached.
|
|
case c == '<' && !inSingle && !inDouble && parenDepth == 0 &&
|
|
i+1 < len(input) && input[i+1] == '(':
|
|
current.WriteByte('<')
|
|
current.WriteByte('(')
|
|
parenDepth++
|
|
i++ // skip '('; outer loop will add 1 more → 2 chars consumed
|
|
case c == '$' && !inSingle && i+1 < len(input) && (input[i+1] == '(' || input[i+1] == '{'):
|
|
// Mark that the next ( opens a substitution — don't increment depth here
|
|
if input[i+1] == '(' {
|
|
pendingDollar = true
|
|
}
|
|
current.WriteByte(c)
|
|
case c == '(' && !inSingle && !inDouble && (parenDepth > 0 || pendingDollar):
|
|
parenDepth++
|
|
pendingDollar = false
|
|
current.WriteByte(c)
|
|
case c == ')' && !inSingle && !inDouble && parenDepth > 0:
|
|
parenDepth--
|
|
current.WriteByte(c)
|
|
case c == '{' && !inSingle && !inDouble && parenDepth > 0:
|
|
parenDepth++
|
|
current.WriteByte(c)
|
|
case c == '}' && !inSingle && !inDouble && parenDepth > 0:
|
|
parenDepth--
|
|
current.WriteByte(c)
|
|
case (c == ' ' || c == '\t') && !inSingle && !inDouble && parenDepth == 0:
|
|
flush()
|
|
case c == '#' && !inSingle && !inDouble && parenDepth == 0 && current.Len() == 0:
|
|
// Inline comment: # at start of a new token — discard the rest of the input
|
|
goto doneTokenizing
|
|
default:
|
|
pendingDollar = false
|
|
current.WriteByte(c)
|
|
}
|
|
}
|
|
doneTokenizing:
|
|
flush()
|
|
|
|
// Handle variable assignment on token[0]: FOO=bar
|
|
if len(rawTokens) > 0 {
|
|
tok := rawTokens[0]
|
|
clean := strings.TrimPrefix(tok, "\x00q")
|
|
if eqIdx := strings.Index(clean, "="); eqIdx > 0 {
|
|
name := clean[:eqIdx]
|
|
if isValidIdentifier(name) && !strings.Contains(clean[:eqIdx], "$") {
|
|
value := strings.ReplaceAll(s.expandWord(clean[eqIdx+1:]), "\x01", " ")
|
|
s.vars[name] = value
|
|
os.Setenv(name, value)
|
|
rawTokens = rawTokens[1:]
|
|
if len(rawTokens) == 0 {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var result []string
|
|
for _, tok := range rawTokens {
|
|
quoted := strings.HasPrefix(tok, "\x00q")
|
|
if quoted {
|
|
tok = tok[2:]
|
|
}
|
|
expanded := s.expandWord(tok)
|
|
// Handle multi-word expansion from $@ and ${arr[@]}.
|
|
// Exception: process substitution tokens (<(...)) must stay as one
|
|
// token so extractRedirects can recognise them. Inside a process
|
|
// substitution the \x01 separators are argument boundaries for the
|
|
// inner command, so join them with spaces instead of splitting.
|
|
if strings.Contains(expanded, "\x01") {
|
|
if strings.HasPrefix(expanded, "<(") {
|
|
expanded = strings.ReplaceAll(expanded, "\x01", " ")
|
|
result = append(result, expanded)
|
|
continue
|
|
}
|
|
parts := strings.Split(expanded, "\x01")
|
|
for _, p := range parts {
|
|
if p != "" {
|
|
result = append(result, p)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if !quoted && strings.ContainsAny(expanded, "*?[") {
|
|
result = append(result, s.expandGlob(expanded)...)
|
|
} else {
|
|
result = append(result, expanded)
|
|
}
|
|
}
|
|
return result
|
|
}
|