This is a unix shell written in haskell.
- Install Haskell Tool Stack
- Run
stack run
- Run external command (e.g.
ls -la
) - Run builtin command. One of:
jobs
- prints info about jobs started by this shellkill [job number]
- terminates the given job by sending SIGTERM signal to itbg [job number]
- continues currently suspended job in backgroundfg [job number]
- continues job and moves it to foregroundcd [new directory]
- changes current directoryquit
- exits the shell
- Run command in background (e.g.
sleep 1000 &
) - Redirect input or output to file (e.g.
ls -la > test.txt
orcat < test.txt
) - Combine commands with a pipeline (e.g.
ls -la | wc -l
)
Since haskell is purely functional, we can't have one, global state. That's why all stateful code is inside a State monad (or to be specific in a StateT JobsState IO ()
state monad transformer combined with IO monad). This makes it very easy to instantly tell which functions will modify state.
The JobsState.hs
file contains only pure functions, which given current state and some parameters, produce new state. This makes it very easy to test logic of updating state in separation. Then code inside State monad uses these functions to orchestrate update operations.
While most of the shell concurrency uses the OS directly (fork and execve to create a new unix process), there is one place where Haskell's multithreading had to be used.
- When shell receives SIGCHLD signal (meaning that a child process has changed its state), GHC runs the
Jobs.sigchldHandler
function in a new Haskell thread - That handler gathers info about all children that changed state and sends it atomically over an STM (Software Transactional Memory) channel (bascially a FIFO queue).
- Later main thread reads atomically from that channel (within an IO monad), whenever it wants to have the most up to date state. See
StmChannelCommunication.updateStateFromChannelBlocking
andStmChannelCommunication.updateStateFromChannelNonBlocking
.
Nice thing about STM is that it guarantees that each STM computation will be executed atomically without using locks. If there is more than one computation running at the same time, each one executes, but only one of them commits the changes it made. All the other computations are simply restarted, until they are able to commit their changes (that is there was no conflict).