1. 目前实现的功能

  • 打印仿Shell的命令行提示符,有当前的工作目录
  • 基本Shell命令的执行
  • 重定向符的执行
  • 多管道符的执行
  • 内建命令:cd

2. 动图预览

3. 源码

#include <stdio.h>                                                                                                             
#include <stdlib.h>  // getenv()
#include <unistd.h>  // usleep() duip2() fork()
#include <string.h>  // memset()
#include <ctype.h>  // isspace()
#include <fcntl.h>  // open()
#include <sys/wait.h>  // waitpid()

// 打印命令行提示符
void print_prompt() {
    // 登陆用户名通过环境变量 LOGNAME 得到
    char* username = getenv("LOGNAME");

    // 当前路径使用popen执行命令行命令pwd来获得
    char* pwd = (char *)malloc(sizeof(char) * 1024);
    FILE* fp = popen("pwd", "r");
    fscanf(fp, "%s", pwd);

    printf("%s@%s$ ", username, pwd);
    fflush(stdout);

    free(pwd);
}

// 获取命令行输入
char* get_command() {
    char* input = (char *)malloc(sizeof(char) * 1024);
    memset(input, 0, 1024);

    // %[^\n] : 从标准输入缓冲区取数据直到遇到\n
    // %*c : 从缓冲区取出一个字符并丢掉
    //       目的是消耗掉剩余的\n
    if(scanf("%[^\n]%*c", input) != 1) {  // scanf 返回成功读取到了几个格式化数据,即几个%s之类
        getchar();
    }

    return input;
}

// 解析输入中是否有重定向符
// redirect为输出型参数,0表示没有,1表示覆盖重定向,2表示追加重定向
// 返回值为重定向后的文件名  
char* parse_redirection(char* input, int* redirect) {
    char* ptr = input;
    *redirect = 0;
    char* filename = (char *)malloc(sizeof(char) * 1024);
    memset(filename, 0, 1024);

    while(*ptr != '\0' && *ptr != '>') {
        ptr++;
    }

    if(*ptr == '>') {  // >
        *redirect = 1;
        *ptr++ = '\0';

        if(*ptr == '>') {  // >>
            *redirect = 2;
            *ptr++ = '\0';
        }

        // parse file name
        while(*ptr != '\0' && isspace(*ptr)) {
            ptr++;                            
        }

        if(*ptr == '\0') {  // there is no file name
            *redirect = 0;
            return NULL;
        }
        else {
            filename = ptr;
            while(*ptr != '\0' && !isspace(*ptr)) {
                ptr++;
            }
            *ptr = '\0';
        }

        return filename;
    }
    return NULL;
}

// 解析输入中的命令部分的所有参数,返回二级指针argv,表示所有参数
char** parse_command(char* input) {
    // 申请命令行参数数组,大小为32
    char** argv = (char **)malloc(sizeof(char *) * 32);
    int argc = 0;

    char* ptr = input;
    while(*ptr != '\0') {
        if(isspace(*ptr)) {
            *ptr = '\0';
            ptr++;
        }         
        else {
            argv[argc++] = ptr;

            // 跳过这个参数
            while(*ptr != '\0' && !isspace(*ptr)) {
                ptr++;
            }

            // 此时ptr要么指向一个空白字符,要么'\0'
        }
    }

    argv[argc] = NULL;
    return argv;
}

// 将输入根据管道符分割成不同的命令
int divide_input(char* input, char* commands[]) {
    int cnt = 0;
    char* ptr = input;

    commands[cnt++] = ptr++;
    while(*ptr != '\0') {
        if(*ptr == '|') {
            *ptr++ = '\0';
            commands[cnt++] = ptr;
            continue;
        }
        ptr++;
    }

    return cnt;
}

// 内置命令cd的实现
void built_in_cd(const char* path) {
    if(chdir(path) == -1) {
        perror("chdir error");
    }
}

int main() {
    int ret;  // 承载所有系统调用的返回值

    while(1) {
        // print prompt
        print_prompt();

        // get_command
        char* input = get_command();
        if(strlen(input) == 0) {  // 如果用户只输入了一个回车
            continue;
        }

        // 根据管道符将命令分割成不同的子命令
        char** commands = (char **)malloc(sizeof(char*) * 10);
        int cnt = divide_input(input, commands);  // cnt表示子命令的个数

        // 申请多个不同命令间通信用的管道,暂时一次性申请十个
        int pipefd[10][2] = { { 0 } };
        for(int i = 0; i < cnt - 1; i++) {  // cnt表示子命令的个数,所以只需要cnt-1个管道
            pipe(pipefd[i]);
        }                                                                          

        // 循环cnt次执行所有cnt个子命令
        for(int i = 0; i < cnt; i++) {
            // 解析当前子命令中是否有重定向符,如果有就在函数内部做好重定向的相关操作
            // 可知如果既有管道符又有重定向符,则按我的代码的逻辑最终标准输入或标准输出会
            // 重定向到文件上而不是管道上,最终管道符会失效。
            int redirect;
            char* filename = parse_redirection(commands[i], &redirect);

            // 将当前子命令拆分成不同的命令行参数
            char** argv = parse_command(commands[i]);

            // 处理Shell内建命令,现已实现 cd
            if(!strcmp(argv[0], "cd")) {
                built_in_cd(argv[1]);
                continue;
            }

            // 创建子进程
            pid_t pid = fork();

            if(pid < 0) {
                perror("fork error");
                exit(-1);
            }
            else if(pid == 0) {
                // 子进程中首先处理重定向符
                int fd;
                if(redirect == 1) {
                    fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
                    dup2(fd, 1);                                            
                }
                else if(redirect == 2) {
                    fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
                    dup2(fd, 1);
                }

                // 第二步处理管道符
                if(i != 0) {
                    // 将子进程的标准输入重定向到上一对管道读端
                    ret = dup2(pipefd[i - 1][0], STDIN_FILENO);
                    if(ret == -1) {
                        perror("dup2 error");
                        exit(-1);
                    }

                    // 关闭上一对管道不需要的写端
                    close(pipefd[i - 1][1]);
                }
                if(i != cnt - 1) {
                    // 将子进程的标准输出重定向到这一对管道的写端
                    ret = dup2(pipefd[i][1], STDOUT_FILENO);
                    if(ret == -1) {
                        perror("dup2 error");
                        exit(-1);
                    }

                    // 关闭这一对管道不需要的读端
                    close(pipefd[i][0]);
                }

                // 执行程序替换
                ret = execvp(argv[0], argv);
                if(ret == -1) {
                    perror("execvp error");
                    exit(-1);
                }
            }
            else {
                // 关闭父进程的管道。注意,不能在fork()第一个子进程后就关闭父进程第一对管道,这样做的话
                // 第一个子进程退出后第一对管道就丢失了,而fork()的第二个子进程还需要从第一对管道中读取
                // 输入。所以正确的关闭逻辑应该是在fork()第二个子进程后才关闭第一对管道。这样在父进程中
                // 关闭多余管道的目的是为了让grep等阻塞命令退出,关闭所有管道的写端,grep的读操作就会立
                // 刻退出。
                if(i != 0) {
                    close(pipefd[i - 1][0]);
                    close(pipefd[i - 1][1]);
                }
                waitpid(pid, NULL, 0);
            }
        }
    }
}
Last modification:November 10th, 2019 at 08:11 am