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

z0gSh1u發表於2020-09-20

這裡可以找到程式碼

github.com/z0gSh1u/expshell

支援的特性

  • 單條指令的執行
  • 引號引起的引數(如 $ some_program "hello, world"
  • 重定向(>、< )
  • 管道(|)
  • 內建指令(如 cd、history、quit)
  • 指令別名(如 ll → ls -l)
  • 家目錄(~)

執行截圖

run_pic

如何寫一個簡單的 Shell

這裡簡單介紹寫 Shell 時比較關鍵的一些部分,具體請檢視原始碼。

展示提示符

show_command_prompt 函式。

command_prompt 是在每行最開始顯示的一段與使用者名稱、路徑等相關的提示資訊。ExpShell 顯示的 prompt 形如 [root@localhost tmp]>。用 > 而非 #、$ 作為提示符,以區分原生 Shell。

  • 獲取使用者名稱

    passwd *pwd = getpwuid(getuid());
    string username(pwd->pw_name);
    
  • 獲取當前目錄

    getcwd(char_buf, CHAR_BUF_SIZE);
    string cwd(char_buf);
    
    • prompt 中目錄只顯示最近一級,此處用 / 來 split 後取最後一個即可
    • 家目錄需要摺疊為 ~,這裡順便把家目錄地址存到全域性變數 home_dir,後續要用到
  • 獲取主機名

    gethostname(char_buf, CHAR_BUF_SIZE);
    string hostname(char_buf);
    
    • 有時 hostname 會是形如 localhost.locald.xxx 的形式,也 split 處理一下
  • 輸出之即可

    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 函式執行命令
      • 這篇博文 瞭解 exec 族函式,可見 execvp 在當前場景最為合適
      • 第二個引數是一個末元素為 NULL 的 char**(char*[]),內容為 argv
  • 對於 pipe_cmd

    • 為 pipe_cmd 的 left 和 right 分別 fork 子程式執行,並使用管道讓這兩個兄弟程式通訊

    • 這張圖很好地說明了父子程式使用管道通訊的方法

      pipe_and_close

    • 依據上圖,不難類比出兄弟程式進行 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) 可以進一步獲得子程式的 exit code

相關文章