实现自己的 shell

文章介绍了如何构建一个自定义的Linuxshell,包括解析命令行参数以支持ls和ll的别名,处理管道、输入输出重定向和后台运行的功能。同时,讨论了如何处理CD命令以及信号屏蔽(如阻止Ctrl+C中断)。代码示例展示了如何分割命令、设置后台运行标志、检查重定向状态以及执行管道。此外,还强调了避免内存错误和使用调试工具如gdb和valgrind的重要性。
摘要由CSDN通过智能技术生成

为什么实现一个自己 shell?

你是一个 Window/Android 用户,你可以直接用图形化桌面直接双击跑一个程序,没压力吧,轻松吧。
图形化工具为我们屏蔽了底层进程调用的细节。或者你是一个 Linux 用户,你喜欢用 bash 执行 grep
命令来查找文件中的内容,喜欢用 git 来提交代码,喜欢用 ls 来查看目录内容,有时你还会去使用
将多个命令一起使用:cat cat.txt | grep "smelly cat" | wc -c。作为一个 Linux Hacker,你理应
当了解这些图形化工具背后的原理,利用它实现一些更好玩的事情。

TASK

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

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

要求:

  • 不得出现内存泄漏,内存越界等错误
  • 学会如何使用 gdb 进行调试,使用 valgrind 等工具进行检测
知识要点
  1. 懂得如何使用 shell
  2. 理解 shell 原理
  3. Linux系统编程:进程控制
  4. gdb
  5. valgrind

直接讲解代码

分割命令
void parse_args(char* line, char** args) {
    char* token = strtok(line, " \t\r\n\a");
    args[0] = token;
    int i = 1;
    if(strcmp(args[0],"ls")==0)
        {
            args[i++]=(char*)"--color=auto";
        }
        if(strcmp(args[0],"ll")==0)
        {
            args[0]=(char*)"ls";
            args[i++]=(char*)"-l";
            args[i++]=(char*)"--color=auto";
        }
    while (token != NULL) {
        token = strtok(NULL, " \t\r\n\a");
        args[i++] = token;
    }
    COUNT=i-1;
}

这里使用了string.h库中的strtok函数(详情见man手册)来分割字符串,方便之后使用execvp函数来执行命令,这里检测如果命令是ls的话,那么就加上–color=auto,这是为了让ls的结果具有颜色显示的效果,使界面更加美观。并且将ll识别为ls -l

设置后台运行与管道的标志
void parse(char *line){
    HT=false;
    PP=false;
    for(int i=0;i<strlen(line);i++){
        if(line[i]=='&'){
            HT=true;
            line[i]=' ';
        }
        if(line[i]=='|'){
            PP=true;
        }
    }
}

这里的HT和PP都是bool类型,每次进入函数都先熄灭它们两个,如果遍历命令中出现了&或|,那就点亮它们

重定向状态获取
char *CheckRedir(char *start)
{
    char *end=start+strlen(start)-1;
 
    while(end>=start)
    {
        if(*end=='>')
        {
            if(*(end-1)=='>')
            {
                redir_status=APPEND_REDIR;
                *(end-1)='\0';
                end++;
                while(*end==' ')
                    ++end;
                break;
            }
            redir_status=OUTPUT_REDIR;
            *end='\0';
            end++;
            while(*end==' ')
                ++end;
            break;
        }
        else if(*end=='<')
        {
            redir_status=INPUT_REDIR;
            *end='\0';
            end++;
            while(*end==' ')
                ++end;
            break;
        }
        else
        {
            end--;
        }
    }
    if(end>=start)
    {
        return end;
    }
    else
    {
        return NULL;
    }
}

这里先看一下我的宏

#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
#define NONE_REDIR 0

int redir_status=NONE_REDIR;

四种状态分别表示 重定向输入,输出,追加和无重定向工作,初始化状态为无重定向工作

这里观察命令中是否存在>>,>,<等标记从而确定重定向的状态,并返回需要重定向的文件名

找管道和执行管道(初始版/笑哭)
int has_pipe(char** args, int* pipe_position) {
    for (int i = 0; args[i] != NULL; i++) {
        if (strcmp(args[i], "|") == 0) {
            *pipe_position = i;
            return 1;
        }
    }
    return 0;
}

void run_command_with_pipe(char** args1, char** args2) {
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    pid_t pid1 = fork();
    if (pid1 == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid1 == 0) {
        close(pipefd[0]);
        dup2(pipefd[1], STDOUT_FILENO);
        close(pipefd[1]);
        execvp(args1[0], args1);
        perror("execvp");
        exit(EXIT_FAILURE);
    }

    pid_t pid2 = fork();
    if (pid2 == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid2 == 0) {
        close(pipefd[1]);
        dup2(pipefd[0], STDIN_FILENO);
        close(pipefd[0]);
        execvp(args2[0], args2);
        perror("execvp");
        exit(EXIT_FAILURE);
    }

    close(pipefd[0]);
    close(pipefd[1]);
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);
}
执行管道
void DoPipe(char **argv, int count)
{
    pid_t pid;
    int ret[10];
    int number=0;
    for(int i=0;i<count;i++)
    {
    if(!strcmp(argv[i],"|"))
    {
        ret[number++]=i;
    }
    }
    int cmd_count=number+1;
    char* cmd[cmd_count][10];
    for(int i=0;i<cmd_count;i++)
    {
    if(i==0)
    {
        int n=0;
        for(int j=0;j<ret[i];j++)
        {
        cmd[i][n++]=argv[j];
        }
        cmd[i][n]=NULL;
    }
    else if(i==number)
    {
        int n=0;
        for(int j=ret[i-1]+1;j<count;j++)
        {
        cmd[i][n++]=argv[j];
        }
        cmd[i][n]=NULL;
    }
    else 
    {
        int n=0;
        for(int j=ret[i-1]+1;j<ret[i];j++)
        {
        cmd[i][n++]=argv[j];
        }
        cmd[i][n]=NULL;
    }
    }
    int fd[number][2];  
    for(int i=0;i<number;i++)
    {
    pipe(fd[i]);
    }
    int i=0;
    for(i=0;i<cmd_count;i++)
    {
    pid=fork();
    if(pid==0)
    break;
    }
    if(pid==0)
    {
    if(number)
    {
        if(i==0)
        {
        dup2(fd[0][1],1); 
        close(fd[0][0]);
        for(int j=1;j<number;j++)
        {
            close(fd[j][1]);
            close(fd[j][0]);
        }
        }
        else if(i==number)
        {
        dup2(fd[i-1][0],0);
        close(fd[i-1][1]);
        for(int j=0;j<number-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(int j=0;j<number;j++)
        {
                if(j!=i&&j!=(i-1))
                {
                close(fd[j][0]);
                close(fd[j][1]);
                }
        }
        }
    }
    execvp(cmd[i][0],cmd[i]);
    perror("execvp");
    exit(1);
    }
    for(i=0;i<number;i++)
    {
        close(fd[i][0]);
        close(fd[i][1]);
    }
    for(int j=0;j<cmd_count;j++)
    wait(NULL);
}
执行命令
void run_command(char** args) {
    // int pipe_position;
    // if (has_pipe(args, &pipe_position)) {
    //     char* args1[MAX_ARGS];
    //     char* args2[MAX_ARGS];
    //     memcpy(args1, args, (pipe_position) * sizeof(char*));
    //     args1[pipe_position] = NULL;
    //     memcpy(args2, &args[pipe_position + 1], (MAX_ARGS - pipe_position - 1) * sizeof(char*));
    //     run_command_with_pipe(args1, args2);
    // } 
    if(PP){
        DoPipe(args,COUNT);
    }
    else {
        pid_t pid = fork();
        if (pid == -1) {
            perror("fork");
            exit(EXIT_FAILURE);
        }
        if (pid == 0) {
            execvp(args[0], args);
            perror("execvp");
            exit(EXIT_FAILURE);
        } else {
            wait(NULL);
        }
    }
}

这里如果PP亮着就说明有管道,就执行dopipe,没有管道就是单一命令,直接fork(),子进程execvp执行命令,父进程wait就行。另外这里注释掉的是我曾经的只能处理一个管道的代码。

主函数
int main() {
    char line[MAX_LINE];
    char* args[MAX_ARGS];
    char prior[100]={'\0'};
    args[0]="\0";
    signal(SIGINT,SIG_IGN); //屏蔽ctrl+c
    int x=dup(0),y=dup(1);
    while (1) {
        printf("[dyx-super-shell]# ");
        if (fgets(line, MAX_LINE, stdin) == NULL) {
            perror("fgets");
            exit(EXIT_FAILURE);
        }
        // line=readline(line);
        if(strcmp(line,"\n")==0||!line)
            continue;
        line[strlen(line)-1]='\0';
        // add_history(line);
        parse(line);
        char *sep=CheckRedir(line);
        parse_args(line, args);
        if (args[0] == NULL) {
            continue;
        }
        if (strcmp(args[0], "exit") == 0) {
            break;
        }
        if (strcmp(args[0], "cd") == 0)
        {
            if (args[1] && strcmp(args[1], "-") == 0)
            {
                if (prior)
                {
                    char buf[100];
                    getcwd(buf, 100);
                    printf("%s\n", prior);
                    chdir(prior);
                    strcpy(prior, buf);
                }
                else
                { 
                    printf("bash: cd: OLDPWD 未设定\n");
                }
            }
            else
            {
                getcwd(prior, 100);
                chdir(args[1]);
            }
            continue;
        }
        if(HT){
                int fd=-1;
                fd=open("/dev/null",O_WRONLY);
                dup2(fd,0);
                dup2(fd,1);
                close(fd);
        }
        if(sep!=NULL)
        {
            int fd=-1;
            switch(redir_status) {
                case INPUT_REDIR:
                    fd=open(sep,O_RDONLY);
                    dup2(fd,0);
                    close(fd);
                    break;
                case OUTPUT_REDIR:
                    fd=open(sep,O_WRONLY|O_TRUNC|O_CREAT,0666);
                    dup2(fd,1);
                    close(fd);
                    break;
                case APPEND_REDIR:
                    fd=open(sep,O_WRONLY|O_APPEND|O_CREAT,0666);
                    dup2(fd,1);
                    close(fd);
                    break;
                default:
                    printf("bug?\n");
                    break;
            }
        }
        run_command(args);
        // free(line);
        // line=NULL;
        dup2(x,0);
        dup2(y,1);
    }
    return 0;
}

讲一下重要的部分

if (strcmp(args[0], "exit") == 0) {
            break;
        }
        if (strcmp(args[0], "cd") == 0)
        {
            if (args[1] && strcmp(args[1], "-") == 0)
            {
                if (prior)
                {
                    char buf[100];
                    getcwd(buf, 100);
                    printf("%s\n", prior);
                    chdir(prior);
                    strcpy(prior, buf);
                }
                else
                { 
                    printf("bash: cd: OLDPWD 未设定\n");
                }
            }
            else
            {
                getcwd(prior, 100);
                chdir(args[1]);
            }
            continue;
        }

如果命令是exit和cd,直接执行,不需要fork,这里实现cd -就是在cd前现将此时的位置存起来,下次cd - 时直接切回来

 if(sep!=NULL)
        {
            int fd=-1;
            switch(redir_status) {
                case INPUT_REDIR:
                    fd=open(sep,O_RDONLY);
                    dup2(fd,0);
                    close(fd);
                    break;
                case OUTPUT_REDIR:
                    fd=open(sep,O_WRONLY|O_TRUNC|O_CREAT,0666);
                    dup2(fd,1);
                    close(fd);
                    break;
                case APPEND_REDIR:
                    fd=open(sep,O_WRONLY|O_APPEND|O_CREAT,0666);
                    dup2(fd,1);
                    close(fd);
                    break;
                default:
                    printf("bug?\n");
                    break;
            }
        }

这里将重定向的工作做好

if(HT){
        int fd=-1;
        fd=open("/dev/null",O_WRONLY);
        dup2(fd,0);
        dup2(fd,1);
        close(fd);
}

有后台运行就重入到dev/null

源码地址:https://github.com/mejomejo/mytask/blob/main/myshell/dyx.c

参考资料
  • man 手册.
  • MichaelKerrisk.Linux/UNIX系统编程手册[M].北京:人民邮电出版社.
  • W.RichardStevens.Stephen.UNIX环境高级编程[M].第3版.戚正伟,译.北京:人民邮电出版社.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值