123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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. CommandTypeBuiltinPwd
  68. )
  69. func getCommand(ctx *Context) (Command, error) {
  70. fmt.Print("$ ")
  71. command_line, err := bufio.NewReader(ctx.stdin).ReadString('\n')
  72. if err != nil {
  73. return makeCommandNone(), err
  74. }
  75. command, err := parseCommand(command_line)
  76. // fmt.Printf("getCommand():command=%#v\n", command)
  77. if err != nil {
  78. fmt.Fprintln(ctx.stderr, err)
  79. return makeCommandNone(), nil
  80. }
  81. return command, nil
  82. }
  83. func tokenize(str string) []string {
  84. tokens := strings.Fields(str)
  85. return tokens
  86. }
  87. type ParseCommandError struct {
  88. code ParseCommandErrorCode
  89. value string
  90. }
  91. type ParseCommandErrorCode int
  92. const (
  93. ParseCommandErrorCodeNotFound ParseCommandErrorCode = iota
  94. )
  95. func (e ParseCommandError) Error() string {
  96. switch e.code {
  97. case ParseCommandErrorCodeNotFound:
  98. return fmt.Sprintf("%s: not found", e.value)
  99. default:
  100. return fmt.Sprintf("unknown ParseCommandErrorCode code: %d .value=%q", e.code, e.value)
  101. }
  102. }
  103. func parseCommand(command_line string) (Command, error) {
  104. tokens := tokenize(command_line)
  105. if len(tokens) == 0 {
  106. return makeCommandNone(), nil
  107. }
  108. if tokens[0] == "exit" {
  109. return makeCommandBuiltin(CommandTypeBuiltinExit, tokens), nil
  110. }
  111. if tokens[0] == "pwd" {
  112. return makeCommandBuiltin(CommandTypeBuiltinPwd, tokens), nil
  113. }
  114. if tokens[0] == "echo" {
  115. return makeCommandBuiltin(CommandTypeBuiltinEcho, tokens), nil
  116. }
  117. if tokens[0] == "type" {
  118. return makeCommandBuiltin(CommandTypeBuiltinType, tokens), nil
  119. }
  120. exec_path, err := exec.LookPath(tokens[0])
  121. if err != nil {
  122. return makeCommandNone(), ParseCommandError{ParseCommandErrorCodeNotFound, tokens[0]}
  123. }
  124. return makeCommandExternal(tokens, exec_path), nil
  125. }
  126. func handleCommand(ctx Context, command Command) ExitStatus {
  127. // fmt.Printf("handleCommand():command=%#v\n", command)
  128. switch command.command_type {
  129. case CommandTypeNone:
  130. return 0
  131. case CommandTypeBuiltinExit:
  132. os.Exit(0)
  133. case CommandTypeBuiltinEcho:
  134. fmt.Fprintln(ctx.stdout, strings.Join(command.tokens[1:], " "))
  135. case CommandTypeBuiltinPwd:
  136. if len(command.tokens) != 1 {
  137. fmt.Fprintln(ctx.stderr, "usage: pwd")
  138. return 2
  139. }
  140. cwd_path, err := os.Getwd()
  141. if err != nil {
  142. panic("error getting current working directory")
  143. }
  144. fmt.Fprintln(ctx.stdout, cwd_path)
  145. case CommandTypeBuiltinType:
  146. if len(command.tokens) != 2 {
  147. fmt.Fprintln(ctx.stderr, "usage: type COMMAND")
  148. return 2
  149. }
  150. parsed, err := parseCommand(command.tokens[1])
  151. if err != nil {
  152. fmt.Fprintln(ctx.stderr, err)
  153. return 0
  154. }
  155. switch parsed.command_type {
  156. case CommandTypeNone:
  157. panic("impossible parseCommand() result")
  158. case CommandTypeExternal:
  159. fmt.Fprintf(ctx.stdout, "%s is %s\n", parsed.tokens[0], parsed.exec_path)
  160. default:
  161. fmt.Fprintf(ctx.stdout, "%s is a shell builtin\n", parsed.tokens[0])
  162. }
  163. case CommandTypeExternal:
  164. cmd := exec.Command(command.exec_path, command.tokens[1:]...)
  165. cmd.Args = command.tokens
  166. cmd.Stdout = ctx.stdout
  167. cmd.Stderr = ctx.stderr
  168. cmd.Stdin = ctx.stdin
  169. err := cmd.Run()
  170. if err != nil {
  171. if exiterr, ok := err.(*exec.ExitError); ok {
  172. return ExitStatus(exiterr.ExitCode())
  173. // fmt.Printf("handleCommand():exiterr=%#v\n", exiterr.ExitCode())
  174. } else {
  175. fmt.Printf("handleCommand():err=%#v\n", err)
  176. return ExitStatus(65535)
  177. }
  178. }
  179. return ExitStatus(0)
  180. default:
  181. panic("unknown command type")
  182. }
  183. return ExitStatus(65535)
  184. }
  185. func main() {
  186. ctx := makeContext()
  187. for {
  188. command, err := getCommand(&ctx)
  189. if err != nil {
  190. fmt.Fprintln(os.Stderr, "Error reading input: ", err)
  191. os.Exit(1)
  192. }
  193. _ = handleCommand(ctx, command)
  194. }
  195. }