main.go 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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. args []string
  31. exec_path string
  32. name string
  33. }
  34. func makeCommandNone() Command {
  35. return Command{
  36. command_type: CommandTypeNone,
  37. args: nil,
  38. exec_path: "",
  39. name: "",
  40. }
  41. }
  42. func makeCommandBuiltin(command_type CommandType, tokens []string) Command {
  43. switch command_type {
  44. case CommandTypeNone:
  45. panic("invalid usage")
  46. case CommandTypeExternal:
  47. panic("invalid usage")
  48. default:
  49. if len(tokens) < 1 {
  50. panic("assertion failed: token list must not be empty")
  51. }
  52. return Command{
  53. command_type: command_type,
  54. name: tokens[0],
  55. args: tokens[1:],
  56. }
  57. }
  58. }
  59. func makeCommandExternal(tokens []string, exec_path string) Command {
  60. if len(tokens) < 1 {
  61. panic("assertion failed: token list must not be empty")
  62. }
  63. return Command{
  64. command_type: CommandTypeExternal,
  65. name: tokens[0],
  66. args: tokens[1:],
  67. exec_path: exec_path,
  68. }
  69. }
  70. type CommandType int
  71. const (
  72. CommandTypeNone CommandType = iota
  73. CommandTypeExternal
  74. CommandTypeBuiltinExit
  75. CommandTypeBuiltinEcho
  76. CommandTypeBuiltinType
  77. CommandTypeBuiltinPwd
  78. CommandTypeBuiltinCd
  79. )
  80. func getCommand(ctx *Context) (Command, error) {
  81. fmt.Print("$ ")
  82. command_line, err := bufio.NewReader(ctx.stdin).ReadString('\n')
  83. if err != nil {
  84. return makeCommandNone(), err
  85. }
  86. command, err := parseCommand(command_line)
  87. // fmt.Printf("getCommand():command=%#v\n", command)
  88. if err != nil {
  89. fmt.Fprintln(ctx.stderr, err)
  90. return makeCommandNone(), nil
  91. }
  92. return command, nil
  93. }
  94. func tokenize(str string) []string {
  95. tokens := strings.Fields(str)
  96. return tokens
  97. }
  98. type ParseCommandError struct {
  99. code ParseCommandErrorCode
  100. value string
  101. }
  102. type ParseCommandErrorCode int
  103. const (
  104. ParseCommandErrorCodeNotFound ParseCommandErrorCode = iota
  105. )
  106. func (e ParseCommandError) Error() string {
  107. switch e.code {
  108. case ParseCommandErrorCodeNotFound:
  109. return fmt.Sprintf("%s: not found", e.value)
  110. default:
  111. return fmt.Sprintf("unknown ParseCommandErrorCode code: %d .value=%q", e.code, e.value)
  112. }
  113. }
  114. func parseCommand(command_line string) (Command, error) {
  115. tokens := tokenize(command_line)
  116. if len(tokens) == 0 {
  117. return makeCommandNone(), nil
  118. }
  119. if tokens[0] == "exit" {
  120. return makeCommandBuiltin(CommandTypeBuiltinExit, tokens), nil
  121. }
  122. if tokens[0] == "pwd" {
  123. return makeCommandBuiltin(CommandTypeBuiltinPwd, tokens), nil
  124. }
  125. if tokens[0] == "cd" {
  126. return makeCommandBuiltin(CommandTypeBuiltinCd, tokens), nil
  127. }
  128. if tokens[0] == "echo" {
  129. return makeCommandBuiltin(CommandTypeBuiltinEcho, tokens), nil
  130. }
  131. if tokens[0] == "type" {
  132. return makeCommandBuiltin(CommandTypeBuiltinType, tokens), nil
  133. }
  134. exec_path, err := exec.LookPath(tokens[0])
  135. if err != nil {
  136. return makeCommandNone(), ParseCommandError{ParseCommandErrorCodeNotFound, tokens[0]}
  137. }
  138. return makeCommandExternal(tokens, exec_path), nil
  139. }
  140. func bltnChdir(ctx Context, path string) ExitStatus {
  141. err := os.Chdir(path)
  142. if err != nil {
  143. fmt.Fprintf(ctx.stderr, "cd: %s: No such file or directory\n", path)
  144. return 4
  145. }
  146. return 0
  147. }
  148. func handleCommand(ctx Context, command Command) ExitStatus {
  149. // fmt.Printf("handleCommand():command=%#v\n", command)
  150. switch command.command_type {
  151. case CommandTypeNone:
  152. return 0
  153. case CommandTypeBuiltinExit:
  154. os.Exit(0)
  155. case CommandTypeBuiltinEcho:
  156. fmt.Fprintln(ctx.stdout, strings.Join(command.args, " "))
  157. return 0
  158. case CommandTypeBuiltinCd:
  159. switch len(command.args) {
  160. case 0:
  161. home_path := os.Getenv("HOME")
  162. if len(home_path) == 0 {
  163. fmt.Fprintln(ctx.stderr, "error: $HOME environment variable is empty or unset")
  164. return 4
  165. }
  166. return bltnChdir(ctx, home_path)
  167. case 1:
  168. if command.args[0] == "~" {
  169. home_path := os.Getenv("HOME")
  170. if len(home_path) == 0 {
  171. fmt.Fprintln(ctx.stderr, "error: $HOME environment variable is empty or unset")
  172. return 4
  173. }
  174. return bltnChdir(ctx, home_path)
  175. }
  176. return bltnChdir(ctx, command.args[0])
  177. default:
  178. fmt.Fprintln(ctx.stderr, "usage: cd PATH")
  179. return 2
  180. }
  181. case CommandTypeBuiltinPwd:
  182. if len(command.args) != 0 {
  183. fmt.Fprintln(ctx.stderr, "usage: pwd")
  184. return 2
  185. }
  186. cwd_path, err := os.Getwd()
  187. if err != nil {
  188. panic("error getting current working directory")
  189. }
  190. fmt.Fprintln(ctx.stdout, cwd_path)
  191. return 0
  192. case CommandTypeBuiltinType:
  193. if len(command.args) != 1 {
  194. fmt.Fprintln(ctx.stderr, "usage: type COMMAND")
  195. return 2
  196. }
  197. parsed, err := parseCommand(command.args[0])
  198. if err != nil {
  199. fmt.Fprintln(ctx.stderr, err)
  200. return 0
  201. }
  202. switch parsed.command_type {
  203. case CommandTypeNone:
  204. panic("impossible parseCommand() result")
  205. case CommandTypeExternal:
  206. fmt.Fprintf(ctx.stdout, "%s is %s\n", parsed.name, parsed.exec_path)
  207. default:
  208. fmt.Fprintf(ctx.stdout, "%s is a shell builtin\n", parsed.name)
  209. }
  210. return 0
  211. case CommandTypeExternal:
  212. cmd := exec.Command(command.exec_path, command.args...)
  213. cmd.Args[0] = command.name
  214. cmd.Stdout = ctx.stdout
  215. cmd.Stderr = ctx.stderr
  216. cmd.Stdin = ctx.stdin
  217. err := cmd.Run()
  218. if err != nil {
  219. if exiterr, ok := err.(*exec.ExitError); ok {
  220. return ExitStatus(exiterr.ExitCode())
  221. // fmt.Printf("handleCommand():exiterr=%#v\n", exiterr.ExitCode())
  222. } else {
  223. fmt.Printf("handleCommand():err=%#v\n", err)
  224. return ExitStatus(65535)
  225. }
  226. }
  227. return 0
  228. default:
  229. panic("unknown command type")
  230. }
  231. return ExitStatus(65535)
  232. }
  233. func main() {
  234. ctx := makeContext()
  235. for {
  236. command, err := getCommand(&ctx)
  237. if err != nil {
  238. fmt.Fprintln(os.Stderr, "Error reading input: ", err)
  239. os.Exit(1)
  240. }
  241. _ = handleCommand(ctx, command)
  242. }
  243. }