【Linux】自定义Shell


前言

在上一篇文章中,我们详细介绍了进程创建、进程终止、进程等待和进程程序替换的内容,内容还是挺多的,希望大家可以多去练习熟悉一下,那么本篇文章将带大家详细讲解自定义Shell的内容,接下来一起看看吧!


一. shell原理

当我们执行程序创建进程时,我们的shell命令行解释器也就是bash都做了什么工作呢?

  1. 输出命令行提示符
  2. 获取并解析输入的指令
  3. 判断是否为内建命令,如果不是则创建子进程执行命令

用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。
在这里插入图片描述
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序,并等待这个进程结束。所以要写一个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)

二. 自定义shell的实现

2.1 输出命令行提示符

在这里插入图片描述
bash在每次都会输出命令行提示符,然后等待我们用户输入。

看这个命令行提示符,它包含以下信息:

  • 用户名USER
  • 主机名HOSTNAME
  • 当前工作路径PWD
  • 这些在环境变量表中都能够找到,所以就可以使用getenv来获取。

这样我们需要获取环境变量USERHOSTNAMEPWD

#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "

const char* GetUserName()    
{    
    const char* name = getenv("USER");    
    return name == NULL ? "None" : name;                                                                                                                                            
}    
    
const char* GetHostName()    
{    
    const char* hostname = getenv("HOSTNAME");    
    return hostname == NULL ? "None" : hostname;    
}    
    
const char* GetPWD()    
{    
    //connst char* pwd = getenv("PWD");    
    const char* pwd = getcwd(cwd,sizeof(cwd));    
    if(pwd != NULL)    
    {    
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);    
        putenv(cwdenv);    
    }    
    return pwd == NULL ? "None" : pwd;    
}
std::string DirName(const char* pwd)
{
#define SLASH "/"
    std::string dir = pwd;
    if(dir==SLASH) return SLASH;
    auto pos = dir.rfind(SLASH);
    if(pos==std::string::npos) return "BUG?";
    return dir.substr(pos+1);
}

void MakeCommandLine(char cmd_prompt[], int size)
{
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPWD()).c_str());
}

void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt,sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}

在这里插入图片描述
可以看到效果就跟上面的差不多了,为了区分系统的shell,所以将$改为#

2.2 获取用户输入的信息

接下来就应该获取命令行信息了
在这里插入图片描述
我们输入一行指令,然后再按回车就可以执行命令,所以我们输入的命令中是可以带空格的。我们不应该使用scanf/cin进行输入了,可以使用fgets来输入一行字符串。

bool GetCommandLine(char* out, int size)                                                                              
{    
    char* c = fgets(out, size, stdin);    
    if(c==NULL) return 1;    
    out[strlen(out)-1] = '\0'; //清理\n    
    if(strlen(out)==0) return false;    
    return true;    
}

2.3 命令行解析

获取了用户输入的信息,但是获得的只是一个字符串,而我们想执行用户输入的命令,就要先对这个字符串进行解析;生成对应的命令行参数表,才能去执行。

命令行参数个数g_argc,命令行参数表g_argv;我们可以设置成全局的,这样每次只用修改g_argcg_argv即可。

我们可以使用strtok函数来分割字符串,可以以空格为分隔符进行分割,获得的每一个字符串存储到命令行参数表g_argv,再++命令行参数个数g_argc即可。

char *strtok(char *str, const char *delim);

功能:将字符串 strdelim 中的字符分割成若干子串(token)。
特点:修改原字符串,把匹配到的分隔符改成 ‘\0’

使用流程

步骤调用方式说明
第一次strtok(str, delim)传入非 NULL 字符串,返回第一个 token
后续strtok(NULL, delim)传入 NULL,继续从上一次位置往后找
#define MAXARGC 128                       
char* g_argv[MAXARGC];       
int g_argc = 0;

bool CommandParse(char* commandline)
{
#define SEP " "
    g_argc = 0;
    g_argv[g_argc++] = strtok(commandline, SEP);

    while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
    g_argc--;
    return g_argc > 0 ? true : false;
}

2.4 创建子进程执行命令

解析命令行并且生成命令行参数表之后,现在就可以去执行命令了,那么怎么执行命令呢?

我们的shell并不是自己去执行,而是创建子进程,然后让子进程去执行命令shell等待子进程退出

// last exit code
int lastcode = 0;

int Execute()
{

    pid_t id = fork();
    if(id==0)                                                                                                         
    {
        // child
        execvp(g_argv[0], g_argv);
        exit(1);
    }

    // father
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    //(void)rid; // rid使用一下
    if(rid>0)
    {
        lastcode = WEXITSTATUS(status);
    }
    return 0;
}

在这里插入图片描述

三. 自定义shell的扩展实现

3.1 环境变量表

上面的shell我们可以运行了,但是没有考虑内建命令环境变量表等这些东西,所以我们把它完善一下。

bash启动时,它的环境变量表从我们系统的配置文件中来,但是这里没办法从系统配置文件中读,所以我们这里就只能从父进程bash获取环境变量表,通过environ来获取从父进程继承下来的环境变量表。

但是我们也要有我们自己的一张环境变量表,所以要手动维护一张自己的环境变量表,并且把它们导出来,通过putenv来导出这些环境变量。

#define MAX_ENVS 100    
char* g_env[MAX_ENVS+1];    
int g_envs = 0;

void InitEnv()
{
    extern char** environ;                                                                                            
    memset(g_env, 0, sizeof(g_env));
    g_envs = 0;

    // 本来要从配置文件来
    // 1. 获取环境变量
    for(int i=0;environ[i];i++)
    {
        // 申请空间
        g_env[i] = (char*)malloc(strlen(environ[i]+1));
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs++] = (char*)"HAHA=for_test"; //for_test
    g_env[g_envs] = NULL;

    // 2. 导成环境变量
    for(int i=0;g_env[i];i++)
    {
        putenv(g_env[i]);
    }
}

3.2 内建命令

内建命令指的是bash不创建子进程去执行,而是bash自己去执行的命令,我们现在知道的内建命令有cd、export、echo、alias等。

cd

cd命令肯定不能让子进程去执行,因为修改的子进程的路径,而不是shell自己的路径,所以要让shell自己去执行,我们可以使用chdir系统调用来修改当前工作路径:

在这里插入图片描述

关于cd命令

  • cd:进入用户的家目录
  • cd ~:进入用户的家目录
  • cd where:进入指定路径
  • cd -:进入上次的工作路径
char cwd[1024];
char cwdenv[1024];
char oldpwd[1024];

const char* GetOldPwd()
{
    const char* oldpwd = getenv("OLDPWD");
    return oldpwd == NULL ? "None" : oldpwd;
}

const char* GetHome()
{
    const char *home = getenv("HOME");
    return home == NULL ? "" : home;
}

void UpdateOldPwd()
{
    snprintf(oldpwd, sizeof(oldpwd), "OLDPWD=%s", cwd);
    //printf("%s\n",oldpwd);
    putenv(oldpwd);
}

bool Cd()              
{        
                                         
    if(g_argc == 1)    
    {                           
        std::string home = GetHome();    
        if(home.empty()) return true;                                                                                                                                                                     
        chdir(home.c_str());    
        UpdateOldPwd();    
        GetPWD();                         
    }                        
    else{                          
        std::string where = g_argv[1];    
        if(where == "-"){    
            chdir(GetOldPwd());    
            UpdateOldPwd();      
            GetPWD();          
        }else if(where == "~"){    
            chdir(GetHome());    
            UpdateOldPwd();          
            GetPWD();          
        }else{           
            chdir(where.c_str());    
            UpdateOldPwd();    
            GetPWD();    
        }    
    }    
    return true;    
}

echo

echo命令也是内建命令,我们知道,echo $?可以查看最近一次进程退出时的退出码。

查看最近一次进程退出时的退出码,这些退出码在哪里呢?
肯定不会在子进程中,那就在bash

所以在我们自定义的shell中,我们可以定义一个全局变量,每次执行一次命令就对其进行一次修改。

// last exit code
int lastcode = 0;

void Echo()
{
    if(g_argc == 2)
    {
        // echo "hello world"
        // echo $?
        // echo $PATH
        std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char* env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else
        {
            std::cout << opt << std::endl;
        }
    }
}

export

该命令的作用就是设置环境变量

void Export()
{
    std::string addenv = g_argv[1];
    auto pos = addenv.find("=");
    std::string name = addenv.substr(0,pos);
    std::string val = addenv.substr(pos+1);
    bool vis = false;
    int i = 0;
    for(;g_env[i];i++)
    {
        std::string s = g_env[i];
        auto j = s.find('=');
        std::string envname = s.substr(0,j);
        if(envname == name){
            vis = true;
            break;
        } 
    }
    if(vis)
    {
        std::string oldenv = g_env[i];
        std::string oldval = oldenv.substr(oldenv.find('=')+1);
        if(addenv.size() > oldenv.size())
        {
            free(g_env[i]);
            g_env[i] = (char*)malloc(addenv.size()+1);
        }
        strcpy(g_env[i], addenv.c_str());
        putenv(g_env[i]);
    }
    else
    {                                                                                                                                                                                                     
        if(g_envs < MAX_ENVS)
        {
            g_env[g_envs] = (char*)malloc(addenv.size()+1);
            strcpy(g_env[g_envs], addenv.c_str());
            putenv(g_env[g_envs]);
            g_env[g_envs++] = NULL;
        }
    }
}

alias

经过测试我们可以发现,bash支持ll,而我们的shell是不支持的;

我们知道ll是别名,如果想要我们的shell也支持别名,我们就要在shell中新增一张别名表,维护好这张别名表,就可以支持对其它命令取别名了。

可以使用unordered_map或者map来存储这张别名表。

std::unordered_map<std::string,std::string> alias_list;

void Alias()
{
    std::string name = g_argv[1];
    for(int i=2;g_argv[i];i++)
    {
        std::string s = g_argv[i];
        std::string ss = " ";
        ss += s;
        name+=ss;
    }
    //std::cout << name << std::endl;
    auto pos = name.find('=');
    std::string name1 = name.substr(0,pos);
    std::string name2 = "";
    if(name[pos+1]=='"' || name[pos+1]=='\'')
    {
        name2 = name.substr(pos+2);
        name2.pop_back();
    }
    else{
        name2 = name.substr(pos+1);
    }
    alias_list.insert({name1,name2});
}

bool AliasManage()
{
    std::string cmd = g_argv[0];                                                                                                                                                                          
    if(alias_list.count(cmd))
    {
        return CommandParse((char*)alias_list[cmd].c_str());
    }
    return true;
}

bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    else if(cmd == "export")
    {
        Export();
        return true;
    }
    else if(cmd == "alias")
    {
        Alias();
        return true;
    }
    
    return false;
}

int main()
{
    // shell 启动的时候,从系统中获取环境变量
    // 我们的环境变量信息应该从父shell统一来
    
    InitEnv();
                                                                                                                                                                                                          
    while(1)
    {
        // 1. 输出命令行提示符
        PrintCommandPrompt();
        //printf("[%s@%s %s]# ", GetUserName(), GetHostName(), GetPWD());

        // 2. 获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline,sizeof(commandline)))
        {
            continue;
        }

        // 3. 命令行分析  "ls -a -l" -> "ls" "-a" "-l"
        if(!CommandParse(commandline))
            continue;
        
        // 4. 别名处理
        if(!AliasManage())
            continue;

        // 5. 检测并处理内建命令
        if(CheckAndExecBuiltin())
            continue;

        // 6. 执行命令
        Execute();

        //PrintArgv();
        //printf("echo %s\n", commandline);
    }

    return 0;
}

在这里插入图片描述

除此之外,还有非常多的内建命令,这里就不一一实现了,大家感兴趣的话可以自己动手试试!

3.3 源代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstring>
#include <unordered_map>

#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "

// 下面是shell定义的全局数据

// 1. 命令行参数表
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc = 0;

// 2. 环境变量表
#define MAX_ENVS 100
char* g_env[MAX_ENVS+1];
int g_envs = 0;

// 3. 别名映射表
std::unordered_map<std::string,std::string> alias_list;

char cwd[1024];
char cwdenv[1024];
char oldpwd[1024];

// last exit code
int lastcode = 0;

const char* GetUserName()
{
    const char* name = getenv("USER");
    return name == NULL ? "None" : name;
}

const char* GetHostName()
{
    const char* hostname = getenv("HOSTNAME");
    return hostname == NULL ? "None" : hostname;
}

const char* GetPWD()
{
    //connst char* pwd = getenv("PWD");
    const char* pwd = getcwd(cwd,sizeof(cwd));
    if(pwd != NULL)
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
        putenv(cwdenv);
    }
    return pwd == NULL ? "None" : pwd;
}

const char* GetOldPwd()
{
    const char* oldpwd = getenv("OLDPWD");
    return oldpwd == NULL ? "None" : oldpwd;
}

const char* GetHome()
{
    const char *home = getenv("HOME");
    return home == NULL ? "" : home;
}

void UpdateOldPwd()
{
    snprintf(oldpwd, sizeof(oldpwd), "OLDPWD=%s", cwd);
    //printf("%s\n",oldpwd);
    putenv(oldpwd);
}

void InitEnv()
{
    extern char** environ;
    memset(g_env, 0, sizeof(g_env));
    g_envs = 0;

    // 本来要从配置文件来
    // 1. 获取环境变量
    for(int i=0;environ[i];i++)
    {
        // 申请空间
        g_env[i] = (char*)malloc(strlen(environ[i]+1));
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs++] = (char*)"HAHA=for_test"; //for_test
    g_env[g_envs] = NULL;

    // 2. 导成环境变量
    for(int i=0;g_env[i];i++)
    {
        putenv(g_env[i]);
    }
}

//command
bool Cd()
{
    
    if(g_argc == 1)
    {
        std::string home = GetHome();
        if(home.empty()) return true;
        chdir(home.c_str());
        UpdateOldPwd();
        GetPWD();
    }
    else{
        std::string where = g_argv[1];
        if(where == "-"){
            chdir(GetOldPwd());
            UpdateOldPwd();
            GetPWD();
        }else if(where == "~"){
            chdir(GetHome());
            UpdateOldPwd();
            GetPWD();
        }else{
            chdir(where.c_str());
            UpdateOldPwd();
            GetPWD();
        }
    }
    return true;
}

void Echo()
{
    if(g_argc == 2)
    {
        // echo "hello world"
        // echo $?
        // echo $PATH
        std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char* env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else
        {
            std::cout << opt << std::endl;
        }
    }
}

void Export()
{
    std::string addenv = g_argv[1];
    auto pos = addenv.find("=");
    std::string name = addenv.substr(0,pos);
    std::string val = addenv.substr(pos+1);
    bool vis = false;
    int i = 0;
    for(;g_env[i];i++)
    {
        std::string s = g_env[i];
        auto j = s.find('=');
        std::string envname = s.substr(0,j);
        if(envname == name){
            vis = true;
            break;
        } 
    }
    if(vis)
    {
        std::string oldenv = g_env[i];
        std::string oldval = oldenv.substr(oldenv.find('=')+1);
        if(addenv.size() > oldenv.size())
        {
            free(g_env[i]);
            g_env[i] = (char*)malloc(addenv.size()+1);
        }
        strcpy(g_env[i], addenv.c_str());
        putenv(g_env[i]);
    }
    else
    {
        if(g_envs < MAX_ENVS)
        {
            g_env[g_envs] = (char*)malloc(addenv.size()+1);
            strcpy(g_env[g_envs], addenv.c_str());
            putenv(g_env[g_envs]);
            g_env[g_envs++] = NULL;
        }
    }
}

void Alias()
{
    std::string name = g_argv[1];
    for(int i=2;g_argv[i];i++)
    {
        std::string s = g_argv[i];
        std::string ss = " ";
        ss += s;
        name+=ss;
    }
    //std::cout << name << std::endl;
    auto pos = name.find('=');
    std::string name1 = name.substr(0,pos);
    std::string name2 = "";
    if(name[pos+1]=='"' || name[pos+1]=='\'')
    {
        name2 = name.substr(pos+2);
        name2.pop_back();
    }
    else{
        name2 = name.substr(pos+1);
    }
    alias_list.insert({name1,name2});
}

std::string DirName(const char* pwd)
{
#define SLASH "/"
    std::string dir = pwd;
    if(dir==SLASH) return SLASH;
    auto pos = dir.rfind(SLASH);
    if(pos==std::string::npos) return "BUG?";
    return dir.substr(pos+1);
}

void MakeCommandLine(char cmd_prompt[], int size)
{
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPWD()).c_str());
}

void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt,sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}

bool GetCommandLine(char* out, int size)
{
    char* c = fgets(out, size, stdin);
    if(c==NULL) return 1;
    out[strlen(out)-1] = '\0'; //清理\n
    if(strlen(out)==0) return false;
    return true;
}

// 3. 命令行解析
bool CommandParse(char* commandline)
{
#define SEP " "
    g_argc = 0;
    g_argv[g_argc++] = strtok(commandline, SEP);

    while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
    g_argc--;
    return g_argc > 0 ? true : false;
}

void PrintArgv()
{
    for(int i = 0; g_argv[i]; i++)
    {
        printf("argv[%d]->%s\n", i, g_argv[i]);
    }
    printf("argc: %d\n", g_argc);
}


bool AliasManage()
{
    std::string cmd = g_argv[0];
    if(alias_list.count(cmd))
    {
        return CommandParse((char*)alias_list[cmd].c_str());
    }
    return true;
}

bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    else if(cmd == "export")
    {
        Export();
        return true;
    }
    else if(cmd == "alias")
    {
        Alias();
        return true;
    }
    
    return false;
}

int Execute()
{

    pid_t id = fork();
    if(id==0)
    {
        // child
        execvp(g_argv[0], g_argv);
        exit(1);
    }

    // father
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    //(void)rid; // rid使用一下
    if(rid>0)
    {
        lastcode = WEXITSTATUS(status);
    }
    return 0;
}

int main()
{
    // shell 启动的时候,从系统中获取环境变量
    // 我们的环境变量信息应该从父shell统一来
    
    InitEnv();

    while(1)
    {
        // 1. 输出命令行提示符
        PrintCommandPrompt();
        //printf("[%s@%s %s]# ", GetUserName(), GetHostName(), GetPWD());

        // 2. 获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline,sizeof(commandline)))
        {
            continue;
        }

        // 3. 命令行分析  "ls -a -l" -> "ls" "-a" "-l"
        if(!CommandParse(commandline))
            continue;
        
        // 4. 别名处理
        if(!AliasManage())
            continue;

        // 5. 检测并处理内建命令
        if(CheckAndExecBuiltin())
            continue;

        // 6. 执行命令
        Execute();

        //PrintArgv();
        //printf("echo %s\n", commandline);
    }

    return 0;
}

最后

本篇关于自定义Shell的内容到这里就结束了,其中还有很多细节值得我们去探究,需要我们不断地学习。如果本篇内容对你有帮助的话就给一波三连吧,对以上内容有异议或者需要补充的,欢迎大家来讨论!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值