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