fixed bash-for-windows
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ release/
|
||||
*.exe
|
||||
*.o
|
||||
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, ...)
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```bash
|
||||
|
||||
@@ -43,7 +43,7 @@ func runFile(path string, args []string) error {
|
||||
return fmt.Errorf("%s: %v", path, err)
|
||||
}
|
||||
sh := shell.New()
|
||||
sh.SetArgs(append([]string{path}, args...))
|
||||
sh.SetArgs(args)
|
||||
sh.SetVar("0", path)
|
||||
return sh.Execute(string(data))
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ var aliases = make(map[string]string)
|
||||
|
||||
func (s *Shell) initBuiltins() {
|
||||
s.builtins = map[string]func([]string) error{
|
||||
"{": s.builtinTrue,
|
||||
"}": s.builtinTrue,
|
||||
// Shell builtins
|
||||
"cd": s.builtinCd,
|
||||
"pwd": s.builtinPwd,
|
||||
@@ -442,12 +444,18 @@ func (s *Shell) builtinPrintf(args []string) error {
|
||||
fmtArgs := args[1:]
|
||||
|
||||
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
|
||||
for {
|
||||
consumed := 0
|
||||
i := 0
|
||||
for i < len(format) {
|
||||
if format[i] == '%' && i+1 < len(format) {
|
||||
i++
|
||||
// Optional width/precision
|
||||
// Optional width/precision flags
|
||||
specStart := i
|
||||
for i < len(format) && (format[i] == '-' || format[i] == '0' || (format[i] >= '1' && format[i] <= '9') || format[i] == '.') {
|
||||
i++
|
||||
@@ -460,6 +468,7 @@ func (s *Shell) builtinPrintf(args []string) error {
|
||||
if argIdx < len(fmtArgs) {
|
||||
arg = fmtArgs[argIdx]
|
||||
argIdx++
|
||||
consumed++
|
||||
}
|
||||
switch format[i] {
|
||||
case 's':
|
||||
@@ -490,11 +499,15 @@ func (s *Shell) builtinPrintf(args []string) error {
|
||||
result.WriteString(fmt.Sprintf("%o", n))
|
||||
case '%':
|
||||
result.WriteByte('%')
|
||||
argIdx-- // no arg consumed
|
||||
if consumed > 0 {
|
||||
consumed-- // %% consumes no arg
|
||||
}
|
||||
default:
|
||||
result.WriteByte('%')
|
||||
result.WriteByte(format[i])
|
||||
argIdx--
|
||||
if consumed > 0 {
|
||||
consumed--
|
||||
}
|
||||
}
|
||||
i++
|
||||
} else if format[i] == '\\' && i+1 < len(format) {
|
||||
@@ -512,6 +525,16 @@ func (s *Shell) builtinPrintf(args []string) error {
|
||||
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])
|
||||
@@ -522,6 +545,12 @@ func (s *Shell) builtinPrintf(args []string) error {
|
||||
i++
|
||||
}
|
||||
}
|
||||
// Stop looping if all args consumed, no args given, or format has
|
||||
// no specifiers (consumed==0 means infinite loop risk).
|
||||
if argIdx >= len(fmtArgs) || consumed == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
fmt.Fprint(s.Stdout, result.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,17 +21,40 @@ func (s *Shell) executeIf(block string) error {
|
||||
phase := "if_cond"
|
||||
var curCond []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 {
|
||||
w := firstWord(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 {
|
||||
case w == "if" && phase == "if_cond":
|
||||
if rest != "" {
|
||||
curCond = append(curCond, rest)
|
||||
}
|
||||
case w == "then":
|
||||
case w == "then" && (phase == "if_cond" || phase == "elif_cond"):
|
||||
if rest != "" {
|
||||
curBody = append(curBody, rest)
|
||||
}
|
||||
@@ -67,8 +90,17 @@ func (s *Shell) executeIf(block string) error {
|
||||
curCond = append(curCond, stmt)
|
||||
case "body":
|
||||
curBody = append(curBody, stmt)
|
||||
// Track depth for nested blocks starting in body
|
||||
switch w {
|
||||
case "if", "for", "while", "until", "case":
|
||||
depth++
|
||||
}
|
||||
case "else":
|
||||
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:
|
||||
inDouble = !inDouble
|
||||
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] == '(':
|
||||
pendingDollar = true
|
||||
current.WriteByte(c)
|
||||
@@ -154,6 +160,15 @@ func (s *Shell) executeCommand(input string) error {
|
||||
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+=(...)
|
||||
if name, appendMode, elems, ok := s.parseArrayAssign(input); ok {
|
||||
if appendMode {
|
||||
@@ -264,6 +279,20 @@ func (s *Shell) extractRedirects(tokens []string) ([]string, []redirect) {
|
||||
case strings.HasPrefix(tok, ">") && len(tok) > 1:
|
||||
redirects = append(redirects, redirect{1, ">", tok[1:]})
|
||||
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 <(...)
|
||||
case tok == "<":
|
||||
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 {
|
||||
case ">":
|
||||
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)
|
||||
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.Stdin = s.Stdin
|
||||
cmd.Stdout = s.Stdout
|
||||
@@ -374,6 +421,25 @@ func (s *Shell) executeExternal(cmdName string, args []string) error {
|
||||
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 {
|
||||
tokens := s.tokenize(input)
|
||||
if len(tokens) == 0 {
|
||||
|
||||
@@ -82,7 +82,6 @@ func (s *Shell) expandWord(word string) string {
|
||||
result.WriteByte('$')
|
||||
break
|
||||
}
|
||||
|
||||
// $'...' ANSI C string
|
||||
if word[i] == '\'' {
|
||||
i++ // skip opening '
|
||||
@@ -232,7 +231,11 @@ func (s *Shell) expandWord(word string) string {
|
||||
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["*"])
|
||||
@@ -278,29 +281,95 @@ func (s *Shell) evalVarExpr(expr string) string {
|
||||
return strconv.Itoa(len(s.getVar(rest)))
|
||||
}
|
||||
|
||||
// Array indexing: ${arr[@]}, ${arr[*]}, ${arr[N]}
|
||||
if bracketIdx := strings.Index(expr, "["); bracketIdx >= 0 && strings.HasSuffix(expr, "]") {
|
||||
// Make sure there's no operator before the bracket
|
||||
// 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]
|
||||
hasOp := strings.ContainsAny(prefix, ":-:=:+%#")
|
||||
if !hasOp {
|
||||
// No operator before the bracket
|
||||
if !strings.ContainsAny(prefix, ":=+%#") {
|
||||
arrName := prefix
|
||||
idx := expr[bracketIdx+1 : len(expr)-1]
|
||||
if idx == "@" {
|
||||
idx := expr[bracketIdx+1 : closeIdx]
|
||||
rest := expr[closeIdx+1:] // may be empty or ":-…" / ":+…" / ":=…"
|
||||
|
||||
arrVal := ""
|
||||
isMulti := false
|
||||
multiVal := ""
|
||||
|
||||
switch idx {
|
||||
case "@":
|
||||
arr := s.getArray(arrName)
|
||||
return strings.Join(arr, "\x01")
|
||||
if len(arr) == 0 {
|
||||
arrVal = ""
|
||||
} else {
|
||||
isMulti = true
|
||||
multiVal = strings.Join(arr, "\x01")
|
||||
}
|
||||
if idx == "*" {
|
||||
case "*":
|
||||
arr := s.getArray(arrName)
|
||||
return strings.Join(arr, " ")
|
||||
}
|
||||
// Numeric index
|
||||
arrVal = strings.Join(arr, " ")
|
||||
default:
|
||||
n := s.evalArith(idx)
|
||||
arr := s.getArray(arrName)
|
||||
if n >= 0 && n < len(arr) {
|
||||
return arr[n]
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,6 +645,17 @@ func (s *Shell) tokenize(input string) []string {
|
||||
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] == '(' {
|
||||
@@ -633,8 +713,17 @@ doneTokenizing:
|
||||
tok = tok[2:]
|
||||
}
|
||||
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.HasPrefix(expanded, "<(") {
|
||||
expanded = strings.ReplaceAll(expanded, "\x01", " ")
|
||||
result = append(result, expanded)
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(expanded, "\x01")
|
||||
for _, p := range parts {
|
||||
if p != "" {
|
||||
|
||||
@@ -119,6 +119,8 @@ func (s *Shell) SetVar(name, value 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 {
|
||||
// Normalize CRLF to LF
|
||||
input = strings.ReplaceAll(input, "\r\n", "\n")
|
||||
@@ -206,9 +208,9 @@ func parseBlocks(input string) []string {
|
||||
funcKwDepth = 0
|
||||
for _, p := range splitStatements(stmt[braceIdx+1:]) {
|
||||
switch firstWord(p) {
|
||||
case "if", "for", "while", "until", "case":
|
||||
case "if", "for", "while", "until", "case", "{":
|
||||
funcKwDepth++
|
||||
case "fi", "done", "esac":
|
||||
case "fi", "done", "esac", "}":
|
||||
funcKwDepth--
|
||||
}
|
||||
}
|
||||
@@ -226,7 +228,7 @@ func parseBlocks(input string) []string {
|
||||
}
|
||||
|
||||
switch w {
|
||||
case "if", "for", "while", "until", "case":
|
||||
case "if", "for", "while", "until", "case", "{":
|
||||
kwDepth++
|
||||
}
|
||||
kwDepth += embeddedKwDepth(stmt)
|
||||
@@ -255,9 +257,9 @@ func parseBlocks(input string) []string {
|
||||
continue
|
||||
}
|
||||
switch w {
|
||||
case "if", "for", "while", "until", "case":
|
||||
case "if", "for", "while", "until", "case", "{":
|
||||
funcKwDepth++
|
||||
case "fi", "done", "esac":
|
||||
case "fi", "done", "esac", "}":
|
||||
funcKwDepth--
|
||||
}
|
||||
funcKwDepth += embeddedKwDepth(stmt)
|
||||
@@ -273,6 +275,7 @@ func parseBlocks(input string) []string {
|
||||
// embeddedKwDepth returns the net depth change from keywords that appear
|
||||
// after do/then/else/elif within a single statement (excluding the first word,
|
||||
// which is handled separately by the caller).
|
||||
func EmbeddedKwDepthForTest(s string) int { return embeddedKwDepth(s) }
|
||||
func embeddedKwDepth(stmt string) int {
|
||||
words := strings.Fields(stmt)
|
||||
delta := 0
|
||||
@@ -375,6 +378,12 @@ func parseHeredocMarkers(line string) (string, []string) {
|
||||
out.WriteByte(c)
|
||||
i++
|
||||
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
|
||||
i += 2 // skip <<
|
||||
stripTabs := false
|
||||
@@ -429,6 +438,7 @@ func splitStatements(input string) []string {
|
||||
inSingle := false
|
||||
inDouble := false
|
||||
parenDepth := 0
|
||||
braceDepth := 0 // { } command groups — don't split ; inside them
|
||||
|
||||
for i := 0; i < len(input); i++ {
|
||||
c := input[i]
|
||||
@@ -445,7 +455,14 @@ func splitStatements(input string) []string {
|
||||
case c == ')' && !inSingle && !inDouble && parenDepth > 0:
|
||||
parenDepth--
|
||||
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] == ';' {
|
||||
// Double semicolon — flush current token, then emit ";;" as a token
|
||||
if s := strings.TrimSpace(current.String()); s != "" {
|
||||
@@ -461,7 +478,7 @@ func splitStatements(input string) []string {
|
||||
}
|
||||
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 != "" {
|
||||
result = append(result, s)
|
||||
}
|
||||
@@ -553,12 +570,15 @@ func (s *Shell) executeBlock(block string) error {
|
||||
if isFuncDefStart(block) {
|
||||
return s.defineFunction(block)
|
||||
}
|
||||
for _, line := range strings.Split(block, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
// Use splitStatements instead of strings.Split("\n") so that multi-line
|
||||
// constructs (e.g. process substitutions spanning several lines) are kept
|
||||
// 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
|
||||
}
|
||||
if err := s.executeLine(line); err != nil {
|
||||
if err := s.executeLine(stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -591,20 +611,40 @@ func splitBySemicolon(line string) []string {
|
||||
current := strings.Builder{}
|
||||
inSingle := false
|
||||
inDouble := false
|
||||
parenDepth := 0 // tracks $(...) and <(...) nesting
|
||||
pendingDollar := false
|
||||
|
||||
for i := 0; i < len(line); i++ {
|
||||
c := line[i]
|
||||
switch {
|
||||
case c == '\'' && !inDouble:
|
||||
case c == '\'' && !inDouble && parenDepth == 0:
|
||||
inSingle = !inSingle
|
||||
current.WriteByte(c)
|
||||
case c == '"' && !inSingle:
|
||||
case c == '"' && !inSingle && parenDepth == 0:
|
||||
inDouble = !inDouble
|
||||
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())
|
||||
current.Reset()
|
||||
default:
|
||||
pendingDollar = false
|
||||
current.WriteByte(c)
|
||||
}
|
||||
}
|
||||
@@ -625,37 +665,58 @@ func (s *Shell) executeAndOrList(line string) error {
|
||||
inSingle := false
|
||||
inDouble := false
|
||||
dbDepth := 0 // double-bracket [[ depth
|
||||
parenDepth := 0 // $( ) depth — don't split && || inside subshells
|
||||
pendingDollar := false
|
||||
|
||||
for i := 0; i < len(line); i++ {
|
||||
c := line[i]
|
||||
switch {
|
||||
case c == '\'' && !inDouble:
|
||||
case c == '\'' && !inDouble && parenDepth == 0:
|
||||
inSingle = !inSingle
|
||||
current.WriteByte(c)
|
||||
case c == '"' && !inSingle:
|
||||
case c == '"' && !inSingle && parenDepth == 0:
|
||||
inDouble = !inDouble
|
||||
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++
|
||||
current.WriteByte(c)
|
||||
current.WriteByte(line[i+1])
|
||||
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--
|
||||
current.WriteByte(c)
|
||||
current.WriteByte(line[i+1])
|
||||
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})
|
||||
current.Reset()
|
||||
op = "&&"
|
||||
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})
|
||||
current.Reset()
|
||||
op = "||"
|
||||
i++
|
||||
default:
|
||||
pendingDollar = false
|
||||
current.WriteByte(c)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user