实现自己的 shell

TASK

打造一个绝无伦比的 xxx-super-shell (xxx 是你的名字),它能实现下面这些功能:

  • 实现 管道 (也就是 |)
  • 实现 输入输出重定向(也就是 < > >>)
  • 实现 后台运行(也就是 &
  • 实现 cd,要求支持能切换到绝对路径,相对路径和支持 cd -
  • 屏蔽一些信号(如 ctrl + c 不能终止)
  • 界面美观
  • 开发过程记录、总结、发布在个人博客中

要求:

  • 不得出现内存泄漏,内存越界等错误
  • 学会如何使用 gdb 进行调试,使用 valgrind 等工具进行检测

Example

xxx@xxx ~ $ ./xxx-super-shell
xxx@xxx ~ $ echo ABCDEF
xxx@xxx ~ $ echo ABCDEF > ./1.txt
xxx@xxx ~ $ cat 1.txt
xxx@xxx ~ $ ls -t >> 1.txt
xxx@xxx ~ $ ls -a -l | grep abc | wc -l > 2.txt
xxx@xxx ~ $ python < ./1.py | wc -c
xxx@xxx ~ $ mkdir test_dir
xxx@xxx ~/test_dir $ cd test_dir
xxx@xxx ~ $ cd -
xxx@xxx ~/test_dir $ cd -
xxx@xxx ~ $ ./xxx-super-shell # shell 中嵌套 shell
xxx@xxx ~ $ exit
xxx@xxx ~ $ exit

注:
示例请参考 BashZsh 命令


代码

main

int main()
{
    //屏蔽信号
    signal(SIGINT, SIG_IGN);//用户在终端上按下 Ctrl-C 键触发的中断信号
    signal(SIGHUP, SIG_IGN);//由终端关闭或会话结束时触发的挂起信号
    while (1)
    {
        memset(has, 0, sizeof(has_));
        prompt(); // 打印提示符$
        getcwd(path, MAX_PATH_LENGTH);
        char cmd[MAX_COMMAND_LENGTH]; // 存一整行命令
        fgets(cmd, MAX_COMMAND_LENGTH, stdin);
        Split_command(cmd);
        has_(cmd);
        parse_command(argv, argc);
        printf("\n");
    }
}

打印提示符

void prompt(void)
{
    // 打印提示符和当前路径
    printf("zmr-super-shell:%s$ ", getcwd(path, MAX_PATH_LENGTH));
    fflush(stdout); // 清空标准输出缓冲区,确保之前的输出内容被立即写入到输出设备中,如果不清空缓冲区,可能会导致输出的内容不及时或不完整
}

分割命令 strtok()

void Split_command(char *cmd)
{
    argv[0] = strtok(cmd, SEP);
    int i = 1;
    while (argv[i] = strtok(NULL, SEP))
    {
        i++;
    }
    argc = i;          // 命令数
    argv[argc] = NULL; // argv[argc-1]存最后一个命令
}

看看有啥命令参数

void has_(char *cmd)
{
    if (argv[0] == NULL)
    {
        return;
    }

    // 内部命令
    if (strcmp(argv[0], "cd") == 0)
    {
        has[cd_]++;
    }
    else if (strcmp(argv[0], "echo") == 0)
    {
        has[echo_]++;
    }
    else if (strcmp(argv[0], "exit") == 0)
    {
        has[exit_]++;
    }

    // 外部命令
    int i;
    for (i = 0; i < argc; i++)
    {
        if (strcmp(argv[i], ">") == 0)
        {
            has[output]++;
        }
        if (strcmp(argv[i], ">>") == 0)
        {
            has[append_output]++;
        }
        if (strcmp(argv[i], "<") == 0)
        {
            has[input]++;
        }
        if (strcmp(argv[i], "&") == 0)
        {
            has[Background_running]++;
        }
        if (strcmp(argv[i], "&") == 0)
        {
            has[pipeline]++;
        }
        if (strcmp(argv[i], "ls") == 0)
        {
            has[ls_]++;
        }
    }
}

解析命令

  • 根据有的东东(><>>|cd&)来调用相应的函数
void parse_command(char *argv[], int argc)
{
    // 内部
    if (has[cd_])
    {
        mycd(argv);
    }
    if (has[echo_])
    {
        // 好像不需要?
    }
    if (has[exit_])
    {
        exit(0);
    }

    // 外部
    if (has[output])
    {
        myOutRe(argv);
    }
    if (has[append_output])
    {
        myOutRePlus(argv);
    }
    if (has[input])
    {
        myInRe(argv);
    }
    if (has[pipeline])
    {
        myPipe(argv, argc);
    }
    if (has[ls_])
    {
        myls(argv);
    }
}

cd

void mycd(char *argv[])
{
    // 处理 cd - 命令,切换到上一个工作路径
    if (strcmp(argv[1], "-") == 0)
    {
                                 // getenv()函数是一个C标准库中的函数,用于获取环境变量列表中相应变量的值
        chdir(getenv("OLDPWD")); // 将当前工作目录更改为之前的工作目录。使用了getenv()获取OLDPWD的值,然后将其作为参数传递给chdir()
                                 // “OLDPWD”,它是一个Shell内置环境变量,表示之前的工作目录
        getcwd(path, MAX_PATH_LENGTH);
        printf("%s\n", path); // 要先把切换后的路径打印一下
    }
    // 处理 cd 命令没有参数或cd ~ 的情况,切换到用户主目录
    else if (strcmp(argv[1], "~") == 0 || argv[1] == NULL)
    {
        chdir(getenv("HOME"));
        getcwd(path, MAX_PATH_LENGTH);
    }
    // 处理普通 cd 命令,切换到指定路径
    else
    {
        chdir(argv[1]);
        getcwd(path, MAX_PATH_LENGTH);
    }

    return;
}

>

void myOutRe(char *argv[])
{
    char file_name[MAX];
    char *arg[MAX_ARGS];
    int i;
    for (i = 0; i < argc; i++)
    {
        arg[i] = argv[i];
        if (strcmp(argv[i], ">") == 0)
        {
            strcpy(file_name, argv[i + 1]); // 第i+1个就是文件名了
            arg[i] = NULL;                  // 把第i个存的>给置为NULL
            break;
        }
    }

    int cmd_num = i;                                              // arg中的命令个数,ls -a > 1.txt记的命令个数是2
    int fdout = dup(1);                                           // 让标准输出获取一个新的文件描述符,( 标准输入、标准输出和标准错误输出默认分别使用文件描述符 0、1 和 2 )
    int fd = open(file_name, O_WRONLY | O_CREAT | O_TRUNC, 0666); // 函数调用打开了一个文件名为file_name的文件,打开模式为只写模式(O_WRONLY),如果文件不存在则创建这个文件(O_CREAT),如果文件已经存在则将其长度截断为零(O_TRUNC),文件权限设置为 0666
    dup2(fd, 1);                                                  // 将文件描述符 fd 复制到标准输出的文件描述符 1 中,实现了标准输出的重定向。此时,所有输出到标准输出的内容都会被重定向到打开的文件中

    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork()");
        exit(1);
    }
    else if (pid == 0)
    {
        if (has[pipeline])
        { // 有管道时
            myPipe(arg, cmd_num);
        }
        else
        {
            execvp(arg[0], arg);
            perror("execvp()");
            exit(1);
        }
    }
    else
    {
        waitpid(pid, NULL, 0); // 等待指定进程 pid 结束,并在子进程完成后立即返回。其中,status 参数被设置为 NULL,表示不关心子进程的退出状态,而 options 参数被设置为 0,表示没有特殊选项。
    }

    dup2(fdout, 1); // 将标准输出文件描述符恢复到原来的设置,以确保后续输出能够正常显示在终端上
    close(fd);
}

>>

void myOutRePlus(char *argv[])
{
    char file_name[MAX];
    char *arg[MAX_ARGS];
    int i;
    for (i = 0; i < argc; i++)
    {
        arg[i] = argv[i];
        if (strcmp(argv[i], ">>") == 0)
        {
            strcpy(file_name, argv[i + 1]);
            arg[i] = NULL;
            break;
        }
    }

    int cmd_num = i;
    int fdout = dup(1);
    int fd = open(file_name, O_WRONLY | O_CREAT | O_APPEND, 0666); // O_APPEND :在写入文件时将数据追加到文件末尾
    dup2(fd, 1);

    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork()");
        exit(1);
    }
    else if (pid == 0)
    {
        if (has[pipeline])
        { // 有管道时
            myPipe(arg, cmd_num);
        }
        else
        {
            execvp(arg[0], arg);
            perror("execvp()");
            exit(1);
        }
    }
    else
    {
        waitpid(pid, NULL, 0);
    }

    dup2(fdout, 1);
    close(fd);
}

<

void myInRe(char *argv[])
{
    char file_name[MAX];
    char *arg[MAX_ARGS];
    int i;
    for (i = 0; i < argc; i++)
    {
        arg[i] = argv[i];
        if (strcmp(argv[i], ">>") == 0)
        {
            strcpy(file_name, argv[i + 1]);
            arg[i] = NULL;
            break;
        }
    }

    int cmd_num = i;
    int fdin = dup(0);                        // 标准输入使用文件描述符 0
    int fd = open(file_name, O_RDONLY, 0666); // O_RDONLY :表示以只读模式打开文件
    dup2(fd, 0);

    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork()");
        exit(1);
    }
    else if (pid == 0)
    {
        if (has[pipeline])
        { // 有管道时
            myPipe(arg, cmd_num);
        }
        else
        {
            execvp(arg[0], arg);
            perror("execvp()");
            exit(1);
        }
    }
    else
    {
        waitpid(pid, NULL, 0);
    }

    dup2(fdin, 0);
    close(fd);
}

管道 |

思路

该函数实现了管道命令的功能。它将命令分割成多个子命令,并创建对应的子进程。子进程中对管道的读写端进行操作,连接多个子进程,实现管道的功能。具体解析如下:

  1. 首先统计命令中的管道数量和子命令数量,分别置于变量pipe_num和cmd_num之中。

  2. 以二维数组cmd[cmd_num][MAX_ARGS]的形式存储分割后的子命令。分割规则如下:

    • 对于第一个子命令,从argv[0]开始读取,一直到第一个“|”为止。

    • 对于中间的子命令,从前一个“|”之后的一个命令开始读取,一直到下一个“|”为止。

    • 对于最后一个子命令,从最后一个“|”之后的一个命令开始读取,一直到argv[argc-1]为止。

  3. 根据管道数量创建对应数量的管道,并将管道的读写端分别存储在fd[pipe_num][2]数组中。

  4. 创建多个子进程。

    • 对于第一个子进程,重定向其标准输出到第一个管道的写入端,关闭其他管道的读写端。
    • 对于最后一个子进程,重定向其标准输入到最后一个管道的读取端,关闭其他管道的读写端。
    • 对于中间的子进程,重定向其标准输入到前一个管道的读端,重定向其标准输出到后一个管道的写端,关闭除前后两个管道以外的其他管道的读写端。
  5. 在子进程中执行对应的命令。

  6. 在父进程中关闭所有管道的读写端,并等待所有子进程完成。

多管协调

在这里插入图片描述

void myPipe(char *argv[], int argc) // ls -a 算两个命令
{
    int i, j;
    int pipe_num = 0; // 记录管道数
    int cmd_num = 0;  // 记录命令个数  ls -a 算一个命令
    // cmd_num=has[pipeline];  --->不能直接将命令里的所有管道给统计起来,因为><>>函数里有调用该函数,传入的参数为存><>>前面的命令的数组arg及数组里的命令个数ls -a 算两个命令,而cmd_num中ls -a 算一个命令
    int index[8]; // 记录管道在命令中的位置,好分割命令,最大管道数为8
    for (i = 0; i < argc; i++)
    {
        if (strcmp(argv[i], "|") == 0)
        {
            index[i]++;
            pipe_num++;
        }
    }
    cmd_num = pipe_num + 1;

    char *cmd[cmd_num][MAX_ARGS]; // 装命令的数组 ls -a | grep abc | wc -l > 1.txt 中ls -a装在cmd[0]里面,ls、-a分别装在cmd[0][0]、cmd[0][1]里面
    for (i = 0; i < cmd_num; i++)
    {
        if (i == 0) // 第一个命令
        {
            int n = 0;
            for (int j = 0; j < index[i]; j++)
            {
                cmd[i][n++] = argv[j];
            }
            cmd[i][n] = NULL;
        }
        else if (i == cmd_num - 1) // 最后一个命令
        {
            int n = 0;
            for (int j = index[i - 1] + 1; j < cmd_num - 1; j++)
            {
                cmd[i][n++] = argv[j];
            }
            cmd[i][n] = NULL;
        }
        else
        {
            int n = 0;
            for (int j = index[i - 1] + 1; j < index[i]; j++)
            {
                cmd[i][n++] = argv[j];
            }
            cmd[i][n] = NULL;
        }
    }

    int fd[pipe_num][2];           // 管道描述符,pipe_num个管道,就有这么多对描述符
    for (i = 0; i < pipe_num; i++) // 根据管道数量创建管道
    {
        pipe(fd[i]);
    }

    pid_t pid;
    for (i = 0; i < cmd_num; i++)
    {
        pid = fork();
        if (pid == 0) // 因为要创建多个并列的子进程而不是五代同堂
            break;
    }

    if (pid < 0)
    {
        perror("fork()");
        exit(1);
    }

    // 子进程
    else if (pid == 0 && pipe_num > 0)
    {
        if (i == 0) // 第一个管道(子进程)
        {
            dup2(fd[0][1], 1);             // 子进程的标准输出被重定向到第一个管道的写入端
            close(fd[0][0]);               // 关闭读端
            for (j = 1; j < pipe_num; j++) // 将其他管道读写端全部关闭,可以避免不必要的资源浪费,提高程序的效率。如果不关闭其他管道的读写端,当程序执行到读写其他管道时,由于这些管道的读写端没有被关闭,就会出现无法预料的行为,导致程序出错或崩溃
            {
                close(fd[j][1]);
                close(fd[j][0]);
            }
        }
        else if (i == pipe_num) // 最后一个管道(子进程)
        {
            dup2(fd[pipe_num - 1][0], 0); // 子进程的标准输入被重定向到最后一个管道的读取端
            close(fd[pipe_num - 1][1]);   // 关闭写端
            for (j = 0; j < pipe_num - 1; j++)
            {
                close(fd[j][1]);
                close(fd[j][0]);
            }
        }
        else // 其他管道(子进程)
        {
            dup2(fd[i - 1][0], 0); // 前一个管道的读端打开
            close(fd[i - 1][1]);   // 前一个写端关闭
            dup2(fd[i][1], 1);     // 后一个管道的写端打开
            close(fd[i][0]);       // 后一个读端关闭
            for (j = 0; j < pipe_num; j++)
            {
                if (j != i - 1 && j != i)
                {
                    close(fd[j][0]);
                    close(fd[j][1]);
                }
            }
        }

        execvp(cmd[i][0], cmd[i]);
        perror("execvp()");
        exit(1);
    }

    // 父进程
    else if (pid > 0)
    {
        for (i = 0; i < pipe_num; i++)
        { // 父进程创完子进程后就把所有读写端给关了
            close(fd[i][0]);
            close(fd[i][i]);
        }
        for (i = 0; i < cmd_num; i++) // 父进程等待子进程
        {
            waitpid(pid, NULL, 0);
        }
    }
}

ls

void myls(char *argv[])
{
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork()");
        exit(1);
    }
    else if (pid == 0)
    {
        execvp(argv[0], argv);
        perror("execvp()");
        exit(1);
    }
    else
    {
        // if(has[Background_running]){   //有&时
        //     has[Background_running]=0;
        //     printf("%d\n",pid);
        //     return ;
        // }
        waitpid(pid, NULL, 0);
    }
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值