天天看點

使用 Python 建立你自己的 Shell (上)

使用 Python 建立你自己的 Shell (上)

我很想知道一個 shell (像 bash,csh 等)内部是如何工作的。于是為了滿足自己的好奇心,我使用 python 實作了一個名為yosh (your own shell)的 shell。本文章所介紹的概念也可以應用于其他程式設計語言。

(提示:你可以在這裡查找本博文使用的源代碼,代碼以 mit 許可證釋出。在 mac os x 10.11.5 上,我使用 python

2.7.10 和 3.4.3 進行了測試。它應該可以運作在其他類 unix 環境,比如 linux 和 windows 上的 cygwin。)

讓我們開始吧。

步驟 0:項目結構

對于此項目,我使用了以下的項目結構。

yosh_project 

|-- yosh 

 |-- __init__.py 

 |-- shell.py 

yosh_project 為項目根目錄(你也可以把它簡單命名為 yosh)。

yosh 為包目錄,且 __init__.py 可以使它成為與包的目錄名字相同的包(如果你不用 python 編寫的話,可以忽略它。)

shell.py 是我們主要的腳本檔案。

步驟 1:shell 循環

當啟動一個 shell,它會顯示一個指令提示符并等待你的指令輸入。在接收了輸入的指令并執行它之後(稍後文章會進行詳細解釋),你的 shell 會重新回到這裡,并循環等待下一條指令。

在 shell.py 中,我們會以一個簡單的 main 函數開始,該函數調用了 shell_loop() 函數,如下:

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 

    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() 會是如下這樣:

        ### 切分指令輸入 

        cmd_tokens = tokenize(cmd) 

        ### 執行該指令并擷取新的狀态 

        status = execute(cmd_tokens) 

這就是我們整個 shell 循環。如果我們使用 python shell.py 啟動我們的 shell,它會顯示指令提示符。然而如果我們輸入指令并按回車,它會抛出錯誤,因為我們還沒定義 tokenize 函數。

為了退出 shell,可以嘗試輸入 ctrl-c。稍後我将解釋如何以優雅的形式退出 shell。

步驟 2:指令切分tokenize

當使用者在我們的 shell 中輸入指令并按下Enter鍵,該指令将會是一個包含指令名稱及其參數的長字元串。是以,我們必須切分該字元串(分割一個字元串為多個元組)。

咋一看似乎很簡單。我們或許可以使用 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 的庫,它能夠幫助我們如魔法般地分割指令。(提示:我們也可以使用正規表達式,但它不是本文的重點。)

import shlex 

... 

def tokenize(string): 

    return shlex.split(string) 

然後我們将這些元組發送到執行程序。

步驟 3:執行

這是 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 指令,接着按下Enter鍵。

在我們敲下Enter鍵之後,問題是我們的 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,也就是父程序,可以免受記憶體替換的危險。

讓我們看看修改的代碼。

    ### 分叉一個子 shell 程序 

    ### 如果目前程序是子程序,其 `pid` 被設定為 `0` 

    ### 否則目前程序是父程序的話,`pid` 的值 

    ### 是其子程序的程序 id。 

    pid = os.fork() 

    if pid == 0: 

    ### 子程序 

        ### 用被 exec 調用的程式替換該子程序 

        os.execvp(cmd_tokens[0], cmd_tokens) 

    elif pid > 0: 

    ### 父程序 

        while true: 

            ### 等待其子程序的響應狀态(以程序 id 來查找) 

            wpid, status = os.waitpid(pid, 0) 

            ### 當其子程序正常退出時 

            ### 或者其被信号中斷時,結束等待狀态 

            if os.wifexited(status) or os.wifsignaled(status): 

                break 

當我們的父程序調用 os.fork() 時,你可以想象所有的源代碼被拷貝到了新的子程序。此時此刻,父程序和子程序看到的是相同的代碼,且并行運作着。

如果運作的代碼屬于子程序,pid 将為 0。否則,如果運作的代碼屬于父程序,pid 将會是子程序的程序 id。

當 os.execvp 在子程序中被調用時,你可以想象子程序的所有源代碼被替換為正被調用程式的代碼。然而父程序的代碼不會被改變。

當父程序完成等待子程序退出或終止時,它會傳回一個狀态,訓示繼續 shell 循環。

運作

現在,你可以嘗試運作我們的 shell 并輸入 mkdir test_dir2。它應該可以正确執行。我們的主 shell 程序仍然存在并等待下一條指令。嘗試執行 ls,你可以看到已建立的目錄。

但是,這裡仍有一些問題。

第一,嘗試執行 cd test_dir2,接着執行 ls。它應該會進入到一個空的 test_dir2 目錄。然而,你将會看到目錄并沒有變為 test_dir2。

第二,我們仍然沒有辦法優雅地退出我們的 shell。

作者:supasate choochaisri

來源:51cto