翻譯自: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 的例子:編程語言
有像 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
如今,咱們已經能夠帶着參數執行命令了。現有的這些功能,距離達到一個最小的可用性,只差了一點點。你也許在使用咱們的 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。下面是一些能夠改善的點:
修改光標所在行的顯示:
至此,本文已接近尾聲,我但願你讀的愉快。如你所見,SHELL 背後的概念十分簡單。
Go 一樣是一門十分簡單的編程語言,它幫助咱們更快的獲得想要的結果。咱們無需關心內存管理。Rob Pike 和 Ken Thompson,以及 Robert Griesemer 共同創造了 Go,他們也創造了 Unix,因此,使用 Go 編寫 SHELL 是個很好的選擇。
我也一直在學習,若是你發現本文有哪些可改進的地方,請聯繫我。