目錄
思路
創建自己的命令行
獲取用戶命令
分割命令
檢查是否是內建命令
cd命令實現
進程程序替換執行程序
總代碼
結語
思路
int main()
{while (1){// 1. 自己的命令行PrintCommandLine();// 2. 獲取用戶命令char command[SIZE];int n = GetUserCommand(command, sizeof(command));if (n <= 0)return 1;// 3. 分割指令SplitCommand(command);// for(int i = 0; gArgv[i]; i++)// printf("[%d]: %s\n", i, gArgv[i]);// 4. 檢查是否是內建命令n = CheckBuildin();if(n) continue;// 5. 進程程序替換執行命令ExecuteCommand();}return 0;
}
上圖中的五個函數,就是我們的思路
首先就是先寫命令行:
就是這個,這個獲取環境變量 + 指針操作 + 打印即可,較為簡單
接著就是需要獲取用戶輸入的指令,并且將其分割,放進數組里面,這里會涉及到strtok函數的使用
最后將我們的函數分割完之后,就是判斷是否是內建命令了
如果是內建命令的話,這里就只實現cd和echo $?,我們就單獨函數實現
如果不是內建命令的話,我們就正常進行程序替換即可
這里最關鍵的點就是程序替換了,因為我們要使用的所有的命令都在磁盤上保存著,這時我們fork創建子進程,并用程序替換將磁盤上待替換的程序加載到內存中并將這個子進程給覆蓋,最終跑起來
創建自己的命令行
#define SkipPath(p) \do \{ \p += strlen(p) - 1; \while (*p != '/') \p--; \} while (0)const char *GetHome()
{const char *home = getenv("HOME");return home;
}const char *GetHostName()
{const char *hostname = getenv("HOSTNAME");return hostname;
}const char *GetCwd()
{const char *cwd = getenv("PWD");return cwd;
}const char *GetUserName()
{const char *name = getenv("USER");return name;
}void PrintCommandLine()
{char line[SIZE];const char *UserName = GetUserName();const char *Cwd = GetCwd();const char *HostName = GetHostName();SkipPath(Cwd);snprintf(line, sizeof line, "[%s@%s %s]:>", UserName, HostName, strlen(Cwd) == 1 ? "/" : Cwd + 1);printf("%s", line);fflush(stdout);
}
這里沒什么好說的,主要就是getenv獲取環境變量,我們的參數就是從環境變量中來
需要講的一點就是do...while(0)那個宏,這里我們如果直接獲取PWD的話,就是所有都打出來:
這個是效果
但是整體是這樣的
所以我們需要將指針移動到最后面那里,然后只顯示最后一段
但是這里為什么用的是宏呢?因為這里是用 c 語言實現的,寫一個函數的話還要二級指針,太麻煩了,宏方便點
而使用do...while(0)是為了形成一個代碼塊,我們可以整體使用,并且在下面調用的時候可以隨便加 “ ; ”,這樣調用起來就和函數一樣了
最終效果如下:
獲取用戶命令
int GetUserCommand(char command[], size_t n)
{char *s = fgets(command, n, stdin);if (s == NULL)return 2;// 處理 \ncommand[strlen(command) - 1] = ZERO;// printf("命令: %s", command);return strlen(command);
}
其實如果是cpp的話,就直接getline了,但是c語言實現的話我們就用 fgets 獲取一行
直接獲取即可,完了之后放進提前開辟并傳進函數里面的數組中
需要說的一點就是,我們用戶輸入的指令看起來是這樣的:
ls -a -l
但其實最后還會加一個回車表示確定,所以實際上就是這樣子的:
ls -a -l\n
綜上,我們就需要將最后一個位置設置為\0,上面的ZERO其實就是\0,只不過設置成了宏而已
分割命令
void SplitCommand(char *command)
{gArgv[0] = strtok(command, SEP);int index = 1;while (gArgv[index++] = strtok(NULL, SEP));// 在最后一次判斷的時候,發現沒有字符串了,strtok 就會返回NULL// 正好讓 gArgv 的最后一個位置變為 NULL,這樣在后面進程程序替換的時候就可以直接用
}
這里其實就是strtok函數的使用技巧
先說說為什么要分割:
我們獲取了程序之后,不是說所有的功能都是我們自己寫,像ls、cd、mkdir等等,這些指令在我們電腦中的磁盤上人家都幫我們寫好了,shell也是調用的這些文件
所以我們要做的,就是在用戶輸入指令的時候,我們能將這些程序加載進來
這里就需要用到進程程序替換了,并且我們現在只有用戶輸入的數據,我們到后面肯定是execvp用起來最好,所以分割完直接放函數里面當參數即可,就實現這個功能了
再來說說 strtok
當你第一次使用strtok的時候,傳入一個數組當參數(第一個參數位置,第二個是分隔符)
當你從第二次開始,第一個參數直接傳NULL,就代表使用你一剛開始用個的那個數組
這也就是為什么一開始傳command數組,后面傳NULL的原因
接著最妙的就是,當strtok檢測到沒有可以分割的了,就會返回NULL,而我們的數組(execvp函數要求)最后一個位置必須以NULL結尾
檢查是否是內建命令
int CheckBuildin()
{int yes = 0;const char *enter_cmd = gArgv[0];if(strcmp(enter_cmd, "cd") == 0){yes = 1;Cd();}else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0){yes = 1;printf("%d\n", lastcode);lastcode = 0;}return yes;
}
直接就是if、else檢查判斷就行,有多少個內建命令就有多少個if、else
cd命令實現
void Cd()
{const char* path = gArgv[1];if(path == NULL) path = GetHome();chdir(path);// 更新環境變量char cwd[SIZE];char temp[SIZE];getcwd(temp, sizeof temp);snprintf(cwd, sizeof(cwd), "PWD=%s", temp);setenv("PWD", cwd, 1);
}
這里主要用到的是chdir函數,ps:chdir支持直接使用 . 或者 ..
先說獲取path,程序走到這里,那么用戶輸入的指令一定是cd,那么就會有像cd ..這樣的指令
那么我們獲取完之后,分割的指令中,第二個就一定是path,直接獲取就好(下標是1)
當然也有可能用戶直接輸入一個cd,后面啥也不跟,我們隨便處理一下,給他返回到家目錄即可
最后我們在chdir之后,還需要更新一下路徑
這里我們先獲取環境變量中的PWD,然后使用snprintf寫入數組中,以PWD:%s的格式
最后直接用setenv函數修改(或者說更新)環境變量即可
當然你也可以在最后使用putenv,這樣的話,我們的cwd數組就需要開辟在全局,不然函數運行結束之后,cwd數組也就沒了,這樣putenv找不到他,就會直接異常終止我們的程序,顯示段錯誤(Segmentation fault)
setenv就不用,這里推薦這個,因為他更現代、好用
進程程序替換執行程序
void ExecuteCommand()
{pid_t id = fork();if (id == 0){// 子進程execvp(gArgv[0], gArgv);exit(errno);}else{// 父進程int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0){if(WIFEXITED(status) == 0){lastcode = WEXITSTATUS(status);printf("exit code: %d, exit signal:%d\n", WEXITSTATUS(status), WTERMSIG(status));}}}
}
就是execvp函數的使用,創建子進程直接替換,然后父進程在外面waitpid阻塞等待即可,也不用父進程干什么事情
總代碼
MyShell.c
點開這個鏈接,里面就是代碼,放在gitee里面了
結語
這篇文章到這里就結束啦!!~( ̄▽ ̄)~*
如果覺得對你有幫助的,可以多多關注一下喔