当前位置: 首页 > news >正文

深入了解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获取的是当前工作目录的绝对路径,所以我们这里要进行一下分割;

详细代码如下:

//命令行提示符格式
#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进行输入。

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

输入:

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

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

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

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命令行参数表指向对应位置即可。

在这里插入图片描述

#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等待子进程退出。

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执行我们的环境遍历表。

//环境变量表最大数量
#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 -:进入上次的工作路径
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中,我们可以定义一个全局变量,每次执行一次命令就对其进行一次修改。

//最近一次进程退出时的退出码
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是如何工作的

附源码:

#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){//childexecvp(g_argv[0],g_argv);exit(2);}//parentint 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;
}

相关文章:

  • 《智能网联汽车 自动驾驶功能道路试验方法及要求》 GB/T 44719-2024——解读
  • ES常识5:主分词器、子字段分词器
  • Nodejs核心机制
  • 支持selenium的chrome driver更新到136.0.7103.92
  • 【Java EE初阶 --- 多线程(初阶)】线程安全问题
  • 百度AI战略解析:文心一言与自动驾驶的双轮驱动
  • Hibernate 性能优化:告别慢查询,提升数据库访问性能
  • 基于 PostgreSQL 的 ABP vNext + ShardingCore 分库分表实战
  • 使用FastAPI和React以及MongoDB构建全栈Web应用05 FastAPI快速入门
  • 红黑树(C++)
  • A1062 PAT甲级JAVA题解 Talent and Virtue
  • 大语言模型通过MCP控制STM32-支持Ollama、DeepSeek、openai等
  • 【C++】内存管理 —— new 和 delete
  • D. Explorer Space(dfs+剪枝)
  • 深入理解深度Q网络DQN:基于python从零实现
  • 三、c语言练习四题
  • 前端项目打包部署流程j
  • 无人机空中物流优化:用 Python 打造高效配送模型
  • 华为IP(6)
  • 中空电机在安装垂直轴高速电机后无法动平衡的原因及解决方案
  • 万科:存续债券均正常付息兑付
  • 城事 | 重庆新增热门打卡地标,首座熊猫主题轨交站亮相
  • 中方发布会:中美经贸高层会谈取得了实质性进展,达成了重要共识
  • 金科股份重整方案通过,正式进入重整计划执行环节
  • 浙江首个核酸药谷落子杭州,欢迎订阅《浪尖周报》第23期
  • 第19届威尼斯建筑双年展开幕,中国案例呈现“容·智慧”