[譯] 使用 Go 語言編寫一個簡單的 SHELL

翻譯自:https://sj14.gitlab.io/post/2...git

介紹

在本文中,咱們將使用 Go 語言,編寫一個最小的 UNIX(-like)操做系統 SHELL,它只須要大概 60 行代碼。你須要稍微瞭解一些 Go 語言(知道如何編譯簡單的項目),以及簡單使用 UNIX Shell。shell

UNIX 很是簡單,簡單到一個天才都能理解它的簡單性 - Dennis Ritchie

固然,我並不是天才,我也不太肯定 Dennis Ritchie 所說的,是否也包括運行於用戶空間的工具。Shell 只是完整操做系統的一小部分(相較於內核,它真的是一個簡單的部分),但我但願在本文的結尾,你能夠感到吃驚,吃驚於編寫一個 SHELL,所用到的知識如此少。編程

什麼是SHELL

給 SHELL 下定義有點困難。我認爲 SHELL 能夠理解爲你所使用的操做系統,基本的用戶界面。你能夠在 SHELL 中輸入命令,而後接收一些反饋輸出。若是想了解更多信息,或者更明確的定義,請查閱 維基百科)。數組

一些 SHELL 的例子:編程語言

有像 Windows 和 GNOME 這種圖形界面 SHELL,但大多數 IT 相關人員(至少我是),當談論起 SHELL,指的是基於文本的 SHELL(上面列表的頭兩項)。固然,也能夠簡化的定義爲非圖形界面 SHELL。函數

事實上,SHELL 的功能能夠定義爲輸入命令,而後接收該命令的輸出。想看個例子?運行 ls 命令,輸出目錄的內容。工具

Input:

ls
Output:

Applications            etc
Library                home
...

就是這樣,十分簡單。讓咱們開始吧!gitlab

輸入循環

要執行一個命令,咱們必須接收輸入。而輸入來自咱們人類,使用鍵盤進行的。post

鍵盤是咱們的標準輸入設備(os.Stdin),咱們能夠訪問並讀取它。當咱們按下回車鍵的時候,會建立新的一行。這行新的文本以 \n 結尾。當敲擊回車鍵的時候,全部存儲在輸入區的內容將被輸入。學習

reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')

讓咱們將這些代碼輸入進咱們的 main.go 文件,ReadingString 方法被嵌套在 for 循環中,因此咱們能夠反覆輸入命令。當讀取輸入,發生錯誤時,咱們能夠將錯誤信息輸出到標準錯誤處理設備(os.Stderr)。若是咱們使用 fmt.Println,但並無指定輸出設備,這個錯誤信息仍是會輸出到標準輸出設備中(os.Stdout)。這並不會改變 SHELL 的功能,可是輸出到單獨的設備,能夠方便過濾輸出,以進行下一步處理。

func main() {
    reader := bufio.NewReader(os.Stdin)
    for {
        // Read the keyboad input.
        input, err := reader.ReadString('\n')
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
        }
    }
}

執行命令

如今,咱們打算執行輸入的命令。增長一個名爲 execInput 的新的函數,他接收輸入的字符串做爲參數。首先,咱們移除輸入結尾的換行符。接下來,經過 exec.Command(input) 來準備執行命令,設置參數,以及捕獲輸出的結果和錯誤。最後,經過 cmd.Run() 來執行。

func execInput(input string) error {
    // Remove the newline character.
    input = strings.TrimSuffix(input, "\n")

    // Prepare the command to execute.
    cmd := exec.Command(input)

    // Set the correct output device.
    cmd.Stderr = os.Stderr
    cmd.Stdout = os.Stdout

    // Execute the command and return the error.
    return cmd.Run()
}

原型

接着,在循環語句上面,添加一個美化做用的指示器(>),在循環語句下面,添加新的 execInput 函數,此時,主要功能就完成了。

func main() {
    reader := bufio.NewReader(os.Stdin)
    for {
        fmt.Print("> ")
        // Read the keyboad input.
        input, err := reader.ReadString('\n')
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
        }

        // Handle the execution of the input.
        if err = execInput(input); err != nil {
            fmt.Fprintln(os.Stderr, err)
        }
    }
}

是時候執行一次測試了。使用 go run main.go 構建並運行我們的 SHELL。你將看到輸入標識符 >,此時能夠接受輸入。舉個例子,咱們能夠執行 ls 命令。

> ls
LICENSE
main.go
main_test.go

不錯,能夠運行!我們的程序此時能夠執行 ls 命令,並輸出當前目錄的內容。你能夠像退出其餘程序同樣,使用 ctrl+c,退出它。

參數

讓咱們命令後面加個參數,如 ls -l

> ls -l

執此時執行會報錯:exec: "ls -l": executable file not found in $PATH

這是由於咱們的 SHELL 嘗試執行 ls -l,可是並無找到叫這個名字的程序。咱們的意思是執行 ls,帶上 -l 的參數。當前,咱們的程序還不支持接受命令參數。要修復這個問題,須要修改 execLine 函數,將要執行的命令以空格拆分。

func execInput(input string) error {
    // Remove the newline character.
    input = strings.TrimSuffix(input, "\n")

    // Split the input to separate the command and the arguments.
    args := strings.Split(input, " ")

    // Pass the program and the arguments separately.
    cmd := exec.Command(args[0], args[1:]...)
    ...
}

程序的名字如今存儲在 args[0] 中,程序執行的參數存儲在數組其餘索引中。執行 ls -l 如今能夠獲得預期的結果。

> ls -l
total 24
-rw-r--r--  1 simon  staff  1076 30 Jun 09:49 LICENSE
-rw-r--r--  1 simon  staff  1058 30 Jun 10:10 main.go
-rw-r--r--  1 simon  staff   897 30 Jun 09:49 main_test.go

更改目錄(cd)

如今,咱們已經能夠帶着參數執行命令了。現有的這些功能,距離達到一個最小的可用性,只差了一點點。你也許在使用咱們的 Shell 時候,已經注意到了:你沒法經過 cd 改變當前命令執行的目錄。

> cd /
> ls
LICENSE
main.go
main_test.go

不,這不是咱們根目錄的內容。那爲何 cd 命令不起做用呢?要理解這點很容易:沒有真正的 cd 程序,該功能是 SHELL 的內置命令。

咱們必須對 execInput 函數再次進行修改。在 Split 方法後面,咱們添加 switch 結構語句,並將 args[0] 做爲它的參數。當這個命令是 cd,咱們檢查它後面是否還有參數,若是沒有指定參數,咱們沒法改變當前目錄(在大多數 SHELL 中,不指定參數,將跳轉到主目錄)。當 args[1] 中有一個後續參數時(存儲路徑的參數),咱們使用 os.Chdir(args[1]) 更改目錄。在 case 塊的末尾,咱們返回 execInput 函數以中止其餘處理。

由於如此簡單,咱們在 cd 塊後面,再添加一個 exit 命令,exit 能夠用來退出當前SHELL(另外一個退出方法是 CTRL+C)。

// Split the input to separate the command and the arguments.
args := strings.Split(input, " ")

// Check for built-in commands.
switch args[0] {
case "cd":
    // 'cd' to home dir with empty path not yet supported.
    if len(args) < 2 {
        return  errors.New("path required")
    }
    // Change the directory and return the error.
    return os.Chdir(args[1])
case "exit":
    os.Exit(0)
}
...

能夠看到,此時輸出的內容,相較於以前的輸出結果,更像是咱們的根目錄。

> cd /
> ls
Applications
Library
Network
System
...

至此,咱們已經完成了這個簡單的 SHEEL 的編寫。

考慮改善的地方

此時,若是你以爲有些無聊,你能夠嘗試改進這個 SHELL。下面是一些能夠改善的點:

  • 修改光標所在行的顯示:

    • 增長當前目錄
    • 增長機器名稱
    • 增長當前用戶
  • 經過輸入 up/down 鍵,來翻閱輸入的歷史

結尾

至此,本文已接近尾聲,我但願你讀的愉快。如你所見,SHELL 背後的概念十分簡單。

Go 一樣是一門十分簡單的編程語言,它幫助咱們更快的獲得想要的結果。咱們無需關心內存管理。Rob Pike 和 Ken Thompson,以及 Robert Griesemer 共同創造了 Go,他們也創造了 Unix,因此,使用 Go 編寫 SHELL 是個很好的選擇。

我也一直在學習,若是你發現本文有哪些可改進的地方,請聯繫我。

相關文章
相關標籤/搜索