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