Skip to content

team-pakchoi/msh

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Team Pakchoi 🥬

42 result

code-size last-commit

pakchoi cover image Illustration by srngch

Table of Contents
  1. Requirements
  2. Implementation
  3. Project Structure
  4. Environment
  5. Compile
  6. Execute
  7. Example
  8. Test
  9. Logics
  10. Links

Msh

We made a simple shell like a little bash to learn a lot about processes and file descriptors.

Requirements

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

Implementation

  • prompt
  • history
  • builtins
    • echo with option -n
    • cd with only a relative or absolute path
    • pwd without options
    • export without options
    • unset without options
    • env without options or arguments
    • exit 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.

Project Structure

./
├── includes/		# header files
├── libft/		# library files
├── src/		# source files
│   ├── builtin/	# builtin commands
│   └── util/		# utility functions
├── test/		# test command files
└── Makefile

Environment

  • MacOS 12.3.1(Monterey, Intel)

Developed and tested in this environment.

Compile

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.

Clone

$ git clone https://github.com/42pakchoi/msh
$ cd src
$ make

Execute

Run compiled executable file in the src folder.

$ ./msh
~/path_to_pwd/msh $ 

Example

# 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

Test command

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

Check memory leak

$ leaks -atExit -- ./msh

Runs leaks when the msh exits.

Check priority of $PATH

$ 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

Logics

Flow Chart

Entire

graph LR
    e1([Start]) --> e2[initial]
    e2 --> e3[[Command Loop]]
    e3  --> e4[free]
    e4 --> e5([Exit])
Loading

Command Loop

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])
Loading

deal_command()

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])
Loading

Steps to parse input

  1. String char *g_mini.prompt_input is allocated from readline
    "echo hello $USER | cat -e > out.txt" // g_mini.prompt_input
  2. g_mini.prompt_input is split by a operator(>, >>, <, <<, |), into string array char **arr
    "echo hello $USER" // arr[0]
    "cat -e" // arr[1]
    "out.txt" // arr[2]
  3. Elements of arr is split by white space into a string array char **strarr
    "echo" // strarr[0] 
    "hello" // strarr[1] 
    "$USER" // strarr[2] 
  4. Translate environment variables name to value if $ is found in the element of strarr
    "echo" // strarr[0] 
    "hello" // strarr[1] 
    "pakchoi" // strarr[2] 
  5. Keep strarr and a operator data in t_cmd structure and add it to the list t_cmd *g_mini.cmd

Priority of command execution

  1. exec_assign(): If string is input in the form name=[value], assign it as a environment variable
  2. exec_builtin(): If the command is builtin command of msh, then run it
  3. exec_execve(): If the command is not builtin command, then run it with execve()

File Descriptor and pipe

Setting child process to execute commands

Setting child process to execute commands 1

Loop 1

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

Loop 1-1 : pipe

Create a pipe from the main process before creating the child process. Setting child process to execute commands 1-1

Loop 1-2 : fork

The child process to execute the command is forked. The forked child process has the same fd because it duplicated the main process. Setting child process to execute commands 1-2

Loop 1-3 : close & dup

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. Setting child process to execute commands 1-3 Setting child process to execute commands 1-3

Setting child process to execute commands 2

Loop 2

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

Loop 2-1 : pipe & fork

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. Setting child process to execute commands 2-1

Loop 2-2 : close & dup

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. Setting child process to execute commands 2-2 Setting child process to execute commands 2-2

Setting child process to execute commands 3

Loop 3

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.

Setting child process to execute commands 4

Links