This repository contains a minimalist POSIX-style shell written in Go as part of the Codecrafters challenge. The interactive loop in app/main.go repeatedly prompts for input, lexes it via lexingCommand(), classifies the words with tokenize(), parses the resulting tokens into an abstract syntax tree (AST) with parse(), and executes that AST by dispatching through the node implementations in app/ast.go.
go run ./appYou can also let Codecrafters invoke the binary by editing your_program.sh, which simply proxies to go run during local development.
- Read–Eval Loop:
main()prints a$prompt, reads a line, and short-circuits on empty input. - Lexing:
lexingCommand()tokenizes raw bytes into shell "words" while respecting quotes and escapes. - Token Classification:
tokenize()maps each word to aTokentyped by theTokenTypeenum. - Parsing:
parsePipe(),parseRedirect(), andparseCommand()collaboratively build an AST ofNodeimplementations. - Execution: Each
NodeimplementsExecute()to realize commands, pipes, redirects, and built-ins.
- Uses a linear scan with
inQuotes/quoteCharstate plus astrings.Builderaccumulator. - Recognizes
"and'quotes, supporting escaped quotes when inside double quotes. - Treats whitespace as delimiters only outside of quotes; inside quotes it is preserved.
- Handles backslash escaping outside quotes by copying the next rune literally.
- Outputs a
[]stringrepresenting logical shell tokens ready for classification.
- Iterates once over the word slice and emits a parallel
[]Token. - Each
Tokencouples aTokenTypediscriminator with the original lexeme, enabling the parser to reason over redirections (>,>>,2>), pipes (|), and generic words.
- Pipe splitting:
parsePipe()searches the token list from right to left for the lowest-precedence pipe and recursively builds a binary tree ofPipeNode. - Redirect accumulation:
parseRedirect()walks left-to-right, hoisting redirect operators (and their filenames) into a slice of structs before delegating the remaining words toparseCommand(). - Command validation:
parseCommandenforces that commands begin with aWORDtoken, collects remaining arguments, and seeds streams (Stdin,Stdout,Stderr) with the process defaults. - Type erasure through interfaces: The parser returns the
Nodeinterface, allowing later execution to treat commands, pipes, and redirects polymorphically.
CommandNode: Stores the command name, argument slice, and IO streams.PipeNode: A binary tree node whoseLeftandRightchildren are arbitraryNodeimplementations.RedirectNode: Decorates anotherNodewith a redirect type and filename; multiple redirects wrap each other like a stack.
CommandNode.Execute()first checks for built-ins (constant-time lookup viaslices.ContainsonbuiltInCommands). If the command is external, it resolves the binary path usingfindExecutable()and executes viaexec.Commandwith inherited/redirected streams.PipeNode.Execute()creates anio.Pipe, rewires the left node's stdout and the right node's stdin viasetNodeOutput()andsetNodeInput(), and runs both branches concurrently in goroutines synchronized bysync.WaitGroup.RedirectNode.Execute()opens the appropriate file descriptor (create, append, or read), mutates stream references on the wrapped node usingsetNodeInput,setNodeOutput, orsetNodeError(), and then delegates execution.
executeBuiltIn()dispatches to handlers likehandleCd(),handlePwd(),handleType(), andhandleEcho().exitsimply callsos.Exit(0).handleTypereusesbuiltInCommandsplusfindExecutableto report whether a symbol is a builtin or an external binary.handleCdsupports~expansion by readingHOMEbefore callingos.Chdir.
findExecutable()splitsPATH, scans each directory (viaos.ReadDir), and appliessort.Searchto locate the command name before validating executability withisExecutable().- Earlier resolution results are not cached, so each command lookup is O(P·log F) where
Pis the number ofPATHentries andFis the number of files per directory (from the binary search).
setNodeInput(),setNodeOutput(), andsetNodeErrorrecursively descend through nested redirect/pipe nodes to rebind streams at the concreteCommandNodeleaves, ensuring that higher-level constructs stay immutable while redirects adjust execution context.
- Adding syntax: Introduce new token kinds in
TokenType, teachtokenize()to emit them, and update the parser stages to recognize the new grammar. - Custom built-ins: Append to
builtInCommands, add a handler inexecuteBuiltIn(), and implement the logic alongside the existinghandle*functions. - Process control features: The current executor handles foreground jobs; background execution or job control would require augmenting
CommandNode.Executeand the REPL loop to manage process groups.
The project showcases:
- A deterministic finite automaton-based lexer (
lexingCommand) for shell quoting semantics. - A handcrafted recursive-descent parser that builds an AST of composable stream-processing nodes.
- Concurrency and streaming via
io.Pipeplussync.WaitGroupto implement pipelines without buffering entire command output. - Stream-redirection wrappers that mutate IO targets lazily by walking the AST just before execution.
- Built-in command dispatch tightly integrated with filesystem-based binary discovery.
Together these components provide a concise yet faithful model of how traditional Unix shells translate user input into running processes.