main.go 4.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "io"
  6. "os"
  7. "os/exec"
  8. "strings"
  9. )
  10. type ExitStatus uint16
  11. type Context struct {
  12. stdout io.Writer
  13. stderr io.Writer
  14. stdin io.Reader
  15. }
  16. func makeContext() Context {
  17. return Context{
  18. stdout: os.Stdout,
  19. stderr: os.Stderr,
  20. stdin: os.Stdin,
  21. }
  22. }
  23. type GetCommandResult int
  24. const (
  25. GetCommandResultOk GetCommandResult = iota
  26. GetCommandResultError
  27. )
  28. type Command struct {
  29. command_type CommandType
  30. tokens []string
  31. exec_path string
  32. }
  33. func makeCommandNone() Command {
  34. return Command{
  35. command_type: CommandTypeNone,
  36. tokens: nil,
  37. exec_path: "",
  38. }
  39. }
  40. func makeCommandBuiltin(command_type CommandType, tokens []string) Command {
  41. switch command_type {
  42. case CommandTypeNone:
  43. panic("invalid usage")
  44. case CommandTypeExternal:
  45. panic("invalid usage")
  46. default:
  47. return Command{
  48. command_type: command_type,
  49. tokens: tokens,
  50. }
  51. }
  52. }
  53. func makeCommandExternal(tokens []string, exec_path string) Command {
  54. return Command{
  55. command_type: CommandTypeExternal,
  56. tokens: tokens,
  57. exec_path: exec_path,
  58. }
  59. }
  60. type CommandType int
  61. const (
  62. CommandTypeNone CommandType = iota
  63. CommandTypeExternal
  64. CommandTypeBuiltinExit
  65. CommandTypeBuiltinEcho
  66. CommandTypeBuiltinType
  67. )
  68. func getCommand(ctx *Context) (Command, error) {
  69. fmt.Print("$ ")
  70. command_line, err := bufio.NewReader(ctx.stdin).ReadString('\n')
  71. if err != nil {
  72. return makeCommandNone(), err
  73. }
  74. command, err := parseCommand(command_line)
  75. // fmt.Printf("getCommand():command=%#v\n", command)
  76. if err != nil {
  77. fmt.Fprintln(ctx.stderr, err)
  78. return makeCommandNone(), nil
  79. }
  80. return command, nil
  81. }
  82. func tokenize(str string) []string {
  83. tokens := strings.Fields(str)
  84. return tokens
  85. }
  86. type ParseCommandError struct {
  87. code ParseCommandErrorCode
  88. value string
  89. }
  90. type ParseCommandErrorCode int
  91. const (
  92. ParseCommandErrorCodeNotFound ParseCommandErrorCode = iota
  93. )
  94. func (e ParseCommandError) Error() string {
  95. switch e.code {
  96. case ParseCommandErrorCodeNotFound:
  97. return fmt.Sprintf("%s: not found", e.value)
  98. default:
  99. return fmt.Sprintf("unknown ParseCommandErrorCode code: %d .value=%q", e.code, e.value)
  100. }
  101. }
  102. func parseCommand(command_line string) (Command, error) {
  103. tokens := tokenize(command_line)
  104. if len(tokens) == 0 {
  105. return makeCommandNone(), nil
  106. }
  107. if tokens[0] == "exit" {
  108. return makeCommandBuiltin(CommandTypeBuiltinExit, tokens), nil
  109. }
  110. if tokens[0] == "echo" {
  111. return makeCommandBuiltin(CommandTypeBuiltinEcho, tokens), nil
  112. }
  113. if tokens[0] == "type" {
  114. return makeCommandBuiltin(CommandTypeBuiltinType, tokens), nil
  115. }
  116. exec_path, err := exec.LookPath(tokens[0])
  117. if err != nil {
  118. return makeCommandNone(), ParseCommandError{ParseCommandErrorCodeNotFound, tokens[0]}
  119. }
  120. return makeCommandExternal(tokens, exec_path), nil
  121. }
  122. func handleCommand(ctx Context, command Command) ExitStatus {
  123. // fmt.Printf("handleCommand():command=%#v\n", command)
  124. switch command.command_type {
  125. case CommandTypeNone:
  126. return 0
  127. case CommandTypeBuiltinExit:
  128. os.Exit(0)
  129. case CommandTypeBuiltinEcho:
  130. fmt.Fprintln(ctx.stdout, strings.Join(command.tokens[1:], " "))
  131. case CommandTypeBuiltinType:
  132. if len(command.tokens) != 2 {
  133. fmt.Fprintln(ctx.stderr, "usage: type COMMAND")
  134. return 2
  135. }
  136. parsed, err := parseCommand(command.tokens[1])
  137. if err != nil {
  138. fmt.Fprintln(ctx.stderr, err)
  139. return 0
  140. }
  141. switch parsed.command_type {
  142. case CommandTypeNone:
  143. panic("impossible parseCommand() result")
  144. case CommandTypeExternal:
  145. fmt.Fprintf(ctx.stdout, "%s is %s\n", parsed.tokens[0], parsed.exec_path)
  146. default:
  147. fmt.Fprintf(ctx.stdout, "%s is a shell builtin\n", parsed.tokens[0])
  148. }
  149. case CommandTypeExternal:
  150. cmd := exec.Command(command.exec_path, command.tokens[1:]...)
  151. cmd.Args = command.tokens
  152. cmd.Stdout = ctx.stdout
  153. cmd.Stderr = ctx.stderr
  154. cmd.Stdin = ctx.stdin
  155. err := cmd.Run()
  156. if err != nil {
  157. if exiterr, ok := err.(*exec.ExitError); ok {
  158. return ExitStatus(exiterr.ExitCode())
  159. // fmt.Printf("handleCommand():exiterr=%#v\n", exiterr.ExitCode())
  160. } else {
  161. fmt.Printf("handleCommand():err=%#v\n", err)
  162. return ExitStatus(65535)
  163. }
  164. }
  165. return ExitStatus(0)
  166. default:
  167. panic("unknown command type")
  168. }
  169. return ExitStatus(65535)
  170. }
  171. func main() {
  172. ctx := makeContext()
  173. for {
  174. command, err := getCommand(&ctx)
  175. if err != nil {
  176. fmt.Fprintln(os.Stderr, "Error reading input: ", err)
  177. os.Exit(1)
  178. }
  179. _ = handleCommand(ctx, command)
  180. }
  181. }