如何寫一個簡單的shell
看完《UNIX環境高級編程》后我就一直想寫一個簡單的shell來作為練習,因為有事斷斷續續的寫了好幾個月,如今寫了差不多來總結一下。
源代碼放在了Github: https://github.com/yibo141/Oh-Shell
簡單的分析
我們的shell不像bash那樣復雜全面,只是實現其中的一小部分功能:命令歷史,命令補全,支持IO重定向和管道。一共分成幾個部分:主
函數文件,輸出出錯信息,解析命令等。
我們打開bash對照着做,首先bash有命令提示符,我們要做的和bash的命令提示符一樣。然后我們讀取用戶輸入的命令並使用readline庫
將命令添加到歷史命令列表。然后我們將命令傳遞給解析命令函數,解析命令並執行。
輸出命令提示符
打開bash后我們看到初始的命令提示符如下圖所示:
以root登錄后用cd
命令切換到別的工作目錄后如下圖所示:
我們發現bash的命令提示符格式為[用戶名]@[主機名]:[當前工作目錄][$或#]
。可以看出用戶的home目錄以~表示,而結尾部分的$或#是
指如果當前用戶是普通用戶,則命令提示符是$;如果當前用戶是root用戶,則命令提示符為#。
那么該如何實現呢?我們用getpwuid
函數獲取用戶的信息,包括用戶名和用戶ID;用gethostname
函數獲取主機名;用getcwd
函數獲
取當前用戶的當前工作目錄。有了用戶ID我們就能判斷當前用戶是不是root,因為root用戶的用戶ID為0。有了當前工作目錄我們就能判斷
工作目錄是不是在用戶的home目錄下,如果在home目錄下我們就將home目錄的部分替換成~。這樣打印提示符的任務就完成了,此部分代碼
的實現在main.c文件中的getPrompt
函數中。
命令歷史和命令補全
Linux默認保存最后輸入的500個命令歷史。我們可以用GNU的readline庫實現命令歷史和命令補全。安裝就按其中的說明的步驟進
行就行。readline的庫有很多功能,但我們只需要其中的readline
函數和add_history
函數就行了。readline
函數以一個字符串為參
數作為提示符,返回用戶輸入的一行命令;add_history
函數以一個字符串作為參數,並將此字符串添加到歷史命令列表里。
打印出錯消息
在err.h頭文件中聲明了3個錯誤處理函數:err_ret
函數打印出錯消息並返回,err_quit
和err_sys
函數打印出錯消息並退出程序。其
后兩個函數在發生致命錯誤時調用,而err_ret
函數在發生非致命錯誤時調用。其實現在err.c文件中。其實現是參照《UNIX環境高級編程》
中的出錯處理函數。
實現IO重定向和管道
處理諸如cat < in.data | grep str | sort > out.data
這樣的命令需要實現IO重定向和管道。默認情況下在shell中運行的程序其標准輸
入(描述符為0)和標准輸出(描述符為1)都關聯到終端,IO重定向是指將程序的標准輸入和標准輸出重定向到文件或其他設備。shell從描
述符0讀,從描述符1寫。如果我們想將標准輸入重定向就得關閉描述符0,再將其他文件或設備關聯到描述符0。對於標准輸出也一樣。
管道是將進程的標准輸入和/或標准輸出通過一個數據通道與另一個進程關聯。以本節開頭的那個例子來說,grep
的標准輸入來自cat
程
序的輸出,標准輸出則作為sort
程序的標准輸入。我們可以用pipe
函數創建一個管道,其參數是一個int型數組(假設為fd[2]),數組有
兩個元素。經由數組返回兩個文件描述符,fd[0]為讀而打開,fd[1]為寫而打開。fd[1]的輸出是fd[0]的輸入。如果想重定向標准輸入,則將
標准輸入與fd[0]相關聯,如果想重定向標准輸出,則將標准輸出與fd[1]相關聯。
命令解析
我認為命令解析是這個程序中最難的部分了,可能是我沒學過編譯原理的原因。所以我死扣了好長時間還是不會怎樣解析shll命令(現在也沒
搞通)。我偶然發現在xv6課程主頁上有一個homework是關於這個的,它給出了解析命令的框架,將關鍵的部分空出來
讓學生補全,所以我就照搬出來用了。所以實現的功能也有限,還有命令列表和后台命令不會解析。等以后我看了編譯原理后再把這個坑填了。
總結
這個shell雖然簡單,但也讓我學到了很多知識,查了很多資料。看完《UNIX環境高級編程》后收獲了很多,最高興的莫過於將學到的知識用在
實際的程序中。就寫到這里吧。
Reference:
《UNIX環境高級編程 第三版》