如何用Python創建自己的 Shell(上)

巴扎黑
發布: 2017-03-18 11:53:41
原創
1448 人瀏覽過

我很想知道一個 shell (像 bash,csh 等)內部是如何運作的。於是為了滿足自己的好奇心,我使用 Python 實作了一個名為yosh (Your Own Shell)的 Shell。本文章所介紹的概念也可以應用在其他程式語言上。

(提示:你可以在這裡找到本博文使用的源代碼,代碼以MIT 許可證發布。在Mac OS X 10.11.5 上,我使用Python 2.7.10 和3.4.3 進行了測試。

步驟 0:項目結構

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

yosh_project
|-- yosh
   |-- __init__.py
   |-- shell.py
登入後複製

yosh_project

 為專案根目錄(你也可以簡單地命名它為 yosh)。

yosh

 為套件目錄,且 __init__.py 可以使它成為與套件的目錄名字相同的套件(如果你不用Python 寫的話,可以忽略它。 步驟 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 將顯示一個命令提示符,並等待讀取命令輸入。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush: python; gutter: true; first-line: 1">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(&amp;#39;&gt; &amp;#39;) sys.stdout.flush() ### 读取命令输入 cmd = sys.stdin.readline()</pre><div class="contentsignin">登入後複製</div></div>之後,我們切分指令(tokenize)輸入並執行(execute)(我們即將實作 

tokenize

 和 execute 函數)。

因此,我們的 shell_loop() 會是如下這樣:<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush: python; gutter: true; first-line: 1">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(&amp;#39;&gt; &amp;#39;) sys.stdout.flush() ### 读取命令输入 cmd = sys.stdin.readline() ### 切分命令输入 cmd_tokens = tokenize(cmd) ### 执行该命令并获取新的状态 status = execute(cmd_tokens)</pre><div class="contentsignin">登入後複製</div></div>這就是我們整個 shell 迴圈。如果我們使用 python shell.py 啟動我們的 shell,它會顯示命令提示字元。然而如果我們輸入指令並按回車,它會拋出錯誤,因為我們還沒定義 

tokenize

 函數。

為了退出 shell,可以嘗試輸入 ctrl-c。稍後我將解釋如何以優雅的形式退出 shell。 步驟2:命令切分tokenize當使用者在我們的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 的函式庫,它能夠幫助我們如魔法般地分割指令。 (提示:我們也可以使用正規表示式,但它不是本文的重點。)

import sys
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 指令,接著按下回車鍵。 在我們敲下回車鍵之後,問題是我們的 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,也就是父进程,可以免受内存替换的危险。

让我们看看修改的代码。

...
def execute(cmd_tokens):
    ### 分叉一个子 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
    ### 返回状态以告知在 shell_loop 中等待下一个命令
    return SHELL_STATUS_RUN
...
登入後複製

当我们的父进程调用 os.fork() 时,你可以想象所有的源代码被拷贝到了新的子进程。此时此刻,父进程和子进程看到的是相同的代码,且并行运行着。

如果运行的代码属于子进程,pid 将为 0。否则,如果运行的代码属于父进程,pid 将会是子进程的进程 id。

当 os.execvp 在子进程中被调用时,你可以想象子进程的所有源代码被替换为正被调用程序的代码。然而父进程的代码不会被改变。

当父进程完成等待子进程退出或终止时,它会返回一个状态,指示继续 shell 循环。

运行

现在,你可以尝试运行我们的 shell 并输入 mkdir test_dir2。它应该可以正确执行。我们的主 shell 进程仍然存在并等待下一条命令。尝试执行 ls,你可以看到已创建的目录。

但是,这里仍有一些问题。

第一,尝试执行 cd test_dir2,接着执行 ls。它应该会进入到一个空的 test_dir2 目录。然而,你将会看到目录并没有变为 test_dir2

第二,我们仍然没有办法优雅地退出我们的 shell。

我们将会在下篇解决诸如此类的问题。

以上是如何用Python創建自己的 Shell(上)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板