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

這裏能夠找到代碼

github.com/z0gSh1u/expshellgit

支持的特性

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

運行截圖

run_pic

如何寫一個簡單的 Shell

這裏簡單介紹寫 Shell 時比較關鍵的一些部分,具體請查看源代碼。github

展現提示符

show_command_prompt 函數。shell

command_prompt 是在每行最開始顯示的一段與用戶名、路徑等相關的提示信息。ExpShell 顯示的 prompt 形如 [root@localhost tmp]>。用 > 而非 #、$ 做爲提示符,以區分原生 Shell。緩存

  • 獲取用戶名函數

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

    getcwd(char_buf, CHAR_BUF_SIZE);
    string cwd(char_buf);
    • prompt 中目錄只顯示最近一級,此處用 / 來 split 後取最後一個便可
    • 家目錄須要摺疊爲 ~,這裏順便把家目錄地址存到全局變量 home_dir,後續要用到
  • 獲取主機名.net

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

    cout << "[" << username << "@" << hostname << " " << cwd << "]> ";

解析命令

  • 爲存儲解析結果,定義以下四個類:blog

    • 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
相關文章
相關標籤/搜索