目录标题
- 前提
- 准备工作
前提
平时使用指令操作linux系统的时候可能会输入一些不好的指令,这些指令可能会对操作系统内核造成影响,所以就有了命令行解释器这个东西,它会过滤掉那些不好的指令从而让linux系统更加的安全,比如说我们输入一些不存在的命令,bash就会直接将这些指令过滤掉告诉我们该指令不存在,对于正确的指令bash就会帮我们执行这个指令的功能:
那么接下来我们就要模拟实现一个命令行解释,当然我们这里的模拟实现肯定是只是模拟个大概,具体的细节方面跟官方的肯定是不能比的,所以本篇文章的目的就是让大家大概的了解一下命令行解释器的工作原理。
准备工作
首先命令行解释器长这样:
它会告诉我们一些基本的信息比如说第一个xbb就表示当前登录用户的名字,@后面的VM-4-10-centos就表示当前使用的主机名,最后的~表明的就是当前的路径是~也就是xbb的家路径,我们将路径切换到其他地方去这个命令行解释器的内容就会发生改变:
我们在命令行解释器的后面输入对应的指令,执行完指令的内容之后命令行解释器就会自动在下一行打印出机器的相关信息,那么这里就是通过while循环来进行实现,在while循环的开始先使用printf语句打印出相关的信息,这里的信息可以通过环境变量来进行打印,在程序中获取环境变量的方法就是通过getenv函数,由于路径打印出来太长了我们自己实现的bash就不打印出路径的信息,比如说下面的代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
while(1)
{
printf("[%s@%s 当前路径]",getenv("LOGNAME"),getenv("HOSTNAME"));
fflush(stdout);
break;
}
return 0;
}
由于linux指令要在信息后面进行输入所以结尾不要加\n,while循环最后的break是为了方便我们进行测试到后面这个break是要删除的,那么上面的代码运行结果如下:
可以看到当前的能够正常的打印机器的信息,那么接下来我们就要输入具体的指令,首先得创建一个字符数组来记录输入的指令,由于指令分为具体的指令和指令对应的方法,所以还得创建一个字符指针数组用来记录指令字符串被切割生成的子串,这里可以通过fgets函数将键盘输入的数据存储到字符里面,这里大家要注意的一点就是得将字符数组的最后一个\n去掉不然会影响后序程序的运行
然后再使用strtok函数将字符数组里面的数据进行切割,平时我们在输入指令的时候是通过空格来作为分隔符,那么strtok的第二个参数我们就可以输入一个空格字符串
那么这一步的代码就如下:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#define NUM 1024
char linecommand[NUM];//用来记录指令
char* myargv[NUM];//用来记录字串产生的子串
int main()
{
while(1)
{
printf("[%s@%s 当前路径]",getenv("LOGNAME"),getenv("HOSTNAME"));
fflush(stdout);
char*s=fgets(linecommand,NUM,stdin);
assert(s!=NULL);
(void)s;
linecommand[strlen(linecommand)-1]=0;
myargv[0]=strtok(linecommand," ");
int i=1;
while(myargv[i++]=strtok(NULL," "));
//当字符串分割完之后就会返回NULL然后就会顺便吧myargv数组的后一个元素
//初始化为NULL来作为结尾
}
return 0;
}
分割完子串之后就可以使用fork创建子进程使用if语句将父子进程执行的代码分开,然后使用execvp函数对子进程实行进程替换让其执行指令的内容,execvp的函数介绍如下:
第一个参数表示执行的系统指名,第二个参数表明执行的方法,执行完execvp函数就可以使用exit函数来结束子进程,然后父进程就可以在外面实现进程等待,收集子进程执行程序的信息,既然这里要收集子进程的信息,那么外面就得再创建两个全局变量来记录子进程的信息,那么这里的代码就如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
#define NUM 1024
char linecommand[NUM];//用来记录指令
char* myargv[NUM];//用来记录字串产生的子串
int lastcode =0;
int lastsign=0;
int main()
{
while(1)
{
printf("[%s@%s 当前路径]",getenv("LOGNAME"),getenv("HOSTNAME"));
fflush(stdout);
char*s=fgets(linecommand,NUM,stdin);
assert(s!=NULL);
(void)s;
linecommand[strlen(linecommand)-1]=0;
myargv[0]=strtok(linecommand," ");
int i=1;
W> while(myargv[i++]=strtok(NULL," "));
//当字符串分割完之后就会返回NULL然后就会顺便吧myargv数组的后一个元素
//初始化为NULL来作为结尾
pid_t id=fork();
if(id==0)
{
execvp(myargv[0],myargv);
exit(1);
}
int status=0;
pid_t ret=waitpid(id,&status,0);
assert(ret>0);
(void) ret;
lastcode=((status>>8)&0xFF);
lastsign=(status&0x7F);
}
return 0;
}
写道这里命令行解释器的外壳我们已经大致完成了,接下来我们就要测试一下上面这段代码的正确性:
这就是我们上面代码的运行结果,可以看到大致是实现成功的,但是这里有几个细节问题:第一个就是使用ls指令打印当前路径下的文件时没有显示颜色对吧,我们使用别人的ls指令是可以显示颜色的比如说下面的图片:
原因就是别人的ls指令会自动地添加一个"–color=auto"的选项,那么为了实现这个功能我们就可以在第一次切割子串的时候判断以下第一个字符是否是ls字符串,如果是的话我们就在myargv数组的后面手动的添加
"--color=auto"
方法比如说下面的代码:
if(mygrv[0]!=NULL&&strcmp(myargv[0],"ls")==0)
{
myargv[i]="--color=auto";
++i;
}
while(myargv[i++]=strtok(NULL," "));
再进行以下测试就可以看到我们实现的命令行解释器在执行ls指令的时候就会有颜色
上面的代码还有第二个问题就是cd指令无法修改路径,比如说下面的图片:
我们实现的命令行解释器好像无法指向cd指令,那这是为什么呢?原因很简单我们是通过创建子进程然后通过进程替换的方式来执行指令的文件,cd指令的作用是修改当前的工作路径,但是子进程和父进程是相互独立的,子进程的工作路径被修改了不会影响到父进程的工作路径,然后再执行pwd指令的时候创建的子进程又会从父进程中继承工作路径,所以打印的内容依然是不会发生变化的,所以要想解决这个问题就不能通过子进程来修改工作路径,而是得通过父进程来进行修改,所以当切割的第一个子串是cd的话我们就让父进程来执行这个指令的功能,系统提供了一个名为chdir的函数,这个函数可以修改当前进程的工作目录,所以通过这个函数我们就可以实现父进程的路径切换,我们来看看这个函数的参数:
这个函数只需要一个想要去的路径就可以,而cd指令后面跟着的一般都是想要去的路径,所以这里我们就可以加一个if语句进行判断,如果第二个子串的内容不为空的话我们就执行chdir函数,那这里的代码就如下:
if(myargv[0]!=NULL&&strcmp(myargv[0],"cd")==0)
{
if(myargv[1]!=NULL)
{
chdir(myargv[1]);
continue;
}
}
将这个代码添加上去cd指令就可以正常地运行了
最后一个问题就是echo指令,使用echo $?指令可以在屏幕上面打印出上一条指令执行地结果如何,但是我们我们我们这里实现地echo指令却只能打印出来一个$?,那么解决这个问题的方法也还是在父进程中单独进行处理,如果子串的第一个是echo,第二个不为空的话就单独进行处理:
if(myargv[0]!=NULL&&myargv[1]!=NULL&&strcmp(myargv[0],"echo")==0)
{
}
处理的方式为:如果子串的第二个为$?的话就打印上一个子进程的退出信息也就是lastcode和lastsign的值,如果不为$?的话就直接打印第二个子串的值,那这里的代码就如下:
if(myargv[0]!=NULL&&myargv[1]!=NULL&&strcmp(myargv[0],"echo")==0)
{
if(strcmp(myargv[1],"$?")==0)
{
printf("%d %d\n",lastcode,lastsign);
}
else
{
printf("%s\n",myargv[1]);
}
continue; }
测试一下代码就可以看到正常的运行了
那么这就是我们实现的简单的命令行解释器,完整的代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
#define NUM 1024
char linecommand[NUM];//用来记录指令
char* myargv[NUM];//用来记录字串产生的子串
int lastcode =0;
int lastsign=0;
int main()
{
while(1)
{
printf("[%s@%s 当前路径]",getenv("LOGNAME"),getenv("HOSTNAME"));
fflush(stdout);
char*s=fgets(linecommand,NUM,stdin);
assert(s!=NULL);
(void)s;
linecommand[strlen(linecommand)-1]=0;
myargv[0]=strtok(linecommand," ");
int i=1;
if(myargv[0]!=NULL,strcmp(myargv[0],"ls")==0)
{
myargv[i]="--color=auto";
++i;
}
while(myargv[i++]=strtok(NULL," "));
//当字符串分割完之后就会返回NULL然后就会顺便吧myargv数组的后一个元素
//初始化为NULL来作为结尾
if(myargv[0]!=NULL&&strcmp(myargv[0],"cd")==0)
{
if(myargv[1]!=NULL)
{
chdir(myargv[1]);
continue;
}
}
if(myargv[0]!=NULL&&myargv[1]!=NULL&&strcmp(myargv[0],"echo")==0)
{
if(strcmp(myargv[1],"$?")==0)
{
printf("%d %d\n",lastcode,lastsign);
}
else
{
printf("%s\n",myargv[1]);
}
continue;
}
pid_t id=fork();
if(id==0)
{
execvp(myargv[0],myargv);
exit(1);
}
int status=0;
pid_t ret=waitpid(id,&status,0);
assert(ret>0);
(void) ret;
lastcode=((status>>8)&0xFF);
lastsign=(status&0x7F);
}
return 0;
}