shell
的原理
我们知道,我们程序启动时创建的进程,它的父进程都是bash
也就是shell
命令行解释器;
那bash
都做了哪些工作呢?
根据已有的知识,我们可以简单理解为:
- 输出命令行提示符
- 获取并解析我们输入的指令
- 执行内建命令或者创建子进程执行命令

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

自定义shell
实现
根据上述bash
的工作原理,我们现在实现一个简单的自定义shell
;
要想实现一个自定义shell
,我们就要执行以下过程:
- 获取命令行
- 解析命令行
- 创建子进程,让子进程执行命令(使用程序替换)
shell
等待子进程退出
当然,还存在一部分内建命令,它是由bash
自主实现的;我们要进行特殊处理;
1. 输出命令行提示符
在实现自定义shell
之前,我们来看

我们的bash
在每次都会输出命令行提示符,然后等待我们用户输入;
看这个命令行提示符,它包含以下信息:
- 用户名
USER
;- 主机名
HOSTNAME
;- 当前工作路径
PWD
;这些在我们的环境变量表中都能够找到,所以我们就可以使用
getenv
来获取。

所以这个就非常容易实现了,直接按照格式输出即可;
这样我们需要获取环境变量USER
、HOSTNAME
、PWD
等;
但是我们会发现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
;我们可以设置成全局的,这样每次通过修改argc
和argv
中最后一个指针为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
自己去执行的命令;我们现在知道内建命令有
cd
、export
、echo
等。
cd
cd
命令,仔细想一想,肯定不会是子进程执行的;因为子进程执行它修改的是子进程的工作路径。
我们要让shell
去执行cd
命令,肯定不能使用程序替换了,我们可以使用chdir
系统调用来修改当前工作路径;

cd
命令:
cd
:会进入用户的家目录cd ~
:进入用户的家目录cd
where
:进入指定路径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
。
除此之外呢,还有非常多的内建命令,比如export
、unset
等;这里就不实现了。
别名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;
}