劉 雍
(海南熱帶海洋學院 計算機科學與技術學院,海南 三亞 572022)
在全球超級計算機操作系統TOP500強排行榜中,Linux操作系統的占比在最近幾十年一直呈現快速上升趨勢,且保持在85%以上[1]。又隨著移動互聯網的快速發展,Linux內核亦越來越頻繁地被移植和使用到嵌入式、物聯網、樹莓派及Android等智能設備中[2-3], 不斷擴展的Linux Socket網絡編程實現信息實時交互、在線存儲、實時監控等,自然成了主要開發的方向[4],因此,對Linux的各種命令的整合和解析也隨之成為熱門的課題。
以Linux操作系統原理為導向,從內核編程的角度,重新構建Shell命令從讀取到執行的各環節,是命令解析器開發研究的核心,而解析和模擬的本質則是一個對多進程控制的過程[5-6]。針對Linux固有命令較多,并且相當多的命令反而極少被使用的現狀[7],本研究給出了一種設計Shell解析命令器通用模型的思路與方法:即把使用頻率較高的命令整合在一個幫助界面,并提供記憶歷史命令等輔助功能, 以實現更安全、更友好、更個性化的定制服務。隨著程序版權意識越來越強[8],個性化設計亦是本研究的重點之一。
個性化命令解析器用C語言編程設計,遵照Linux默認的Bash Shell結構特點,處理終端命令從輸入、到解析直至執行全過程。在確保能正常調用Linux固有的命令集的前提下[9],個性化命令解析器實現了一個自定義的常用命令的集合,提供“help”幫助界面,解析命令的功能及用法,且增設了命令記憶功能,其較之傳統的單一的字符界面,用戶操作方便,交互性較好。
設計個性化命令解析器的思路和步驟主要包括以下三個環節。
第一步,初始化解析器,提供個性化命令行提示格式,獲取當前用戶信息、主機信息及目錄信息等;第二步,切割用戶輸入命令行,獲取命令及參數,第一個字符串為命令字,其余依次是參數序列;第三步,對上一個步驟獲取的命令進行解析,包括判斷、調用和執行等環節。首先判斷命令與當前有限的自定義的命令是否匹配,若匹配則執行“help”分支,創建進程調用自定義函數[10];否則,判斷命令是否為系統內部命令,對肯定的情況進行“內部命令”分支,調用系統默認的命令解析,以保證Linux內部命令正常使用;若對以上兩種情況都無法識別的命令,將執行“無法識別的命令”分支,返回系統出錯信息。程序設計重點在于自定義命令的函數框架及API調用[11],個性化命令解析器工作流程如圖1所示。

圖1 個性化命令解析器工作流程
Linux 中Bash Shell的組成部分從左到右依次是:登陸時的用戶名、主機信息和當前目錄[12]。其中,使用getpwuid函數獲取密碼數據庫結構指針數據struct passwd*pw,后輸出passwd成員變量char*pw_name即為用戶登錄名;通過函數原型int uname(struct utsname*buf) ,獲取主機信息,utsname成員變量nodename即為主機名;獲取當前路徑使用函數的原型 char*getcwd(char*buf,size_t size),但是該函數獲取的是當前的絕對路徑,因而還需要把絕對路徑轉化為相對路徑;提示部分區分#與$,如果函數getuid()的返回值為0,代表是根用戶root輸出#,否則輸出$。以上四個部分用以下語句在一行內輸出:
printf("[ly′%s@%s %s]%c",pw->pw_name,ptr,p,flag);
假如默認終端的原格式為“[root@hntou user]#(絕對路徑為/home/user)”,命令解析器初始化后可顯示為“[ly′root@hntou user]#”,意為當前root用戶ly,登錄主機hntou,所在的相對路徑是user的目錄,還可以改變終端顯示的字體顏色和背景顏色,實現個性化友好顯示。
在自定義的終端下,如何識別用戶輸入執行的命令及其參數是關鍵步驟之一,這里使用切割的方法。方法是使用strtok函數按空格分離用戶輸入的字符串數據(回車結束),分割后的各部分分別賦值給指針數組,第一個字符數組argv[0]即為命令,后面argv[1]、argv[2]等為命令的參數,可用語句printf("listargv[%d]:%s/n",cnt,p)測試用戶輸入后的分割情形,其結果如下所示。
Your cmd is:copy ./a /home/user/bak_a
listargv[0]:copy
listargv[1]:./a
listargv[2]:/home/user/bak_a
把上一步結果中的命令(argv[0])及參數列表(argv[1],argv[2]等)傳遞給內核,解析并調用對應功能的函數。使用strncmp函數去匹配命令集,首先調用自定義函數,即定制的個性化可執行程序;其次調用Linux固有的內部命令,本質上使用fork()函數創建新的進程,在子進程中調用exec系列函數執行新進程[13],如使用以下語句:
execl("/bin/sh","sh","-c",argv[0],NULL);
最后,如果以上子進程執行不成功,意為命令argv[0]無法識別,或者參數出錯,系統則輸出出錯信息,讓父進程回收子進程資源。
根據圖1可知,當前命令解析器提供了至少6個常用命令解析,命令字及相關功能,如表1所示。

表1 自定義命令解析表
如果以上自定義的命令函數全部寫在main()函數所在的.c的文件里,代碼量將會相當龐大,而且不利于調度和功能擴展,為了程序書寫方便,且優化編譯,一般做法是書寫各命令解析為獨立的.c源文件,如mycd.c、list.c、copy.c等,而后把這些.c的文件包含到一個自定義的myshell.h的頭文件中,當然.h文件中還包括相當多函數調用的系統頭文件[14],而后編譯時,只編譯包含main()函數的主函數的一個.c文件。
以更改用戶路徑這一子功能mycd的命令解析為例,給出各子功能基本通用的程序設計框架。
第一步,編寫核心文件mycd.c,主要任務是自定義函數void cd_cmd(char *path),函數名為cd_cmd,參數是用戶輸入的路徑字符串,即函數功能實現切換目錄(舊目錄)到用戶輸入的路徑(新目錄)中。根據Linux操作系統的原理,要對參數分情況討論,如符號“~”代表用戶家目錄,“.”代表當前目錄,“..”代表上一級目錄,“/”代表根目錄。而且還要處理權限的問題,如普通用戶無法切換到根用戶家目錄。在切換目錄的實際操作中,主要步驟大約分三步,首先,獲取用戶信息包括其家目錄信息,取結構體數據struct passwd,其中pw_dir成員變量是家目錄;其次,獲取當前目錄,使用函數getcwd(),把獲取當前工作目錄的絕對路徑,賦值給old_path地址變量;最后,切換目錄,語句strcopy(old_path,new_path)則先給new_path賦值,再調用chdir(new_path)系統函數切換到新路徑中。
第二步,把子功能的核心文件包含到自定義的主要頭文件myshell.h中,該實例使用#include “mycd.c”語句,方便主函數調用。
第三步,在包含有myshell.h頭文件的main函數中調用以上第一步的cd_cmd函數。
if(strncmp(argv[0],"mycd",4)==0) //如果argv[0]所代表的命令是mycd關鍵字
{
cd_cmd(argv[1]);//調用函數
continue;
}
同樣的方法,如果再新增一個文件刪除命令myrm的解析,則只需要再書寫一個獨立的myrm.c源文件,并在myshell.h的頭文件中加上一句 #include “myrm.c”,最后在main函數中調用即可。限于篇幅,其他命令解析就不在此贅述。
為突出個性化友好界面,增強交互性,判斷(strcmp(arglist[0],"help/0")==0)則給出了幫助help命令,打開運行終端并將一直處于運行之中,所以總體來說,main()函數是一個while(1)的循環結構。依次調用初始化終端函數、命令行字符串切割函數、各子功能主要函數、內置命令函數,調用正確則輸出執行結果,否則輸出出錯信息[15]。主函數中用整型變量cmd_cnt為計數器,以記錄歷史命令的個數,函數strcpy(history[cmd_cnt],cmd)則實現把所有的命令放置于一個字符串數組中。如在個性化的終端提示符下,如果用戶輸入是系統不能識別的EXIT命令,則輸出出錯信息“Command Error!”,且友好提示“You may need ′help′”,根據提示輸入“help”命令后,界面下顯示的正是自定義的主要命令集,對各命令功能均有描述,具體情形參考如下。
[root@hntou shell_test]# ./shell
[ly′root@hntou shell_test]#EXIT
Command Error!
You may need ′help′
[ly′root@hntou shell_test]#help
mypwd:展示目前工作目錄.
mycd:切換指定工作目錄.
copy:復制目錄或文件到指定位置.
myps:顯示進程信息.
myls:顯示文件信息.
mytime:顯示進程運行時間.
mytree:顯示目錄結構.
myrm:遞歸刪除文件、文件夾.
mymv:移動、重命名文件、文件夾.
myline:顯示文件、文件夾內全部文件的函數.
myhis:顯示輸入歷史.
exit:退出shell.
上述主界面給出常用命令的幫助信息,界面友好,操作方便。個性化命令解析體現在以下四個方面:第一,功能全面,用戶可首選自定義的命令,也可以調用Linux固有內部命令,如查看當前目錄或文件信息,示例既使用了主界面提供的myls命令,也使用原ls命令驗證,確保了Linux內部命令不缺失;第二,增強了命令解析的交互性,如實現文件復制操作調用的是copy命令,比原cp命令更符合用戶的理解,且有操作成功的交互性提示;第三,可擴展性較好,可根據用戶需要靈活增減相應功能的命令;第四,由于Linux shell默認不會記憶歷史命令[16],所以使用myhis命令方便查看歷史命令序列,相關操作參考如下。
[ly′root@hntou shell_test]#myls-l ./
-rwxr-xr-x 1 root root 53016 Jan 26 21:37 shell
-rw-r--r-- 1 root root 114 Jan 26 21:37 file
[ly′root@hntou shell_test]#copy file bak_file
Copy Finished!
[ly′root@hntou shell_test]#ls
bak_file file shell
[ly′root@hntou shell_test]#myhis

**Print Input History until now:**
EXIT
help
list-l ./
copy file bak_file
ls
myhis

[ly′root@hntou shell_test]#exit
Bye~
[root@hntou shell_test]#
本研究遵照Linux用戶的使用習慣,構造了對Linux常用命令重新解析的方法和思路,給出了C語言程序設計的模型,實現了Shell命令提示格式個性化顯示、命令解析多樣化、主界面友好的設計及記憶歷史命令等功能,框架嚴謹,思路清晰,方法靈活。該命令解析器個性化特征突出,易于擴展,在移動互聯網設備方面適用性會越來越好。