1. 程式人生 > >寫一個簡單的 Linux Shell (C++)

寫一個簡單的 Linux Shell (C++)

## 這裡可以找到程式碼 [github.com/z0gSh1u/expshell](https://github.com/z0gSh1u/expshell) ## 支援的特性 - 單條指令的執行 - 引號引起的引數(如 `$ some_program "hello, world"` ) - 重定向(\>、\< ) - 管道(|) - 內建指令(如 cd、history、quit) - 指令別名(如 ll → ls -l) - 家目錄(~) ## 執行截圖 ![run_pic](https://i.loli.net/2020/09/17/ACyfLkH49c2dlBQ.png) ## 如何寫一個簡單的 Shell 這裡簡單介紹寫 Shell 時比較關鍵的一些部分,具體請檢視原始碼。 ### 展示提示符 見 `show_command_prompt` 函式。 command_prompt 是在每行最開始顯示的一段與使用者名稱、路徑等相關的提示資訊。ExpShell 顯示的 prompt 形如 `[root@localhost tmp]>`。用 > 而非 #、$ 作為提示符,以區分原生 Shell。 - 獲取使用者名稱 ```cpp passwd *pwd = getpwuid(getuid()); string username(pwd->pw_name); ``` - 獲取當前目錄 ```cpp getcwd(char_buf, CHAR_BUF_SIZE); string cwd(char_buf); ``` - prompt 中目錄只顯示最近一級,此處用 `/` 來 split 後取最後一個即可 - 家目錄需要摺疊為 `~`,這裡順便把家目錄地址存到全域性變數 `home_dir`,後續要用到 - 獲取主機名 ```cpp gethostname(char_buf, CHAR_BUF_SIZE); string hostname(char_buf); ``` - 有時 hostname 會是形如 `localhost.locald.xxx` 的形式,也 split 處理一下 - 輸出之即可 ```cpp cout << "[" << username << "@" << hostname << " " << cwd << "]> "; ``` ### 解析命令 - 為儲存解析結果,定義如下四個類: - cmd:各種 cmd 的基類 - exec_cmd:形如 `argv[0] argv[1] ...` 的普通命令 - pipe_cmd:管道命令,形如 `left: cmd* | right: cmd*` - redirect_cmd:重定向命令,形如 `cmd_: cmd* > (or <) file` - (最基礎的)解析 exec_cmd 見 `parse_exec_cmd` 函式。注意這裡使用 `string_split_protect` 函式來 split 出 argv,這樣可以保持被引號引起的帶空格的 argument 不被拆分。 - 解析一條命令 見 `parse` 函式。採用分治法遞迴地解析命令。 - 從左到右掃描字串 - 如果是普通字元,則讀入快取 - 如果是重定向符號,將當前快取解析為 exec_cmd,作為左手邊 cmd;繼續不斷讀入直到再次遇到符號或字串結束,作為右手邊 file,構建 redirect_cmd - 如果是管道符號,遞迴地呼叫 `parse` 解析右側剩餘,解析結果作為本層遞迴的右手邊,構建 pipe_cmd - 解析內建命令 主要支援 cd 、history 和 quit 命令。 - 呼叫 `exit(0)` 即可實現 quit - history 命令根據記錄列印即可 - 對於 cd,考慮如下情況 - 無參 cd 等價於 `cd ~` - 對於形如 `cd ~/some_path` 的命令,使用 `home_dir` 替換 `~` - 其他情況呼叫 `chdir` 即可 ### 執行命令 主要見 `run_cmd` 函式。該函式接收一個 `cmd*`,遞迴地完成其鏈上所有 cmd 的執行。 - 對於 exec_cmd - 檢查別名,替換別名,例如 ll → ls -l - 使用 `execvp` 函式執行命令 - 看 [這篇博文](https://blog.csdn.net/yychuyu/article/details/80173039) 瞭解 exec 族函式,可見 `execvp` 在當前場景最為合適 - 第二個引數是一個末元素為 NULL 的 char**(char\*[]),內容為 argv - 對於 pipe_cmd - 為 pipe_cmd 的 left 和 right 分別 fork 子程序執行,並使用管道讓這兩個兄弟程序通訊 - 這張圖很好地說明了父子程序使用管道通訊的方法 ![pipe_and_close](https://i.loli.net/2020/09/16/BdNUL7pRGfF2rgD.png) - 依據上圖,不難類比出兄弟程序進行 IPC 的方法如下 - 父程序 pipe - 父程序 fork 兩次 - child1 關讀端,重定向寫端,執行命令,關寫端 - child2 關寫端,重定向讀端,執行命令,關讀端 - 父程序關閉讀、寫端,並 wait - 對於 redirect_cmd - 開啟 file,獲得 fd - 重定向 stdin 或 stdout 到 fd - 執行命令 - 關閉 fd ### 主函式 在一個死迴圈中讀入當前命令,如果不是 builtin_command,則 fork 子程序進行解析和執行(避免阻塞 ExpShell 自身),執行完成後子程序 exit。 ### 其他細節 - pipe、open、dup2 等方法返回值小於 0 均表示出現錯誤,需要觸發 panic - 對於 wait 方法的狀態字,當 `WIFEXITED(status)` 為 0 時表示子程序異常退出,使用 `WEXITSTATUS(status)` 可以進一步獲得子程序的 ex