fixed bash-for-windows
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ release/
|
|||||||
*.exe
|
*.exe
|
||||||
*.o
|
*.o
|
||||||
tmp/
|
tmp/
|
||||||
|
waifufetch/
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -68,6 +68,42 @@ bash script.sh # run a script file
|
|||||||
bash script.sh arg1 # pass arguments ($1, $2, ...)
|
bash script.sh arg1 # pass arguments ($1, $2, ...)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Running bash scripts
|
||||||
|
|
||||||
|
Any file with a `#!/usr/bin/env bash` or `#!/bin/bash` shebang is automatically detected and executed through bash-for-windows — no need to invoke `bash` explicitly.
|
||||||
|
|
||||||
|
**Run by passing the path directly:**
|
||||||
|
```powershell
|
||||||
|
bash myscript.sh
|
||||||
|
bash myscript # extension is optional
|
||||||
|
bash C:\scripts\deploy.sh production
|
||||||
|
```
|
||||||
|
|
||||||
|
**Or put the script on PATH and call it by name:**
|
||||||
|
|
||||||
|
If the script is in a directory that is on your `PATH` (e.g. the bash-for-windows install directory), you can call it directly from the interactive shell or from PowerShell:
|
||||||
|
|
||||||
|
```
|
||||||
|
waifufetch
|
||||||
|
waifu
|
||||||
|
deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Bash-for-windows detects the shebang, runs the script through its own interpreter, and passes any arguments as `$1`, `$2`, etc.
|
||||||
|
|
||||||
|
**CRLF line endings are handled automatically.** Scripts checked out on Windows often have `\r\n` line endings. Bash-for-windows strips the carriage returns before executing, so `#!/usr/bin/env bash\r` in the shebang line never causes the `env: 'bash\r': No such file or directory` error you get with WSL.
|
||||||
|
|
||||||
|
**Adding a script to PATH:**
|
||||||
|
|
||||||
|
The easiest place to drop scripts is the same directory bash-for-windows is installed in:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$d = "$env:LOCALAPPDATA\Programs\BashForWindows"
|
||||||
|
Copy-Item .\myscript $d\myscript
|
||||||
|
```
|
||||||
|
|
||||||
|
That directory is already on `PATH` after running `install.ps1`, so the script is immediately callable from any shell.
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func runFile(path string, args []string) error {
|
|||||||
return fmt.Errorf("%s: %v", path, err)
|
return fmt.Errorf("%s: %v", path, err)
|
||||||
}
|
}
|
||||||
sh := shell.New()
|
sh := shell.New()
|
||||||
sh.SetArgs(append([]string{path}, args...))
|
sh.SetArgs(args)
|
||||||
sh.SetVar("0", path)
|
sh.SetVar("0", path)
|
||||||
return sh.Execute(string(data))
|
return sh.Execute(string(data))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ var aliases = make(map[string]string)
|
|||||||
|
|
||||||
func (s *Shell) initBuiltins() {
|
func (s *Shell) initBuiltins() {
|
||||||
s.builtins = map[string]func([]string) error{
|
s.builtins = map[string]func([]string) error{
|
||||||
|
"{": s.builtinTrue,
|
||||||
|
"}": s.builtinTrue,
|
||||||
// Shell builtins
|
// Shell builtins
|
||||||
"cd": s.builtinCd,
|
"cd": s.builtinCd,
|
||||||
"pwd": s.builtinPwd,
|
"pwd": s.builtinPwd,
|
||||||
@@ -442,84 +444,111 @@ func (s *Shell) builtinPrintf(args []string) error {
|
|||||||
fmtArgs := args[1:]
|
fmtArgs := args[1:]
|
||||||
|
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
|
|
||||||
|
// POSIX: if more arguments than format specifiers, re-apply the format
|
||||||
|
// until all args are consumed. Track consumed count per pass to detect
|
||||||
|
// a format with no specifiers (avoid infinite loop).
|
||||||
argIdx := 0
|
argIdx := 0
|
||||||
i := 0
|
for {
|
||||||
for i < len(format) {
|
consumed := 0
|
||||||
if format[i] == '%' && i+1 < len(format) {
|
i := 0
|
||||||
i++
|
for i < len(format) {
|
||||||
// Optional width/precision
|
if format[i] == '%' && i+1 < len(format) {
|
||||||
specStart := i
|
i++
|
||||||
for i < len(format) && (format[i] == '-' || format[i] == '0' || (format[i] >= '1' && format[i] <= '9') || format[i] == '.') {
|
// Optional width/precision flags
|
||||||
|
specStart := i
|
||||||
|
for i < len(format) && (format[i] == '-' || format[i] == '0' || (format[i] >= '1' && format[i] <= '9') || format[i] == '.') {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
spec := format[specStart:i]
|
||||||
|
if i >= len(format) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
arg := ""
|
||||||
|
if argIdx < len(fmtArgs) {
|
||||||
|
arg = fmtArgs[argIdx]
|
||||||
|
argIdx++
|
||||||
|
consumed++
|
||||||
|
}
|
||||||
|
switch format[i] {
|
||||||
|
case 's':
|
||||||
|
if spec != "" {
|
||||||
|
result.WriteString(fmt.Sprintf("%-"+spec+"s", arg)) //nolint
|
||||||
|
} else {
|
||||||
|
result.WriteString(arg)
|
||||||
|
}
|
||||||
|
case 'd':
|
||||||
|
n := toInt(arg)
|
||||||
|
if spec != "" {
|
||||||
|
result.WriteString(fmt.Sprintf("%"+spec+"d", n))
|
||||||
|
} else {
|
||||||
|
result.WriteString(strconv.Itoa(n))
|
||||||
|
}
|
||||||
|
case 'f':
|
||||||
|
f, _ := strconv.ParseFloat(arg, 64)
|
||||||
|
if spec != "" {
|
||||||
|
result.WriteString(fmt.Sprintf("%"+spec+"f", f))
|
||||||
|
} else {
|
||||||
|
result.WriteString(fmt.Sprintf("%f", f))
|
||||||
|
}
|
||||||
|
case 'x':
|
||||||
|
n := toInt(arg)
|
||||||
|
result.WriteString(fmt.Sprintf("%x", n))
|
||||||
|
case 'o':
|
||||||
|
n := toInt(arg)
|
||||||
|
result.WriteString(fmt.Sprintf("%o", n))
|
||||||
|
case '%':
|
||||||
|
result.WriteByte('%')
|
||||||
|
if consumed > 0 {
|
||||||
|
consumed-- // %% consumes no arg
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result.WriteByte('%')
|
||||||
|
result.WriteByte(format[i])
|
||||||
|
if consumed > 0 {
|
||||||
|
consumed--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
} else if format[i] == '\\' && i+1 < len(format) {
|
||||||
|
i++
|
||||||
|
switch format[i] {
|
||||||
|
case 'n':
|
||||||
|
result.WriteByte('\n')
|
||||||
|
case 't':
|
||||||
|
result.WriteByte('\t')
|
||||||
|
case 'r':
|
||||||
|
result.WriteByte('\r')
|
||||||
|
case '\\':
|
||||||
|
result.WriteByte('\\')
|
||||||
|
case 'a':
|
||||||
|
result.WriteByte('\a')
|
||||||
|
case 'b':
|
||||||
|
result.WriteByte('\b')
|
||||||
|
case 'e', 'E':
|
||||||
|
result.WriteByte(0x1b) // ESC
|
||||||
|
case '0', '1', '2', '3', '4', '5', '6', '7':
|
||||||
|
// Octal escape \NNN (up to 3 octal digits)
|
||||||
|
oct := int(format[i] - '0')
|
||||||
|
for k := 0; k < 2 && i+1 < len(format) && format[i+1] >= '0' && format[i+1] <= '7'; k++ {
|
||||||
|
i++
|
||||||
|
oct = oct*8 + int(format[i]-'0')
|
||||||
|
}
|
||||||
|
result.WriteByte(byte(oct))
|
||||||
|
default:
|
||||||
|
result.WriteByte('\\')
|
||||||
|
result.WriteByte(format[i])
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result.WriteByte(format[i])
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
spec := format[specStart:i]
|
}
|
||||||
if i >= len(format) {
|
// Stop looping if all args consumed, no args given, or format has
|
||||||
break
|
// no specifiers (consumed==0 means infinite loop risk).
|
||||||
}
|
if argIdx >= len(fmtArgs) || consumed == 0 {
|
||||||
arg := ""
|
break
|
||||||
if argIdx < len(fmtArgs) {
|
|
||||||
arg = fmtArgs[argIdx]
|
|
||||||
argIdx++
|
|
||||||
}
|
|
||||||
switch format[i] {
|
|
||||||
case 's':
|
|
||||||
if spec != "" {
|
|
||||||
result.WriteString(fmt.Sprintf("%-"+spec+"s", arg)) //nolint
|
|
||||||
} else {
|
|
||||||
result.WriteString(arg)
|
|
||||||
}
|
|
||||||
case 'd':
|
|
||||||
n := toInt(arg)
|
|
||||||
if spec != "" {
|
|
||||||
result.WriteString(fmt.Sprintf("%"+spec+"d", n))
|
|
||||||
} else {
|
|
||||||
result.WriteString(strconv.Itoa(n))
|
|
||||||
}
|
|
||||||
case 'f':
|
|
||||||
f, _ := strconv.ParseFloat(arg, 64)
|
|
||||||
if spec != "" {
|
|
||||||
result.WriteString(fmt.Sprintf("%"+spec+"f", f))
|
|
||||||
} else {
|
|
||||||
result.WriteString(fmt.Sprintf("%f", f))
|
|
||||||
}
|
|
||||||
case 'x':
|
|
||||||
n := toInt(arg)
|
|
||||||
result.WriteString(fmt.Sprintf("%x", n))
|
|
||||||
case 'o':
|
|
||||||
n := toInt(arg)
|
|
||||||
result.WriteString(fmt.Sprintf("%o", n))
|
|
||||||
case '%':
|
|
||||||
result.WriteByte('%')
|
|
||||||
argIdx-- // no arg consumed
|
|
||||||
default:
|
|
||||||
result.WriteByte('%')
|
|
||||||
result.WriteByte(format[i])
|
|
||||||
argIdx--
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
} else if format[i] == '\\' && i+1 < len(format) {
|
|
||||||
i++
|
|
||||||
switch format[i] {
|
|
||||||
case 'n':
|
|
||||||
result.WriteByte('\n')
|
|
||||||
case 't':
|
|
||||||
result.WriteByte('\t')
|
|
||||||
case 'r':
|
|
||||||
result.WriteByte('\r')
|
|
||||||
case '\\':
|
|
||||||
result.WriteByte('\\')
|
|
||||||
case 'a':
|
|
||||||
result.WriteByte('\a')
|
|
||||||
case 'b':
|
|
||||||
result.WriteByte('\b')
|
|
||||||
default:
|
|
||||||
result.WriteByte('\\')
|
|
||||||
result.WriteByte(format[i])
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
} else {
|
|
||||||
result.WriteByte(format[i])
|
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Fprint(s.Stdout, result.String())
|
fmt.Fprint(s.Stdout, result.String())
|
||||||
|
|||||||
@@ -21,17 +21,40 @@ func (s *Shell) executeIf(block string) error {
|
|||||||
phase := "if_cond"
|
phase := "if_cond"
|
||||||
var curCond []string
|
var curCond []string
|
||||||
var curBody []string
|
var curBody []string
|
||||||
|
depth := 0 // nesting depth of if/for/while/until/case inside the body
|
||||||
|
|
||||||
|
addToBody := func(s string) {
|
||||||
|
switch phase {
|
||||||
|
case "body":
|
||||||
|
curBody = append(curBody, s)
|
||||||
|
case "else":
|
||||||
|
elseBody = append(elseBody, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, stmt := range stmts {
|
for _, stmt := range stmts {
|
||||||
w := firstWord(stmt)
|
w := firstWord(stmt)
|
||||||
rest := afterWord(stmt)
|
rest := afterWord(stmt)
|
||||||
|
|
||||||
|
// When depth > 0, we're inside a nested block; all keywords are body content.
|
||||||
|
if depth > 0 {
|
||||||
|
switch w {
|
||||||
|
case "if", "for", "while", "until", "case":
|
||||||
|
depth++
|
||||||
|
case "fi", "done", "esac":
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
addToBody(stmt)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// depth == 0: structural keywords for the outer if
|
||||||
switch {
|
switch {
|
||||||
case w == "if" && phase == "if_cond":
|
case w == "if" && phase == "if_cond":
|
||||||
if rest != "" {
|
if rest != "" {
|
||||||
curCond = append(curCond, rest)
|
curCond = append(curCond, rest)
|
||||||
}
|
}
|
||||||
case w == "then":
|
case w == "then" && (phase == "if_cond" || phase == "elif_cond"):
|
||||||
if rest != "" {
|
if rest != "" {
|
||||||
curBody = append(curBody, rest)
|
curBody = append(curBody, rest)
|
||||||
}
|
}
|
||||||
@@ -67,8 +90,17 @@ func (s *Shell) executeIf(block string) error {
|
|||||||
curCond = append(curCond, stmt)
|
curCond = append(curCond, stmt)
|
||||||
case "body":
|
case "body":
|
||||||
curBody = append(curBody, stmt)
|
curBody = append(curBody, stmt)
|
||||||
|
// Track depth for nested blocks starting in body
|
||||||
|
switch w {
|
||||||
|
case "if", "for", "while", "until", "case":
|
||||||
|
depth++
|
||||||
|
}
|
||||||
case "else":
|
case "else":
|
||||||
elseBody = append(elseBody, stmt)
|
elseBody = append(elseBody, stmt)
|
||||||
|
switch w {
|
||||||
|
case "if", "for", "while", "until", "case":
|
||||||
|
depth++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ func splitPipe(input string) []string {
|
|||||||
case c == '"' && !inSingle && parenDepth == 0:
|
case c == '"' && !inSingle && parenDepth == 0:
|
||||||
inDouble = !inDouble
|
inDouble = !inDouble
|
||||||
current.WriteByte(c)
|
current.WriteByte(c)
|
||||||
|
// Process substitution <(...): don't split | inside.
|
||||||
|
case c == '<' && !inSingle && !inDouble && i+1 < len(input) && input[i+1] == '(':
|
||||||
|
parenDepth++
|
||||||
|
current.WriteByte('<')
|
||||||
|
current.WriteByte('(')
|
||||||
|
i++
|
||||||
case c == '$' && !inSingle && !inDouble && i+1 < len(input) && input[i+1] == '(':
|
case c == '$' && !inSingle && !inDouble && i+1 < len(input) && input[i+1] == '(':
|
||||||
pendingDollar = true
|
pendingDollar = true
|
||||||
current.WriteByte(c)
|
current.WriteByte(c)
|
||||||
@@ -154,6 +160,15 @@ func (s *Shell) executeCommand(input string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// { cmd; cmd; } command group
|
||||||
|
if strings.HasPrefix(input, "{") {
|
||||||
|
end := strings.LastIndex(input, "}")
|
||||||
|
if end > 0 {
|
||||||
|
inner := strings.TrimSpace(input[1:end])
|
||||||
|
return s.Execute(inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Detect array assignment NAME=(...) or NAME+=(...)
|
// Detect array assignment NAME=(...) or NAME+=(...)
|
||||||
if name, appendMode, elems, ok := s.parseArrayAssign(input); ok {
|
if name, appendMode, elems, ok := s.parseArrayAssign(input); ok {
|
||||||
if appendMode {
|
if appendMode {
|
||||||
@@ -264,6 +279,20 @@ func (s *Shell) extractRedirects(tokens []string) ([]string, []redirect) {
|
|||||||
case strings.HasPrefix(tok, ">") && len(tok) > 1:
|
case strings.HasPrefix(tok, ">") && len(tok) > 1:
|
||||||
redirects = append(redirects, redirect{1, ">", tok[1:]})
|
redirects = append(redirects, redirect{1, ">", tok[1:]})
|
||||||
i++
|
i++
|
||||||
|
// <<< here-string
|
||||||
|
case tok == "<<<":
|
||||||
|
if i+1 < len(tokens) {
|
||||||
|
val := tokens[i+1] + "\n"
|
||||||
|
tmpf, err := os.CreateTemp("", "herestr*")
|
||||||
|
if err == nil {
|
||||||
|
tmpf.WriteString(val)
|
||||||
|
tmpf.Close()
|
||||||
|
redirects = append(redirects, redirect{0, "<", tmpf.Name()})
|
||||||
|
}
|
||||||
|
i += 2
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
// < (stdin) — also handle process substitution <(...)
|
// < (stdin) — also handle process substitution <(...)
|
||||||
case tok == "<":
|
case tok == "<":
|
||||||
if i+1 < len(tokens) {
|
if i+1 < len(tokens) {
|
||||||
@@ -311,7 +340,11 @@ func (s *Shell) withRedirects(redirects []redirect, fn func() error) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, r := range redirects {
|
for i, r := range redirects {
|
||||||
|
if r.dest == "/dev/null" {
|
||||||
|
redirects[i].dest = os.DevNull
|
||||||
|
r = redirects[i]
|
||||||
|
}
|
||||||
switch r.mode {
|
switch r.mode {
|
||||||
case ">":
|
case ">":
|
||||||
if r.dest == "&1" {
|
if r.dest == "&1" {
|
||||||
@@ -361,6 +394,20 @@ func (s *Shell) executeExternal(cmdName string, args []string) error {
|
|||||||
fmt.Fprintf(s.Stderr, "%s: command not found\n", cmdName)
|
fmt.Fprintf(s.Stderr, "%s: command not found\n", cmdName)
|
||||||
return exitCodeErr{127}
|
return exitCodeErr{127}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the file is a bash/sh script, run it through our own interpreter.
|
||||||
|
// This avoids the CRLF shebang problem (#!/usr/bin/env bash\r) and lets
|
||||||
|
// us execute scripts that have no .exe extension on Windows.
|
||||||
|
if data, err := os.ReadFile(path); err == nil && isShellScript(data) {
|
||||||
|
sh := New()
|
||||||
|
sh.Stdin = s.Stdin
|
||||||
|
sh.Stdout = s.Stdout
|
||||||
|
sh.Stderr = s.Stderr
|
||||||
|
sh.SetArgs(args)
|
||||||
|
sh.SetVar("0", path)
|
||||||
|
return sh.Execute(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
cmd := exec.Command(path, args...)
|
cmd := exec.Command(path, args...)
|
||||||
cmd.Stdin = s.Stdin
|
cmd.Stdin = s.Stdin
|
||||||
cmd.Stdout = s.Stdout
|
cmd.Stdout = s.Stdout
|
||||||
@@ -374,6 +421,25 @@ func (s *Shell) executeExternal(cmdName string, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isShellScript returns true when data begins with a #!/…bash or #!/…sh shebang.
|
||||||
|
func isShellScript(data []byte) bool {
|
||||||
|
if len(data) < 2 || data[0] != '#' || data[1] != '!' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
nl := bytes.IndexByte(data, '\n')
|
||||||
|
if nl < 0 {
|
||||||
|
nl = len(data)
|
||||||
|
}
|
||||||
|
line := strings.TrimRight(string(data[:nl]), "\r")
|
||||||
|
for _, word := range strings.Fields(line[2:]) {
|
||||||
|
base := filepath.Base(word)
|
||||||
|
if base == "bash" || base == "sh" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Shell) executeCommandBg(input string) error {
|
func (s *Shell) executeCommandBg(input string) error {
|
||||||
tokens := s.tokenize(input)
|
tokens := s.tokenize(input)
|
||||||
if len(tokens) == 0 {
|
if len(tokens) == 0 {
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ func (s *Shell) expandWord(word string) string {
|
|||||||
result.WriteByte('$')
|
result.WriteByte('$')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// $'...' ANSI C string
|
// $'...' ANSI C string
|
||||||
if word[i] == '\'' {
|
if word[i] == '\'' {
|
||||||
i++ // skip opening '
|
i++ // skip opening '
|
||||||
@@ -232,7 +231,11 @@ func (s *Shell) expandWord(word string) string {
|
|||||||
result.WriteString(s.vars["#"])
|
result.WriteString(s.vars["#"])
|
||||||
i++
|
i++
|
||||||
case '@':
|
case '@':
|
||||||
result.WriteString(strings.Join(s.args, "\x01"))
|
if len(s.args) == 0 {
|
||||||
|
result.WriteString("\x01") // empty-expansion sentinel → 0 tokens
|
||||||
|
} else {
|
||||||
|
result.WriteString(strings.Join(s.args, "\x01"))
|
||||||
|
}
|
||||||
i++
|
i++
|
||||||
case '*':
|
case '*':
|
||||||
result.WriteString(s.vars["*"])
|
result.WriteString(s.vars["*"])
|
||||||
@@ -278,29 +281,95 @@ func (s *Shell) evalVarExpr(expr string) string {
|
|||||||
return strconv.Itoa(len(s.getVar(rest)))
|
return strconv.Itoa(len(s.getVar(rest)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Array indexing: ${arr[@]}, ${arr[*]}, ${arr[N]}
|
// Array indexing: ${arr[@]}, ${arr[*]}, ${arr[N]}, ${arr[N]:-default}, etc.
|
||||||
if bracketIdx := strings.Index(expr, "["); bracketIdx >= 0 && strings.HasSuffix(expr, "]") {
|
// Find the bracket pair and evaluate the array access even when an operator
|
||||||
// Make sure there's no operator before the bracket
|
// (:- := :+) follows the closing ].
|
||||||
prefix := expr[:bracketIdx]
|
if bracketIdx := strings.Index(expr, "["); bracketIdx >= 0 {
|
||||||
hasOp := strings.ContainsAny(prefix, ":-:=:+%#")
|
closeIdx := -1
|
||||||
if !hasOp {
|
// Find matching ] — must balance nested $((…)) parens inside the index
|
||||||
arrName := prefix
|
depth := 0
|
||||||
idx := expr[bracketIdx+1 : len(expr)-1]
|
for k := bracketIdx + 1; k < len(expr); k++ {
|
||||||
if idx == "@" {
|
if expr[k] == '(' {
|
||||||
arr := s.getArray(arrName)
|
depth++
|
||||||
return strings.Join(arr, "\x01")
|
} else if expr[k] == ')' {
|
||||||
|
depth--
|
||||||
|
} else if expr[k] == ']' && depth == 0 {
|
||||||
|
closeIdx = k
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if idx == "*" {
|
}
|
||||||
arr := s.getArray(arrName)
|
if closeIdx > bracketIdx {
|
||||||
return strings.Join(arr, " ")
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Numeric index
|
|
||||||
n := s.evalArith(idx)
|
|
||||||
arr := s.getArray(arrName)
|
|
||||||
if n >= 0 && n < len(arr) {
|
|
||||||
return arr[n]
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,6 +645,17 @@ func (s *Shell) tokenize(input string) []string {
|
|||||||
inDouble = !inDouble
|
inDouble = !inDouble
|
||||||
wasQuoted = true
|
wasQuoted = true
|
||||||
current.WriteByte(c)
|
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] == '{'):
|
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
|
// Mark that the next ( opens a substitution — don't increment depth here
|
||||||
if input[i+1] == '(' {
|
if input[i+1] == '(' {
|
||||||
@@ -633,8 +713,17 @@ doneTokenizing:
|
|||||||
tok = tok[2:]
|
tok = tok[2:]
|
||||||
}
|
}
|
||||||
expanded := s.expandWord(tok)
|
expanded := s.expandWord(tok)
|
||||||
// Handle multi-word expansion from $@ and ${arr[@]}
|
// 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.Contains(expanded, "\x01") {
|
||||||
|
if strings.HasPrefix(expanded, "<(") {
|
||||||
|
expanded = strings.ReplaceAll(expanded, "\x01", " ")
|
||||||
|
result = append(result, expanded)
|
||||||
|
continue
|
||||||
|
}
|
||||||
parts := strings.Split(expanded, "\x01")
|
parts := strings.Split(expanded, "\x01")
|
||||||
for _, p := range parts {
|
for _, p := range parts {
|
||||||
if p != "" {
|
if p != "" {
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ func (s *Shell) SetVar(name, value string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute runs commands from the given input string.
|
// Execute runs commands from the given input string.
|
||||||
|
func PreprocessForTest(input string) string { return preprocessHeredocs(input) }
|
||||||
|
func ParseBlocksForTest(input string) []string { return parseBlocks(input) }
|
||||||
func (s *Shell) Execute(input string) error {
|
func (s *Shell) Execute(input string) error {
|
||||||
// Normalize CRLF to LF
|
// Normalize CRLF to LF
|
||||||
input = strings.ReplaceAll(input, "\r\n", "\n")
|
input = strings.ReplaceAll(input, "\r\n", "\n")
|
||||||
@@ -206,9 +208,9 @@ func parseBlocks(input string) []string {
|
|||||||
funcKwDepth = 0
|
funcKwDepth = 0
|
||||||
for _, p := range splitStatements(stmt[braceIdx+1:]) {
|
for _, p := range splitStatements(stmt[braceIdx+1:]) {
|
||||||
switch firstWord(p) {
|
switch firstWord(p) {
|
||||||
case "if", "for", "while", "until", "case":
|
case "if", "for", "while", "until", "case", "{":
|
||||||
funcKwDepth++
|
funcKwDepth++
|
||||||
case "fi", "done", "esac":
|
case "fi", "done", "esac", "}":
|
||||||
funcKwDepth--
|
funcKwDepth--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +228,7 @@ func parseBlocks(input string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch w {
|
switch w {
|
||||||
case "if", "for", "while", "until", "case":
|
case "if", "for", "while", "until", "case", "{":
|
||||||
kwDepth++
|
kwDepth++
|
||||||
}
|
}
|
||||||
kwDepth += embeddedKwDepth(stmt)
|
kwDepth += embeddedKwDepth(stmt)
|
||||||
@@ -255,9 +257,9 @@ func parseBlocks(input string) []string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch w {
|
switch w {
|
||||||
case "if", "for", "while", "until", "case":
|
case "if", "for", "while", "until", "case", "{":
|
||||||
funcKwDepth++
|
funcKwDepth++
|
||||||
case "fi", "done", "esac":
|
case "fi", "done", "esac", "}":
|
||||||
funcKwDepth--
|
funcKwDepth--
|
||||||
}
|
}
|
||||||
funcKwDepth += embeddedKwDepth(stmt)
|
funcKwDepth += embeddedKwDepth(stmt)
|
||||||
@@ -273,6 +275,7 @@ func parseBlocks(input string) []string {
|
|||||||
// embeddedKwDepth returns the net depth change from keywords that appear
|
// embeddedKwDepth returns the net depth change from keywords that appear
|
||||||
// after do/then/else/elif within a single statement (excluding the first word,
|
// after do/then/else/elif within a single statement (excluding the first word,
|
||||||
// which is handled separately by the caller).
|
// which is handled separately by the caller).
|
||||||
|
func EmbeddedKwDepthForTest(s string) int { return embeddedKwDepth(s) }
|
||||||
func embeddedKwDepth(stmt string) int {
|
func embeddedKwDepth(stmt string) int {
|
||||||
words := strings.Fields(stmt)
|
words := strings.Fields(stmt)
|
||||||
delta := 0
|
delta := 0
|
||||||
@@ -375,6 +378,12 @@ func parseHeredocMarkers(line string) (string, []string) {
|
|||||||
out.WriteByte(c)
|
out.WriteByte(c)
|
||||||
i++
|
i++
|
||||||
case c == '<' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == '<':
|
case c == '<' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == '<':
|
||||||
|
// <<< here-string: not a heredoc, pass through all three < chars unchanged
|
||||||
|
if i+2 < len(line) && line[i+2] == '<' {
|
||||||
|
out.WriteString("<<<")
|
||||||
|
i += 3
|
||||||
|
break
|
||||||
|
}
|
||||||
// Possible heredoc
|
// Possible heredoc
|
||||||
i += 2 // skip <<
|
i += 2 // skip <<
|
||||||
stripTabs := false
|
stripTabs := false
|
||||||
@@ -429,6 +438,7 @@ func splitStatements(input string) []string {
|
|||||||
inSingle := false
|
inSingle := false
|
||||||
inDouble := false
|
inDouble := false
|
||||||
parenDepth := 0
|
parenDepth := 0
|
||||||
|
braceDepth := 0 // { } command groups — don't split ; inside them
|
||||||
|
|
||||||
for i := 0; i < len(input); i++ {
|
for i := 0; i < len(input); i++ {
|
||||||
c := input[i]
|
c := input[i]
|
||||||
@@ -445,7 +455,14 @@ func splitStatements(input string) []string {
|
|||||||
case c == ')' && !inSingle && !inDouble && parenDepth > 0:
|
case c == ')' && !inSingle && !inDouble && parenDepth > 0:
|
||||||
parenDepth--
|
parenDepth--
|
||||||
current.WriteByte(c)
|
current.WriteByte(c)
|
||||||
case c == ';' && !inSingle && !inDouble && parenDepth == 0:
|
// Track { } command groups but not ${...} variable expansions
|
||||||
|
case c == '{' && !inSingle && !inDouble && parenDepth == 0 && (i == 0 || input[i-1] != '$'):
|
||||||
|
braceDepth++
|
||||||
|
current.WriteByte(c)
|
||||||
|
case c == '}' && !inSingle && !inDouble && parenDepth == 0 && braceDepth > 0:
|
||||||
|
braceDepth--
|
||||||
|
current.WriteByte(c)
|
||||||
|
case c == ';' && !inSingle && !inDouble && parenDepth == 0 && braceDepth == 0:
|
||||||
if i+1 < len(input) && input[i+1] == ';' {
|
if i+1 < len(input) && input[i+1] == ';' {
|
||||||
// Double semicolon — flush current token, then emit ";;" as a token
|
// Double semicolon — flush current token, then emit ";;" as a token
|
||||||
if s := strings.TrimSpace(current.String()); s != "" {
|
if s := strings.TrimSpace(current.String()); s != "" {
|
||||||
@@ -461,7 +478,7 @@ func splitStatements(input string) []string {
|
|||||||
}
|
}
|
||||||
current.Reset()
|
current.Reset()
|
||||||
}
|
}
|
||||||
case c == '\n' && !inSingle && !inDouble && parenDepth == 0:
|
case c == '\n' && !inSingle && !inDouble && parenDepth == 0 && braceDepth == 0:
|
||||||
if s := strings.TrimSpace(current.String()); s != "" {
|
if s := strings.TrimSpace(current.String()); s != "" {
|
||||||
result = append(result, s)
|
result = append(result, s)
|
||||||
}
|
}
|
||||||
@@ -553,12 +570,15 @@ func (s *Shell) executeBlock(block string) error {
|
|||||||
if isFuncDefStart(block) {
|
if isFuncDefStart(block) {
|
||||||
return s.defineFunction(block)
|
return s.defineFunction(block)
|
||||||
}
|
}
|
||||||
for _, line := range strings.Split(block, "\n") {
|
// Use splitStatements instead of strings.Split("\n") so that multi-line
|
||||||
line = strings.TrimSpace(line)
|
// constructs (e.g. process substitutions spanning several lines) are kept
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
// together as a single logical unit rather than being broken apart.
|
||||||
|
for _, stmt := range splitStatements(block) {
|
||||||
|
stmt = strings.TrimSpace(stmt)
|
||||||
|
if stmt == "" || strings.HasPrefix(stmt, "#") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := s.executeLine(line); err != nil {
|
if err := s.executeLine(stmt); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -591,20 +611,40 @@ func splitBySemicolon(line string) []string {
|
|||||||
current := strings.Builder{}
|
current := strings.Builder{}
|
||||||
inSingle := false
|
inSingle := false
|
||||||
inDouble := false
|
inDouble := false
|
||||||
|
parenDepth := 0 // tracks $(...) and <(...) nesting
|
||||||
|
pendingDollar := false
|
||||||
|
|
||||||
for i := 0; i < len(line); i++ {
|
for i := 0; i < len(line); i++ {
|
||||||
c := line[i]
|
c := line[i]
|
||||||
switch {
|
switch {
|
||||||
case c == '\'' && !inDouble:
|
case c == '\'' && !inDouble && parenDepth == 0:
|
||||||
inSingle = !inSingle
|
inSingle = !inSingle
|
||||||
current.WriteByte(c)
|
current.WriteByte(c)
|
||||||
case c == '"' && !inSingle:
|
case c == '"' && !inSingle && parenDepth == 0:
|
||||||
inDouble = !inDouble
|
inDouble = !inDouble
|
||||||
current.WriteByte(c)
|
current.WriteByte(c)
|
||||||
case c == ';' && !inSingle && !inDouble:
|
// Process substitution <(...): don't split ; | && || inside.
|
||||||
|
case c == '<' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == '(':
|
||||||
|
parenDepth++
|
||||||
|
current.WriteByte('<')
|
||||||
|
current.WriteByte('(')
|
||||||
|
i++
|
||||||
|
// Command substitution $(...): don't split ; inside.
|
||||||
|
case c == '$' && !inSingle && !inDouble && i+1 < len(line) && line[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:
|
||||||
parts = append(parts, current.String())
|
parts = append(parts, current.String())
|
||||||
current.Reset()
|
current.Reset()
|
||||||
default:
|
default:
|
||||||
|
pendingDollar = false
|
||||||
current.WriteByte(c)
|
current.WriteByte(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -624,38 +664,59 @@ func (s *Shell) executeAndOrList(line string) error {
|
|||||||
op := ""
|
op := ""
|
||||||
inSingle := false
|
inSingle := false
|
||||||
inDouble := false
|
inDouble := false
|
||||||
dbDepth := 0 // double-bracket [[ depth
|
dbDepth := 0 // double-bracket [[ depth
|
||||||
|
parenDepth := 0 // $( ) depth — don't split && || inside subshells
|
||||||
|
pendingDollar := false
|
||||||
|
|
||||||
for i := 0; i < len(line); i++ {
|
for i := 0; i < len(line); i++ {
|
||||||
c := line[i]
|
c := line[i]
|
||||||
switch {
|
switch {
|
||||||
case c == '\'' && !inDouble:
|
case c == '\'' && !inDouble && parenDepth == 0:
|
||||||
inSingle = !inSingle
|
inSingle = !inSingle
|
||||||
current.WriteByte(c)
|
current.WriteByte(c)
|
||||||
case c == '"' && !inSingle:
|
case c == '"' && !inSingle && parenDepth == 0:
|
||||||
inDouble = !inDouble
|
inDouble = !inDouble
|
||||||
current.WriteByte(c)
|
current.WriteByte(c)
|
||||||
case c == '[' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == '[':
|
// Process substitution <(...): don't split && || inside.
|
||||||
|
case c == '<' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == '(':
|
||||||
|
parenDepth++
|
||||||
|
current.WriteByte('<')
|
||||||
|
current.WriteByte('(')
|
||||||
|
i++
|
||||||
|
case c == '$' && !inSingle && !inDouble && i+1 < len(line) && line[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 && i+1 < len(line) && line[i+1] == '[':
|
||||||
dbDepth++
|
dbDepth++
|
||||||
current.WriteByte(c)
|
current.WriteByte(c)
|
||||||
current.WriteByte(line[i+1])
|
current.WriteByte(line[i+1])
|
||||||
i++
|
i++
|
||||||
case c == ']' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == ']' && dbDepth > 0:
|
case c == ']' && !inSingle && !inDouble && parenDepth == 0 && i+1 < len(line) && line[i+1] == ']' && dbDepth > 0:
|
||||||
dbDepth--
|
dbDepth--
|
||||||
current.WriteByte(c)
|
current.WriteByte(c)
|
||||||
current.WriteByte(line[i+1])
|
current.WriteByte(line[i+1])
|
||||||
i++
|
i++
|
||||||
case c == '&' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == '&' && dbDepth == 0:
|
case c == '&' && !inSingle && !inDouble && parenDepth == 0 && i+1 < len(line) && line[i+1] == '&' && dbDepth == 0:
|
||||||
|
pendingDollar = false
|
||||||
tokens = append(tokens, tok{current.String(), op})
|
tokens = append(tokens, tok{current.String(), op})
|
||||||
current.Reset()
|
current.Reset()
|
||||||
op = "&&"
|
op = "&&"
|
||||||
i++
|
i++
|
||||||
case c == '|' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == '|' && dbDepth == 0:
|
case c == '|' && !inSingle && !inDouble && parenDepth == 0 && i+1 < len(line) && line[i+1] == '|' && dbDepth == 0:
|
||||||
|
pendingDollar = false
|
||||||
tokens = append(tokens, tok{current.String(), op})
|
tokens = append(tokens, tok{current.String(), op})
|
||||||
current.Reset()
|
current.Reset()
|
||||||
op = "||"
|
op = "||"
|
||||||
i++
|
i++
|
||||||
default:
|
default:
|
||||||
|
pendingDollar = false
|
||||||
current.WriteByte(c)
|
current.WriteByte(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user