Illustration by srngch
Table of Contents
We made a simple shell like a little bash to learn a lot about processes and file descriptors.
Program name | msh |
Makefile | all , clean , fclean , re |
Arguments | None |
External functs. | readline , rl_on_new_line , rl_replace_line , rl_redisplay , add_history , printf , malloc , free , write , open , read , close , fork , wait , waitpid , wait3 , wait4 , signal , kill , exit , getcwd , chdir , stat , lstat , fstat , unlink , execve , dup , dup2 , pipe , opendir , readdir , closedir , strerror , errno , isatty , ttyname , ttyslot , ioctl , getenv , tcsetattr , tcgetattr , tgetent , tgetflag , tgetnum , tgetstr , tgoto , tputs |
- prompt
- history
- builtins
echo
 with optionÂ-n
cd
 with only a relative or absolute pathpwd
 without optionsexport
 without optionsunset
 without optionsenv
 without options or argumentsexit
 without options
- interpretation
"
only for$
- redirections
<
redirect input>
redirect output<<
heredoc>>
redirect output with append mode
- pipes:
|
- environment variables:
$
- exit status:
$?
- signal interactive like bash
ctrl-c
print a new prompt on a newline.ctrl-d
exit the shell.ctrl-\
do nothing.
./
├── includes/ # header files
├── libft/ # library files
├── src/ # source files
│ ├── builtin/ # builtin commands
│ └── util/ # utility functions
├── test/ # test command files
└── Makefile
- MacOS 12.3.1(Monterey, Intel)
Developed and tested in this environment.
Install the following dependencies:
$ brew install readline
$ brew info readline
# export LDFLAGS="-L/usr/local/opt/readline/lib"
# export CPPFLAGS="-I/usr/local/opt/readline/include"
Check flag LDFLAGS
and CPPFLAGS
in Makefile
is same as on brew info readline
.
$ git clone https://github.com/42pakchoi/msh
$ cd src
$ make
Run compiled executable file in the src
folder.
$ ./msh
~/path_to_pwd/msh $
# msh builtin commands
~/path_to_pwd/msh $ echo "Hello world!"
Hello world!
# commands in PATH
~/path_to_pwd/msh $ ls
Makefile includes msh test
README.md libft src
# command `exit` or press `ctrl-d` to exit the msh
~/path_to_pwd/msh $ exit
Test using files with multiple lines of command in test
directory. Each line of the file is in the following format: command >> result.txt
$ bash -i < test.txt # run interactive mode bash with test file
$ mv result.txt result_bash.txt # change file name to keep result of bash
$ ./msh < test.txt # run msh with test file
$ diff result.txt result_bash.txt # compare result of bash and msh
$ cat result.txt # show result if you want
$ leaks -atExit -- ./msh
Runs leaks when the msh exits.
$ cp /bin/cat /tmp/ls # copy cat to /tmp/ls
$ ./msh
~/path_to_pwd/msh $ unset PATH
~/path_to_pwd/msh $ export PATH=/tmp:/bin
~/path_to_pwd/msh $ ls # should be /tmp/ls (it is cat actually) and not /bin/ls
graph LR
e1([Start]) --> e2[initial]
e2 --> e3[[Command Loop]]
e3 --> e4[free]
e4 --> e5([Exit])
graph TD
start([Command Loop]) --> s1[update prompt string]
s1 --> s2{"<code>deal_prompt()</code><br/>print prompt string<br/>receive input(readline)"}
s2 -- string --> s3["<code>save_history()</code>"]
s3 --> s4["<code>deal_command()</code><br/>parse input string<br/>run command<br/>if redir, set a pipe"]
s4 --> s5["<code>remove_cmd_list()</code><br/>free t_cmd list"]
s5 --> s6["<code>restore_ori_stdin()</code><br/>if redir, restore pipe"]
s6 --> s7[free input string<br/>that was allocated<br/>from readline]
s7 --> s2
s2 -- "eof<br/>(ctrl-d)" ------> return
return([return])
graph TD
start(["deal_command() start"]) --> s1[parse_prompt_input]
s1 --> s2["check syntax error"]
s2 --> s3{"has heredoc?"}
s3 -- true --> s4["input from heredoc"]
s4 --> s5
s3 -- false --> s5{"number of commands?"}
s5 -- single --> s6["fork one child process"]
s6 --> s7["run command in child"]
s5 -- multi --> s8["fork processes and set pipes"]
s8 --> s9["run commands"]
s7--> return
s9 --> return
return([return])
- String
char *g_mini.prompt_input
is allocated fromreadline
"echo hello $USER | cat -e > out.txt" // g_mini.prompt_input
g_mini.prompt_input
is split by a operator(>
,>>
,<
,<<
,|
), into string arraychar **arr
"echo hello $USER" // arr[0] "cat -e" // arr[1] "out.txt" // arr[2]
- Elements of
arr
is split by white space into a string arraychar **strarr
"echo" // strarr[0] "hello" // strarr[1] "$USER" // strarr[2]
- Translate environment variables name to value if
$
is found in the element ofstrarr
"echo" // strarr[0] "hello" // strarr[1] "pakchoi" // strarr[2]
- Keep
strarr
and a operator data int_cmd
structure and add it to the listt_cmd *g_mini.cmd
exec_assign()
: If string is input in the formname=[value]
, assign it as a environment variableexec_builtin()
: If the command is builtin command of msh, then run itexec_execve()
: If the command is not builtin command, then run it withexecve()
Create a child process and pipe to execute the first command. The input of the pipe is connected to the child process, and the output is connected to the main process for the subsequent child process.
Loop 1 details
Create a pipe from the main process before creating the child process.
The child process to execute the command is forked. The forked child process has the same fd because it duplicated the main process.
In the child process, replicate the pipe input fd to the STDOUT fd and connect it. And close the fd that you will not use.
Create a child process and pipe as before. The difference, however, is that it connects to previously generated pipes to the process generated.
Loop 2 details
Create pipes and child processes as before. The difference is that I have one more fd. This fd is the pipe fd of previously generated child processes.
Replicate the pipe fd to the STDIN and STDOUT of the child process. To STDIN, connect the out fd of the previously generated pipe, and to STDOUT, connect the in fd of the pipe generated this time. And close the fd that you will not use.
In the last loop, you don't connect the child process to the pipe. The command of the last child process is printed on the screen.