用C语言实现简易的shell程序,支持多重管道及重定向

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程序的相关内容啦,如果有发现什么问题,欢迎大家一起探讨。


版权声明:本文为feng964497595原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。