├── README.md ├── builtins.go ├── exec.go ├── job.go ├── parse.go ├── shell.gif └── shell.go /README.md: -------------------------------------------------------------------------------- 1 | # Mini-Shell 2 | 3 | A toy Unix Shell with job control. 4 | 5 | ## Demo 6 | 7 | ![](./shell.gif) 8 | 9 | Warning: some bugs exist. 10 | 11 | ## Features 12 | 13 | ### Builtin Commands 14 | 15 | `exit`: Quits the shell. 16 | 17 | `jobs`: Lists all running or suspended background jobs. 18 | 19 | `kill`: Sends signals to processes. 20 | 21 | Arguments: [-signal] [PID] 22 | 23 | `kill` must always be accompanied by signal flag and PID of the target process. 24 | 25 | Signals: 26 | 27 | - 9 or KILL [SIGKILL] 28 | - 18 or STOP [SIGTSTP] 29 | - 19 or CONT [SIGCONT] 30 | - 2 or INT [SIGINT] 31 | 32 | Eg: 33 | 34 | ```shell 35 | $ kill -9 12346 36 | or 37 | $ kill -KILL 12346 38 | ``` 39 | 40 | ### Foreground Process 41 | 42 | Foreground process can be terminated by sending a `SIGINT` signal with `Ctrl-c` and suspended by sending `SIGTSTP` with `Ctrl-z`. 43 | 44 | ## Development 45 | 46 | Developed and tested on go version go1.4.2 darwin/amd64. 47 | 48 | ## License 49 | 50 | MIT 51 | -------------------------------------------------------------------------------- /builtins.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "syscall" 9 | ) 10 | 11 | func isBuiltinCmd(cmd string) bool { 12 | switch cmd { 13 | case "kill", "jobs", "fg", "bg", "exit": 14 | return true 15 | } 16 | return false 17 | } 18 | 19 | func exit(cmd []string) error { 20 | os.Exit(1) 21 | return nil 22 | } 23 | 24 | func kill(cmd []string) error { 25 | sig := map[string]os.Signal{ 26 | "-KILL": syscall.SIGKILL, "-9": syscall.SIGKILL, 27 | "-STOP": syscall.SIGTSTP, "-18": syscall.SIGTSTP, 28 | "-CONT": syscall.SIGCONT, "-19": syscall.SIGCONT, 29 | "-INT": syscall.SIGINT, "-2": syscall.SIGINT, 30 | } 31 | 32 | sigCmd := cmd[1] 33 | _, ok := sig[sigCmd] 34 | if !ok { 35 | return fmt.Errorf("kill: unknown argument") 36 | } 37 | 38 | pid, err := strconv.Atoi(cmd[2]) 39 | if err != nil { 40 | return err 41 | } 42 | p, err := os.FindProcess(pid) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // Temp workaround SIGCONT issue. 48 | cmdStr := strings.Join(cmd, " ") 49 | if sigCmd == "-CONT" || sigCmd == "-19" { 50 | jobHandler(pid, contState, cmdStr) 51 | } 52 | 53 | p.Signal(sig[sigCmd]) 54 | return nil 55 | } 56 | 57 | func lsJobs(cmd []string) error { 58 | for k, v := range jobsList { 59 | i := v[0] 60 | pid := k 61 | state := v[1] 62 | cmd := v[2] 63 | fmt.Printf("[%s] %d %s\t%s\n", i, pid, state, cmd) 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /exec.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "os/signal" 8 | "strings" 9 | "syscall" 10 | ) 11 | 12 | func execBuiltinCmd(cmd []string) { 13 | builtin := map[string]func([]string) error{ 14 | "kill": kill, 15 | "jobs": lsJobs, 16 | "exit": exit, 17 | } 18 | err := builtin[cmd[0]](cmd) 19 | if err != nil { 20 | fmt.Println(err) 21 | } 22 | } 23 | 24 | func execFgCmd(cmd []string, sigStateChanged chan string) { 25 | cmdStr := strings.Join(cmd, " ") 26 | 27 | // TODO: Extract start process into common function. 28 | argv0, err := exec.LookPath(cmd[0]) 29 | if err != nil { 30 | if cmd[0] != "" { 31 | fmt.Printf("Unknown command: %s\n", cmd[0]) 32 | } 33 | // Don't execute new process with empty return. Will cause panic. 34 | sigPrompt <- struct{}{} 35 | return 36 | } 37 | var procAttr os.ProcAttr 38 | procAttr.Files = []*os.File{os.Stdin, os.Stdout, os.Stderr} 39 | 40 | p, err := os.StartProcess(argv0, cmd, &procAttr) 41 | if err != nil { 42 | fmt.Printf("Start process %s, %s failed: %v", err, argv0, cmd) 43 | } 44 | 45 | for { 46 | sigChild := make(chan os.Signal) 47 | defer close(sigChild) 48 | // SIGCONT not receivable: https://github.com/golang/go/issues/8953 49 | // This causes some bugs. Eg. CONT signal not captured by handler means subsequent KILL or STOP signals will be ignored by this handler. 50 | signal.Notify(sigChild, syscall.SIGTSTP, syscall.SIGINT, syscall.SIGCONT, syscall.SIGKILL) 51 | defer signal.Stop(sigChild) 52 | 53 | var ws syscall.WaitStatus 54 | // Ignoring error. May return "no child processes" error. Eg. Sending Ctrl-c on `cat` command. 55 | wpid, _ := syscall.Wait4(p.Pid, &ws, syscall.WUNTRACED, nil) 56 | 57 | if ws.Exited() { 58 | break 59 | } 60 | if ws.Stopped() { 61 | jobHandler(wpid, runningState, cmdStr) 62 | jobHandler(wpid, suspendedState, cmdStr) 63 | // Return prompt when fg has become bg 64 | sigPrompt <- struct{}{} 65 | } 66 | //if ws.Continued() { 67 | // state = contState 68 | //} 69 | if ws == 9 { 70 | jobHandler(wpid, killedState, cmdStr) 71 | break 72 | } 73 | } 74 | 75 | p.Wait() 76 | sigPrompt <- struct{}{} 77 | } 78 | 79 | func execBgCmd(cmd []string, sigStateChanged chan string) { 80 | cmdStr := strings.Join(cmd, " ") 81 | 82 | argv0, err := exec.LookPath(cmd[0]) 83 | if err != nil { 84 | if cmd[0] != "" { 85 | fmt.Printf("Unknown command: %s\n", cmd[0]) 86 | } 87 | sigPrompt <- struct{}{} 88 | return 89 | } 90 | var procAttr os.ProcAttr 91 | procAttr.Files = []*os.File{os.Stdin, os.Stdout, os.Stderr} 92 | 93 | p, err := os.StartProcess(argv0, cmd, &procAttr) 94 | if err != nil { 95 | fmt.Printf("Start process %s, %s failed: %v", err, argv0, cmd) 96 | } 97 | jobHandler(p.Pid, runningState, cmdStr) 98 | sigPrompt <- struct{}{} 99 | 100 | //FIXME: Bg processes should not receive keyboard signals sent to fg process. 101 | 102 | for { 103 | sigChild := make(chan os.Signal) 104 | defer close(sigChild) 105 | signal.Notify(sigChild, syscall.SIGCHLD) 106 | defer signal.Stop(sigChild) 107 | 108 | var ws syscall.WaitStatus 109 | wpid, _ := syscall.Wait4(p.Pid, &ws, syscall.WUNTRACED, nil) 110 | 111 | if ws.Exited() { 112 | jobHandler(wpid, doneState, cmdStr) 113 | break 114 | } 115 | if ws.Stopped() { 116 | jobHandler(wpid, suspendedState, cmdStr) 117 | sigPrompt <- struct{}{} 118 | } 119 | //if ws.Continued() { 120 | // state = contState 121 | //} 122 | if ws == 9 { 123 | jobHandler(wpid, killedState, cmdStr) 124 | break 125 | } 126 | } 127 | 128 | p.Wait() 129 | sigPrompt <- struct{}{} 130 | } 131 | -------------------------------------------------------------------------------- /job.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // FIXME: There must be a better way to organise this. 9 | func jobHandler(pid int, state string, cmd string) { 10 | var job string 11 | switch state { 12 | case runningState: 13 | _, ok := jobsList[pid] 14 | if !ok { 15 | i := strconv.Itoa(len(jobsList) + 1) 16 | 17 | job = fmt.Sprintf("[%s] %d %s\n", i, pid, state) 18 | jobInfo := []string{i, state, cmd} 19 | jobsList[pid] = jobInfo 20 | } 21 | 22 | case suspendedState: 23 | jobInfo := jobsList[pid] 24 | 25 | i := jobInfo[0] 26 | cmdLine := jobInfo[2] 27 | job = fmt.Sprintf("[%s] %d %s\t%s\n", i, pid, state, cmdLine) 28 | jobInfo = []string{i, state, cmd} 29 | jobsList[pid] = jobInfo 30 | 31 | case contState: 32 | jobInfo := jobsList[pid] 33 | 34 | i := jobInfo[0] 35 | cmdLine := jobInfo[2] 36 | job = fmt.Sprintf("[%s] %d %s\t%s\n", i, pid, state, cmdLine) 37 | jobInfo = []string{i, runningState, cmdLine} 38 | jobsList[pid] = jobInfo 39 | 40 | case killedState: 41 | jobInfo := jobsList[pid] 42 | i := jobInfo[0] 43 | cmdLine := jobInfo[2] 44 | job = fmt.Sprintf("[%s] %d %s\t%s\n", i, pid, state, cmdLine) 45 | delete(jobsList, pid) 46 | case terminatedState: 47 | jobInfo := jobsList[pid] 48 | i := jobInfo[0] 49 | cmdLine := jobInfo[2] 50 | job = fmt.Sprintf("[%s] %d %s\t%s\n", i, pid, state, cmdLine) 51 | delete(jobsList, pid) 52 | case doneState: 53 | jobInfo := jobsList[pid] 54 | i := jobInfo[0] 55 | cmdLine := jobInfo[2] 56 | job = fmt.Sprintf("[%s] %d %s\t%s\n", i, pid, state, cmdLine) 57 | delete(jobsList, pid) 58 | default: 59 | } 60 | fmt.Printf(job) 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func readCmdLine() (string, error) { 10 | reader := bufio.NewReader(os.Stdin) 11 | cmdLine, err := reader.ReadString('\n') 12 | if err != nil { 13 | return "", err 14 | } 15 | cmdLine = strings.TrimSuffix(cmdLine, "\n") 16 | return cmdLine, nil 17 | } 18 | 19 | func parseCmdLine(cmdLine string) ([]string, bool) { 20 | cmdLine, bg := removeBgSymb(cmdLine) 21 | cmd := strings.Split(cmdLine, " ") 22 | return cmd, bg 23 | } 24 | 25 | func removeBgSymb(cmdLine string) (string, bool) { 26 | if strings.HasSuffix(cmdLine, "&") { 27 | cmdLine = strings.TrimSuffix(cmdLine, "&") 28 | // Normalize "cmd &" and "cmd&". 29 | cmdLine = strings.TrimSuffix(cmdLine, " ") 30 | return cmdLine, true 31 | } 32 | return cmdLine, false 33 | } 34 | -------------------------------------------------------------------------------- /shell.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audreylim/mini-shell/f6959c3a9bab757b7d6a8750c2ab0dcce233c6eb/shell.gif -------------------------------------------------------------------------------- /shell.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var ( 8 | sigStateChanged = make(chan string) 9 | sigPrompt = make(chan struct{}) 10 | 11 | // Not thread safe. 12 | jobsList = make(map[int][]string) 13 | ) 14 | 15 | const ( 16 | runningState = "running" 17 | suspendedState = "suspended" 18 | contState = "continue" 19 | killedState = "killed" 20 | terminatedState = "terminated" 21 | doneState = "done" 22 | ) 23 | 24 | const prettyPrompt = "\033[35m\u2764\033[m \033[36m\u2764\033[m \033[37m\u2764\033 " 25 | 26 | func main() { 27 | prompt: 28 | fmt.Printf(prettyPrompt) 29 | 30 | cmdLine, err := readCmdLine() 31 | if err != nil { 32 | fmt.Println(err) 33 | } 34 | cmd, bg := parseCmdLine(cmdLine) 35 | 36 | if isBuiltinCmd(cmd[0]) { 37 | execBuiltinCmd(cmd) 38 | } else { 39 | if bg { 40 | go execBgCmd(cmd, sigStateChanged) 41 | <-sigPrompt 42 | } else { 43 | go execFgCmd(cmd, sigStateChanged) 44 | <-sigPrompt 45 | } 46 | } 47 | 48 | goto prompt 49 | } 50 | --------------------------------------------------------------------------------