【Linux】模拟实现Shell(下)

发布于:2025-09-02 ⋅ 阅读:(15) ⋅ 点赞:(0)

【Linux】模拟实现Shell(上)上篇实现的shell能处理一些基本命令和cd、echo这样的内建命令,以及让shell实现好了两张表,环境变量表和命令行参数表。这篇我们会在此基础上对shell的重定向操作进行模拟实现。

1.优化命令行提示符

我们对这个命令行提示符可以做个改进,Ubuntu下Shell的路径这里用~代替了家目录,我们也可以实现成这样。

std::string CurDir()
{
    std::string home = GetHome();
    std::string pwd = GetPwd();
    if(home > pwd) //在 根目录/ 或 /home 路径下
        return pwd;

    std::string ret = "~"; //正好在家目录下,直接就使用~
    if(home == pwd)
        return ret;
    
    int pos = 0;
    for(int i = 0; home[i]; i++) //找到家目录往后的路径
    {
        pos++;
    }
    ret += pwd.substr(pos);  //往后的路径加在ret里
    return  ret;
}

#define FORMAT "%s@%s:%s# " //设置命令行提示符的格式
void PrintCmdPrompt()
{
    printf(FORMAT,UserName(), HostName(),CurDir().c_str());
}

现在我们的命令行提示符就和Shell更相似了,为了区分,还是用#做结尾,不用$。

2.重定向分析

在命令行输入命令的时候一般就是用> 、<、 >>这样的符号重定向,如"ls -a -l > file.txt",而且这些重定向符号的左右两边可能带空格,也可能不带空格。

2.1 输入重定向

还是以 "ls -a -l < file.txt" 为例:

  • 我们需要做的是把 < 的两边拆分,拆成 "ls -a -l" 和 "file.txt" ,< 的左边部分是要执行的命令,右边部分是要打开的目标文件
  • 并且我们还需要判断重定向方式,是>,还是<,还是>>。

这个步骤要在命令行分析之前做,叫重定向分析

int main()
{
    EnvInit(); //初始化环境变量
    while(1)
    {
        //1.打印命令行提示符
        PrintCmdPrompt();

        //2.获取命令行参数
        char commandline[COMMAND_SIZE];
        if(!GetArguments(commandline, sizeof(commandline))) //获取失败
            continue;
        
        //3.重定向分析
        RedirCheck(commandline); 

        //4.解析命令行参数
        if(!CommandParse(commandline))
            continue;
        //PrintArg();
        
        //5.检测是否为内建命令,是内建命令就执行
        if(CheckAndExeBuiltinCmd())
            continue; 

        //6.执行命令
        Execute();
    }
    return 0;
}

实现这个RedirCheck函数之前,我们先定义4个宏,表示重定向的方式,还要一个变量存储重定向到哪个文件。

#define NONE_REDIR 0   //没有重定向
#define INPUT_REDIR 1  //输入重定向
#define OUTPUT_REDIR 2 //输出重定向
#define APPEND_REDIR 3 //追加重定向
int redir_type = NONE_REDIR; //默认设为没有重定向
std::string filename; 

然后就可以开始实现RedirCheck函数了,这个函数参数就是commandline,返回值为void。

首先每次都重置重定向的方式,以及清空file里的内容。

void RedirCheck(char* cmd)
{
    redir_type = NONE_REDIR;
    filename.clear();
    
}

将重定向符两边的内容分开时,这里我选择从后往前找,先写输入重定向< 的逻辑。从后往前找,找到'<'就停止,并且把 ‘<' 置成0,就可以将两边分开。

bool RedirCheck(char* cmd)
{
    redir_type = NONE_REDIR;
    filename.clear();
    
    int begin = 0;
    int end = strlen(cmd)-1;
    while(begin < end)
    {
        if(cmd[end] == '<') //输入重定向
        {
            redir_type = INPUT_REDIR; 
            cmd[end] = 0; 
           
        }
        
        end--;
    }
}

<的两边可能有多个空格,也可能没有空格,如果<的左边有多个空格,不用管没因为后续解析命令行参数时用的strtok函数不会返回空串;如果<的右边有多个空格,我们需要清除这些空格,让end指向文件的开头。

void ClearSpaces(char *cmd, int& end) //传地址过去,直接改变end
{
   while(isspace(cmd[end]))
   {
       end++;
   }
}

bool RedirCheck(char* cmd)
{
    redir_type = NONE_REDIR;
    filename.clear();
    
    int begin = 0;
    int end = strlen(cmd)-1;
    while(begin < end)
    {
        if(cmd[end] == '<') //输入重定向
        {
            redir_type = INPUT_REDIR; 
            cmd[end] = 0;
            ClearSpaces(cmd, ++end); //清除空格
           
        }
       
        end--;
    }
}

然后文件名的起始地址就是这个数字组的起始地址加上end。

void ClearSpaces(char *cmd, int& end)
{
   while(isspace(cmd[end]))
   {
       end++;
   }
}

bool RedirCheck(char* cmd)
{
    redir_type = NONE_REDIR;
    filename.clear();
    
    int begin = 0;
    int end = strlen(cmd)-1;
    while(begin < end)
    {
        if(cmd[end] == '<') //输入重定向
        {
            redir_type = INPUT_REDIR; 
            cmd[end] = 0;
            ClearSpaces(cmd, ++end); //清除空格
            filename = cmd+end;
            break;
        }
        
        end--;
    }
}

2.2 输出重定向和追加重定向

输出重定向是 >,追加重定向是>>,从后往前扫描时,end停留的前一个位置还是>的话,就是追加重定向,否则是输出重定向。

void ClearSpaces(char *cmd, int& end)
{
   while(isspace(cmd[end]))
   {
       end++;
   }
}

bool RedirCheck(char* cmd)
{
    redir_type = NONE_REDIR;
    filename.clear();
    
    int begin = 0;
    int end = strlen(cmd)-1;
    while(begin < end)
    {
        if(cmd[end] == '<') //输入重定向
        {
            redir_type = INPUT_REDIR; 
            cmd[end] = 0;
            ClearSpaces(cmd, ++end); //清除空格
            filename = cmd+end;
            break;
        }
        else if(cmd[end] == '>') // > 或 >>
        {
            if(cmd[end-1] == '>') // >> 追加重定向
            {           
            }
            else // > 输出重定向
            {               
            }
            break;
        }
        end--;
    }
}

对>>来说,我们把两个>>都置为0,对于>,就是把这一个位置置为0,其他逻辑和输入重定向是一样的,冗余部分整理一下后代码如下。

void ClearSpaces(char *cmd, int& end)
{
   while(isspace(cmd[end]))
   {
       end++;
   }
}

bool RedirCheck(char* cmd)
{
    redir_type = NONE_REDIR;
    filename.clear();
    
    int begin = 0;
    int end = strlen(cmd)-1;
    while(begin < end)
    {
        if(cmd[end] == '<') //输入重定向
        {
            redir_type = INPUT_REDIR; 
            cmd[end] = 0;
            ClearSpaces(cmd, ++end); //清除空格
            filename = cmd+end;
            break;
        }
        else if(cmd[end] == '>') // > 或 >>
        {
            if(cmd[end-1] == '>') // >> 追加重定向
            {
                redir_type = APPEND_REDIR;
                cmd[end-1] = 0;
                cmd[end] = 0;   
            }
            else // > 输出重定向
            {
                redir_type = OUTPUT_REDIR;
                cmd[end] = 0;           
            }
            ClearSpaces(cmd, ++end); //清除空格
            filename = cmd+end;
            break;
        }
        end--;
    }
}

我们可以把重定向的类型以及文件名打出来看看。

int main()
{
    EnvInit(); //初始化环境变量
    while(1)
    {

        //1.打印命令行提示符
        PrintCmdPrompt();

        //2.获取命令行参数
        char commandline[COMMAND_SIZE];
        if(!GetArguments(commandline, sizeof(commandline))) //获取失败
            continue;
        
        //3.重定向分析
        RedirCheck(commandline); 
        printf("redir:%d, filename:%s\n", redir_type, filename.c_str());

        //4.解析命令行参数
        if(!CommandParse(commandline))
            continue;
        //PrintArg();
        
        //5.检测是否为内建命令,是内建命令就执行
        if(CheckAndExeBuiltinCmd())
            continue; 

        //6.执行命令
        Execute();
    }
    return 0;
}

可以看到,不管我们重定向符号左右有多少空格,都是没问题的。

3.重定向执行

重定向不能让父进程重定向,会影响到整个shell,要在子进程重定向,所以就要在子进程里检查重定向情况,直接通过redir_type检查。

int Execute()
{
    pid_t id = fork();
    if(id == 0) //子进程:程序替换
    {
        if(redir_type == INPUT_REDIR) // <
        {            
        }
        else if(redir_type == OUTPUT_REDIR) // >
        {            
        }
        else if(redir_type == APPEND_REDIR) // >>
        {
        }
       
        execvp(g_argv[0], g_argv);
    }
    //父进程:阻塞等待
    int status;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        if(WIFEXITED(status)) //正常退出
        {
            last_exit_code = WEXITSTATUS(status); //获取退出码
        }
        else
            last_exit_code = 100;
    }
    return 0;
}

输入重定向:

重定向文件肯定是先要把文件打开,这里用到函数open,返回值是文件描述符,第一个参数是要打开的文件名,第二个参数flags是打开的方法,第三个参数是如果文件不存在,要设置文件的起始权限,一般是0666。

打开文件之后进行重定向,这里重定向要用到函数dup2,两个参数都是要传文件描述符的。

标准输入(stdin)的文件描述符为0,标准输出(stdout)文件描述符为1,标准错误(stderr)文件描述符为2。输入重定向就是原本要从stdin里获取数据,变成从指定文件获取数据。

重定向:打开文件的方式+dup2

    if(id == 0) //子进程:程序替换
    {
        int fd = -1;
        if(redir_type == INPUT_REDIR) // <
        {
            fd = open(filename.c_str(), O_RDONLY, 0666); //只读形式打开
            if(fd < 0) exit(1); //打开失败
            dup2(fd, 0); //重定向
            close(fd);
        }
        else if(redir_type == OUTPUT_REDIR) // >
        {            
        }
        else if(redir_type == APPEND_REDIR) // >>
        {
        }
      
        execvp(g_argv[0], g_argv);
    }

输出重定向:

输出重定向open文件的方式就是创建+写入+覆盖式,所以方式就是O_CREAT | O_WRONLY | O_TRUNC,重定向就是原本像显示器stdout输出数据,变成向文件输出数据。

    if(id == 0) //子进程:程序替换
    {
        int fd = -1;
        if(redir_type == INPUT_REDIR) // <
        {
            fd = open(filename.c_str(), O_RDONLY, 0666); //只读形式打开
            if(fd < 0) exit(1); //打开失败
            dup2(fd, 0); //重定向
            close(fd);      
        }
        else if(redir_type == OUTPUT_REDIR) // >
        {
            fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
            if(fd < 0) exit(2);
            dup2(fd, 1);
            close(fd);
        }
        else if(redir_type == APPEND_REDIR) // >>
        {        
        }
      
        execvp(g_argv[0], g_argv);
    }

追加重定向:

追加重定向和输出重定向只有打开方式上的区别,追加重定向打开方式是创建+写入+追加式所以方式就是O_CREAT | O_WRONLY | O_APPEND。

    if(id == 0) //子进程:程序替换
    {
        int fd = -1;
        if(redir_type == INPUT_REDIR) // <
        {
            fd = open(filename.c_str(), O_RDONLY, 0666); //只读形式打开
            if(fd < 0) exit(1); //打开失败
            dup2(fd, 0); //重定向
            close(fd);
        }
        else if(redir_type == OUTPUT_REDIR) // >
        {
            fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
            if(fd < 0) exit(2);
            dup2(fd, 1);
            close(fd);
        }
        else if(redir_type == APPEND_REDIR) // >>
        {
            fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
            if(fd < 0) exit(3);
            dup2(fd, 1);
            close(fd);
        }
        
        execvp(g_argv[0], g_

此时shell的重定向操作就完成了,我们来检验一下。

4.结尾

到这里这个简单版的shell就实现好了,别的功能大家自己实现,下面这张图有利于我们理解文件。

程序替换会影响程序替换的结果吗?不会,因为进程有自己的文件描述符表,还有自己的进程地址空间,两者是独立的。

本次分享就到这里了,我们下篇见~


网站公告

今日签到

点亮在社区的每一天
去签到