引言:本篇文章主要是简单实现一个shell命令行解释器,可以支持基础常见的linux的命令,支持内建命名echo、cd,同时支持重定向的操作!
因代码是在linux下实现,引入的大多头文件是Linux的系统调用,建议在linux环境下使用。
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
这些头文件包含了一些需要使用的库函数和系统调用。
这些常量和变量用于存储命令行输入、命令参数、重定向类型和重定向文件等信息。
- #define OPTION 64
- #define NUM 1024
- //定义重定向类型
- #define NONE_REDIR 0
- #define INPUT_REDIR 1
- #define OUTPUT_REDIR 2
- #define APPEND_REDIR 3
- //定义个宏函数,用于将字符串指针移动到无空格的第一个字符上
- #define trimSpace(start) do{\
- while(*start == ' '){\
- start++;}\
- }while(0)
- //保存键盘上输入的命令
- char lineComend[NUM];
- char* myargv[OPTION];//定义一个char*的指针数组,用于存放参数
- int lastCode = 0;//父进程记录上次子进程执行结果
- int lastSig = 0;
- //记录是否是重定向和类型
- int redirType = NONE_REDIR;
- char *redirFile = NULL;//记录重定向文件名
这个函数用于查找命令行参数中的重定向符号(>和<)并解析相关的重定向文件和类型。
- void find_Redirect(char* argv){
- char* start = NULL;
- for(int i = 0;i<(int)strlen(argv);i++){
- if(argv[i] == '>'||argv[i]=='<'){
- if(argv[i]=='<'){
- //cat < log.txt
- argv[i] = '\0';
- //去除重定向后面的空格,只保留文件名部分,存放到全局变量redirFile中
- start = argv+i+1;
- trimSpace(start);
- redirFile = start;
- redirType = INPUT_REDIR;
- }else{
- if(argv[i+1] == '>'){
- //cat xiaomi >> log.txt
- argv[i] = '\0';
- start = argv+i+2;
- trimSpace(start);
- redirFile = start;
- redirType = APPEND_REDIR;
- }else{
- argv[i] = '\0';
- start = argv+i+1;
- trimSpace(start);
- redirFile = start;
- redirType = OUTPUT_REDIR;
- }
- }
- }
- }
- }
- 主函数包含一个无限循环,在每次循环中等待用户输入命令,并处理命令执行、重定向和内置命令等逻辑。
- 在循环的开始,通过
printf
输出命令提示符,然后使用fgets
读取用户输入的命令行,并处理去除结尾的换行符。- 接下来,调用
find_Redirect
函数来查找重定向符号,并解析重定向文件和类型。- 然后,使用
strtok
函数对输入命令进行切割,将切割后的命令参数存储到myargv
数组中。同时,处理一些特殊的内置命令,如cd
和echo
。- 对于
echo
命令,根据重定向类型进行输出重定向。- 如果不是内置命令,则创建子进程,并在子进程中执行命令。根据重定向类型,进行输入输出重定向。
- 最后,使用
waitpid
等待子进程执行完毕,并获取子进程的退出状态码和信号。
- int main(){
- //一开始先初始化重定向文件和类型
- while(1){
- redirFile = NULL;
- redirType = NONE_REDIR;
- printf("[suhh@ziqiang address]$ ");
- fflush(stdout);
- //用户输入
- assert(fgets(lineComend,sizeof(lineComend)-1,stdin)!=NULL);
- //清除数组最后一个\n
- lineComend[strlen(lineComend)-1] = '\0';
- //ls -a -l
- //切割字符串 靠' '
- find_Redirect(lineComend);
- myargv[0] = strtok(lineComend," ");
- // char * echo_str = NULL;
- if(strcmp(myargv[0],"echo")==0){
- myargv[1] = myargv[0] + strlen(myargv[0]) + 1;//取出剩余字串
- goto echo_start;
- }
- //strtok 会把剩余的放到静态变量中,下次调用只用穿null
- int i = 1;
- //myargv[end]=NULL
- if(myargv[0]!=NULL&&(strcmp(myargv[0],"ls")==0||strcmp(myargv[0],"ll")==0)){
- myargv[i++] = (char*)"--color=auto";
- if(strcmp(myargv[0],"ll")==0) {
- myargv[i++] = (char*)"-l";
- myargv[0] = (char*)"ls";
- }
- //让ls命令默认加上颜色
- }
- while(myargv[i++] = strtok(NULL," ")){}
- if(strcmp(myargv[0],"cd")==0){
- //如果是cd命令,不能用子进程执行,如果用子进程执行不会影响父进程,达不到效果
- //这时这就是内建命令
- if(myargv[1]!=NULL)chdir(myargv[1]);
- continue;
- }
- echo_start:
- if(myargv[0]!=NULL && strcmp(myargv[0],"echo")==0){
- //这也是内建命令
- //判断重定向类型
- int fd = 0;
- if((redirType == OUTPUT_REDIR)||(redirType == APPEND_REDIR)){
- int flags = O_WRONLY | O_CREAT;
- if(redirType == APPEND_REDIR) flags = flags | O_APPEND;
- else flags = flags | O_TRUNC;
- fd = open(redirFile,flags,0666);
- //先把标准输出流备份一下
- dup2(1,5);
- //重定向
- dup2(fd,1);
- }
- if(myargv[1]!=NULL&&strcmp(myargv[1],"$?")==0){
- printf("%d,%d\n",lastCode,lastSig);
- }else{
- if(myargv[1]!=NULL){
- //如果用户输入 echo "hello" 去除“”
- if(myargv[1][0] =='"'){
- myargv[1] = (char*)&myargv[1][1];
- }
- int str_len = strlen(myargv[1]);
- if((myargv[1][str_len-1] == '"')||(myargv[1][str_len -2] == '"')){
- if(myargv[1][str_len-1] == '"') str_len --;
- else str_len -= 2;
- }
- for(int i = 0 ;i
- printf("%c",myargv[1][i]);
- }
- printf("\n");
- }
- }
- if((redirType == OUTPUT_REDIR)||(redirType == APPEND_REDIR)){
- //恢复标准输出流
- close(fd);
- dup2(5,1);
- }
- continue;
- }
- pid_t it = fork();
- assert(it>=0);
- if(it == 0){
- //判断重定向
- //cat < log.txt
- if(redirType == INPUT_REDIR){
- int rfd = open(redirFile,O_RDONLY);
- if(rfd == -1){
- perror("错误:open");
- exit(errno);
- }
- dup2(rfd,0);
- }else if((redirType == OUTPUT_REDIR)||(redirType == APPEND_REDIR)){
- int flags = O_WRONLY | O_CREAT;
- if(redirType == OUTPUT_REDIR) flags |= O_TRUNC;
- else flags |= O_APPEND;
- int wfd = open(redirFile,flags,0666);
- assert(wfd>=0);
- (void)wfd;
- dup2(wfd,1);
- }
- //子进程执行进程程序替换
- execvp(myargv[0],myargv);
- exit(1);
- }
- int status = 0;
- int wp = waitpid(it,&status,0);//阻塞等待
- assert(wp>=0);
- (void)wp;
- lastCode = (status>>8)&0xFF;
- lastSig = status&0x7F;
- printf("exit_code:%d,exit_signal:%d\n",lastCode,lastSig);
- }
- return 0;
- }
二、 涉及知识点
1. fork()函数
fork函数属于系统调用,用于创建子进程,返回子进程pid给父进程,返回0给子进程.
- fork之后会有两个进程分别执行后续的代码(其实在fork函数体内,父子进程已经产生,所以有两个返回值)
- 子进程拥有自己的PCB和虚拟地址,这些都是和父进程一样的,也就是说,父进程的虚拟地址的内容拷贝了一份给子进程。
- 因为子进程拥有和父进程一样的虚拟地址,体现在代码上,就是子进程也可以”共有“父进程定义的变量等,但如果子进程要改变它的值,就会发生写时拷贝,操作系统会在物理内存中重新开辟一段空间,再通过页表,重新映射到虚拟地址处。换句话说,子进程以为访问的虚拟地址是父进程一起共用的,实际上是另一个物理地址。
2. 进程程序替换
将指定进程加载到内存中,替换原本进程的代码段和数据段,让这个进程以为在执行自己的代码,其实是在执行别人的代码。
在C语言中提供了丰富的函数用于程序替换
函数原型: int execl(const char *path, const char *arg, ...);
举例:execl("/user/bin/ls","ls",NULL);
解释:第一个参数是填要替换的程序在哪,路径
第二个参数是要填这个程序要怎么执行
第三个参数是要填这个程序要带什么参数,要以NULL结尾
错误返回-1
函数原型:int execlp(const char *file, const char *arg, ...);
举例:execlp("ls","ls",NULL);
解释:在上一个函数原型的基础上了一个p,代表path,此时无需传入程序地址,只需告诉程序叫什么,会自动在环境变量中进行可执行程序的查找。
第一个参数填程序名
第二个参数是要填这个程序要怎么执行
第三个参数是要填这个程序要带什么参数,要以NULL结尾
错误返回-1
函数原型:int execv(const char *path, char *const argv[])
举例:char* argv[] ={"ls","-a","-l","--color = auto",NULL};
execv("/user/bin/ls",argv);
解释:这次加了一个v,可以将所有可执行参数放到一个字符串指针数组中,统一传递。
第一个参数是填要替换的程序在哪,路径
第二个参数是要填字符串指针数组
错误返回-1
函数原型:int execvp(const char *file, char *const argv[]);
举例:execvp("ls",argv);
解释:这个加了v和p,拥有了前两个的特性。本文中的代码就是用的这个
第一个参数填程序名
第二个参数是要填字符串指针数组
错误返回-1