fixed bash-for-windows

This commit is contained in:
Cametendo
2026-05-31 21:49:13 +02:00
parent 7b3a101946
commit 9037606447
8 changed files with 437 additions and 123 deletions

View File

@@ -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)
}
}
@@ -624,38 +664,59 @@ func (s *Shell) executeAndOrList(line string) error {
op := ""
inSingle := 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++ {
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)
}
}