我很想知道一個 shell (像 bash,csh 等)內部是如何工做的。因而爲了知足本身的好奇心,我使用 Python 實現了一個名爲yosh(Your Own Shell)的 Shell。本文章所介紹的概念也能夠應用於其餘編程語言。html
(提示:你能夠在這裏查找本博文使用的源代碼,代碼以 MIT 許可證發佈。在 Mac OS X 10.11.5 上,我使用 Python 2.7.10 和 3.4.3 進行了測試。它應該能夠運行在其餘類 Unix 環境,好比 Linux 和 Windows 上的 Cygwin。)讓咱們開始吧。
python
對於此項目,我使用瞭如下的項目結構。linux
yosh_project |-- yosh |-- __init__.py |-- shell.py yosh_project
爲項目根目錄(你也能夠把它簡單命名爲 yosh)。yosh 爲包目錄,且 __init__.py 可使它成爲與包的目錄名字相同的包(若是你不用 Python 編寫的話,能夠忽略它。)shell.py 是咱們主要的腳本文件。正則表達式
當啓動一個 shell,它會顯示一個命令提示符並等待你的命令輸入。在接收了輸入的命令並執行它以後(稍後文章會進行詳細解釋),你的 shell 會從新回到這裏,並循環等待下一條指令。在 shell.py 中,咱們會以一個簡單的 main 函數開始,該函數調用了 shell_loop() 函數,以下:shell
def shell_loop(): # Start the loop here def main(): shell_loop() if __name__ == "__main__": main()
接着,在 shell_loop() 中,爲了指示循環是否繼續或中止,咱們使用了一個狀態標誌。在循環的開始,咱們的 shell 將顯示一個命令提示符,並等待讀取命令輸入。編程
import sys SHELL_STATUS_RUN = 1 SHELL_STATUS_STOP = 0 def shell_loop(): status = SHELL_STATUS_RUN while status == SHELL_STATUS_RUN: ### 顯示命令提示符 sys.stdout.write('> ') sys.stdout.flush() ### 讀取命令輸入 cmd = sys.stdin.readline()
以後,咱們切分命令(tokenize)輸入並進行執行(execute)(咱們即將實現 tokenize 和 execute 函數)。所以,咱們的 shell_loop() 會是以下這樣:bash
import sys SHELL_STATUS_RUN = 1 SHELL_STATUS_STOP = 0 def shell_loop(): status = SHELL_STATUS_RUN while status == SHELL_STATUS_RUN: ### 顯示命令提示符 sys.stdout.write('> ') sys.stdout.flush() ### 讀取命令輸入 cmd = sys.stdin.readline() ### 切分命令輸入 cmd_tokens = tokenize(cmd) ### 執行該命令並獲取新的狀態 status = execute(cmd_tokens)
這就是咱們整個 shell 循環。若是咱們使用 python shell.py 啓動咱們的 shell,它會顯示命令提示符。然而若是咱們輸入命令並按回車,它會拋出錯誤,由於咱們還沒定義 tokenize 函數。爲了退出 shell,能夠嘗試輸入 ctrl-c。稍後我將解釋如何以優雅的形式退出 shell。編程語言
()當用戶在咱們的 shell 中輸入命令並按下回車鍵,該命令將會是一個包含命令名稱及其參數的長字符串。所以,咱們必須切分該字符串(分割一個字符串爲多個元組)。咋一看彷佛很簡單。咱們或許可使用 cmd.split(),以空格分割輸入。它對相似 ls -a my_folder 的命令起做用,由於它可以將命令分割爲一個列表 ['ls', '-a', 'my_folder'],這樣咱們便能輕易處理它們了。函數
然而,也有一些相似 echo "Hello World" 或 echo 'Hello World' 以單引號或雙引號引用參數的狀況。若是咱們使用 cmd.spilt,咱們將會獲得一個存有 3 個標記的列表 ['echo', '"Hello', 'World"'] 而不是 2 個標記的列表 ['echo', 'Hello World']。幸運的是,Python 提供了一個名爲 shlex 的庫,它可以幫助咱們如魔法般地分割命令。(提示:咱們也可使用正則表達式,但它不是本文的重點。)oop
import sys import shlex ... def tokenize(string): return shlex.split(string) ...
而後咱們將這些元組發送到執行進程。
這是 shell 中核心而有趣的一部分。當 shell 執行 mkdir test_dir 時,到底發生了什麼?(提示: mkdir 是一個帶有 test_dir 參數的執行程序,用於建立一個名爲 test_dir 的目錄。)execvp 是這一步的首先須要的函數。在咱們解釋 execvp 所作的事以前,讓咱們看看它的實際效果。
import os ... def execute(cmd_tokens): ### 執行命令 os.execvp(cmd_tokens[0], cmd_tokens) ### 返回狀態以告知在 shell_loop 中等待下一個命令 return SHELL_STATUS_RUN ...
再次嘗試運行咱們的 shell,並輸入 mkdir test_dir 命令,接着按下回車鍵。在咱們敲下回車鍵以後,問題是咱們的 shell 會直接退出而不是等待下一個命令。然而,目錄正確地建立了。所以,execvp 實際上作了什麼?
execvp 是系統調用 exec 的一個變體。第一個參數是程序名字。v 表示第二個參數是一個程序參數列表(參數數量可變)。p 表示將會使用環境變量 PATH 搜索給定的程序名字。在咱們上一次的嘗試中,它將會基於咱們的 PATH 環境變量查找mkdir 程序。(還有其餘 exec 變體,好比 execv、execvpe、execl、execlp、execlpe;你能夠 google 它們獲取更多的信息。)exec 會用即將運行的新進程替換調用進程的當前內存。在咱們的例子中,咱們的 shell 進程內存會被替換爲 mkdir 程序。接着,mkdir 成爲主進程並建立 test_dir 目錄。最後該進程退出。
這裏的重點在於咱們的 shell 進程已經被 mkdir 進程所替換。這就是咱們的 shell 消失且不會等待下一條命令的緣由。所以,咱們須要其餘的系統調用來解決問題:fork。fork 會分配新的內存並拷貝當前進程到一個新的進程。咱們稱這個新的進程爲子進程,調用者進程爲父進程。而後,子進程內存會被替換爲被執行的程序。所以,咱們的 shell,也就是父進程,能夠免受內存替換的危險。
讓咱們看看修改的代碼。...
當咱們的父進程調用 os.fork() 時,你能夠想象全部的源代碼被拷貝到了新的子進程。此時此刻,父進程和子進程看到的是相同的代碼,且並行運行着。若是運行的代碼屬於子進程,pid 將爲 0。不然,若是運行的代碼屬於父進程,pid 將會是子進程的進程 id。
當 os.execvp 在子進程中被調用時,你能夠想象子進程的全部源代碼被替換爲正被調用程序的代碼。然而父進程的代碼不會被改變。當父進程完成等待子進程退出或終止時,它會返回一個狀態,指示繼續 shell 循環。
運行如今,你能夠嘗試運行咱們的 shell 並輸入 mkdir test_dir2。它應該能夠正確執行。咱們的主 shell 進程仍然存在並等待下一條命令。嘗試執行 ls,你能夠看到已建立的目錄。
可是,這裏仍有一些問題:
第一,嘗試執行 cd test_dir2,接着執行 ls。它應該會進入到一個空的 test_dir2 目錄。然而,你將會看到目錄並無變爲 test_dir2。
第二,咱們仍然沒有辦法優雅地退出咱們的 shell。
本文轉載地址:https://www.linuxprobe.com/python-shell-first.html