package bash import ( "fmt" "os" "path/filepath" "strings" "github.com/chzyer/readline" "github.com/cametendo/bash-for-windows/internal/shell" ) func Run() error { args := os.Args[1:] if len(args) > 0 { switch args[0] { case "-c": if len(args) < 2 { return fmt.Errorf("-c: option requires an argument") } sh := shell.New() if len(args) > 2 { sh.SetArgs(args[2:]) } return sh.Execute(strings.Join(args[1:], " ")) case "--version": fmt.Println("bash-for-windows 2.0.0 (Go-based)") fmt.Println("Provides bash-compatible shell for Windows.") return nil default: return runFile(args[0], args[1:]) } } return interactive() } func runFile(path string, args []string) error { data, err := os.ReadFile(path) if err != nil { return fmt.Errorf("%s: %v", path, err) } sh := shell.New() sh.SetArgs(append([]string{path}, args...)) sh.SetVar("0", path) return sh.Execute(string(data)) } func historyFile() string { home, err := os.UserHomeDir() if err != nil { return "" } // On Windows this lands in %USERPROFILE%\.bash_history return filepath.Join(home, ".bash_history") } func interactive() error { sh := shell.New() // Build completer completer := readline.NewPrefixCompleter( readline.PcItemDynamic(func(line string) []string { return dynamicComplete(sh, line) }), ) rl, err := readline.NewEx(&readline.Config{ HistoryFile: historyFile(), AutoComplete: completer, InterruptPrompt: "^C", EOFPrompt: "exit", HistorySearchFold: true, FuncFilterInputRune: filterInput, }) if err != nil { // Fall back to dumb interactive mode return interactiveDumb(sh) } defer rl.Close() fmt.Fprintln(os.Stdout, "bash-for-windows v2.0.0 (type 'exit' or Ctrl+D to quit)") var multiLine strings.Builder for { prompt := buildPrompt(sh) if multiLine.Len() > 0 { prompt = "> " } rl.SetPrompt(prompt) line, err := rl.Readline() if err != nil { if err.Error() == "Interrupt" { // Ctrl+C: abort current multi-line input multiLine.Reset() fmt.Fprintln(os.Stdout) continue } break // EOF } if multiLine.Len() > 0 { multiLine.WriteString("\n") } multiLine.WriteString(line) input := multiLine.String() if shell.IsIncomplete(input) { continue // wait for more input } multiLine.Reset() input = strings.TrimSpace(input) if input == "" { continue } if err := sh.Execute(input); err != nil { fmt.Fprintf(os.Stderr, "bash: %v\n", err) } } return nil } // interactiveDumb is a fallback that doesn't need readline. func interactiveDumb(sh *shell.Shell) error { fmt.Fprintln(os.Stdout, "bash-for-windows v2.0.0") var multiLine strings.Builder buf := make([]byte, 4096) for { prompt := buildPrompt(sh) if multiLine.Len() > 0 { prompt = "> " } fmt.Fprint(os.Stdout, prompt) n, err := os.Stdin.Read(buf) if n > 0 { chunk := string(buf[:n]) if multiLine.Len() > 0 { multiLine.WriteString("\n") } multiLine.WriteString(strings.TrimRight(chunk, "\r\n")) input := multiLine.String() if shell.IsIncomplete(input) { continue } multiLine.Reset() input = strings.TrimSpace(input) if input == "" { continue } if execErr := sh.Execute(input); execErr != nil { fmt.Fprintf(os.Stderr, "bash: %v\n", execErr) } } if err != nil { break } } return nil } func buildPrompt(sh *shell.Shell) string { pwd, _ := os.Getwd() home, _ := os.UserHomeDir() if home != "" && strings.HasPrefix(pwd, home) { pwd = "~" + pwd[len(home):] } // Show exit code in prompt if non-zero exitCode := sh.GetVar("?") suffix := "$ " if exitCode != "0" && exitCode != "" { suffix = "[" + exitCode + "]$ " } return pwd + suffix } // dynamicComplete provides tab completion for commands and paths. func dynamicComplete(sh *shell.Shell, line string) []string { line = strings.TrimLeft(line, " \t") var completions []string // Check if we're completing the first word (command) or an argument (path) words := strings.Fields(line) completingCommand := len(words) == 0 || (len(words) == 1 && !strings.HasSuffix(line, " ")) if completingCommand { prefix := "" if len(words) == 1 { prefix = words[0] } // Builtin/function names — access via the shell instance // (we can't iterate unexported fields, so use a public method) for _, name := range sh.BuiltinNames() { if strings.HasPrefix(name, prefix) { completions = append(completions, name) } } // PATH executables for _, dir := range filepath.SplitList(os.Getenv("PATH")) { entries, err := os.ReadDir(dir) if err != nil { continue } for _, e := range entries { name := e.Name() // Strip .exe on completion display name = strings.TrimSuffix(name, ".exe") if strings.HasPrefix(name, prefix) { completions = append(completions, name) } } } } else { // Path completion prefix := "" if len(words) > 0 { prefix = words[len(words)-1] if strings.HasSuffix(line, " ") { prefix = "" } } dir := filepath.Dir(prefix) base := filepath.Base(prefix) if prefix == "" || strings.HasSuffix(prefix, "/") || strings.HasSuffix(prefix, "\\") { dir = prefix base = "" } entries, err := os.ReadDir(dir) if err == nil { for _, e := range entries { name := e.Name() if strings.HasPrefix(name, base) { p := filepath.Join(dir, name) if e.IsDir() { p += "/" } completions = append(completions, p) } } } } return completions } func filterInput(r rune) (rune, bool) { // Block Ctrl+Z (26) — on Windows this would suspend; we handle it gracefully if r == 26 { return r, false } return r, true }