本文旨在編寫一個簡單的shell外殼程序!功能類似于shell的一些基本操作!雖然不能全部實現shell的一些功能!但是通過此文章,自己寫一個簡單的shell程序也是不成問題!并且通過此文章,可以讓讀者對linux中一些環境變量等基本概念有更深的理解!希望讀完本篇文章能對讀者有一定的收獲!文末會附帶自己編寫shell的源碼!
好的廢話少說,正文開始!
首先我們先來看一下linux中的shell長什么樣子!
這是其shell剛啟動的時候的樣子!其外貌就是一個中括號內部加上一系列的東西!其實當我們認真觀察,不難發現,里面包括的就是“用戶名“+“@”+“主機名字”+“當前工作路徑!”那么發現了此規律之后我們不難實現此描述框!那么接下來我們就著手與這些描述框的實現!
linux描述框的實現!
其中要想獲得我們的用戶名!我們其實可以通過環境變量進行獲取,那么該如何獲取環境變量的值呢?這里就不得不引進一個獲得環境變量的值的函數了!
getenv(“USER”)
通過查詢man手冊,我們可以發現getenv()函數只需要傳遞一個參數即可!那么此參數是什么呢?其實此參數就是我們想要獲得環境變量的值的名字!所以要想獲得用戶名,我們可以直接使用getenv(USER),即可獲得我們想要的用戶名!我們可以驗證一下USER對應的環境變量是否真的是我們所要的環境變量名!我們可以通過echo $USER? 此命令來判斷是否真的是我們想要的用戶名!
不難看出,USER對應的環境變量確實是我們的用戶名!
getenv(“HOSTNAME”)
既然有了用戶名,那么我們的主機名如何獲得呢?思路還是調用getenv(HOSTNAME)操作!獲取主機名!同樣的也可以通過echo命令進行驗證!這里就不再累贅了!
getenv(“PWD”)
最后再來獲取我們的當前工作目錄!也是調用getenv函數!同樣的可以通過echo命令進行驗證!
那么這些基本的環境變量都出來了,我們是否可以通過上述思路來創建一個簡單的描述框呢?
代碼如下:
其中這里為了方便起見,直接將各個函數進行封裝!保證代碼的健壯性!
其中還需要擴充的幾點有:
為了區別與系統的shell,我們在描述框后面加上一個#以區分系統的$ !這樣我們的描述框已經基本實現了!
獲取用戶指令以及將其分割!
那么基本的描述框已經實現了!我們還需要做的一點就是獲取用戶輸入的指令!那么如何獲取用戶的指令呢?思路很簡單:定義一個數組,然后將用戶輸入的字符放到數組中即可!!那么能否用scanf函數呢?答案是肯定不行!因為用戶輸入的指令一般都是指令+選項!其中指令和選項之間都是有著空格來間隔區分的!那么應該如何獲取用戶的輸入呢?答案很簡單,用fgets函數即可,那么接下來我們就來介紹一下fgets函數的用法!!
fgets函數
?通過查詢man手冊可以看出,其中fgets函數中有三個參數,第一個是就是緩沖區即(將要被寫到哪里的地址!)第二個參數表示此緩沖區的大小!第三個參數是用哪些流進行寫入!一般第三個參數我們都選擇(stdin標準輸入流)進行寫入!
既然介紹了fgets函數的用法,那么我們就知道我們需要創建一個數組來存放即將要寫入的數據!數組的大小自己來定義即可!
那么用戶的指令獲取成功之后,我們需要將用戶的指令進行打散然后利用execvp進行替換即可!那么如何進行打散這段字符串呢?這里就不得不引進我們C語言中的strtok函數了!
strtok()函數!
查詢man手冊可以得知,strtok有兩個參數!其中第一個參數是將要打散的原字符串,第二個字符串指的是用于打散的標記符都有哪些。
返回值:第一次調用,返回標記符第一次出現的位置,然后并將標記符轉化為\0,此時會記住此位置!然后再次使用的使用第一個參數只需要傳NULL指針即可!如果最終不可再進行分割的時候,返回值就會返回NULL!這樣就可以將原字符串進行打散!我們的目的是想要將其打散放在一個數組中,方便之后使用!所以我們還需要自己再定義一個指針數組用于存放分割后的各個字符串!
通過以上的思路,我們就可以將用戶命令和將命令打散此功能進行實現了!
代碼如下:
其中第60行是將最后的\n轉化為\0,防止其進行跳行!!其中在commandSplit函數中,我們還設置了條件宏!用于檢查我們的代碼是否將原字符串進行正確的打斷!如果最后不想要打印出分割后的字符串,可以將宏定義取消即可!其中char*out[]表示打散后的數組!!char *in 表示的是原字符串!spint是一個宏定義用來標明分割字符串都有哪些,這里分割符只要空格!
至此,描述框和獲取用戶指令都已經實現了!
完成進程替換!
那么我們如何將用戶的指令轉化為shell的操作呢?這里就得引進進程替換的概念!我們需要將進程進行替換來讓他執行我們想讓他執行的代碼!
那么進程替換有很多中調用方式?我們應該選擇那種呢?其實很簡單!我們已經將用戶的命令行進行打斷分散處理了,所以我們完全可以根據v的特性來進行選擇,又因為我們并不知道用戶以后需要輸入的指令,所以我們也不知道其指令所在路徑,所以我們就可以使用execvp這個系統調用來進行進程替換!其中v我們已經有了!p默認為我們提供了路徑,所以用戶的指令肯定是存放在v[0]上的!所以我們的進程替換就可以寫出來了!
需要注意的是,我們要進行進程替換的時候,一定不要讓我們的父進程進行替換!因為一旦父進程進行替換的時候,如果進程掛掉了,那么我們的shell不就是結束了么,所以我們可以使用fork來創建子進程來進行進程替換,而父進程只需要等待子進程退出,回收其資源即可!
下面來看一下進程替換的代碼!
至此,進程替換的指令也可以實現了,我們自定義的shell程序也能實現ls? top pwd 等操作了!但是對于其他命令我們自定義的shell程序卻不能正確的執行了!例如cd命令,還有export命令!這是為什么呢?這就不得不引進內建命令了!
內建命令
何為內建命令呢?內建命令就是這些命令只能由bash自己執行!而不能讓子進程進行執行!那么我們常見的linux中有哪些命令是內建命令呢,下面就來簡單的介紹幾個內建命令,并且在我們自定義的shell中實現這些內建命令!!
cd命令!!
其中cd是一種常見的內建命令!這個指令只能交付給父進程自己執行,而不能交付給子進程讓子進程執行!因為cd指的就是改變當前的路徑,如果交給子進程進行執行,那么父進程的路徑將不會修改!那么該如何進行編寫我們shell中的cd命令呢?
代碼如下:
其中cd主要進行的操作就是將當前的工作目錄進行修改!那么如何修改當前的工作目錄呢?這里就不得不引進chdir這個系統調用了!
chdir()
其中chdir函數只有一個參數,這個參數代表的是將要修改的路徑!我們只需要定義一個字符數組,然后將我們要修改的路徑存放到此數組中,然后將此數組就進行傳遞即可完成改變當前的路徑!其中還需要將當前的環境變量PWD也進行修改!創建一個臨時數組和全局數組,全局用于存放環境變量的值!然后將修改后的環境變量的值寫入到全局數組中!最后再將環境變量進行同步!只需要調用putenv就可以將環境變量進行修改!
export命令!
還有一個常見的內建命令就是export,那么什么是export呢?export命令就是將我們定義的變量導入到環境變量之中!下面來看一下如何實現我們自己shell的export命令!
代碼如下:
其中我們需要定義一個全局變量的數組用于存放我們的環境變量的值!如果我們使用的是局部變量的話!就會導致每次用戶輸入命令的時候,我們不更新環境變量,其環境變量就會自動消失!這是因為局部變量的局部性!所以定義一個全局變量是最為合適的!但是此代碼也有一個小bug,就是當再導入一個新的環境變量的時候,之前的那個環境變量就會消失!
既然環境變量也能導出了,那么我們總得知道是否真正的將其導出了,這里就得引出了echo命令了,因為此命令也是內建命令,所以也得交給我們的父進程自己執行!下面就來寫一下關于echo命令的代碼!
ehco命令!
其中echo命令簡單分為三個功能!第一個是回顯出退出碼!第二個是顯示出環境變量的值!第三個就是普通的回顯字符串!
對于第一個回顯錯誤碼:我們只需要判斷其分割后的第二個字符串是否是“$”即可!然后根據$后面跟的字符即可判斷出來,如果$后面跟的是"?"字符的話,那就是顯示出退出碼的信息!如果“$”后面跟的不是"?"而是一個字符串!那么就是顯示出其環境變量的值!最后如果連"$"字符都沒有的話,那就是簡單的回顯字符串了!
這就是echo命令實現的簡單邏輯了!
但是我們寫的shell還有大多數功能沒有實現,比如本地變量的存儲,以及重定向的操作!對于本地變量的存儲,我們可以用malloc在堆內申請空間來存儲變量中的值,對于重定向!我們可以利用dup函數進行重定向的操作!下面來看一下簡單的檢查是否有重定向的函數吧!
重定向:
其中SkipSpace是一個宏,其作用就是跳過空格的!我們檢查是否有重定向,順便也能將文件名與命令相分割,然后在execu函數體內進行重定向的操作!
這樣我們的shell也能支持重定向的功能了!
至此我們自己的shell已經初步完成了,它能完成一些簡單的操作!!希望讀完本文,讀者也嘗試寫一下shell的實現!
下面將源碼附在下面!
源碼
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
#define Size 50
#define NUM 1024
#define spint " "
//#define debug 1#define NOredir 0
#define AppendRedir 3
#define InputRedir 1
#define OutputRedir 2#define SkipSpace(pos) do{ while(isspace(*pos)) pos++; }while(0)char *filename=NULL;
int redir=NOredir;
int lastcode=0;
char enval[1024];
char cwd[1024];
// char eni[1024];
const char* getUser()
{char* user=getenv("USER");if(user){return user;}else{return "none";}}const char*getHost()
{char *host=getenv("HOSTNAME");if(host){return host;}else{return "none";}
}char*gethome()
{char *pwd=getenv("PWD");if(pwd){return pwd;}else{return "none";}
}int getcommand(char*command,int n)
{printf("[%s@%s %s]#",getUser(),getHost(),gethome());char*r=fgets(command,n,stdin);if(r==NULL) return 0 ;command[strlen(command)-1]='\0';return 1;
}void commandSplit(char *in,char *out[])
{int argc=0;out[argc++] =strtok(in,spint);while(out[argc++]=strtok(NULL,spint));
#ifdef debug int i=0;for(i=0;out[i];i++){// printf("%d:%s\n",i,out[i]);printf("%s\n",out[i]);}// printf("\n");
#endif
}//只需要將用戶的命令行數組指令傳遞過來即可!
int execute(char* argv[])
{pid_t rit=fork();if(rit==0){int fd=0;if(redir==InputRedir){fd = open(filename, O_RDONLY); // 差錯處理我們不做了dup2(fd, 0);}else if(redir==OutputRedir){fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);dup2(fd, 1);}else if(redir==AppendRedir){fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);dup2(fd, 1);}else{//do nothing}//子進程!用于進程切換!而不是讓父進程bash直接自己運行!//其中進程替換直接用execvp函數即可,因為我們有了用戶的命令行了!execvp(argv[0],argv);exit(0);//如果替換失敗就會退出!負責代表進程替換成功!}else{int status=0;//父進程!只需要等待子進程退出即可!pid_t ret=waitpid(rit,&status,0);if(ret==rit){// printf("wait success\n");lastcode = WEXITSTATUS(status);// printf("%d",lastcode);// return 0;}}return 0;
}void cd(const char*path)
{chdir(path);char tem[1024];getcwd(tem,sizeof(tem));sprintf(cwd,"PWD=%s",tem);putenv(cwd);}
//檢查是否為內建命令并執行!
int dobuildin(char*argv[])
{//cd命令!if(strcmp(argv[0],"cd")==0){char *path=NULL;if(argv[1]==NULL) {path=gethome(); }else path=argv[1];cd(path);return 1;}else if(strcmp(argv[0],"export")==0){ if(argv[1]==NULL) return 1; strcpy(enval,argv[1]);// strcpy(envir,argv[1]);// putenv(envir);//此處需要用全局變量數組來存儲env 因為一旦使用局部變量的時候,會隨著用戶輸入的指令putenv(enval);//此處需要用全局變量數組來存儲env 因為一旦使用局部變量的時候,會隨著用戶輸入的指令//環境變量會消失!return 1;}else if(strcmp(argv[0],"echo")==0){//與系統中的echo保持一致!if(argv[1]==NULL){printf("\n");return 1;}if(*(argv[1])=='$'&&strlen(argv[1])>1){char *val=argv[1]+1;if(strcmp(val,"?")==0){printf("%d\n",lastcode);lastcode=0;}else{char *enval=getenv(val);if(enval) printf("%s\n",enval);else{printf("\n");}// return 1;}return 1;}else{printf("%s\n",argv[1]);return 1;}// return 1;}else if(0){}return 0;
}void checkRedir(char usercommand[], int len)
{// ls -a -l > log.txt// ls -a -l >> log.txtchar *end = usercommand + len - 1;char *start = usercommand;while(end>start){if((*end) == '>'){if(*(end-1) == '>'){*(end-1) = '\0';filename = end+1;SkipSpace(filename);redir = AppendRedir;break;}else{*end = '\0';filename = end+1;SkipSpace(filename);redir = OutputRedir;break;}}else if(*end == '<'){*end = '\0';filename = end+1;SkipSpace(filename); // 如果有空格,就跳過redir = InputRedir;break;}else{end--;}}
}int main()
{while(1){char userCommand[NUM];char* argv[Size];//顯示框架!獲取用戶輸入的指令!int n= getcommand(userCommand,sizeof(userCommand));// if(n==0) continue;// printf("%s")//將用戶的命令進行切割!checkRedir(userCommand,strlen(userCommand));commandSplit(userCommand,argv);//判斷命令是否為內建命令1!int k=dobuildin(argv);if(k) continue;//創建子進程用于進行進程替換!execute(argv);}// return 0;
}