使用 Python 建立你本身的 Shell (上)

 我很想知道一個 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。)讓咱們開始吧。
pythonpython

步驟 0:項目結構

對於此項目,我使用瞭如下的項目結構。linux

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() 函數,以下: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。編程語言

linux-shell_01

步驟 2:命令切分

()當用戶在咱們的 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)
...

而後咱們將這些元組發送到執行進程。

步驟 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

當咱們的父進程調用 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

相關文章
相關標籤/搜索