写一个自己的命令行解释器
当我点开xshell运行服务器的时候bash就被加载到了内存中,此后我在bash上执行的所有程序都是作为bash的子进程。在bash这个进程内创建子进程,并让子进程去执行全新的代码,这不就是程序替换吗?
所以我们让子进程去执行程序替换,在我们的程序内执行命令,那我们不就是写了一个自己的命令行解释器吗?本文将带领各位读者通过实现一个简单的命令行解释器来巩固前面所学的部分知识。
一.搭建框架
当我们打开服务器的时候在最右边有提示符,包括用户名和服务器名称以及当前路径;并且支持多次输入,所以这里可以采用一个死循环,进程替换的事情由子进程来执行,所以肯定要用fork函数。
通过环境变量的学习我们知道,main函数也是有参数的,其中有一个argv指针数组,这个数组中存放的内容就是我输入的指令,argv[0]存放的是我要执行的命令的地址,后面的内容都是我指令附带的选项。所以我也可以创建一个数组存放我输入的指令(以空格为分割,将我要执行的程序和所带的选项分割开来),采用库函数strtok来切割。
至此我们可以根据需求搭建出这样的代码:
#include<stdio.h>
#incldue<string.h>
#include<unistd.h>
#include<assert.h>
#define NUM 1024
#define MAX 20
char LineCommand[NUM];//允许输入指令最大长度为1024
char*myargv[MAX];//执行程序+选项最多20条
int main()
{
while(1)
{
printf("用户名@服务器 当前路径:");
fflush(stdout);//上面没带\n,这里要强制刷新
//将键盘中输入的字符全部存入数组中
char*s=fgets(LineCommand,sizeof(LineCommand)-1,stdin);//留一个位置存放\0
assert(s);//暴力检查,s不能为空
//分割,使用库函数stork
myargv[0]=strtok(LineCommand," ");
int i=1;
while(myargv[i++]=strtok(NULL," "));//循环切割,先将切割后的结果赋值给myargv,再将这个值作为判断,strtok在结束时会返回空
//测试一下是否切割成功
for(int i=0;myargv[i];i++)
{
printf("myargv[%d]:%s\n",i,myargv[i]);
}
}
return 0;
}
可以看到命令是切割成功了,但是在输出的时候好像后面多打了一次换行。这是因为在外面输入指令的时候肯定会输入回车的。所以在存放命令的数组中的最后一个元素就是
\n
,如果不想这样可以在输入完指令以后将最后一个元素换成\0
当切割命令都没问题的时候,就可以开始用子进程执行进程替换来执行系统的指令了。
#include<stdio.h>
#incldue<string.h>
#include<sys/types>
#include<sys/wait.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#define NUM 1024
#define MAX 20
char LineCommand[NUM];//允许输入指令最大长度为1024
char*myargv[MAX];//执行程序+选项最多20条
int main()
{
while(1)
{
printf("用户名@服务器 当前路径:");
fflush(stdout);//上面没带\n,这里要强制刷新
//将键盘中输入的字符全部存入数组中
char*s=fgets(LineCommand,sizeof(LineCommand)-1,stdin);//留一个位置存放\0
assert(s);//暴力检查,s不能为空
LineCommand[strlen(LineCommand)-1]=0;//处理最后一个元素为'\n'
//分割,使用库函数stork
myargv[0]=strtok(LineCommand," ");
int i=1;
while(myargv[i++]=strtok(NULL," "));//循环切割,先将切割后的结果赋值给myargv,再将这个值作为判断,strtok在结束时会返回空
pid_t id=fork();
assert(id!=-1);//fork失败返回-1
if(id==0)
{
//子进程内部执行进程替换,我们有了数组,优先考虑使用带p的
execvp(myargv[0],myargv);
exit(-1);//如果程序替换失败就返回-1
}
//父进程要回收子进程资源
int status=0;
pid_t ret=waitpid(id,&status,0);//非阻塞式等待
}
return 0;
}
可以看到此时就已经可以执行指令了,但是这里还存在着几个问题
1.使用ls指令没有颜色区别:这是因为少了一个“–color=auto”选项的原因,我们可以对部分指令做适当的枚举来解决这个问题
2.
cd ..
无法回退到上级路径:这和当前进程的当前路径有关(当前路径就是这个进程的工作路径),可以通过chdir
来更改3.无法使用
echo $?
查询上次指令的退出码:要拿到上次的退出码我首先要保存上次的退出码,所以还要定义两个变量,此外还要通过枚举让 ? 变成输出上次的退出码而不是向屏幕中打印 ?变成输出上次的退出码而不是向屏幕中打印 ?变成输出上次的退出码而不是向屏幕中打印?
二.通过简单枚举来完善代码
#include<stdio.h>
#incldue<string.h>
#include<sys/types>
#include<sys/wait.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#define NUM 1024
#define MAX 20
char LineCommand[NUM];//允许输入指令最大长度为1024
char*myargv[MAX];//执行程序+选项最多20条
int lastcode=0;//上个程序的退出码
int lastsig=0;//上个程序的退出信号
int main()
{
while(1)
{
printf("用户名@服务器 当前路径:");
fflush(stdout);//上面没带\n,这里要强制刷新
//将键盘中输入的字符全部存入数组中
char*s=fgets(LineCommand,sizeof(LineCommand)-1,stdin);//留一个位置存放\0
assert(s);//暴力检查,s不能为空
LineCommand[strlen(LineCommand)-1]=0;//处理最后一个元素为'\n'
//分割,使用库函数stork
myargv[0]=strtok(LineCommand," ");
int i=1;
//让ls选项带颜色标识
if(myargv[0]!=NULL&&strcmp(myargv[0],"ls")==0)
{
myargv[i++]="--color=auto";
}
while(myargv[i++]=strtok(NULL," "));//循环切割,先将切割后的结果赋值给myargv,再将这个值作为判断,strtok在结束时会返回空
if(myargv[0]!=NULL&&strcmp(myargv[0],"cd")==0)
{
if(myargv[1]!=NULL)
{
chdir(myargv[1]);//通过chdir系统调用,将当前的工作目录改为myargv数组下标为1的元素
continue;//后面的语句不用再执行了,直接下一次循环
}
}
if(myargv[0]!=NULL&&strcmp(myargv[0],"echo")==0)
{
if(myargv[1]!=NULL&&strcmp(myargv[1],"$?")==0)
{
printf("%d %d\n",lastcode,lastsig);
continue;
}
}
pid_t id=fork();
assert(id!=-1);//fork失败返回-1
if(id==0)
{
//子进程内部执行进程替换,我们有了数组,优先考虑使用带p的
execvp(myargv[0],myargv);
exit(-1);//如果程序替换失败就返回-1
}
//父进程要回收子进程资源
int status=0;
pid_t ret=waitpid(id,&status,0);//非阻塞式等待
lastcode=(status>>8)&0xff;
lastsig=status&0x7f;
}
return 0;
}
三.实现重定向
命令行解释器是支持重定向的,但是就我们目前所写的代码来说还没有支持重定向。重定向的本质就是上层用的fd不变,在内核中更改fd对应struct file*的指向。如果不太懂可以去看看博主的基础IO:基础IO
也就是说只要使用dup2
系统调用更改fd中struct file*的指向即可,当我们完善这个功能以后一个简单命令行解释器也就完成了。
追加重定向本质上也是另外一种输出重定向,所以可以将这两个放在一起写,具体实现如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<ctype.h>
#include<assert.h>
#include<errno.h>
#define NUM 1024 //定义最大输入指令大小
#define MAX 20
#define Skipspace(start) do{\
while(isspace(*start)) start++;\
} while(0)
//定义文件重定向的类型,否则后面无法区分
#define NON 0
#define APPEND 1
#define OUTPUT 2
#define INPUT 3
char LineCommand[NUM];//定义输入字符数组
char*myargv[MAX];
//设置退出结果和退出信号
int lastcode=0;
int lastsig=0;
//4-15,增加重定向功能,>输出重定向,>>追加重定向,<输入重定向
//重定向首先要分割文件名和指令,所以在标识重定向的位置要放\0
char*readfile;
int redirType=NON;
void redirect(char *commands)
{
//从左到右开始扫描
char * start=commands;
char*end=commands+strlen(commands);
while(start<end)
{
if(*start=='>')
{
//找到描述符,把这里换成\0
*start='\0';
start++;
if(*start=='>')//说明是追加重定向,start还要向后挪动一个位置
{
start++;//后面可能有空格,要跳过空格
redirType=APPEND;
}
else{
redirType=OUTPUT;
}
Skipspace(start);
readfile=start;//前面定义的是以指针的方式不是没有道理的
break;
}
else if(*start=='<')
{
//开头都是同样的处理
*start='\0';
++start;
redirType=INPUT;
Skipspace(start);
readfile=start;
break;
}
else{
start++;
}
}
}
int main()
{
while(1)
{
redirType=NON;
readfile=NULL;
//写一个自己的shell,首先我的有提示符
printf("用户名@服务器 当前路径:");
fflush(stdout);
//将用户输入的指令作为字符串存入数组中,用fgets函数获取输入的指令 我要将其切割出来
char *s=fgets(LineCommand,sizeof(LineCommand)-1,stdin);//将stdin中输入的字符放到LineCommand中
assert(s!=NULL);
//清除最后一个\n
LineCommand[strlen(LineCommand)-1]=0;
redirect(LineCommand);
//切割,argv存放的第一个字符串是程序
myargv[0]=strtok(LineCommand," ");
int i=1;
if(myargv[0]!=NULL&&strcmp(myargv[0],"ls")==0)
{
myargv[i++]="--color=auto";
}
//
// 在切割之前要把文件名和指令分开
while(myargv[i++]=strtok(NULL," "));//循环切割
if(myargv[0]!=NULL&&strcmp(myargv[0],"cd")==0)
{
//如果是cd命令,并且有输入cd到哪个路径,就将当前的工作路径改为myargv[1]
if(myargv[1]!=NULL)
{
chdir(myargv[1]);
continue;///后面的语句不用再执行了
}
}
#ifdef DEBUG
for(i=0;myargv[i];i++)
{
printf("myargv[%d]:%s\n",i,myargv[i]);
}
#endif
//创建子进程,让子进程替换
pid_t id=fork();
assert(id!=-1);
if(id==0)
{
//因为指令由子进程进程替换来执行,所有重定向肯定也是交由子进程
switch(redirType)
{
case NON:
break;//不重定向
case INPUT:
{
int flag=O_RDONLY;
int fd=open(readfile,flag);
if(fd<0)
{
perror("open");
exit(errno);
}
dup2(fd,0);
}
break;
case OUTPUT:
case APPEND:
{
int flags=O_WRONLY|O_CREAT;
if(redirType==APPEND) flags|=O_APPEND;
else flags|=O_TRUNC;
//先打开文件
int fd=open(readfile,flags,0666);
if(fd<0)
{
perror("open");
exit(errno);
}
//重定向,更改标准输出
dup2(fd,1);
}
break;
default:
//可能有错误
printf("bug?\n");
break;
}
//替换,选用带vp的来换
execvp(myargv[0],myargv);
exit(1);
}
int status=0;
pid_t ret = waitpid(id,&status,0);
//父进程回收子进程,并获取退出码
assert(ret>0);
lastcode=(status>>8)&0xff;
lastsig=(status)&0x7f;
if(myargv[0]!=NULL&&strcmp(myargv[0],"echo")==0)
{
if(myargv[1]!=NULL&&strcmp(myargv[1],"$?")==0)
{
printf("%d %d\n",lastcode,lastsig);
continue;
}
}
}
return 0;
}