package main import ( "bufio" "fmt" "io" "os" "strings" ) func getenv(key string, fallback string) string { value, ok := os.LookupEnv(key) if ok { return value } else { return fallback } } type Context struct { stdout io.Writer stderr io.Writer stdin io.Reader paths []string } func makeContext() Context { path_str := getenv("PATH", "") paths := make([]string, 0) for elem := range strings.SplitSeq(path_str, ":") { if elem == "" { continue } if !strings.HasPrefix(elem, "/") { continue } paths = append(paths, elem) } return Context{ stdout: os.Stdout, stderr: os.Stderr, stdin: os.Stdin, paths: paths, } } type GetCommandResult int const ( GetCommandResultOk GetCommandResult = iota GetCommandResultError ) type Command struct { command_type CommandType tokens []string exec_path string } func makeCommandNone() Command { return Command{ command_type: CommandTypeNone, tokens: nil, exec_path: "", } } func makeCommandBuiltin(command_type CommandType, tokens []string) Command { switch command_type { case CommandTypeNone: panic("invalid usage") case CommandTypeExternal: panic("invalid usage") default: return Command{ command_type: command_type, tokens: tokens, } } } func makeCommandExternal(tokens []string, exec_path string) Command { return Command{ command_type: CommandTypeExternal, tokens: tokens, exec_path: exec_path, } } type CommandType int const ( CommandTypeNone CommandType = iota CommandTypeExternal CommandTypeBuiltinExit CommandTypeBuiltinEcho CommandTypeBuiltinType ) 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, ctx.paths) // 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, paths []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] == "echo" { return makeCommandBuiltin(CommandTypeBuiltinEcho, tokens), nil } if tokens[0] == "type" { return makeCommandBuiltin(CommandTypeBuiltinType, tokens), nil } for _, path := range paths { full_path := fmt.Sprintf("%s/%s", path, tokens[0]) // fmt.Printf("parseCommand():full_path=%#v\n", full_path) stat, err := os.Stat(full_path) if err != nil { continue } if stat.IsDir() { continue } if stat.Mode()&0100 == 0 { continue } return makeCommandExternal(tokens, full_path), nil } return makeCommandNone(), ParseCommandError{ParseCommandErrorCodeNotFound, tokens[0]} } func handleCommand(ctx Context, command Command) { // fmt.Printf("handleCommand():command=%#v\n", command) switch command.command_type { case CommandTypeNone: return case CommandTypeBuiltinExit: os.Exit(0) case CommandTypeBuiltinEcho: fmt.Fprintln(ctx.stdout, strings.Join(command.tokens[1:], " ")) case CommandTypeBuiltinType: if len(command.tokens) != 2 { fmt.Fprintln(ctx.stderr, "usage: type COMMAND") return } parsed, err := parseCommand(command.tokens[1], ctx.paths) if err != nil { fmt.Fprintln(ctx.stderr, err) return } switch parsed.command_type { case CommandTypeNone: panic("impossible parseCommand() result") case CommandTypeExternal: fmt.Fprintf(ctx.stdout, "%s is %s\n", parsed.tokens[0], parsed.exec_path) default: fmt.Fprintf(ctx.stdout, "%s is a shell builtin\n", parsed.tokens[0]) } } } 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) } }