package shell import ( "bytes" "fmt" "io" "os" "os/exec" "path/filepath" "strings" ) type Shell struct { vars map[string]string builtins map[string]func([]string) error lastExit int } func New() *Shell { s := &Shell{ vars: map[string]string{}, lastExit: 0, } s.initBuiltins() s.vars["SHELL"] = "bash-for-windows" s.vars["BASH_VERSION"] = "1.0.0" s.vars["?"] = "0" if pwd, err := os.Getwd(); err == nil { s.vars["PWD"] = pwd } if home, err := os.UserHomeDir(); err == nil { s.vars["HOME"] = home } return s } func (s *Shell) Execute(input string) error { input = strings.TrimSpace(input) if input == "" { return nil } lines := strings.Split(input, "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } if err := s.executeLine(line); err != nil { return err } } return nil } func (s *Shell) executeLine(line string) error { // Tokenize the line into its components, handling &&, ||, ; return s.executeChain(line) } // executeChain: parse && / || / ; with left-to-right precedence func (s *Shell) executeChain(line string) error { // Strategy: split by ; first (semicolons always separate), // then by &&/|| within each segment segments := splitBySemicolon(line) for _, seg := range segments { seg = strings.TrimSpace(seg) if seg == "" { continue } if err := s.executeAndOrList(seg); err != nil { // In ; chains, errors in one command don't stop execution s.setExitCode(err) } } return nil } func splitBySemicolon(line string) []string { var parts []string current := "" inSingle := false inDouble := false for i := 0; i < len(line); i++ { c := line[i] switch { case c == '\'' && !inDouble: inSingle = !inSingle current += string(c) case c == '"' && !inSingle: inDouble = !inDouble current += string(c) case c == ';' && !inSingle && !inDouble: parts = append(parts, current) current = "" default: current += string(c) } } if current != "" { parts = append(parts, current) } return parts } // executeAndOrList: parse && / || with left-to-right precedence func (s *Shell) executeAndOrList(line string) error { type token struct { text string op string // operator BEFORE this token (except first = "") } var tokens []token current := "" op := "" inSingle := false inDouble := false for i := 0; i < len(line); i++ { c := line[i] switch { case c == '\'' && !inDouble: inSingle = !inSingle current += string(c) case c == '"' && !inSingle: inDouble = !inDouble current += string(c) case c == '&' && !inSingle && !inDouble: if i+1 < len(line) && line[i+1] == '&' { if current != "" { tokens = append(tokens, token{current, op}) current = "" } op = "&&" i++ } else { current += string(c) } case c == '|' && !inSingle && !inDouble: if i+1 < len(line) && line[i+1] == '|' { if current != "" { tokens = append(tokens, token{current, op}) current = "" } op = "||" i++ } else { current += string(c) } default: current += string(c) } } if current != "" { tokens = append(tokens, token{current, op}) } if len(tokens) == 0 { return nil } var lastErr error for i, tok := range tokens { cmd := strings.TrimSpace(tok.text) if cmd == "" { continue } shouldRun := true if i > 0 && tok.op == "&&" { shouldRun = (lastErr == nil) } else if i > 0 && tok.op == "||" { shouldRun = (lastErr != nil) } if shouldRun { err := s.executePipeline(cmd) lastErr = err s.setExitCode(err) } } return lastErr } func (s *Shell) setExitCode(err error) { if err != nil { s.vars["?"] = "1" } else { s.vars["?"] = "0" } } func (s *Shell) executePipeline(input string) error { input = strings.TrimSpace(input) if input == "" { return nil } if strings.Contains(input, "|") { return s.doPipe(input) } return s.executeCommand(input) } func (s *Shell) executeCommand(input string) error { parts := s.tokenize(input) if len(parts) == 0 { return nil } cmdName := parts[0] args := parts[1:] if alias, ok := aliases[cmdName]; ok { fullCmd := alias if len(args) > 0 { fullCmd += " " + strings.Join(args, " ") } return s.Execute(fullCmd) } if builtin, ok := s.builtins[cmdName]; ok { return builtin(args) } return s.executeExternal(cmdName, args) } func (s *Shell) tokenize(input string) []string { var tokens []string current := "" inSingle := false inDouble := false for i := 0; i < len(input); i++ { c := input[i] switch { case c == '\'' && !inDouble: inSingle = !inSingle case c == '"' && !inSingle: inDouble = !inDouble case (c == ' ' || c == '\t') && !inSingle && !inDouble: if current != "" { tokens = append(tokens, current) current = "" } case c == '=' && !inSingle && !inDouble: current += string(c) default: current += string(c) } } if current != "" { tokens = append(tokens, current) } for i, tok := range tokens { if strings.HasPrefix(tok, "$") { key := tok[1:] if strings.HasPrefix(key, "{") && strings.HasSuffix(key, "}") { key = key[1 : len(key)-1] } if val, ok := s.vars[key]; ok { tokens[i] = val } else if val := os.Getenv(key); val != "" { tokens[i] = val } } } varAssignIdx := -1 for i, tok := range tokens { if strings.Contains(tok, "=") && i == 0 { eqIdx := strings.Index(tok, "=") if eqIdx > 0 && eqIdx < len(tok)-1 { name := tok[:eqIdx] value := tok[eqIdx+1:] s.vars[name] = value os.Setenv(name, value) varAssignIdx = i } } } if varAssignIdx == 0 && len(tokens) > 1 { tokens = tokens[1:] } return tokens } func (s *Shell) executeExternal(cmdName string, args []string) error { cmdPath := findExecutable(cmdName) if cmdPath == "" { return fmt.Errorf("%s: command not found", cmdName) } cmd := exec.Command(cmdPath, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } func findExecutable(name string) string { if _, err := os.Stat(name); err == nil { if info, _ := os.Stat(name); info != nil && !info.IsDir() { abs, _ := filepath.Abs(name) return abs } } path := os.Getenv("PATH") for _, dir := range filepath.SplitList(path) { fullPath := filepath.Join(dir, name) info, err := os.Stat(fullPath) if err == nil && !info.IsDir() { return fullPath } fullPathExe := fullPath + ".exe" info, err = os.Stat(fullPathExe) if err == nil && !info.IsDir() { return fullPathExe } } return "" } func (s *Shell) doPipe(input string) error { commands := strings.Split(input, "|") type cmdPart struct { name string args []string builtin bool } var parts []cmdPart for _, part := range commands { part = strings.TrimSpace(part) if part == "" { continue } tokens := s.tokenize(part) if len(tokens) == 0 { continue } name := tokens[0] _, isBuiltin := s.builtins[name] parts = append(parts, cmdPart{name, tokens[1:], isBuiltin}) } if len(parts) == 0 { return nil } var prevOutput []byte for i, p := range parts { var input []byte if i > 0 { input = prevOutput } if p.builtin { output, err := s.captureBuiltin(p.name, p.args, input) if err != nil { // Don't return error, let it pass prevOutput = nil if i == len(parts)-1 { return err } continue } if i == len(parts)-1 { fmt.Print(string(output)) } else { prevOutput = output } } else { cmdPath := findExecutable(p.name) if cmdPath == "" { return fmt.Errorf("%s: command not found", p.name) } cmd := exec.Command(cmdPath, p.args...) cmd.Stderr = os.Stderr if i == 0 && len(input) == 0 { cmd.Stdin = os.Stdin } else if len(input) > 0 { stdin, _ := cmd.StdinPipe() go func() { stdin.Write(input) stdin.Close() }() } if i == len(parts)-1 { cmd.Stdout = os.Stdout if err := cmd.Run(); err != nil { return err } } else { output, err := cmd.Output() if err != nil { return err } prevOutput = output } } } return nil } func (s *Shell) captureBuiltin(name string, args []string, input []byte) ([]byte, error) { oldStdout := os.Stdout oldStdin := os.Stdin r, w, _ := os.Pipe() os.Stdout = w if len(input) > 0 { ir, iw, _ := os.Pipe() iw.Write(input) iw.Close() os.Stdin = ir defer ir.Close() } fn := s.builtins[name] err := fn(args) w.Close() os.Stdout = oldStdout os.Stdin = oldStdin var buf bytes.Buffer io.Copy(&buf, r) r.Close() return buf.Bytes(), err }