用C語言實現簡易的shell程式,支援多重管道及重定向

木風feng發表於2018-05-13

1 簡介

用C語言實現的一個簡易的shell,能夠接受使用者輸入的命令並執行操作,支援多重管道及重定向。
程式執行後,會模擬shell用綠色字型顯示當前的使用者名稱、主機名和路徑,等待使用者輸入命令。程式逐次讀取使用者輸入的指令後,將指令按空格拆分成多個字串命令,然後判斷該命令的型別。若命令有誤,則用紅色字型列印出錯誤資訊。

  • 若命令為exit,則呼叫自定義的exit函式,向該程式程式傳送terminal訊號結束該程式。
  • 若命令為cd,則判斷引數,呼叫chdir()函式修改當前的路徑,並返回相應的結果。若修改成功,則使用getcwd()函式更新當前路徑。
  • 若為其它命令,則先判斷是否有合法的管道。若有管道,則在子程式中執行管道符號前面的命令,父程式等待子程式結束後,遞迴處理管道符號後面的命令。若沒有管道,則直接執行命令。在執行命令的時候,先判斷該命令是否存在,以及是否有合法的重定向,再使用execvp()執行相應的操作。

2 功能

  • 顯示當前使用者名稱、主機名和工作路徑
  • exit命令
  • cd命令
  • 判斷命令是否存在
  • 執行外部命令
  • 實現輸入、輸出重定向
  • 遞迴實現多重管道

3 效果展示

3.1 啟動myshell

啟動myshell

圖中第一行為系統shell,為了與系統區分開,我將自定義的shell的預設資訊顯示全部設為綠色。由於該路徑是個連結,連結到/mnt/g/os_homework/myshell,因此顯示出來的路徑是實際路徑。

3.2 執行cd命令

執行cd命令

cd命令的引數可以是相對路徑,也可以是絕對路徑。當引數出錯時,會根據情況用紅色字型列印出相應的錯誤資訊。

3.3 執行外部命令

執行外部命令

圖中演示了“ls -al”、“rm”,以及自定義的可執行程式sum。sum程式要求輸入一個整數n,然後求1~n的和。當命令不存在時,返回錯誤資訊。

3.4 重定向

重定向

圖中展示了輸入、輸出重定向,程式還會判斷重定向是否合法。

3.5 管道

管道

圖中展示了多重管道的演示結果,管道與重定向也可以混用。

3.6 exit命令

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程式的相關內容啦,如果有發現什麼問題,歡迎大家一起探討。

相關文章