package main import ( "bufio" "fmt" "io" "os" "os/exec" "strings" ) type ExitStatus uint16 type Context struct { stdout io.Writer stderr io.Writer stdin io.Reader } func makeContext() Context { return Context{ stdout: os.Stdout, stderr: os.Stderr, stdin: os.Stdin, } } type GetCommandResult int const ( GetCommandResultOk GetCommandResult = iota GetCommandResultError ) type Command struct { command_type CommandType args []string exec_path string name string } func makeCommandNone() Command { return Command{ command_type: CommandTypeNone, args: nil, exec_path: "", name: "", } } func makeCommandBuiltin(command_type CommandType, tokens []string) Command { switch command_type { case CommandTypeNone: panic("invalid usage") case CommandTypeExternal: panic("invalid usage") default: if len(tokens) < 1 { panic("assertion failed: token list must not be empty") } return Command{ command_type: command_type, name: tokens[0], args: tokens[1:], } } } func makeCommandExternal(tokens []string, exec_path string) Command { if len(tokens) < 1 { panic("assertion failed: token list must not be empty") } return Command{ command_type: CommandTypeExternal, name: tokens[0], args: tokens[1:], exec_path: exec_path, } } type CommandType int const ( CommandTypeNone CommandType = iota CommandTypeExternal CommandTypeBuiltinExit CommandTypeBuiltinEcho CommandTypeBuiltinType CommandTypeBuiltinPwd CommandTypeBuiltinCd ) func getCommand(ctx *Context) (Command, error) { fmt.Print("$ ") command_line, err := bufio.NewReader(ctx.stdin).ReadString('\n') if err != nil { return makeCommandNone(), err } command, err := parseCommand(command_line) // fmt.Printf("getCommand():command=%#v\n", command) if err != nil { fmt.Fprintln(ctx.stderr, err) return makeCommandNone(), nil } return command, nil } func tokenize(str string) []string { tokens := strings.Fields(str) return tokens } type ParseCommandError struct { code ParseCommandErrorCode value string } type ParseCommandErrorCode int const ( ParseCommandErrorCodeNotFound ParseCommandErrorCode = iota ) func (e ParseCommandError) Error() string { switch e.code { case ParseCommandErrorCodeNotFound: return fmt.Sprintf("%s: not found", e.value) default: return fmt.Sprintf("unknown ParseCommandErrorCode code: %d .value=%q", e.code, e.value) } } func parseCommand(command_line string) (Command, error) { tokens := tokenize(command_line) if len(tokens) == 0 { return makeCommandNone(), nil } if tokens[0] == "exit" { return makeCommandBuiltin(CommandTypeBuiltinExit, tokens), nil } if tokens[0] == "pwd" { return makeCommandBuiltin(CommandTypeBuiltinPwd, tokens), nil } if tokens[0] == "cd" { return makeCommandBuiltin(CommandTypeBuiltinCd, tokens), nil } if tokens[0] == "echo" { return makeCommandBuiltin(CommandTypeBuiltinEcho, tokens), nil } if tokens[0] == "type" { return makeCommandBuiltin(CommandTypeBuiltinType, tokens), nil } exec_path, err := exec.LookPath(tokens[0]) if err != nil { return makeCommandNone(), ParseCommandError{ParseCommandErrorCodeNotFound, tokens[0]} } return makeCommandExternal(tokens, exec_path), nil } func bltnChdir(ctx Context, path string) ExitStatus { err := os.Chdir(path) if err != nil { fmt.Fprintf(ctx.stderr, "cd: %s: No such file or directory\n", path) return 4 } return 0 } func handleCommand(ctx Context, command Command) ExitStatus { // fmt.Printf("handleCommand():command=%#v\n", command) switch command.command_type { case CommandTypeNone: return 0 case CommandTypeBuiltinExit: os.Exit(0) case CommandTypeBuiltinEcho: fmt.Fprintln(ctx.stdout, strings.Join(command.args, " ")) return 0 case CommandTypeBuiltinCd: switch len(command.args) { case 0: home_path := os.Getenv("HOME") if len(home_path) == 0 { fmt.Fprintln(ctx.stderr, "error: $HOME environment variable is empty or unset") return 4 } return bltnChdir(ctx, home_path) case 1: if command.args[0] == "~" { home_path := os.Getenv("HOME") if len(home_path) == 0 { fmt.Fprintln(ctx.stderr, "error: $HOME environment variable is empty or unset") return 4 } return bltnChdir(ctx, home_path) } return bltnChdir(ctx, command.args[0]) default: fmt.Fprintln(ctx.stderr, "usage: cd PATH") return 2 } case CommandTypeBuiltinPwd: if len(command.args) != 0 { fmt.Fprintln(ctx.stderr, "usage: pwd") return 2 } cwd_path, err := os.Getwd() if err != nil { panic("error getting current working directory") } fmt.Fprintln(ctx.stdout, cwd_path) return 0 case CommandTypeBuiltinType: if len(command.args) != 1 { fmt.Fprintln(ctx.stderr, "usage: type COMMAND") return 2 } parsed, err := parseCommand(command.args[0]) if err != nil { fmt.Fprintln(ctx.stderr, err) return 0 } switch parsed.command_type { case CommandTypeNone: panic("impossible parseCommand() result") case CommandTypeExternal: fmt.Fprintf(ctx.stdout, "%s is %s\n", parsed.name, parsed.exec_path) default: fmt.Fprintf(ctx.stdout, "%s is a shell builtin\n", parsed.name) } return 0 case CommandTypeExternal: cmd := exec.Command(command.exec_path, command.args...) cmd.Args[0] = command.name cmd.Stdout = ctx.stdout cmd.Stderr = ctx.stderr cmd.Stdin = ctx.stdin err := cmd.Run() if err != nil { if exiterr, ok := err.(*exec.ExitError); ok { return ExitStatus(exiterr.ExitCode()) // fmt.Printf("handleCommand():exiterr=%#v\n", exiterr.ExitCode()) } else { fmt.Printf("handleCommand():err=%#v\n", err) return ExitStatus(65535) } } return 0 default: panic("unknown command type") } return ExitStatus(65535) } func main() { ctx := makeContext() for { command, err := getCommand(&ctx) if err != nil { fmt.Fprintln(os.Stderr, "Error reading input: ", err) os.Exit(1) } _ = handleCommand(ctx, command) } }