目录
一、什么是Bash
首先理解shell,Bash,命令行解释器在Linux中扮演的角色:
Shell :用于解释和执行用户的命令,提供用户与操作系统之间的交互接口
Bash :作为Shell的一种实现 ,提供了更多的功能和增强特性,是大多数Linux发行版的默认Shell命令行解释器:负责解释和执行用户输入的命令,是Shell的核心功能之一
Bash也是一个进程,并且是不断运行中的进程,那么在我们输入指令的时候实际上就是Bash创建了子进程,然后子进程进行进程替换为要执行的进程,进而完成我们的命令,
当我们执行一个指令进程之后,可以看到这个进程的父进程是Bash,证明了所有指令都是Bash的子进程
二、实现Bash:
1、整体需求分析:
Bash的任务:就是把一个指令进程进行解释,然后进行进程替换
所以整体思路:
1、实现命令行的外表
2、把用户输入指令的看做一个字符串
3、把这个字符串进行分解
4、分解后创建子进程进行进程替换
5、对特殊的指令(内建命令)进行特殊处理
看看主函数的整体逻辑:
首先就是初始化,这是拿到输入的指令
接着分割字符串,如果没有输入就continue重新读取输入
然后执行内建命令,如果执行成功返回1,没有执行则返回0
最后执行普通命令
2、初始化:
对于这两个可以封装为一个函数
上面,传过来的这个line是一个数组,作用是读取命令行中的一整行字符串,
printf后面通过自己定义的宏(这样个性化的时候就只需修改宏即可)和自己所写的函数来完成命令行外表
接着定义一个char* 类型来在输入流中拿到字符串放在target数组中
最后一行是处理回车符将读取的回车变为 \0 这样就能够得到最干净的字符串了
3、分割字符串:
然后就是对字符串进行分割
如上,这里是用strtok函数进行分割的,具体解析可参考下述文章
strtok()函数详解! - Yuxi001 - 博客园https://www.cnblogs.com/yuxiyuxi/p/17770807.html
如上采用的是使用函数strtok进行分割,DEL是定义的宏,使用其的原因同样是方便修改,总体思路很简单,首先在全局里定义一个数组(这里是char* argv[ ])来放字符串,所以每次分割后都放到argv中,最后返回 i - 1 也就是切割后的子字符串的个数方便后续维护
4、执行普通命令:
前面了解到执行普通命令就是Bash创建子进程然后进行进程替换进而完成
思路:
封装一个函数,在函数中首先fork创建子进程,
在子进程代码块 中使用函数execvp进行进程替换,第一个参数就是分割后的argv中的0下标位置,第二个参数就是argv数组的所有,直接传数组名即可,然后如果替换失败就结束进程,退出码设置为自己所设置的特殊退出码
在父进程代码块中等待子进程即可
这样就可以执行普通指令了
5、内建命令与特殊处理:
1、ls的颜色:
实际上就是在指令ls后面加上 --color 即可,所以可以在内建命令中进行一次特殊处理,即可实现ls的颜色显示
思路:
通过strcmp函数进行判断,发现如果是ls指令的话就在此时的argv数组中的argc位置加上--color,然后把argc++,之后注意把argv的argc位置置为NULL
2、内建命令cd:
为什么cd命令需要进行特殊处理呢?
这是因为Bash已经创建子进程了,然后子进程进行cd到其他目录下,这个时候就虽然确实加载了cd命令,但是是不会影响父进程的,和父进程就没有啥关系了,所以就需要父进程自己去处理,
这里采用的是chdir函数进行内建命令cd的处理:
如下,当修改成功时,返回0,修改失败时返回 -1
实现思路:
在这里的Bash实现,cd命令后面必须跟上一个字符串,然后也是使用strcmp函数继续比较
在实现中直接先使用chdir()函数,里面的参数就是argv数组的第二个,
然后下面的getpwd()是之前写的一个函数,作用是得到当前工作路径下的绝对路径,这样就能够修改环境变量中的PWD了
最后在把环境变量的PWD修改,这里使用sprintf()函数实现,将格式化的数据写入PWD环境变量中
3、export:
同cd命令一样,如果正常替换程序在使用export函数只是修改了子进程的环境变量,就不会影响父进程了,所以就需要对export函数继续特殊处理,
处理思路:
首先依然是用strcmp函数进行判断后进入命令为export的代码块中,在处理的过程中不能简单的认为直接使用putenv导入环境变量
这是因为环境变量表是一个字符指针数组,每个指针指向对应的环境变量字符串,所以当简单地putenv的时候是在这个字符指针数组中找一个没有被使用的地方,指向_argv[1]的,这样就会导致下一次输指令的时候就会覆盖掉被_argv[1]的位置,这样尽管环境变量表中还是指向原来导入的环境变量位置的,但是由于这个位置被其他指令覆盖了,所以每次导入环境变量后再输入别的指令,这个环境变量就没了(这就是类似于浅拷贝)
解决方法:
在堆上开辟一块空间或者定义一个二维数组维护(从易变区到不变区方便维护)
把堆上的空间或者二维数组和_argv[1]处待添加的环境变量交换
然后putenv读取新开辟的空间添加到环境变量表
(其实就是类似于深拷贝的处理过程)
这样输入指令后就能够在环境变量中看到了,并且不会被覆盖
4、echo
echo指令一般情况下是正常打印字符串,但是如果添加了$符号那么就证明是要打印环境变量之类的,所以也需要进行特殊处理:
如上,在进入echo指令区域后进行判断,如果是$?就打印退出码,这个退出码在普通命令中已经得到了如下:
在打印过后就把退出码重新赋值为0表示正常退出,
然后在判断,如果_argv[1]位置处的第一个字符是$就证明需要打印环境变量,此时getenv获得环境变量后打印出来即可,
最后一个else就正常打印即可
如果需要增加其他功能,继续添加else if即可
三、源码:
cpp
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
#define LEFT "["
#define RIGHT "]"
#define LAB "#"
#define MAX_SIZE 1024
#define DEL " \t"
#define ARGC 32
#define EXIT_CODE 1
char target[MAX_SIZE];//拿去命令行中的一整行字符串
char* argv[ARGC];//把字符串分解后放入该数组
int last_normal_code = 0;//退出码
char pwd[MAX_SIZE];
const char* getusrname()
{
return getenv("USER");
}
const char* gethost()
{
return getenv("HOME");
}
void getpwd()
{
//获取当前工作目录下的绝对路径
getcwd(pwd,sizeof(pwd));
}
void Init(char* line)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LAB" ",getusrname(),gethost(),pwd);
char* str = fgets(target,MAX_SIZE,stdin);
assert(str);//输入的指令不能为空,但是一般是不可能的
(void)str;//要把str使用一下,在release版本下assert就没有了,如果不使用可能会报错
//"ls -a -l\n\0"
line[strlen(line)-1] = '\0';
}
int split(char* line)
{
int i = 0;
argv[i++] = strtok(line,DEL);
while(argv[i++] = strtok(NULL,DEL));
return i - 1;
}
int InbuildExe(int argc,char* _argv[])
{
if(argc == 2 && strcmp(_argv[0],"cd") == 0)
{
chdir(_argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",pwd);
return 1;
}
else if(argc == 2 && strcmp(_argv[0],"export") == 0)
{
char* tmp = (char*)malloc(sizeof(char)*strlen(_argv[1]));
strcpy(tmp,_argv[1]);
putenv(tmp);
return 1;
}
else if(argc == 2 && strcmp(_argv[0],"echo") == 0)
{
if(strcmp(_argv[1],"$?")==0)
{
printf("%d\n",last_normal_code);
last_normal_code = 0;
}
else if(*_argv[1] == '$')
{
char* val = getenv(_argv[1]+1);
if(val)
printf("%s\n",val);
}
else
{
printf("%s\n",_argv[1]);
}
return 1;
}
if(strcmp(_argv[0],"ls") == 0)
{
_argv[argc++] = "--color";
_argv[argc] = NULL;
}
return 0;
}
//argv是一个数组,里面的元素是char*,然后用char* _argv[](或者是char** _argv)接收argv的话,是接收的是首元素的地址,
//首元素的地址就有一个*,然后又是char* 类型的有个*,所以就是char** _argv等价于char* argv[]
void NormalExe(char* _argv[])
{
pid_t id = fork();
if(id < 0)
{
perror("进程创建失败\n");
return;
}
else if(id == 0)
{
execvp(_argv[0],_argv);
exit(EXIT_CODE);
}
else
{
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret == id)
{
//printf("等待成功\n");
last_normal_code = WEXITSTATUS(status);//获取进程的退出码
}
}
}
int main()
{
while(1)
{
//初始化
Init(target);
//分割字符串
int ret = split(target);
if(ret == 0) continue;
//执行内建命令
int flag = InbuildExe(ret,argv);
//执行普通命令
if(!flag) NormalExe(argv);
}
return 0;
}