Bläddra i källkod

Refactor into modules and make builtins polymorphic

Alois Mahdal 4 dagar sedan
förälder
incheckning
fd9863008c
5 ändrade filer med 345 tillägg och 246 borttagningar
  1. 203
    0
      app/builtin/builtin.go
  2. 20
    0
      app/core/core.go
  3. 21
    246
      app/main.go
  4. 93
    0
      app/runnable/runnable.go
  5. 8
    0
      app/tokenize/tokenize.go

+ 203
- 0
app/builtin/builtin.go Visa fil

@@ -0,0 +1,203 @@
1
+package builtin
2
+
3
+import "fmt"
4
+import "os"
5
+import "os/exec"
6
+import "strings"
7
+import "github.com/codecrafters-io/shell-starter-go/app/core"
8
+
9
+type Builtin interface {
10
+	Run(ctx *core.Context) core.ExitStatus
11
+	ParseArgs(ctx *core.Context, args []string) error
12
+}
13
+
14
+func Parse(ctx *core.Context, name string, args []string) (Builtin, error) {
15
+	selected, err := selectBuiltin(name)
16
+	if err != nil {
17
+		return nil, err
18
+	}
19
+	if err := selected.ParseArgs(ctx, args); err != nil {
20
+		return nil, err
21
+	}
22
+	return selected, nil
23
+}
24
+
25
+func selectBuiltin(name string) (Builtin, error) {
26
+	switch name {
27
+	case "cd":
28
+		return &CdBuiltin{}, nil
29
+	case "echo":
30
+		return &EchoBuiltin{}, nil
31
+	case "exit":
32
+		return &ExitBuiltin{}, nil
33
+	case "pwd":
34
+		return &PwdBuiltin{}, nil
35
+	case "type":
36
+		return &TypeBuiltin{}, nil
37
+	default:
38
+		return nil, ParseError{Code: ParseErrorCodeUnknownBuiltin, value: name}
39
+	}
40
+}
41
+
42
+type ParseError struct {
43
+	Code  ParseErrorCode
44
+	value string
45
+}
46
+type ParseErrorCode int
47
+
48
+func (self ParseError) Error() string {
49
+	switch self.Code {
50
+	case ParseErrorCodeUsage:
51
+		return fmt.Sprintf("usage: %s", self.value)
52
+	case ParseErrorCodeUnknownBuiltin:
53
+		// this should not be seen in normal REPL (we fallback to external command)
54
+		return fmt.Sprintf("unknown built-in: %s", self.value)
55
+	default:
56
+		panic(self.Code)
57
+	}
58
+}
59
+
60
+const (
61
+	ParseErrorCodeUsage ParseErrorCode = iota
62
+	ParseErrorCodeUnknownBuiltin
63
+)
64
+
65
+func makeUsage(pattern string) ParseError {
66
+	return ParseError{
67
+		Code:  ParseErrorCodeUsage,
68
+		value: pattern,
69
+	}
70
+}
71
+
72
+//
73
+// builtin: "cd"
74
+//
75
+
76
+type CdBuiltin struct {
77
+	path string
78
+}
79
+
80
+func (self *CdBuiltin) ParseArgs(ctx *core.Context, args []string) error {
81
+	usage := makeUsage("cd [PATH]")
82
+	self.path = ""
83
+	switch len(args) {
84
+	case 0:
85
+		return usage
86
+	case 1:
87
+		self.path = args[0]
88
+		return nil
89
+	default:
90
+		return usage
91
+	}
92
+}
93
+
94
+func (self *CdBuiltin) Run(ctx *core.Context) core.ExitStatus {
95
+	chdir := func(path string) core.ExitStatus {
96
+		err := os.Chdir(path)
97
+		if err != nil {
98
+			fmt.Fprintf(ctx.Stderr, "cd: %s: No such file or directory\n", path)
99
+			return 4
100
+		}
101
+		return 0
102
+	}
103
+	if self.path == "~" || self.path == "" {
104
+		home_path := os.Getenv("HOME")
105
+		if len(home_path) == 0 {
106
+			fmt.Fprintln(ctx.Stderr, "error: $HOME environment variable is empty or unset")
107
+			return 4
108
+		}
109
+		return chdir(home_path)
110
+	}
111
+	return chdir(self.path)
112
+}
113
+
114
+//
115
+// builtin: "echo"
116
+//
117
+
118
+type EchoBuiltin struct {
119
+	args []string
120
+}
121
+
122
+func (self *EchoBuiltin) ParseArgs(ctx *core.Context, args []string) error {
123
+	self.args = args
124
+	return nil
125
+}
126
+
127
+func (self *EchoBuiltin) Run(ctx *core.Context) core.ExitStatus {
128
+	fmt.Fprintln(ctx.Stdout, strings.Join(self.args, " "))
129
+	return 0
130
+}
131
+
132
+//
133
+// builtin: "exit"
134
+//
135
+
136
+type ExitBuiltin struct {
137
+}
138
+
139
+func (self *ExitBuiltin) ParseArgs(ctx *core.Context, args []string) error {
140
+	if len(args) != 0 {
141
+		return makeUsage("exit")
142
+	}
143
+	return nil
144
+}
145
+
146
+func (self *ExitBuiltin) Run(ctx *core.Context) core.ExitStatus {
147
+	os.Exit(0)
148
+	return 0
149
+}
150
+
151
+//
152
+// builtin: "pwd"
153
+//
154
+
155
+type PwdBuiltin struct {
156
+}
157
+
158
+func (self *PwdBuiltin) ParseArgs(ctx *core.Context, args []string) error {
159
+	if len(args) != 0 {
160
+		return makeUsage("pwd")
161
+	}
162
+	return nil
163
+}
164
+
165
+func (self *PwdBuiltin) Run(ctx *core.Context) core.ExitStatus {
166
+	cwd_path, err := os.Getwd()
167
+	if err != nil {
168
+		panic("error getting current working directory")
169
+	}
170
+	fmt.Fprintln(ctx.Stdout, cwd_path)
171
+	return 0
172
+}
173
+
174
+//
175
+// builtin: "type"
176
+//
177
+
178
+type TypeBuiltin struct {
179
+	query string
180
+}
181
+
182
+func (self *TypeBuiltin) ParseArgs(ctx *core.Context, args []string) error {
183
+	if len(args) != 1 {
184
+		return makeUsage("type COMMAND")
185
+	}
186
+	self.query = args[0]
187
+	return nil
188
+}
189
+
190
+func (self *TypeBuiltin) Run(ctx *core.Context) core.ExitStatus {
191
+	_, err := selectBuiltin(self.query)
192
+	if err == nil {
193
+		fmt.Fprintf(ctx.Stdout, "%s is a shell builtin\n", self.query)
194
+		return 0
195
+	}
196
+	exec_path, err := exec.LookPath(self.query)
197
+	if err == nil {
198
+		fmt.Fprintf(ctx.Stdout, "%s is %s\n", self.query, exec_path)
199
+		return 0
200
+	}
201
+	fmt.Fprintf(ctx.Stderr, "%s: not found\n", self.query)
202
+	return 0
203
+}

+ 20
- 0
app/core/core.go Visa fil

@@ -0,0 +1,20 @@
1
+package core
2
+
3
+import "io"
4
+import "os"
5
+
6
+type ExitStatus uint16
7
+
8
+type Context struct {
9
+	Stdout io.Writer
10
+	Stderr io.Writer
11
+	Stdin  io.Reader
12
+}
13
+
14
+func MakeContext() Context {
15
+	return Context{
16
+		Stdout: os.Stdout,
17
+		Stderr: os.Stderr,
18
+		Stdin:  os.Stdin,
19
+	}
20
+}

+ 21
- 246
app/main.go Visa fil

@@ -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
 }

+ 93
- 0
app/runnable/runnable.go Visa fil

@@ -0,0 +1,93 @@
1
+package runnable
2
+
3
+import "os/exec"
4
+import "fmt"
5
+
6
+import "github.com/codecrafters-io/shell-starter-go/app/core"
7
+import "github.com/codecrafters-io/shell-starter-go/app/tokenize"
8
+import "github.com/codecrafters-io/shell-starter-go/app/builtin"
9
+
10
+type Runnable interface {
11
+	Run(ctx *core.Context) core.ExitStatus
12
+}
13
+
14
+type ExternalCommand struct {
15
+	Args     []string
16
+	ExecPath string
17
+	Name     string
18
+}
19
+
20
+func (self ExternalCommand) Run(ctx *core.Context) core.ExitStatus {
21
+	cmd := exec.Command(self.ExecPath, self.Args...)
22
+	cmd.Args[0] = self.Name
23
+	cmd.Stdout = ctx.Stdout
24
+	cmd.Stderr = ctx.Stderr
25
+	cmd.Stdin = ctx.Stdin
26
+	err := cmd.Run()
27
+	if err != nil {
28
+		if exiterr, ok := err.(*exec.ExitError); ok {
29
+			return core.ExitStatus(exiterr.ExitCode())
30
+			// fmt.Printf("Handle():exiterr=%#v\n", exiterr.ExitCode())
31
+		} else {
32
+			fmt.Printf("Handle():err=%#v\n", err)
33
+			return core.ExitStatus(65535)
34
+		}
35
+	}
36
+	return 0
37
+}
38
+
39
+type ParseError struct {
40
+	code  ParseErrorCode
41
+	value string
42
+}
43
+type ParseErrorCode int
44
+
45
+const (
46
+	ParseErrorCodeNothing ParseErrorCode = iota
47
+	ParseErrorCodeNotFound
48
+)
49
+
50
+func (e ParseError) Error() string {
51
+	switch e.code {
52
+	case ParseErrorCodeNotFound:
53
+		return fmt.Sprintf("%s: not found", e.value)
54
+	case ParseErrorCodeNothing:
55
+		return ""
56
+	default:
57
+		return fmt.Sprintf("unknown ParseErrorCode code: %d .value=%q", e.code, e.value)
58
+	}
59
+
60
+}
61
+
62
+func Parse(ctx *core.Context, command_line string) (Runnable, error) {
63
+	tokens := tokenize.Tokenize(command_line)
64
+	if len(tokens) == 0 { // ie. empty or whitespace-only input
65
+		return nil, ParseError{
66
+			code: ParseErrorCodeNothing,
67
+		}
68
+	}
69
+	bltn, err := builtin.Parse(ctx, tokens[0], tokens[1:])
70
+	if err == nil { // it was a builtin
71
+		return bltn, nil
72
+	}
73
+	if e, ok := err.(builtin.ParseError); ok {
74
+		switch e.Code {
75
+		case builtin.ParseErrorCodeUsage:
76
+			return nil, err
77
+		case builtin.ParseErrorCodeUnknownBuiltin:
78
+			exec_path, err := exec.LookPath(tokens[0])
79
+			if err != nil { // command not found
80
+				return nil, ParseError{
81
+					code:  ParseErrorCodeNotFound,
82
+					value: tokens[0],
83
+				}
84
+			}
85
+			return ExternalCommand{
86
+				ExecPath: exec_path,
87
+				Name:     tokens[0],
88
+				Args:     tokens[1:],
89
+			}, nil
90
+		}
91
+	}
92
+	panic("unexpected builtin.Parse(); only builtin.ParseError is expected")
93
+}

+ 8
- 0
app/tokenize/tokenize.go Visa fil

@@ -0,0 +1,8 @@
1
+package tokenize
2
+
3
+import "strings"
4
+
5
+func Tokenize(str string) []string {
6
+	tokens := strings.Fields(str)
7
+	return tokens
8
+}