一. 总述
Linux操作系统下没有图像化界面,都是通过shell来处理命令和指令,这里使用简单的150行代码实现一个简单的shell,主要作用就是帮助读者理解shell的运行原理。
二.知识储备
Linux下的知识主要是进程创建,还有文件系统,主要涉及到文件的打开,读写追加等基本的操作,还有文件描述符表,总之就是对文件系统和进程要有一定的知识储备,还有进程替换,当然还有一些API接口,比如说dup2,chdir,还有strtok,fgets等等。
三.思路分析
整体思路就是父进程充当shell的角色,从stdin读取,然后使用strtok分割,然后使用进程替换,让子进程去执行对应的代码。
四.代码实现
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
#include<ctype.h>
#include<sys/stat.h>
#include<fcntl.h>
#define NUM 1024
#define OPT_NUM 64
#define NONE_REDIR 0 //默认输出重定向为0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
void trimSpace(char* start) //消除空格,文件重定向的时候
{
while(isspace(*start))
{
start++;
}
}
int redirType=NONE_REDIR;
char lineCommand[NUM];
char *myargv[OPT_NUM]; //定义了一个指针数组
int lastCode=0;
int lastSig=0;
char* redirFile=NULL;//设重定向的文件名称
//实现命令行解释器
void commandCheck(char*commands)
{
assert(commands);
char*start=commands;
char*end=commands+strlen(commands);
while(start<end)
{
if(*start=='>')
{
*start='\0';
start++;
if(*start=='>')
{
//"ls -a >> file.log"
redirType=APPEND_REDIR;
start++;
}
else
{
//"ls -a > file.log"
redirType=OUTPUT_REDIR;
}
trimSpace(start);
redirFile=start;
break;
}
else if(*start=='<')
{
//"cat < file.txt"
*start='\0';
start++;
trimSpace(start);
//填写重定向信息
redirType=INPUT_REDIR;
redirFile=start;
break;
}
else
{
start++; //指向下一个位置
}
}
}
int main()
{
while(1)
{
redirType=0;
redirFile=NULL;//这个地方要做一个初始化,因为是全局变量,父进程就是那个死循环是不会改变的。
//输出提示符
printf("用户名@主机名 当前路径#");
fflush(stdout);
//用户输入,输入的时候,会输入\n,键盘最后回车就是\n
char* s=fgets(lineCommand,sizeof(lineCommand)-1,stdin); //sizeof会计算所有占用的空间
assert(s!=NULL);
//清楚最后一个\n
lineCommand[strlen(lineCommand)-1]=0;//消除最后一个'\n
commandCheck(lineCommand);
//无论处理什么,都是要进行字符串切割
myargv[0]=strtok(lineCommand," ");
// 如果没有字串了strtok->NULL.恰好myargv要求最后一个得是NULL
int i=1;
if(myargv[0]!=NULL&&strcmp(myargv[0],"ls")==0)
{
myargv[i++]=(char*)"--color=auto"; //注意这个地方加上强制类型转换
}
while(myargv[i++]=strtok(NULL," "));
myargv[i]=NULL;
if(myargv[0]!=NULL &&strcmp(myargv[0],"cd")==0)
{
//后面跟的是路径,所以后面肯定不为NULL
if(myargv[1]!=NULL)
{
//如果是cd命令,不需要创建子进程,让shell自己执行对应的接口
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,lastSig);
}
else
{
printf("%s\n",myargv[1]);
}
continue;
}
//测试时候成功,条件编译
# ifdef DEBUG
for(int 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 NONE_REDIR:
break;
case INPUT_REDIR:
{
int fd=open(redirFile,O_RDONLY);
if(fd<0)
{
perror("open");
exit(2);//终止子进程
}
dup2(fd,0); //这个地方是写,重定向到读端
}
break;
case OUTPUT_REDIR:
case APPEND_REDIR:
{
int flags=O_WRONLY|O_CREAT;
if(redirType==APPEND_REDIR)
{
flags |=O_APPEND;
}
else
{
flags|=O_TRUNC;
}
int fd=open(redirFile,flags,0666);
if(fd<0)
{
perror("open");
exit(2);//直接终止进程
}
dup2(fd,1);
}
break;
default:
break;
}
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);
}
}