欢迎光临
我们一直在努力

【项目实现】 Linux自定义shell实现


目录

前言

Part1. Shell 核心原理前置知识

Part2. 整体代码架构总览

Part3. 模块逐段代码解析

Part3.1. 提示符模块

Part3.2. 读取命令行 GetCommandline

Part3.3. 命令字符串分割 CommandlineParse

Part3.4. 重定向解析模块

Part3.5. Execute (),fork + execvp + dup2 重定向

Part3.6. 内建命令实现

Part3.6.1. cd

Part3.6.2. echo

Part3.6.3. export、alias

Part3.7. 主循环 main

Part4. 结语


前言

  前面学了这么多知识,我们可以写一个简易的自定义myshell来复习一波,接下来来跟随小编的视角来看看吧。


let's go!!!!!!!!


Part1. Shell 核心原理前置知识

Shell 本质就是运行在用户态的命令行解释程序,它的核心工作流程固定:

  • 打印提示符,读取用户输入一行命令;
  • 解析命令字符串,切割成 argv 参数数组;
  • 识别特殊语法:重定向> >> <;
  • 识别别名 alias,替换原始命令;
  • 判断是否为内建命令:cd/echo/export/alias 这类,不能 fork 子进程执行,必须在当前 shell 进程内执行,因为它们要修改 shell 自身的进程环境(当前工作目录、环境变量);
  • 外部命令:调用fork()创建子进程,子进程通过execvp加载程序,父进程waitpid等待子进程结束,回收子进程,保存退出码$?;
  • 循环回到打印提示符,等待下一条输入。

  • 重点区分:内建命令 vs 外部命令 外部命令(ls、cat)是磁盘上的可执行文件,必须 fork+exec; cd 为什么不能 fork 执行?如果 fork 后子进程 chdir,只会修改子进程的工作目录,父 shell 当前目录完全不变,所以 cd 必须在本进程执行。


    Part2. 整体代码架构总览

    全局变量说明

    #define COMMAND_SIZE 1024
    char* argv[MAX_ARGV_SIZE]; // 存放解析后的命令参数数组,execvp要求char*[]格式
    int argc=0; // 参数个数
    int lastcode=0; // 保存上一条命令退出码,对应shell里$?
    std::unordered_map<std::string,std::string> alias_table; // 别名表
    // 重定向标记
    #define NO_REDIR 0
    #define INPUT_REDIR 1
    #define OUTPUT_REDIR 2
    #define APPEND_REDIR 3
    int redir=0;
    std::string file_name; // 重定向目标文件名


    Part3. 模块逐段代码解析

    Part3.1. 提示符模块

    const char* GetPwd()
    {
    const char* pwd=getcwd(cwd_buf,sizeof(cwd_buf));
    if(pwd!=NULL)
    {
    snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",cwd_buf);
    putenv(cwdenv);
    }
    return pwd==NULL?"None":pwd;
    }

    • getcwd获取当前进程工作目录;
    • 同步更新环境变量PWD,其他程序读取 PWD 时拿到最新路径;
    • snprintf拼接提示符字符串;
    • fflush(stdout):关键,printf 默认行缓冲,如果输出不带换行,缓冲区不会自动刷出,提示符会卡住不显示,必须手动 fflush。

      Part3.​​​​​​​2. 读取命令行 GetCommandline

    bool GetCommandline(char* out,int size)
    {
    char* comline=fgets(out,size,stdin);
    if(comline==NULL) return false;
    out[strlen(out)-1]=0; // 把fgets读到的末尾'\\n'替换成'\\0'
    if(strlen(out)==0) return false;
    return true;
    }

    • fgets读取一行输入,会保留回车换行\\n;
    • 手动覆盖掉\\n,方便后续字符串解析;
    • 返回 false 代表空输入,直接跳过本轮循环。

    缺陷:fgets 遇到 Ctrl+D(EOF)返回 NULL,代码仅 return false,没有退出 shell,真实 bash 收到 EOF 直接退出。

    Part3.3. 命令字符串分割 CommandlineParse

    bool CommandlineParse(char* out)
    {
    #define SEP " "
    argc=0;
    argv[argc++]=strtok(out,SEP);
    while((bool)(argv[argc++]=strtok(NULL,SEP)));
    argc–;
    return argc>0?true:false;
    }

    strtok字符串分割,按空格切分,填充 argv 数组。

    注意:strtok 会原地修改原字符串,把分隔符替换成\\0,适合这里解析场景。 致命短板:不支持引号。原生 bash 支持echo "hello world"作为单个参数,strtok 直接按空格切,引号失效;同时连续多个空格可以分割,但 tab 制表符无法识别。

    Part3.4. 重定向解析模块

    void CheckRedir(char* out)
    {
    redir=NO_REDIR;
    std::string s=out;
    if(s.find(">>")!=s.npos)
    {
    redir=OUTPUT_REDIR;
    size_t pos=s.find(">>");
    out[pos]=0; // 在>>位置截断原命令字符串,后面留给重定向文件
    file_name=s.substr(fpos);
    }
    else if(s.find('>')!=s.npos)
    {
    redir=APPEND_REDIR;
    // …逻辑类似
    }
    }

    核心思路:

  • 在原始命令行字符串找到>>/>/<;
  • 在符号位置写入\\0截断命令,前面部分交给命令解析;
  • 提取符号后的文件名保存到file_name;
  • 设置全局redir标记,fork 子进程后执行 dup2 修改文件描述符。
  • 代码里这里有一个 BUG:>>识别后标记为 OUTPUT_REDIR,>识别标记 APPEND_REDIR,宏定义写反了,后续执行逻辑会错乱! 宏定义:OUTPUT_REDIR=2,APPEND_REDIR=3,代码逻辑写反: >>追加 应该绑定 APPEND_REDIR;>覆盖绑定 OUTPUT_REDIR,原代码写反。

    Part3.5. Execute (),fork + execvp + dup2 重定向

    int Execute()
    {
    pid_t id=fork();
    if(id==0)
    {
    int fd=-1;
    if(redir==INPUT_REDIR)
    {
    fd=open(file_name.c_str(),O_RDONLY);
    dup2(fd,0); // fd复制到标准输入0
    close(fd);
    }
    // > >> 的输出重定向同理dup2(fd,1)标准输出
    execvp(argv[0],argv);
    exit(1);
    }
    int status=0;
    pid_t rid=waitpid(id,&status,0);
    if(rid>0)
    {
    lastcode=WEXITSTATUS(status);
    }
    return 0;
    }

    系统调用拆解:

  • fork():复制父进程,得到子进程,子进程复制父进程文件描述符表;
  • 子进程内部:open打开目标文件,dup2(oldfd, newfd)把文件描述符重定向到,dup2 会关闭 newfd 原有文件;
  • execvp:在子进程加载新程序,替换子进程地址空间;execvp 成功不返回;失败才继续往下执行 exit (1);
  • 父进程调用waitpid阻塞等待子进程结束,拿到子进程退出状态,WEXITSTATUS提取退出码存入lastcode(对应$?)。
  • 注意:重定向逻辑只写在外部命令分支,原代码 echo 内建命令没有处理重定向,echo hello > a.txt不会生效。

    Part3.6. 内建命令实现

    Part3.6.1. cd

    bool Cd()
    {
    if(argc==1) // cd不带参数,切home
    {
    PREVPATH=getcwd(prevpath_buf,sizeof(prevpath_buf));
    chdir(home.c_str());
    }
    else
    {
    std::string where=argv[1];
    if(where=="-") // cd – 切回上一次目录
    {
    chdir(PREVPATH);
    }
    }
    }

    核心:chdir修改当前进程工作目录,同时保存上一次路径 PREVPATH,实现cd -。


    Part3.6.2. echo

    支持 echo $?打印上一条退出码,echo $PATH读取环境变量。

    限制:仅支持单个参数,echo a b c只会打印第一个参数。


    Part3.6.3. export、alias

    • export:维护自定义环境变量数组 envs,调用 putenv 注册环境变量;
    • alias:unordered_map 存储别名映射;CheckAlias在解析完命令后,查找别名,如果命中,重新解析别名对应的字符串执行。

    Part3.7. 主循环 main

    while(true)
    {
    PrintCommandPrompt();
    char commandline[COMMANDLINE_SIZE];
    if(!GetCommandline(commandline,sizeof(commandline)))
    continue;
    CheckRedir(commandline);
    if(!CommandlineParse(commandline))
    continue;
    if(CheckAndExecBuiltin())
    continue;
    if(CheckAlias(commandline))
    continue;
    Execute();
    }

    标准事件循环:打印提示符→读命令→解析重定向→分割参数→优先执行内建命令→别名替换→fork 执行外部命令。


    Part4. 结语

      这篇文章我们知道了自定义myshell的实现,接下来,小编还会带来更多Linux相关的知识,敬请期待~

     最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!

     最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~

    赞(0)
    未经允许不得转载:171主机测评 » 【项目实现】 Linux自定义shell实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址