寫一個簡單的 Linux Shell (C++)
阿新 • • 發佈:2020-09-20
## 這裡可以找到程式碼
[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