深入了解linux系统—— 自定义shell

shell的原理

我们知道,我们程序启动时创建的进程,它的父进程都是bash也就是shell命令行解释器;

bash都做了哪些工作呢?

根据已有的知识,我们可以简单理解为:

  1. 输出命令行提示符
  2. 获取并解析我们输入的指令
  3. 执行内建命令或者创建子进程执行命令

就如下图所示,bash读取我们输入的命令,并进行解析;然后创建子进程执行命令(bash等待子进程退出)。

自定义shell实现

根据上述bash的工作原理,我们现在实现一个简单的自定义shell

要想实现一个自定义shell,我们就要执行以下过程:

  • 获取命令行
  • 解析命令行
  • 创建子进程,让子进程执行命令(使用程序替换)
  • shell等待子进程退出

当然,还存在一部分内建命令,它是由bash自主实现的;我们要进行特殊处理;

1. 输出命令行提示符

在实现自定义shell之前,我们来看

我们的bash在每次都会输出命令行提示符,然后等待我们用户输入;

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

  • 用户名USER
  • 主机名HOSTNAME
  • 当前工作路径PWD

这些在我们的环境变量表中都能够找到,所以我们就可以使用getenv来获取。

所以这个就非常容易实现了,直接按照格式输出即可;

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

但是我们会发现bash输出的命令行提示符中的当前工作路径只有当前文件,而我们通过环境变量PWD获取的是当前工作目录的绝对路径,所以我们这里要进行一下分割;

详细代码如下:

cpp 复制代码
//命令行提示符格式
#define CLP "[%s@%s %s]#"
//命令行提示符的最大长度
#define MAX_CLP 100
//获取环境变量
const char* GetUser(){
    return getenv("USER");
}
const char* GetHostName(){
    return getenv("HOSTNAME");
}
const char* GetPwd()
{
    return getenv("PWD");
}
//分割路径
//"/home/lxb/linux/MYSHELL" --> "MYSHELL"
string DirPwd(char s[])
{
#define SLASH "/"
    string str = s;
    if(str == SLASH) return str;                                                                       
    auto pos = str.rfind(SLASH);
    if(pos == std::string::npos) return "err";
    return str.substr(pos+1);
}         
//生成命令行提示符
void CommandLinePrompt(char buffer[])
{
    sprintf(buffer,CLP,GetUser(),GetHostName(),DirPwd(GetPwd());
}
//输出命令行提示符
void PrintCommandPrompt()
{
    char buffer[100];
    CommandLinePrompt(buffer);
    printf("%s",buffer);
    fflush(stdout);
}

2. 获取用户输入的信息

输出了命令行提示符,接下来就要获取用户输入的信息了,也就是输入的命令;

在用户输入时,是会输入空格的,所以这里我们不能使用scanf/cin进行输入;我们要使用fgets进行输入。

而也可能存在只输入一个回车的情况,所以我们要进行特殊判断:当只输入一个回车时就再次输出命令行提示符,然后等待用户输入。

输入:

cpp 复制代码
//命令行信息最大长度    
#define MAX_COMLINE 1024
char* GetCommandLine(char buff[]){
    char* c = fgets(buff,MAX_COMLINE,stdin);
    buff[strlen(buff)-1] = 0;//处理回车
    return c;
}

这里来测试一下输出命令行提示符和获取用户输入信息;

如果获取用户输入信息成功,那就输出获取的输入信息,如果失败或者只输入了一个回车就再次输出命令行提示符,然后等待用户输入。

cpp 复制代码
int main()
{
    while(1){
        //1. 输出命令行提示符
        PrintCommandPrompt();
        //2. 获取用户输入信息
        char buff[MAX_COMLINE];
        char* c = GetCommandLine(buff);
        if(c == NULL)//读取用户输入信息失败
            continue;
        if(strlen(buff) == 0)//只输入了空格
            continue;
        printf("%s\n",buff);
    }
    return 0;
}

3. 命令行解析

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

命令行参数个数g_argc,命令行参数表g_argv;我们可以设置成全局的,这样每次通过修改argcargv中最后一个指针为NULL即可。

这里,我们可以使用strtok函数进行分割命令行参数;

简单描述一下strtok,在str字符串中查找sep字符串的内容,找到并将其修改成\0并返回指向这个字符串的指针。

在分割完成之后,我们直接让g_argv命令行参数表指向对应位置即可。

cpp 复制代码
#define MAX_ARGC 50
//命令行参数表
int g_argc;
char* g_argv[MAX_ARGC];
//解析命令行参数                                                               
//"ls -a -l"--> "ls" "-a" "-l"                                                      
void PrasCommandLine(char buff[]){                                  
    g_argc = 0;                                          
    const char* sep = " ";      
    for(g_argv[g_argc] = strtok(buff,sep);g_argv[g_argc] != NULL; g_argv[g_argc] = strtok(NULL,sep))    
        g_argc++;    
}   

这里还是测试,命令行解析是否成功。

4. 创建子进程执行命令

解析命令行,生成命令行参数表之后,现在就是去执行命令了;

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

cpp 复制代码
void CreateChildExecute(){    
    int id = fork();    
    if(id < 0)    
    {    
        perror("fork");    
        exit(1);    
    }    
    else if (id == 0){    
        //child    
        execvp(g_argv[0],g_argv);    
        exit(2);    
    }    
    //parent    
    wait(NULL);    
}

这里我们使用的程序替换函数是execvp,我们有命令行参数表(数组),而且我们输入的系统命令是不带路径的;

看一下运行效果:

扩展部分

在上述描述中,简单的shell运行就OK了;

但是上述我们没有考虑内建命令环境变量表等这些东西;

环境变量表

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

这里即从bash中获取环境变量;

但是拿到了环境变量表,进程中还是保存的来自父进程bash的环境变量;environ还是执行bash的环境变量表。

我们需要导出环境变量,使用putenv来导出环境变量;然后让environ执行我们的环境遍历表。

cpp 复制代码
//环境变量表最大数量
#define MAX_GENV 500
int g_argc;
char* g_argv[MAX_GARGC];
//环境变量表
int g_envs;    
char* g_env[MAX_GENV];    
//导入环境变量    
void EnvInit(){      
    extern char** environ;    
    memset(g_env,0,sizeof(g_env));    
    g_envs = 0;                              
    //环境变量表要从系统文件中来             
    //这从bash中获取    
    for(int i = 0;environ[i]!=NULL;i++){    
        g_env[i] = (char*) malloc(strlen(environ[i])+1);    
        if(g_env[i] == NULL){    
            perror("malloc");    
            exit(3);    
        }    
        strcpy(g_env[i], environ[i]);    
        g_envs++;    
    }    
    g_env[g_envs] = NULL;    
    //导出环境变量    
    for(int i = 0;i < g_envs;i++){    
        putenv(g_env[i]);    
    }    
    environ = g_env;                                                                                                                                                                              
}

在我们程序启动时,从父进程bash获取环境变量即可。

内建命令

内建命令,指bash不创建子进程去执行,而是bash自己去执行的命令;

我们现在知道内建命令有cdexportecho等。

cd

cd命令,仔细想一想,肯定不会是子进程执行的;因为子进程执行它修改的是子进程的工作路径。

我们要让shell去执行cd命令,肯定不能使用程序替换了,我们可以使用chdir系统调用来修改当前工作路径;

cd命令:

  1. cd:会进入用户的家目录
  2. cd ~:进入用户的家目录
  3. cd where:进入指定路径
  4. cd -:进入上次的工作路径
cpp 复制代码
void CD(){
    std::string oldpwd = getenv("PWD");
    std::string where;
    if(g_argc == 1){
        where = GetHome();
        if(where.empty()) return;
        chdir(where.c_str());                                                                                                                                                                     
    }
    else{
        where = g_argv[1];
        if(strcmp("-", g_argv[1]) == 0){
            where = getenv("OLDPWD");
        }
        else if(strcmp("~", g_argv[1]) == 0){
            where = GetHome();
            if(where.empty()) return;
        }
        chdir(where.c_str());
        //修改环境变量
    }
}

当然呢,这里存在一个问题,当我们cd -进入上次各种目录时就会发现,它进入的一直都是同一个目录;

因为我们这里没有修改环境变量OLDPWD

echo

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

但是在我们的shell中,如果让子进程去执行echo $?,它则是直接输出$?

echo $?,查看最近一次进程退出时的退出码;而这些退出码在哪里呢?

肯定不会在子进程中,那就在bash中了;

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

cpp 复制代码
//最近一次进程退出时的退出码
int last_code;
void Echo(){                                                      
    if(g_argc == 2){    
        std::string str = g_argv[1];    
        if(str == "$?"){    
            std::cout<<last_code<<std::endl;    
        }    
        else if(str[1] == '$'){    
            std::string env_name = str.substr(1);    
            const char* s = getenv(env_name.c_str());    
            if(s)    
                std::cout<<s<<std::endl;    
        }    
        else{    
            std::cout<<str<<std::endl;    
        }    
    }    
}    

这里,设置了last_code,那在每次执行命令之后,都要进行更新last_code

除此之外呢,还有非常多的内建命令,比如exportunset等;这里就不实现了。

别名alias

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

我们知道ll是别名,所以如果想要我们shell支持别名,我们就要在shell中新增一张别名表;

然后维护这张别名表,就可以支持ll等指令的别名了。

这里就不实现了,可以使用unordered_map或者map来存储这张别名表。

到这里本篇文章大致内容就结束了;

本篇文章自定义实现shell,帮助理解进程,以及bash是如何工作的

附源码:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <cstdbool>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
//命令行提示符格式
#define CLP "[%s@%s %s]# "
#define MAX_CLP 100
//命令行信息最大长度
#define MAX_COMLINE 1024
//命令行参数最大个数
#define MAX_GARGC 50
//环境变量表最大数量
#define MAX_GENV 500
int g_argc;
char* g_argv[MAX_GARGC];
//环境变量表
int g_envs;
char* g_env[MAX_GENV];
//最近一次进程退出时的退出码
int last_code = 0;
//导入环境变量
void EnvInit(){
    extern char** environ;
    memset(g_env,0,sizeof(g_env));
    g_envs = 0;
    //环境变量表要从系统文件中来                                                                                                                                                                  
    //这从bash中获取
    for(int i = 0;environ[i]!=NULL;i++){
        g_env[i] = (char*) malloc(strlen(environ[i])+1);
        if(g_env[i] == NULL){
            perror("malloc");
            exit(3);
        }
        strcpy(g_env[i], environ[i]);
        g_envs++;                                                                                                                                                                                 
    }
    g_env[g_envs] = NULL;
    //导出环境变量
    for(int i = 0;i < g_envs;i++){
        putenv(g_env[i]);
    }
    environ = g_env;
}
//获取环境变量
char* GetUser(){
    return getenv("USER");
}
char* GetHostName(){
    return getenv("HOSTNAME");
}
//路径切割
std::string DirPwd(const char s[])
{
#define SLASH "/"
    std::string str = s;
    if(str == SLASH) return str;
    auto pos = str.rfind(SLASH);
    if(pos == std::string::npos) return "err";
    return str.substr(pos+1);
}
const char* GetPwd()
{
    //return getenv("PWD");
    return DirPwd(getenv("PWD")).c_str();
}
const char* GetHome(){
    return getenv("HOME");                                                  
}
//生成命令行提示符
void CommandLinePrompt(char buffer[])
{
    sprintf(buffer,CLP,GetUser(),GetHostName(),GetPwd());
    //sprintf(buffer,CLP,GetUser(),GetHostName(),DirPwd(GetPwd()).c_str());
}
void PrintCommandPrompt()
{
    char buffer[100];
    CommandLinePrompt(buffer);
    printf("%s",buffer);
    fflush(stdout);
}
char* GetCommandLine(char buff[]){
    char* c = fgets(buff,MAX_COMLINE,stdin);
    buff[strlen(buff)-1] = 0;
    return c;
}
void PrasCommandLine(char* buff){
    g_argc = 0;
    const char* sep = " ";
    for(g_argv[g_argc] = strtok(buff,sep); g_argv[g_argc] != NULL; g_argv[g_argc] = strtok(NULL,sep)){
        g_argc++;
    }
}
void CreateChildExecute(){
    int id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1);                                                                                                                                                                                  
    }
    else if (id == 0){
        //child
        execvp(g_argv[0],g_argv);
        exit(2);
    }
    //parent
    int status = 0;
    int rid = wait(&status);
    if(rid > 0)
        last_code = WEXITSTATUS(status);
}
void Cd(){
    std::string oldpwd = getenv("PWD");
    std::string where;
    if(g_argc == 1){
        where = GetHome();
        if(where.empty()) return;                                                                                                                                                                 
        chdir(where.c_str());
    }
    else{
        where = g_argv[1];
        if(strcmp("-", g_argv[1]) == 0){
            where = getenv("OLDPWD");
        }
        else if(strcmp("~", g_argv[1]) == 0){
            where = GetHome();
            if(where.empty()) return;
        }
        chdir(where.c_str());
        //修改环境变量
    }
    //std::string old = std::string("OLDPWD=") + oldpwd;
    //char* arr = (char*)malloc(old.size()+1);
    //for(size_t i = 0;i<old.size();i++){
    //    arr[i] = old[i];
    //}
    //arr[old.size()] = 0;
    //putenv(arr);
}
void Echo(){
    if(g_argc == 2){                                                                                                                                                                              
        std::string str = g_argv[1];
        if(str == "$?"){
            std::cout<<last_code<<std::endl;
        }
        else if(str[1] == '$'){
            std::string env_name = str.substr(1);
            const char* s = getenv(env_name.c_str());
            if(s)
                std::cout<<s<<std::endl;
        }
        else{
            std::cout<<str<<std::endl;
        }
    }
}
//判断内建命令
bool BinCommand(){
    std::string str = g_argv[0];
    if(str == "cd"){
        Cd();
        last_code = 0;
        return true;
    }
    else if(str == "echo"){
        Echo();
        last_code = 0;
        return true;
    }
   return false;            
}
void PrintArgv(){
    for(int i = 0;i < g_argc; i++){
        printf("g_argv[%d] : %s\n",i,g_argv[i]);
    }
}
void PrintEnv(){
    for(int i = 0; i < g_envs;i++){
        printf("g_env[%d] : %s\n",i,g_env[i]);
    }
}
int main()
{
    //获取环境变量表
    EnvInit();
    //PrintEnv();
    while(1){                                                                                                                                                                                     
        //1. 输出命令行提示符
        PrintCommandPrompt();
        //2. 获取用户输入信息
        char buff[MAX_COMLINE];
        char* c = GetCommandLine(buff);
        if(c == NULL)//读取用户输入信息失败
            continue;
        if(strlen(buff) == 0)//只输入了空格
            continue;
        //3. 命令行解析
        PrasCommandLine(buff);
        //4.内建命令
        if(BinCommand())
            continue;
        //5. 创建子进程执行命令
        CreateChildExecute();
    }
    return 0;
}
相关推荐
轻颂呀3 分钟前
Linux中常见开发工具简单介绍
linux
SuperW33 分钟前
Linxu实验五——NFS服务器
运维·服务器
promise52435 分钟前
JVM之jcmd命令详解
java·linux·运维·服务器·jvm·bash·jcmd
Bruce_Liuxiaowei1 小时前
Day 5:Warp高级定制与自动化
运维·warp
溜达的大象1 小时前
docker创建一个centOS容器安装软件(以宝塔为例)的详细步骤
运维·docker·容器
XiaoCCCcCCccCcccC1 小时前
Linux网络基础 -- 局域网,广域网,网络协议,网络传输的基本流程,端口号,网络字节序
linux·c语言·网络·c++·网络协议
果子⌂1 小时前
Linux系统入门第十二章 --Shell编程之正则表达式
linux·运维·服务器
随风奔跑的十八岁1 小时前
java 破解aspose.words 18.6 使用
java·linux·word转pdf·aspose-words
海尔辛1 小时前
学习黑客5 分钟读懂Linux Filesystem Interaction Continued
linux·服务器·学习
学习2年半2 小时前
服务器mysql连接我碰到的错误
运维·服务器·mysql