myshell
A Unix shell written in C, with pipes, redirection, wildcards and a batch mode.
A tokenizer splits each line into words and the symbols <, > and |. The parser turns them into a linked list of command structs, one per command in a pipeline, each holding its arguments, an optional input or output file, and a pointer to the next command. Wildcards like *.txt are expanded before anything runs.
cd, pwd, which and exit are built in and run inside the shell itself. Anything else is looked up in /usr/local/bin, /usr/bin and /bin, then run as its own process: the shell forks a child, the child rewires its input and output with dup2, and execv replaces it with the program.
For a pipeline the shell creates one pipe between each pair of commands and forks one child per command, so they all run at the same time with data flowing through the pipes. The shell waits for every child and reports a failed command's exit code. Given a file instead of a terminal, it runs that file as a batch script.
Highlights
- Pipes, input and output redirection, and wildcards.
- Interactive and batch modes, told apart with isatty.
- Test scripts for built-ins, pipes, redirection, wildcards, errors and edge cases.
Input
Terminal or batch script
Tokenizer and parser
Linked list of commands, wildcards expanded
Built-ins
cd, pwd, which, exit
Programs
fork, dup2, execv, wait
- Input to Tokenizer and parser
- Tokenizer and parser to Built-ins
- Tokenizer and parser to Programs
See it work
A real session
Welcome to myshell!mysh> ls *.txtfruit.txt veg.txtmysh> cat fruit.txt | grep e | wc -l2mysh> sort -r < fruit.txt > sorted.txtmysh> cat sorted.txtcherriesbananasapplesmysh> which ls/usr/bin/lsmysh> cd nowherecd: No such file or directorymysh> ls missing.txtls: cannot access 'missing.txt': No such file or directorymysh: Command Failed: code 2mysh> exitExiting shell...
Recorded from myshell itself, in a folder with fruit.txt, veg.txt and notes.md. Prompts, output and error messages are exactly what it printed.
How cat fruit.txt | grep e | wc -l is parsed
- args
- cat, fruit.txt
- execpath
- /usr/bin/cat
- inputfile
- NULL
- outputfile
- NULL
- next_command
- →
- args
- grep, e
- execpath
- /usr/bin/grep
- inputfile
- NULL
- outputfile
- NULL
- next_command
- →
- args
- wc, -l
- execpath
- /usr/bin/wc
- inputfile
- NULL
- outputfile
- NULL
- next_command
- →
- NULL
Each command becomes a struct, and next_command links them in pipeline order. The list ends at NULL.
Then it runs: one fork per command, joined by pipes
mysh
Makes 2 pipes, forks 3 times, waits
cat fruit.txt
stdout into pipe 1
grep e
stdin from pipe 1, stdout into pipe 2
wc -l
stdin from pipe 2
Terminal
Prints 2
- mysh to cat fruit.txt: fork
- mysh to grep e: fork
- mysh to wc -l: fork
- cat fruit.txt to grep e: pipe 1
- grep e to wc -l: pipe 2
- wc -l to Terminal