对于我们所用的linux,由于他没有图形化界面,所以我们一般是用命令来使用他的,那么他是如何解析我们的命令的呢,今天我们就来实现一个简易的linux命令行解释器

输出命令行提示符

首先,我们可以发现命令行在最开头的地方会有一个命令行提示符(Command Prompt)

就这个图片而言,它是由当前登录用户名(root)主机名(hostname:iZ2ze49v4rphe7jq7w5ov8Z)和当前工作路径(~/666)以及一些符号(@ :#)构成

所以首先我们要实现这个输出的格式,可以发现我们需要获取3个环境变量:登录用户名,主机名和当前工作路径

1.获取登录用户名

static std::string GetUserName()
{
    std::string username = getenv("USER");
    return username.empty() ? "None" : username;
}

这里的static是不希望其他文件去使用该函数,而getenv是一个获取环境变量的函数

char *getenv(const char *name);

就这样就可以很简单的获取环境变量了,其他两个也是这样

2.获取主机名

static std::string GetHostName()
{
    std::string hostname = getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}

3.获取当前工作路径

static std::string GetPwd()
{
    char temp[1024];
    getcwd(temp, sizeof(temp));
    // 顺便更新一下shell自己的环境变量pwd
    snprintf(pwd, sizeof(pwd), "PWD=%s", temp);
    putenv(pwd);

    // /home/whb/code/code
    std::string pwd_lable = temp;
    const std::string pathsep = "/";
    auto pos = pwd_lable.rfind(pathsep);
    if(pos == std::string::npos)
    {
        return "None";
    }

    pwd_lable = pwd_lable.substr(pos+pathsep.size());
    return pwd_lable.empty() ? "/" : pwd_lable;
}

对于cwd和PWD的问题,cwd相当于是进程的一个属性,每个进程都有自己的cwd来表示自己的工作路径。而PWD是一个环境变量,从内容上来说他和cwd实际上是一样的,PWD也是用进程的cwd去维护的,cwd可以用getcwd()获取

目前来看PWD需要cwd,他们两个内容又一样,那么PWD这个环境变量存在的意义是什么呢??

举个例子呢,就是在去写shell底层的时候其实cwd就够用了,只需要用getcwd()就可以直接获取,用chdir()就可以直接更新。但是对于用户而言,难道我用命令去查看当前工作目录的时候我要去调用getenv()这个函数吗?这显然不合理,所以就有了PWD这个环境变量

那现在还有一个问题:既然用户需要知道当前工作目录,那为什么要把cwd设置为进程的属性??直接把cwd搞成一个环境变量不就行了吗?

但是但是,要知道环境变量用户是可以随意修改的,那如果现在只有一个环境变量cwd去表示当前工作路径的话,用户去乱改cwd,就会导致内核管理出以一系列的安全问题。而现在有一个PWD来面向用户,即使用户随意去修改PWD最多出问题的也就用户自己的程序(就是用户自己搞出来的依赖PWD的一些代码或者脚本),而linux本身并不会出什么问题

获取用户输入的命令

bool GetCommandString(char cmd_str_buff[], int len)
{
    if(cmd_str_buff == NULL || len <= 0)
        return false;
    char *res = fgets(cmd_str_buff, len, stdin);
    if(res == NULL)
        return false;
    // ls -a -l\n -> ls -a -l\0
    // ls -a -l>log.txt\n -> ls -a -l\0
    cmd_str_buff[strlen(cmd_str_buff) - 1] = 0;
    return strlen(cmd_str_buff) == 0 ? false : true;
}

这里我们用一个char类型的数组(cmd_str_buff)去将用户命令带出去。fget可以一次读取一行,输入回车停止输入,这里需要注意的是在回车结束的时候换行符其实也会被写入数组中,所以要将strlen(cmd_str_buff)-1为下标的换行符改为0(转为char类型就是'\0'),最后再返回结果即可

重定向检测和设置

在命令行中会有ls -a -l >log.txt这种命令,这种命令就是重定向,我们可以通过来识别命令中有没有>,>>,<这几个符号来判断有没有重定向操作

#define TrimSpace(start) do{\
        while(isspace(*start))\
        {\
            start++;\
        }\
    }while(0)

// "ls -a -l > log.txt" -> "ls -a -l\0log.txt"; 
// filename = log.txt, redir_type = OUTPUT_REDIR;
void CheckRedir(char cmd[])
{
    char *start = cmd;
    char *end = cmd + strlen(cmd) - 1;

    // ls -a -l >>   log.txt
    // ls -a -l
    while(start <= end)
    {
        // > >> <
        if(*start == '>')
        {
            if(*(start+1) == '>')
            {
                redir_type = APPEND_REDIR;
                // 追加重定向
                *start = '\0';
                start += 2;
                TrimSpace(start);
                filename = start;
                break;
            }
            else
            {
                // 输出重定向
                redir_type = OUPUT_REDIR;
                *start = '\0';
                start++;
                TrimSpace(start); // 移除空格
                filename = start;
                break;
            }
        }
        else if(*start == '<')
        {
            //输入重定向
            // cat <    log.txt
            redir_type = INPUT_REDIR;
            *start = '\0';
            start++;
            TrimSpace(start);
            filename = start;
            break;
        }
        else
        {
            start++;
        }
    }
}

解析命令

bool ParseCommandString(char cmd[])
{
    if(cmd == NULL)
        return false;
#define SEP " "
    //3. "ls -a -l" -> "ls" "-a" "-l"
    gargv[gargc++] = strtok(cmd, SEP);
    // 整个数字,最后以NULL结尾
    while((bool)(gargv[gargc++] = strtok(NULL, SEP)));
    // 回退一次,命令行参数的格式
    gargc--;

    return true;
}

将命令拆开即可

执行命令

执行命令通常是创建一个子进程,然后使用exec相关接口进行进程替换来执行命令,但是有些命令不能通过创建子进程执行,比如说cd,这样的命令用子进程执行会毫无意义

比如你执行一个ls的相关命令,用子进程再进行替换,然后在子进程中给你打印出相关文字后子进程再退出,但是如果是cd这样切换目录的命令的话,你想想更改目录的本质是什么?是通过什么来判断你的工作目录的?没错,就是你的cwd,但是如果创建子进程来执行cd,修改的是子进程的cwd,对父进程没有影响,也就是说你执行的cd根本就没用,所以这种命令只能用父进程来执行

还有一种是echo这种的命令,根本没必要开一个新的子进程来执行,直接printf就可以

像cd,echo这种直接在父进程中执行的命令称为内置命令,ls这种开子进程来执行的命令叫外部命令

所以我们首先来判断用户的命令是不是内置命令(这里我们以cd和echo来掩饰,实际上Bash中的内置命令有60到70个)

bool BuiltInCommandExec()
{
    //内建命令: 是shell自己执行的命令,如同shell执行一个自己的函数
    //gargv[0]
    std::string cmd = gargv[0];
    bool ret = false;
    if(cmd == "cd")
    {
        // build
        if(gargc == 2)
        {
            std::string target = gargv[1];
            if(target == "~")
            {
                ret = true;
                chdir(GetHomePath().c_str());
            }
            else{
                ret = true;
                chdir(gargv[1]);
            }
        }
        else if(gargc == 1)
        {
            ret = true;
            chdir(GetHomePath().c_str());
        }
        else
        {
            //BUG
        }
    }
    else if(cmd == "echo")
    {
        if(gargc == 2)
        {
            std::string args = gargv[1];
            if(args[0] == '$')
            {
                if(args[1] == '?')
                {
                    printf("lastcode: %d\n", lastcode);
                    lastcode = 0;
                    ret = true;
                }
                else{
                    const char *name = &args[1];
                    printf("%s\n", getenv(name));
                    lastcode = 0;
                    ret = true;
                }
            }
            else{
                printf("%s\n", args.c_str());
                ret = true;
            }
        }
    }
    return ret;
}

接下来来执行外部命令

void ForkAndExec()
{
    pid_t id = fork();
    if(id < 0)
    {
        //for : XXXXX
        perror("fork"); // errno -> errstring
        return;
    }
    else if(id == 0)
    {
        if(redir_type == OUPUT_REDIR)
        {
            int output = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
            (void)output;
            dup2(output, 1);
        }
        else if(redir_type == INPUT_REDIR)
        {
            int input = open(filename.c_str(), O_RDONLY);
            (void)input;
            dup2(input, 0);
        }
        else if(redir_type == APPEND_REDIR)
        {
            int appendfd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND);
            (void)appendfd;
            dup2(appendfd, 1);
        }
        else{
            //Do Nothing
        }
        // 子进程打开文件了,也进行了重定向,execl*不影响历史打开的文件!
        //子进程
        execvp(gargv[0], gargv);
        exit(0);
    }
    else
    {
        //父进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            lastcode = WEXITSTATUS(status);
        }
    }
}

这里的if else里面是在搭建类似管道一样的东西

如果有重定向命令的话就搭建重定向的管道,再执行execvp去替换子进程去执行重定向的输入输出。如果没有重定向命令的话就不会搭建通道,会直接execvp

由于是子进程进行的重定向,父进程的文件标识符表并不会被弄乱

总结

这个简易的shell大致就是这样

这是所有的代码

main.cc · 邱宏林/邱宏林 - 码云 - 开源中国https://gitee.com/Qhl-666/qiu-honglin/blob/master/main.cc

myshell.cc · 邱宏林/邱宏林 - 码云 - 开源中国https://gitee.com/Qhl-666/qiu-honglin/blob/master/myshell.cc

myshell.h · 邱宏林/邱宏林 - 码云 - 开源中国https://gitee.com/Qhl-666/qiu-honglin/blob/master/myshell.h

Logo

加入「COC·上海城市开发者社区」,成就更好的自己!

更多推荐