模拟终端实验

目标:实现一个简单的shell,它能够解析和执行用户输入的命令。这个shell支持基本的命令,如ls和cp,内部命令,如cd,以及输入/输出重定向和管道。

parse函数:

解析用户输入的命令,并将命令和参数分开存储在argv数组中。如果用户输入的是内部命令,如exit、help或cd,则直接在这个函数中处理


void parse(char *word, char **argv) {
    int count = 0;
    memset(argv, 0, sizeof(char *) * (64));
    char *lefts = NULL;
    const char *split = " ";  //置分隔符
    while (1) {
        char *p = strtok_r(word, split, &lefts);
        if (p == NULL) {
            break;
        }
        argv[count] = p;//将分割后的字符串存入argv数组
        word = lefts;
        count++;
    }
    if (strcmp(argv[0], "exit") == 0){
        printf("Exiting...Bye\n");
        exit(0);
    }
    else if (strcmp(argv[0], "help") == 0) {
        printf("--------\n");
        printf("God helps those who help themselves\n");
        printf("--------\n");
    }
    else if (strcmp(argv[0], "cd") == 0) { //cd命令
        int ch = chdir(argv[1]); //改变当前目录
        f = 1;
        //则改变当前目录
    }
//    else if (strcmp(argv[0], "history") == 0) { //打印历史命令
//        int i = 0;
//        for (i = 0; i < hist_size; i++) {
//            if (hist[i] != NULL) {
//                printf("%d %s\n", i, hist[i]);
//            }
//        }
//    }
}

trim

解析,去掉多余的空格


char *trim(char *string)
{
    int i = 0;
    int j = 0;
    char *ptr = malloc(sizeof(char *) * strlen(string)); //用于分配指定长度的内存块
    for (i = 0; string[i] != '\0'; i++)
        if (string[i] != ' ') { //如果不是空格
            ptr[j] = string[i];
            j++;
        }
    ptr[j] = '\0';
    string = ptr;
    return string;
}

execute函数:

创建一个子进程来执行用户输入的命令。父进程会等待子进程执行完毕。


void execute(char **argv) {
    pid_t pid;
    int status;
    if ((pid = fork()) < 0) {
        printf("error:fork failed.\n");
        exit(1);
    } else if (pid == 0) {
        // 关闭 stderr
        close(2);
        if (execvp(argv[0], argv) >= 0 || !strcmp(argv[0], "cd")) {} else printf("error:invalid command.\n");
        exit(0);
    } else {
        while (wait(&status) != pid)
            ;
    }
}

execute_file函数:

执行输出重定向。它创建一个子进程来执行命令,并将命令的输出重定向到指定的文件。


void execute_file(char **argv, char *output) {
    pid_t pid;
    int status, flag;
    char *file = NULL;

    // 创建子进程
    if ((pid = fork()) < 0) {
        printf("error:fork failed.\n");
        exit(1);
    } else if (pid == 0) { // 子进程
        // 检查是否有输出重定向符号 ">"
        if (strstr(output, ">") > 0) {
            char *p = strtok_r(output, ">", &file);
            output += 1;
            file = trim(file);
            flag = 1;

            // 保存当前标准输出
            int old_stdout = dup(1);
            // 将标准输出重定向到指定文件
            FILE *fp1 = freopen(output, "w+", stdout);
            // 递归调用 execute_file 执行命令
            execute_file(argv, file);
            // 关闭重定向的文件
            fclose(stdout);
            // 恢复原来的标准输出
            FILE *fp2 = fdopen(old_stdout, "w");
            *stdout = *fp2;
            exit(0);
        }
        // 检查是否有输入重定向符号 "<"
        if (strstr(output, "<") > 0) {
            char *p = strtok_r(output, "<", &file);
            file = trim(file);
            flag = 1;
            // 打开输入文件
            int fd = open(file, O_RDONLY);
            if (fd < 0) {
                printf("No such file or directory.");
                exit(0);
            }
            // 将标准输入重定向到指定文件
            dup2(fd, 0);
            close(fd);
        }
        // 检查是否有管道符号 "|"
        if (strstr(output, "|") > 0) {
            fflush(stdout);
            printf("here");
            fflush(stdout);
            char *p = strtok_r(output, "|", &file);
            file = trim(file);
            flag = 1;
            // 解析管道后面的命令
            char *args[64];
            parse(file, args);
            execute(args); // 执行管道后的命令
        }

        // 如果没有重定向或管道,直接执行命令
        int old_stdout = dup(1);
        FILE *fp1 = freopen(output, "w+", stdout);
        if (execvp(argv[0], argv) < 0)
            printf("error:in exec");
        fclose(stdout);
        FILE *fp2 = fdopen(old_stdout, "w");
        *stdout = *fp2;
        exit(0);
    } else { // 父进程
        // 等待子进程结束
        while (wait(&status) != pid);
    }
}


execute_input函数:

执行输入重定向。它创建一个子进程来执行命令,并将指定文件的内容作为命令的输入。


void execute_input(char **argv, char *output) {
    pid_t pid;
    int fd;
    char *file;
    int flag = 0;
    int status;

    // 创建子进程
    if ((pid = fork()) < 0) {
        printf("error:fork failed\n");
        exit(1);
    } else if (pid == 0) { // 子进程
        // 检查是否有输入重定向符号 "<"
        if (strstr(output, "<") > 0) {
            char *p = strtok_r(output, "<", &file);
            file = trim(file);
            flag = 1;
            fd = open(output, O_RDONLY);
            if (fd < 0) {
                printf("No such file or directory.");
                exit(0);
            }
            output = file;
        }
        // 检查是否有输出重定向符号 ">"
        if (strstr(output, ">") > 0) {
            char *p = strtok_r(output, ">", &file);
            file = trim(file);
            flag = 1;
            fflush(stdout);
            int old_stdout = dup(1);
            // 将标准输出重定向到指定文件
            FILE *fp1 = freopen(file, "w+", stdout);
            execute_input(argv, output);
            fclose(stdout);
            FILE *fp2 = fdopen(old_stdout, "w");
            *stdout = *fp2;
            exit(0);
        }
        // 检查是否有管道符号 "|" 看后面的管道
        if (strstr(output, "|") > 0) {
            char *p = strtok_r(output, "|", &file);
            file = trim(file);
            flag = 1;
            char *args[64];
            parse(file, args);
            int pfds[2];
            pid_t pid, pid2;
            int status, status2;
            pipe(pfds);
            int fl = 0;
            if ((pid = fork()) < 0) {
                printf("error:fork failed\n");
                exit(1);
            }
            if ((pid2 = fork()) < 0) {
                printf("error:fork failed\n");
                exit(1);
            }
            if (pid == 0 && pid2 != 0) {
                close(1);
                dup(pfds[1]);
                close(pfds[0]);
                close(pfds[1]);
                fd = open(output, O_RDONLY);
                close(0);
                dup(fd);
                if (execvp(argv[0], argv) < 0) {
                    close(pfds[0]);
                    close(pfds[1]);
                    printf("error:in exec");
                    fl = 1;
                    exit(0);
                }
                close(fd);
                exit(0);
            } else if (pid2 == 0 && pid != 0 && fl != 1) {
                close(0);
                dup(pfds[0]);
                close(pfds[1]);
                close(pfds[0]);
                if (execvp(args[0], args) < 0) {
                    close(pfds[0]);
                    close(pfds[1]);
                    printf("error:in exec");
                    exit(0);
                }
            } else {
                close(pfds[0]);
                close(pfds[1]);
                while (wait(&status) != pid);
                while (wait(&status2) != pid2);
            }
            exit(0);
        }
        // 打开输入文件并将其重定向到标准输入
        fd = open(output, O_RDONLY);
        close(0);
        dup(fd);
        if (execvp(argv[0], argv) < 0) {
            printf("error:in exec");
        }
        close(fd);
        exit(0);
    } else { // 父进程
        // 等待子进程结束
        while (wait(&status) != pid);
    }
}


execute_pipe函数:

执行管道操作。(多管道同理)

管道只能一端写入,另一端读出,这种模式容易造成混乱,因为父进程和子进程都可以同时写 入,也都可以读出。那么,为了避免这种情况,通常的做法是:

父进程关闭读取的 fd[0],只保留写入的 fd[1]; 子进程关闭写入的 fd[1],只保留读取的 fd[0];

创建两个子进程,一个执行管道左边的命令,一个执行右边的命令。左边命令的输出会被重定向到右边命令的输入。

第一个子进程:

  • 关闭标准输出 (close(1)) 并将其重定向到管道写端 (dup(pfds[1]))。
  • 执行命令 execvp(argv[0], argv),如果失败,打印错误信息并终止进程。

第二个子进程:

  • 检查输入重定向符号 (<) 和输出重定向符号 (>),分别处理输入和输出重定向。
  • 关闭标准输入 (close(0)) 并将其重定向到管道读端 (dup(pfds[0]))。
  • 如果有输出重定向,将标准输出重定向到目标文件。
  • 执行命令 execvp(args[0], args),如果失败,打印错误信息并终止进程。

父进程:

  • 关闭管道的读写端 (close(pfds[0]) 和 close(pfds[1]))。
  • 等待两个子进程结束。

void execute_pipe(char **argv, char *output) {
    int pfds[2], flag, old_stdout;
    char *file;
    pid_t pid, pid2;
    int status, status2;
    char *args[64];
    int fl = 0;
    pipe(pfds);  // 创建管道

    // 创建第一个子进程
    if ((pid = fork()) < 0) exit(1);
    // 创建第二个子进程
    if ((pid2 = fork()) < 0) exit(1);

    // 第一个子进程
    if (pid == 0 && pid2 != 0) {
        close(1);             // 关闭标准输出
        dup(pfds[1]);         // 将管道写端复制到标准输出
        close(pfds[0]);       // 关闭管道读端
        close(pfds[1]);       // 关闭管道写端

        // 执行第一个命令
        if (execvp(argv[0], argv) < 0) {
            close(pfds[0]);
            close(pfds[1]);
            printf("error:in exec");
            fl = 1;
            kill(pid2, SIGUSR1);  // 通知第二个子进程
            exit(0);
        }
    } 
    // 第二个子进程
    else if (pid2 == 0 && pid != 0) {
        if (fl == 1) exit(0);

        // 处理输入重定向
        if (strstr(output, "<") > 0) {
            char *p = strtok_r(output, "<", &file);
            file = trim(file);
            flag = 1;
            parse(output, args);   // 将 output 分割到 args 数组
            execute_input(args, file);
            close(pfds[0]);
            close(pfds[1]);
            exit(0);
        }

        // 处理输出重定向
        int blah = 0;
        if (strstr(output, ">") > 0) {
            char *p = strtok_r(output, ">", &file);
            file = trim(file);
            flag = 1;
            parse(output, args);
            blah = 1;
        } else {
            parse(output, args);
        }

        close(0);            // 关闭标准输入
        dup(pfds[0]);        // 将管道读端复制到标准输入
        close(pfds[1]);      // 关闭管道写端
        close(pfds[0]);      // 关闭管道读端

        // 如果有输出重定向,将标准输出重定向到目标文件
        if (blah == 1) {
            old_stdout = dup(1);
            FILE *fp1 = freopen(file, "w+", stdout);
        }

        // 执行第二个命令
        if (execvp(args[0], args) < 0) {
            fflush(stdout);
            printf("error:in exec %d", pid);
            kill(pid, SIGUSR1);
            close(pfds[0]);
            close(pfds[1]);
        }
        fflush(stdout);

        // 如果输出重定向,恢复标准输出
        if (blah == 1) {
            fclose(stdout);
            FILE *fp2 = fdopen(old_stdout, "w");
            *stdout = *fp2;
        }
    } 
    // 父进程
    else {
        close(pfds[0]);      // 关闭管道读端
        close(pfds[1]);      // 关闭管道写端
        while (wait(&status) != pid);
        while (wait(&status2) != pid2);
    }
}


main函数:

主循环,不断地读取用户输入的命令,解析命令,然后执行命令


int main() {
    char line[1024]; //用于存储用户输入的命令
    char *argv[64]; //用于存储解析后的命令
    char *args[64];
    char *left; //用于存储管道符左边的命令
    size_t size = 0; //用于存储用户输入的命令的长度
    char ch; //用于存储用户输入的命令
    int count = 0; //用于存储命令的个数
    char *tri; //用于存储去掉空格后的命令
    char *second; //用于存储管道符右边的命令
    char *file; //用于存储重定向的文件
    for (int i = 0; i < hist_size; i++) {
        hist[i] = (char *) malloc(150);
    }
    while (1) {
        count = 0;
        int flag = 0;
        char *word = NULL;
        char *dire[] = {"pwd"}; 
        fflush(stdout);
        char hostname[1024];
        hostname[1023] = '\0';
        gethostname(hostname, 1023);

        struct passwd *pw;
        pw = getpwuid(geteuid());
        char* username = pw->pw_name;
        char cwd[1024];
        getcwd(cwd, sizeof(cwd));
        printf("\n\033[34m(%s@%s:YoSHELL)\033[0m %s \n", username, hostname, cwd);
        fflush(stdout);
        printf(">>");
        int len = getline(&word, &size, stdin);
        if (*word == '\n')
            continue;
        word[len - 1] = '\0';
        char *file = NULL;
        int i = 0;
        char *temp = (char *) malloc(150);
        strcpy(temp, word);
        parse(temp, argv);

        strcpy(hist[(head + 1) % hist_size], word);   //存储历史记录
        head = (head + 1) % hist_size;
        filled = filled + 1;

        for (i = 0; word[i] != '\0'; i++) {

            if (word[i] == '>') {
                char *p = strtok_r(word, ">", &file);
                file = trim(file);
                flag = 1;
                break;
            } else if (word[i] == '<') {
                char *p = strtok_r(word, "<", &file);
                file = trim(file);
                flag = 2;
                break;
            } else if (word[i] == '|') {
                char *p = strtok_r(word, "|", &left);
                flag = 3;
                break;
            }
        }
        if (strcmp(word, "exit") == 0) {
            exit(0);
        }

        if (flag == 1) {
            parse(word, argv);
            execute_file(argv, file);
        } else if (flag == 2) {
            parse(word, argv);
            execute_input(argv, file);
        } else if (flag == 3) {
            char *argp[64];
            char *output, *file;
            if (strstr(left, "|") > 0) { //多个管道
                char *p = strtok_r(left, "|", &file);
                parse(word, argv);
                parse(left, args);
                parse(file, argp);
                execute_pipe2(argv, args, argp);
            } else {
                parse(word, argv);
                execute_pipe(argv, left);
            }
        } else {
            // 单独处理内部命令,使它们只执行一次
            if (strcmp(argv[0], "cd") == 0) {
            } else if (strcmp(argv[0], "help") == 0) {
            } else {
                parse(word, argv);
                execute(argv);
            }
        }
    }
}


分类:

更新时序:

笺評 (issue)