用C語言實現簡易的shell程式,支援多重管道及重定向
1 簡介
用C語言實現的一個簡易的shell,能夠接受使用者輸入的命令並執行操作,支援多重管道及重定向。
程式執行後,會模擬shell用綠色字型顯示當前的使用者名稱、主機名和路徑,等待使用者輸入命令。程式逐次讀取使用者輸入的指令後,將指令按空格拆分成多個字串命令,然後判斷該命令的型別。若命令有誤,則用紅色字型列印出錯誤資訊。
- 若命令為exit,則呼叫自定義的exit函式,向該程式程式傳送terminal訊號結束該程式。
- 若命令為cd,則判斷引數,呼叫chdir()函式修改當前的路徑,並返回相應的結果。若修改成功,則使用getcwd()函式更新當前路徑。
- 若為其它命令,則先判斷是否有合法的管道。若有管道,則在子程式中執行管道符號前面的命令,父程式等待子程式結束後,遞迴處理管道符號後面的命令。若沒有管道,則直接執行命令。在執行命令的時候,先判斷該命令是否存在,以及是否有合法的重定向,再使用execvp()執行相應的操作。
2 功能
- 顯示當前使用者名稱、主機名和工作路徑
- exit命令
- cd命令
- 判斷命令是否存在
- 執行外部命令
- 實現輸入、輸出重定向
- 遞迴實現多重管道
3 效果展示
3.1 啟動myshell
圖中第一行為系統shell,為了與系統區分開,我將自定義的shell的預設資訊顯示全部設為綠色。由於該路徑是個連結,連結到/mnt/g/os_homework/myshell,因此顯示出來的路徑是實際路徑。
3.2 執行cd命令
cd命令的引數可以是相對路徑,也可以是絕對路徑。當引數出錯時,會根據情況用紅色字型列印出相應的錯誤資訊。
3.3 執行外部命令
圖中演示了“ls -al”、“rm”,以及自定義的可執行程式sum。sum程式要求輸入一個整數n,然後求1~n的和。當命令不存在時,返回錯誤資訊。
3.4 重定向
圖中展示了輸入、輸出重定向,程式還會判斷重定向是否合法。
3.5 管道
圖中展示了多重管道的演示結果,管道與重定向也可以混用。
3.6 exit命令
程式接收到terminal訊號後,退出。
4 關鍵程式碼
扯了這麼多,總得講一下程式碼吧。
程式程式碼已上傳到GitHub中~~
4.1 獲取使用者名稱、主機名及當前工作路徑
void getUsername() { // 獲取當前登入的使用者名稱
struct passwd* pwd = getpwuid(getuid());
strcpy(username, pwd->pw_name);
}
void getHostname() { // 獲取主機名
gethostname(hostname, BUF_SZ);
}
int getCurWorkDir() { // 獲取當前的工作目錄
char* result = getcwd(curPath, BUF_SZ);
if (result == NULL)
return ERROR_SYSTEM;
else return RESULT_NORMAL;
}
4.2 以空格分割命令
int splitCommands(char command[BUF_SZ]) { // 以空格分割命令, 返回分割得到的字串個數
int num = 0;
int i, j;
int len = strlen(command);
for (i=0, j=0; i<len; ++i) {
if (command[i] != ' ') {
commands[num][j++] = command[i];
} else {
if (j != 0) {
commands[num][j] = '\0';
++num;
j = 0;
}
}
}
if (j != 0) {
commands[num][j] = '\0';
++num;
}
return num;
}
其中字串陣列commands的全域性變數,儲存分割後的命令。
4.3 執行exit命令
int callExit() { // 傳送terminal訊號退出程式
pid_t pid = getpid();
if (kill(pid, SIGTERM) == -1)
return ERROR_EXIT;
else return RESULT_NORMAL;
}
4.4 執行cd命令
int callCd(int commandNum) { // 執行cd命令
int result = RESULT_NORMAL;
if (commandNum < 2) {
result = ERROR_MISS_PARAMETER;
} else if (commandNum > 2) {
result = ERROR_TOO_MANY_PARAMETER;
} else {
int ret = chdir(commands[1]);
if (ret) result = ERROR_WRONG_PARAMETER;
}
return result;
}
4.5 判斷命令是否存在
Linux系統中,判斷命令是否存在有多種方法。本程式使用”command -v xxx”來判斷命令xxx是否存在。若命令存在,則會返回該命令的路徑資訊;否則,不會返回資訊。因此可以用這一點來判斷命令是否存在。
程式中使用管道,在子程式中將程式執行後的輸出重定向到輸出檔案識別符號,在父程式中將輸入重定向到輸入檔案識別符號,讀取子程式返回的資訊。若父程式讀取的第一個字元就是EOF,則表示子程式沒有返回資訊,意味著命令不存在。執行完畢後,還原輸入輸出重定向。
int isCommandExist(const char* command) { // 判斷指令是否存在
if (command == NULL || strlen(command) == 0) return FALSE;
int result = TRUE;
int fds[2];
if (pipe(fds) == -1) {
result = FALSE;
} else {
/* 暫存輸入輸出重定向標誌 */
int inFd = dup(STDIN_FILENO);
int outFd = dup(STDOUT_FILENO);
pid_t pid = vfork();
if (pid == -1) {
result = FALSE;
} else if (pid == 0) {
/* 將結果輸出重定向到檔案識別符號 */
close(fds[0]);
dup2(fds[1], STDOUT_FILENO);
close(fds[1]);
char tmp[BUF_SZ];
sprintf(tmp, "command -v %s", command);
system(tmp);
exit(1);
} else {
waitpid(pid, NULL, 0);
/* 輸入重定向 */
close(fds[1]);
dup2(fds[0], STDIN_FILENO);
close(fds[0]);
if (getchar() == EOF) { // 沒有資料,意味著命令不存在
result = FALSE;
}
/* 恢復輸入、輸出重定向 */
dup2(inFd, STDIN_FILENO);
dup2(outFd, STDOUT_FILENO);
}
}
return result;
}
4.6 執行外部命令 ——callCommand()函式
該程式是給主函式呼叫的,引數是命令的長度,主要作用是建立子程式,在子程式中呼叫callCommandWithPipe()函式,該函式可以處理包含管道的命令,父程式獲取子程式的返回碼,並返回給主函式。
int callCommand(int commandNum) { // 給使用者使用的函式,用以執行使用者輸入的命令
pid_t pid = fork();
if (pid == -1) {
return ERROR_FORK;
} else if (pid == 0) {
/* 獲取標準輸入、輸出的檔案識別符號 */
int inFds = dup(STDIN_FILENO);
int outFds = dup(STDOUT_FILENO);
int result = callCommandWithPipe(0, commandNum);
/* 還原標準輸入、輸出重定向 */
dup2(inFds, STDIN_FILENO);
dup2(outFds, STDOUT_FILENO);
exit(result);
} else {
int status;
waitpid(pid, &status, 0);
return WEXITSTATUS(status);
}
}
4.7 可處理多重管道的callCommandWithPipe()函式
因為要遞迴處理多重管道,因此將引數設為左閉右開的指令區間。先判斷有沒有管道符號,若沒有,則直接呼叫callCommandWithRedi()函式去執行命令,該函式可以處理包含重定向資訊的命令。若有管道符號,則先判斷管道符號後續是否有指令,若沒有,則返回錯誤資訊,若有,則執行。
執行時,先啟動管道,在子程式中執行管道符號前半部分的命令,並返回執行後的狀態結果。父程式等待子程式退出後,獲取子程式的返回碼。若子程式沒有正常執行,則讀取子程式輸出的錯誤資訊,並列印到控制檯。否則,遞迴執行管道符號後半部分的命令,並將結果返回給主函式。
int callCommandWithPipe(int left, int right) { // 所要執行的指令區間[left, right),可能含有管道
if (left >= right) return RESULT_NORMAL;
/* 判斷是否有管道命令 */
int pipeIdx = -1;
for (int i=left; i<right; ++i) {
if (strcmp(commands[i], COMMAND_PIPE) == 0) {
pipeIdx = i;
break;
}
}
if (pipeIdx == -1) { // 不含有管道命令
return callCommandWithRedi(left, right);
} else if (pipeIdx+1 == right) { // 管道命令'|'後續沒有指令,引數缺失
return ERROR_PIPE_MISS_PARAMETER;
}
/* 執行命令 */
int fds[2];
if (pipe(fds) == -1) {
return ERROR_PIPE;
}
int result = RESULT_NORMAL;
pid_t pid = vfork();
if (pid == -1) {
result = ERROR_FORK;
} else if (pid == 0) { // 子程式執行單個命令
close(fds[0]);
dup2(fds[1], STDOUT_FILENO); // 將標準輸出重定向到fds[1]
close(fds[1]);
result = callCommandWithRedi(left, pipeIdx);
exit(result);
} else { // 父程式遞迴執行後續命令
int status;
waitpid(pid, &status, 0);
int exitCode = WEXITSTATUS(status);
if (exitCode != RESULT_NORMAL) { // 子程式的指令沒有正常退出,列印錯誤資訊
char info[4096] = {0};
char line[BUF_SZ];
close(fds[1]);
dup2(fds[0], STDIN_FILENO); // 將標準輸入重定向到fds[0]
close(fds[0]);
while(fgets(line, BUF_SZ, stdin) != NULL) { // 讀取子程式的錯誤資訊
strcat(info, line);
}
printf("%s", info); // 列印錯誤資訊
result = exitCode;
} else if (pipeIdx+1 < right){
close(fds[1]);
dup2(fds[0], STDIN_FILENO); // 將標準輸入重定向到fds[0]
close(fds[0]);
result = callCommandWithPipe(pipeIdx+1, right); // 遞迴執行後續指令
}
}
return result;
}
4.8 可處理重定向的callCommandWithRedi()函式
函式首先判斷指令是否存在。若指令不存在,則直接返回錯誤資訊,不需要再繼續執行。
當指令存在時,先判斷是否有合法的重定向,再進行下一步處理。同樣,在子程式中執行程式。C語言對於重定向的處理,可以使用檔案讀寫的方式,也可以使用其它。為了使得程式碼比較簡潔,我使用freopen()這一神器來實現輸入、輸出重定向。隨後使用execvp()函式執行命令。若執行失敗,則會把錯誤編號存在errno中,返回errno;若執行成功,則會返回0。
父程式等待子程式結束,並讀取子程式的返回碼。若不為0,則使用strerror()函式獲取對應的錯誤資訊,並列印到控制檯。
int callCommandWithRedi(int left, int right) { // 所要執行的指令區間[left, right),不含管道,可能含有重定向
if (!isCommandExist(commands[left])) { // 指令不存在
return ERROR_COMMAND;
}
/* 判斷是否有重定向 */
int inNum = 0, outNum = 0;
char *inFile = NULL, *outFile = NULL;
int endIdx = right; // 指令在重定向前的終止下標
for (int i=left; i<right; ++i) {
if (strcmp(commands[i], COMMAND_IN) == 0) { // 輸入重定向
++inNum;
if (i+1 < right)
inFile = commands[i+1];
else return ERROR_MISS_PARAMETER; // 重定向符號後缺少檔名
if (endIdx == right) endIdx = i;
} else if (strcmp(commands[i], COMMAND_OUT) == 0) { // 輸出重定向
++outNum;
if (i+1 < right)
outFile = commands[i+1];
else return ERROR_MISS_PARAMETER; // 重定向符號後缺少檔名
if (endIdx == right) endIdx = i;
}
}
/* 處理重定向 */
if (inNum == 1) {
FILE* fp = fopen(inFile, "r");
if (fp == NULL) // 輸入重定向檔案不存在
return ERROR_FILE_NOT_EXIST;
fclose(fp);
}
if (inNum > 1) { // 輸入重定向符超過一個
return ERROR_MANY_IN;
} else if (outNum > 1) { // 輸出重定向符超過一個
return ERROR_MANY_OUT;
}
int result = RESULT_NORMAL;
pid_t pid = vfork();
if (pid == -1) {
result = ERROR_FORK;
} else if (pid == 0) {
/* 輸入輸出重定向 */
if (inNum == 1)
freopen(inFile, "r", stdin);
if (outNum == 1)
freopen(outFile, "w", stdout);
/* 執行命令 */
char* comm[BUF_SZ];
for (int i=left; i<endIdx; ++i)
comm[i] = commands[i];
comm[endIdx] = NULL;
execvp(comm[left], comm+left);
exit(errno); // 執行出錯,返回errno
} else {
int status;
waitpid(pid, &status, 0);
int err = WEXITSTATUS(status); // 讀取子程式的返回碼
if (err) { // 返回碼不為0,意味著子程式執行出錯,用紅色字型列印出錯資訊
printf("\e[31;1mError: %s\n\e[0m", strerror(err));
}
}
return result;
}
5 結尾
以上就是簡易shell程式的相關內容啦,如果有發現什麼問題,歡迎大家一起探討。
相關文章
- C語言實現一個簡易的Hash table(7)C語言
- linux的I/O重定向和管道的檔案描述符運用,及shell如何實現多執行緒?Linux執行緒
- 簡易C語言文法分析C語言
- c語言實用小程式C語言
- 用JavaScript實現一門程式語言 2 (λanguage語言簡介)JavaScript
- C語言:一種高效、易學的程式語言C語言
- 自己動手實現 Shell 多程式管道符
- 模擬實現簡易版shell
- 易語言實現一個登入程式
- C語言簡易三子棋C語言
- C語言如何實現繼承及容器C語言繼承
- Java 語言實現簡易版掃碼登入Java
- C語言簡單程式碼程式C語言
- 邏輯式程式語言極簡實現(使用C#) - 1. 邏輯式程式語言介紹C#
- 邏輯式程式語言極簡實現(使用C#) - 4. 程式碼實現(完結)C#
- Linux雜談: 實現一種簡單實用的執行緒池(C語言)Linux執行緒C語言
- C語言 二維陣列實現三子棋的思路及程式碼C語言陣列
- 真的可以,用C語言實現物件導向程式設計OOPC語言物件程式設計OOP
- [譯]用javascript實現一門程式語言-語言構想JavaScript
- 中文程式語言——易語言,到底是用來幹什麼的?易語言值得學習嗎?易語言的優勢有什麼?
- 一個小小的 Shell 管道符,內部實現可真不簡單!
- C語言如何實現泛型程式設計?C語言泛型程式設計
- 支援向量機python實現(簡易版)Python
- 使用 R 語言實現簡單的文字識別程式
- 掃雷--C語言實現C語言
- c語言實現階乘C語言
- 簡易版管道模式模式
- 一、程式語言簡介與C++C++
- 聊聊C語言/C++—程式和程式語言C語言C++
- WPF多語言支援:簡單靈活的動態切換,讓你的程式支援多國語言
- C語言實現MD5加密,竟如此簡單!C語言加密
- C語言,實現數字譜到簡譜的轉換(二)C語言
- C語言實現的一個簡單的猜數小遊戲C語言遊戲
- 實驗1_C語言輸入輸出和簡單程式應用程式設計C語言程式設計
- 教你C語言實現通訊錄的詳細程式碼C語言
- 邏輯式程式語言極簡實現(使用C#) - 3. 執行原理C#
- [譯] 用javascript實現一門程式語言-前言JavaScript
- C語言__LINE__實現原理C語言