788 lines
20 KiB
Go
788 lines
20 KiB
Go
package shell
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// Sentinel errors for control flow
|
|
type breakErr struct{ n int }
|
|
type continueErr struct{ n int }
|
|
type returnErr struct{ code int }
|
|
|
|
func (e breakErr) Error() string { return fmt.Sprintf("break %d", e.n) }
|
|
func (e continueErr) Error() string { return fmt.Sprintf("continue %d", e.n) }
|
|
func (e returnErr) Error() string { return fmt.Sprintf("return %d", e.code) }
|
|
|
|
// exitCodeErr carries a non-zero exit code that sets $? without a message.
|
|
// Used by functions, test, false, etc.
|
|
type exitCodeErr struct{ code int }
|
|
|
|
func (e exitCodeErr) Error() string { return "" }
|
|
|
|
type Shell struct {
|
|
vars map[string]string
|
|
arrays map[string][]string
|
|
namerefs map[string]string
|
|
builtins map[string]func([]string) error
|
|
funcs map[string]string // function name → body
|
|
lastExit int
|
|
Stdin io.Reader
|
|
Stdout io.Writer
|
|
Stderr io.Writer
|
|
args []string
|
|
errexit bool
|
|
nounset bool
|
|
pipefail bool
|
|
}
|
|
|
|
func New() *Shell {
|
|
s := &Shell{
|
|
vars: map[string]string{},
|
|
arrays: map[string][]string{},
|
|
namerefs: map[string]string{},
|
|
funcs: map[string]string{},
|
|
Stdin: os.Stdin,
|
|
Stdout: os.Stdout,
|
|
Stderr: os.Stderr,
|
|
}
|
|
s.initBuiltins()
|
|
s.vars["SHELL"] = "bash-for-windows"
|
|
s.vars["BASH_VERSION"] = "5.2.15(1)-release"
|
|
s.vars["?"] = "0"
|
|
s.vars["#"] = "0"
|
|
s.vars["@"] = ""
|
|
s.vars["*"] = ""
|
|
s.vars["!"] = ""
|
|
if pwd, err := os.Getwd(); err == nil {
|
|
s.vars["PWD"] = pwd
|
|
}
|
|
if home, err := os.UserHomeDir(); err == nil {
|
|
s.vars["HOME"] = home
|
|
}
|
|
return s
|
|
}
|
|
|
|
// resolveNR resolves a variable name through the nameref chain (circular-ref safe).
|
|
func (s *Shell) resolveNR(name string) string {
|
|
seen := map[string]bool{}
|
|
for {
|
|
if seen[name] {
|
|
return name
|
|
}
|
|
seen[name] = true
|
|
target, ok := s.namerefs[name]
|
|
if !ok {
|
|
return name
|
|
}
|
|
name = target
|
|
}
|
|
}
|
|
|
|
// getArray returns array (resolving namerefs).
|
|
func (s *Shell) getArray(name string) []string {
|
|
return s.arrays[s.resolveNR(name)]
|
|
}
|
|
|
|
// setArray sets array (resolving namerefs).
|
|
func (s *Shell) setArray(name string, vals []string) {
|
|
s.arrays[s.resolveNR(name)] = vals
|
|
}
|
|
|
|
// appendArray appends to array (resolving namerefs).
|
|
func (s *Shell) appendArray(name string, vals []string) {
|
|
n := s.resolveNR(name)
|
|
s.arrays[n] = append(s.arrays[n], vals...)
|
|
}
|
|
|
|
func (s *Shell) SetArgs(args []string) {
|
|
s.args = args
|
|
s.vars["#"] = fmt.Sprintf("%d", len(args))
|
|
s.vars["@"] = strings.Join(args, " ")
|
|
s.vars["*"] = strings.Join(args, " ")
|
|
for i, a := range args {
|
|
s.vars[fmt.Sprintf("%d", i+1)] = a
|
|
}
|
|
}
|
|
|
|
func (s *Shell) GetVar(name string) string {
|
|
if v, ok := s.vars[name]; ok {
|
|
return v
|
|
}
|
|
return os.Getenv(name)
|
|
}
|
|
|
|
func (s *Shell) SetVar(name, value string) {
|
|
s.vars[name] = value
|
|
}
|
|
|
|
// 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")
|
|
input = strings.ReplaceAll(input, "\r", "\n")
|
|
input = strings.ReplaceAll(input, "\\\n", " ")
|
|
// Pre-process heredocs
|
|
input = preprocessHeredocs(input)
|
|
blocks := parseBlocks(input)
|
|
for _, block := range blocks {
|
|
if err := s.executeBlock(block); err != nil {
|
|
switch err.(type) {
|
|
case breakErr, continueErr, returnErr:
|
|
return err
|
|
}
|
|
s.setExitCode(err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsIncomplete returns true if the input is an incomplete multi-line construct.
|
|
func IsIncomplete(input string) bool {
|
|
stmts := splitStatements(input)
|
|
depth := 0
|
|
inSingle := false
|
|
inDouble := false
|
|
for _, ch := range input {
|
|
switch ch {
|
|
case '\'':
|
|
if !inDouble {
|
|
inSingle = !inSingle
|
|
}
|
|
case '"':
|
|
if !inSingle {
|
|
inDouble = !inDouble
|
|
}
|
|
}
|
|
}
|
|
if inSingle || inDouble {
|
|
return true
|
|
}
|
|
for _, stmt := range stmts {
|
|
w := firstWord(stmt)
|
|
switch w {
|
|
case "if", "for", "while", "until", "case":
|
|
depth++
|
|
case "fi", "done", "esac":
|
|
depth--
|
|
case "{":
|
|
depth++
|
|
case "}":
|
|
if depth > 0 {
|
|
depth--
|
|
}
|
|
}
|
|
if isFuncDefStart(stmt) && strings.HasSuffix(strings.TrimSpace(stmt), "{") {
|
|
depth++
|
|
}
|
|
}
|
|
return depth > 0
|
|
}
|
|
|
|
// parseBlocks groups statements into logical execution units.
|
|
// Multi-line if/for/while/function blocks are gathered into single entries.
|
|
func parseBlocks(input string) []string {
|
|
stmts := splitStatements(input)
|
|
var blocks []string
|
|
var current []string
|
|
kwDepth := 0 // if/for/while/until → fi/done nesting
|
|
inFunc := false
|
|
funcKwDepth := 0 // keyword nesting inside a function body
|
|
|
|
for _, stmt := range stmts {
|
|
stmt = strings.TrimSpace(stmt)
|
|
if stmt == "" || strings.HasPrefix(stmt, "#") {
|
|
continue
|
|
}
|
|
w := firstWord(stmt)
|
|
|
|
if !inFunc {
|
|
// Detect function definition opening with `{`
|
|
if isFuncDefStart(stmt) && strings.Contains(stmt, "{") {
|
|
braceIdx := strings.Index(stmt, "{")
|
|
// Count keywords after the { on this same line
|
|
funcKwDepth = 0
|
|
for _, p := range splitStatements(stmt[braceIdx+1:]) {
|
|
switch firstWord(p) {
|
|
case "if", "for", "while", "until", "case", "{":
|
|
funcKwDepth++
|
|
case "fi", "done", "esac", "}":
|
|
funcKwDepth--
|
|
}
|
|
}
|
|
// If the line also ends with } it's a self-contained function
|
|
if strings.HasSuffix(stmt, "}") {
|
|
current = append(current, stmt)
|
|
blocks = append(blocks, strings.Join(current, "\n"))
|
|
current = nil
|
|
funcKwDepth = 0
|
|
continue
|
|
}
|
|
inFunc = true
|
|
current = append(current, stmt)
|
|
continue
|
|
}
|
|
|
|
switch w {
|
|
case "if", "for", "while", "until", "case", "{":
|
|
kwDepth++
|
|
}
|
|
kwDepth += embeddedKwDepth(stmt)
|
|
current = append(current, stmt)
|
|
switch w {
|
|
case "fi", "done", "esac":
|
|
kwDepth--
|
|
case "}":
|
|
if kwDepth > 0 {
|
|
kwDepth--
|
|
}
|
|
}
|
|
if kwDepth <= 0 && len(current) > 0 {
|
|
kwDepth = 0
|
|
blocks = append(blocks, strings.Join(current, "\n"))
|
|
current = nil
|
|
}
|
|
} else {
|
|
// Inside function body — watch for } at funcKwDepth==0
|
|
if w == "}" && funcKwDepth <= 0 {
|
|
current = append(current, stmt)
|
|
blocks = append(blocks, strings.Join(current, "\n"))
|
|
current = nil
|
|
inFunc = false
|
|
funcKwDepth = 0
|
|
continue
|
|
}
|
|
switch w {
|
|
case "if", "for", "while", "until", "case", "{":
|
|
funcKwDepth++
|
|
case "fi", "done", "esac", "}":
|
|
funcKwDepth--
|
|
}
|
|
funcKwDepth += embeddedKwDepth(stmt)
|
|
current = append(current, stmt)
|
|
}
|
|
}
|
|
if len(current) > 0 {
|
|
blocks = append(blocks, strings.Join(current, "\n"))
|
|
}
|
|
return blocks
|
|
}
|
|
|
|
// 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
|
|
for j := 1; j < len(words); j++ {
|
|
switch words[j-1] {
|
|
case "do", "then", "else", "elif":
|
|
switch words[j] {
|
|
case "if", "for", "while", "until":
|
|
delta++
|
|
case "fi", "done", "esac":
|
|
delta--
|
|
}
|
|
}
|
|
}
|
|
return delta
|
|
}
|
|
|
|
// preprocessHeredocs converts heredoc syntax (<<MARKER) to temp-file redirects.
|
|
// It processes the input line by line, detecting <<MARKER, collecting the body,
|
|
// writing it to a temp file, and replacing <<MARKER with < /tmp/file.
|
|
func preprocessHeredocs(input string) string {
|
|
lines := strings.Split(input, "\n")
|
|
var result []string
|
|
i := 0
|
|
for i < len(lines) {
|
|
line := lines[i]
|
|
// Check for heredoc marker in this line: <<MARKER or <<-MARKER or <<"MARKER" or <<'MARKER'
|
|
// Find all heredoc markers on this line (there could be multiple)
|
|
processed, markers := parseHeredocMarkers(line)
|
|
if len(markers) == 0 {
|
|
result = append(result, line)
|
|
i++
|
|
continue
|
|
}
|
|
// Collect heredoc bodies for each marker
|
|
i++
|
|
newLine := processed
|
|
for _, marker := range markers {
|
|
stripTabs := strings.HasPrefix(marker, "-")
|
|
if stripTabs {
|
|
marker = marker[1:]
|
|
}
|
|
// Strip quotes from marker
|
|
quotedMarker := marker
|
|
if len(marker) >= 2 && ((marker[0] == '\'' && marker[len(marker)-1] == '\'') ||
|
|
(marker[0] == '"' && marker[len(marker)-1] == '"')) {
|
|
marker = marker[1 : len(marker)-1]
|
|
}
|
|
_ = quotedMarker
|
|
// Collect body until marker
|
|
var bodyLines []string
|
|
for i < len(lines) {
|
|
bodyLine := lines[i]
|
|
check := bodyLine
|
|
if stripTabs {
|
|
check = strings.TrimLeft(check, "\t")
|
|
}
|
|
if strings.TrimRight(check, "\r") == marker {
|
|
i++
|
|
break
|
|
}
|
|
bodyLines = append(bodyLines, bodyLine)
|
|
i++
|
|
}
|
|
// Write to temp file
|
|
content := strings.Join(bodyLines, "\n")
|
|
if len(bodyLines) > 0 {
|
|
content += "\n"
|
|
}
|
|
f, err := os.CreateTemp("", "heredoc*")
|
|
if err == nil {
|
|
f.WriteString(content)
|
|
f.Close()
|
|
newLine += " < " + f.Name()
|
|
}
|
|
}
|
|
result = append(result, newLine)
|
|
}
|
|
return strings.Join(result, "\n")
|
|
}
|
|
|
|
// parseHeredocMarkers finds <<MARKER patterns in a line and returns the line with
|
|
// the heredoc redirects removed (replaced), plus the list of markers found.
|
|
func parseHeredocMarkers(line string) (string, []string) {
|
|
var markers []string
|
|
var out strings.Builder
|
|
i := 0
|
|
inSingle := false
|
|
inDouble := false
|
|
|
|
for i < len(line) {
|
|
c := line[i]
|
|
switch {
|
|
case c == '\'' && !inDouble:
|
|
inSingle = !inSingle
|
|
out.WriteByte(c)
|
|
i++
|
|
case c == '"' && !inSingle:
|
|
inDouble = !inDouble
|
|
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
|
|
if i < len(line) && line[i] == '-' {
|
|
stripTabs = true
|
|
i++
|
|
}
|
|
// Skip spaces after <<
|
|
for i < len(line) && line[i] == ' ' {
|
|
i++
|
|
}
|
|
// Read the marker (may be quoted)
|
|
marker := ""
|
|
if i < len(line) && (line[i] == '\'' || line[i] == '"') {
|
|
quote := line[i]
|
|
i++
|
|
for i < len(line) && line[i] != quote {
|
|
marker += string(line[i])
|
|
i++
|
|
}
|
|
if i < len(line) {
|
|
i++ // skip closing quote
|
|
}
|
|
} else {
|
|
// Unquoted marker
|
|
for i < len(line) && line[i] != ' ' && line[i] != '\t' && line[i] != ';' && line[i] != '|' && line[i] != '&' && line[i] != '>' && line[i] != '<' {
|
|
marker += string(line[i])
|
|
i++
|
|
}
|
|
}
|
|
if marker != "" {
|
|
if stripTabs {
|
|
markers = append(markers, "-"+marker)
|
|
} else {
|
|
markers = append(markers, marker)
|
|
}
|
|
// Don't write the <<MARKER to output; it will be replaced by < file
|
|
}
|
|
default:
|
|
out.WriteByte(c)
|
|
i++
|
|
}
|
|
}
|
|
return out.String(), markers
|
|
}
|
|
|
|
// splitStatements splits input on semicolons and newlines, respecting quotes and ((...)).
|
|
// Double semicolons ;; are preserved as a single token (for case statements).
|
|
func splitStatements(input string) []string {
|
|
var result []string
|
|
current := strings.Builder{}
|
|
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]
|
|
switch {
|
|
case c == '\'' && !inDouble && parenDepth == 0:
|
|
inSingle = !inSingle
|
|
current.WriteByte(c)
|
|
case c == '"' && !inSingle && parenDepth == 0:
|
|
inDouble = !inDouble
|
|
current.WriteByte(c)
|
|
case c == '(' && !inSingle && !inDouble:
|
|
parenDepth++
|
|
current.WriteByte(c)
|
|
case c == ')' && !inSingle && !inDouble && parenDepth > 0:
|
|
parenDepth--
|
|
current.WriteByte(c)
|
|
// 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 != "" {
|
|
result = append(result, s)
|
|
}
|
|
current.Reset()
|
|
result = append(result, ";;")
|
|
i++ // skip second ;
|
|
} else {
|
|
// Single semicolon — just a statement separator
|
|
if s := strings.TrimSpace(current.String()); s != "" {
|
|
result = append(result, s)
|
|
}
|
|
current.Reset()
|
|
}
|
|
case c == '\n' && !inSingle && !inDouble && parenDepth == 0 && braceDepth == 0:
|
|
if s := strings.TrimSpace(current.String()); s != "" {
|
|
result = append(result, s)
|
|
}
|
|
current.Reset()
|
|
default:
|
|
current.WriteByte(c)
|
|
}
|
|
}
|
|
if s := strings.TrimSpace(current.String()); s != "" {
|
|
result = append(result, s)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func firstWord(s string) string {
|
|
fields := strings.Fields(s)
|
|
if len(fields) == 0 {
|
|
return ""
|
|
}
|
|
return fields[0]
|
|
}
|
|
|
|
func afterWord(s string) string {
|
|
for i, ch := range s {
|
|
if ch == ' ' || ch == '\t' {
|
|
return strings.TrimSpace(s[i:])
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func isFuncDefStart(stmt string) bool {
|
|
if strings.HasPrefix(stmt, "function ") {
|
|
return true
|
|
}
|
|
for i, ch := range stmt {
|
|
if ch == ' ' || ch == '\t' {
|
|
break
|
|
}
|
|
if ch == '(' {
|
|
name := strings.TrimSpace(stmt[:i])
|
|
return isValidIdentifier(name)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isValidIdentifier(s string) bool {
|
|
if len(s) == 0 {
|
|
return false
|
|
}
|
|
for i, c := range s {
|
|
if i == 0 {
|
|
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_') {
|
|
return false
|
|
}
|
|
} else {
|
|
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *Shell) executeBlock(block string) error {
|
|
block = strings.TrimSpace(block)
|
|
if block == "" || strings.HasPrefix(block, "#") {
|
|
return nil
|
|
}
|
|
w := firstWord(block)
|
|
switch w {
|
|
case "if":
|
|
return s.executeIf(block)
|
|
case "for":
|
|
// Check for C-style for (( ... ))
|
|
trimmed := strings.TrimSpace(block)
|
|
if strings.HasPrefix(trimmed, "for ((") || strings.HasPrefix(trimmed, "for((") {
|
|
return s.executeForC(block)
|
|
}
|
|
return s.executeFor(block)
|
|
case "while":
|
|
return s.executeWhileUntil(block, false)
|
|
case "until":
|
|
return s.executeWhileUntil(block, true)
|
|
case "case":
|
|
return s.executeCase(block)
|
|
}
|
|
if isFuncDefStart(block) {
|
|
return s.defineFunction(block)
|
|
}
|
|
// 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(stmt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Shell) executeLine(line string) error {
|
|
return s.executeChain(line)
|
|
}
|
|
|
|
func (s *Shell) executeChain(line string) error {
|
|
for _, seg := range splitBySemicolon(line) {
|
|
seg = strings.TrimSpace(seg)
|
|
if seg == "" {
|
|
continue
|
|
}
|
|
if err := s.executeAndOrList(seg); err != nil {
|
|
switch err.(type) {
|
|
case breakErr, continueErr, returnErr:
|
|
return err
|
|
}
|
|
s.setExitCode(err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func splitBySemicolon(line string) []string {
|
|
var parts []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 && parenDepth == 0:
|
|
inSingle = !inSingle
|
|
current.WriteByte(c)
|
|
case c == '"' && !inSingle && parenDepth == 0:
|
|
inDouble = !inDouble
|
|
current.WriteByte(c)
|
|
// 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)
|
|
}
|
|
}
|
|
if current.Len() > 0 {
|
|
parts = append(parts, current.String())
|
|
}
|
|
return parts
|
|
}
|
|
|
|
func (s *Shell) executeAndOrList(line string) error {
|
|
type tok struct {
|
|
text string
|
|
op string
|
|
}
|
|
var tokens []tok
|
|
current := strings.Builder{}
|
|
op := ""
|
|
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 && parenDepth == 0:
|
|
inSingle = !inSingle
|
|
current.WriteByte(c)
|
|
case c == '"' && !inSingle && parenDepth == 0:
|
|
inDouble = !inDouble
|
|
current.WriteByte(c)
|
|
// 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 && 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 && 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 && 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)
|
|
}
|
|
}
|
|
if current.Len() > 0 {
|
|
tokens = append(tokens, tok{current.String(), op})
|
|
}
|
|
|
|
var lastErr error
|
|
for i, t := range tokens {
|
|
cmd := strings.TrimSpace(t.text)
|
|
if cmd == "" {
|
|
continue
|
|
}
|
|
run := i == 0
|
|
if i > 0 {
|
|
run = (t.op == "&&" && lastErr == nil) || (t.op == "||" && lastErr != nil)
|
|
}
|
|
if run {
|
|
err := s.executePipeline(cmd)
|
|
lastErr = err
|
|
s.setExitCode(err)
|
|
}
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
func (s *Shell) setExitCode(err error) {
|
|
if err == nil {
|
|
s.vars["?"] = "0"
|
|
s.lastExit = 0
|
|
} else if ec, ok := err.(exitCodeErr); ok {
|
|
s.vars["?"] = fmt.Sprintf("%d", ec.code)
|
|
s.lastExit = ec.code
|
|
} else {
|
|
s.vars["?"] = "1"
|
|
s.lastExit = 1
|
|
}
|
|
}
|
|
|
|
// BuiltinNames returns a sorted list of all registered builtin names (for tab completion).
|
|
func (s *Shell) BuiltinNames() []string {
|
|
names := make([]string, 0, len(s.builtins)+len(s.funcs))
|
|
for k := range s.builtins {
|
|
names = append(names, k)
|
|
}
|
|
for k := range s.funcs {
|
|
names = append(names, k)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// withIO temporarily swaps stdin/stdout/stderr, runs fn, then restores.
|
|
// Pass nil to leave the corresponding stream unchanged.
|
|
func (s *Shell) withIO(stdin io.Reader, stdout io.Writer, stderr io.Writer, fn func() error) error {
|
|
oldIn, oldOut, oldErr := s.Stdin, s.Stdout, s.Stderr
|
|
if stdin != nil {
|
|
s.Stdin = stdin
|
|
}
|
|
if stdout != nil {
|
|
s.Stdout = stdout
|
|
}
|
|
if stderr != nil {
|
|
s.Stderr = stderr
|
|
}
|
|
err := fn()
|
|
s.Stdin, s.Stdout, s.Stderr = oldIn, oldOut, oldErr
|
|
return err
|
|
}
|