1.打印提示符并获取命令行
我们在使用shell的时候,发现我们在输入命令是,前面会有:有用户名,版本,当前路径等信息,这里我们可以用环境变量去获取:
1 #include <stdio.h>2 #include <stdlib.h>3 4 const char* getUsername()5 {6 const char* name = getenv("USER");7 if(name) return name;8 else return "none";9 }10 11 const char* getHostname()12 {13 const char* hostname = getenv("HOSTNAME");14 if(hostname) return hostname;15 else return "none";16 }17 18 const char* getCwd()19 {20 const char* cwd = getenv("PWD");21 if(cwd) return cwd;22 else return "none";23 }24 25 int main()26 {27 printf("%s@%s %s\n",getUsername(),getHostname(),getCwd()); 28 return 0;29 }
写。
看到我们打印出来的是绝对路径, 而shell显示的相对路径, 但为了区分先这样不去裁剪.
1 #include <stdio.h>2 #include <stdlib.h>3 #include <string.h>4 5 #define NUM 1024 6 7 const char* getUsername() 8 { 9 const char* name = getenv("USER"); 10 if(name) return name; 11 else return "none"; 12 } 13 14 const char* getHostname() 15 { 16 const char* hostname = getenv("HOSTNAME"); 17 if(hostname) return hostname; 18 else return "none"; 19 } 20 21 const char* getCwd() 22 { 23 const char* cwd = getenv("PWD"); 24 if(cwd) return cwd; 25 else return "none"; 26 } 27 28 int getUsercommand(char* command, int num) 29 { 30 printf("[%s@%s %s]",getUsername(),getHostname(),getCwd()); 31 char* r = fgets(command,num,stdin);//最终还是会输入\n32 if(r == NULL) return 1;33 34 command[strlen(command)-1] = '\0';//去除输入的换行35 return 0; 36 } 37 38 int main() 39 { 40 char usercommand[NUM];41 //1.打印提示符并且获取命令字符串42 getUsercommand(usercommand,sizeof(usercommand));43 //2. 44 //3. 45 printf("%s",usercommand);//回显命令,用于测试46 return 0;47 }
由于用scanf接收的遇到空格就会停止读取, 所以用fgets, 而且用户输入完命令一定会输入回车, 所以把最后一个回车符删掉.
2.解析命令行
我们在输入命令时, 可能不仅仅只是一段,比如说:"ls -a -l "。但是命令行解释器内部在解析指令时应该传递的是"ls" "-a" "-l"这样的多个个字符串, 所以我们还需要以空格来分割字符串。
1 #include <stdio.h>2 #include <stdlib.h>3 #include <string.h>4 5 #define DEBUG 16 #define NUM 10247 #define SIZE 648 #define SEP " "41 void commandSplit(char* in, char* out[])42 {43 int argc = 1;44 out[0] = strtok(in,SEP);
W> 45 while(out[argc++] = strtok(NULL,SEP));//报警不需要处理46 47 #ifdef DEBUG 48 for(int i = 0; out[i]; i++)49 printf("%d:%s\n",i,out[i]);50 #endif51 }52 53 int main()54 {55 char usercommand[NUM];56 char* argv[SIZE];57 //1.打印提示符并且获取命令字符串58 getUsercommand(usercommand,sizeof(usercommand));59 //2.分割字符串60 commandSplit(usercommand, argv); 61 //3. 62 return 0;63 }
3.执行对应的命令
创建子进程和进程替换, 为了不影响shell, 我们将大部分指令的执行让子进程去完成, 父进程只要阻塞等待子进程完成就好了。
1 #include <stdio.h>2 #include <stdlib.h>3 #include <string.h>4 #include <unistd.h>5 #include <sys/types.h>6 #include <sys/wait.h>7 8 //#define DEBUG 19 #define NUM 102410 #define SIZE 6411 #define SEP " "12 13 const char* getUsername()14 {15 const char* name = getenv("USER");16 if(name) return name;17 else return "none";18 }19 20 const char* getHostname()21 {22 const char* hostname = getenv("HOSTNAME");23 if(hostname) return hostname;24 else return "none";25 }26 27 const char* getCwd()28 {29 const char* cwd = getenv("PWD");30 if(cwd) return cwd;31 else return "none";32 }33 34 int getUsercommand(char* command, int num) 35 {36 printf("[%s@%s %s]",getUsername(),getHostname(),getCwd()); 37 char* r = fgets(command,num,stdin);//最终还是会输入\n38 if(r == NULL) return -1;39 40 command[strlen(command)-1] = '\0';//去除输入的换行41 return strlen(command);42 }43 44 void commandSplit(char* in, char* out[])45 {46 int argc = 1;47 out[0] = strtok(in,SEP);
W> 48 while(out[argc++] = strtok(NULL,SEP));//报警不需要处理49 50 #ifdef DEBUG 51 for(int i = 0; out[i]; i++)52 printf("%d:%s\n",i,out[i]);53 #endif54 }55 56 int execute(char* argv[])57 {58 pid_t id = fork();59 if(id < 0) return 1;60 else if(id == 0)61 {62 //child63 //exec commond64 execvp(argv[0],argv);65 exit(1);66 }67 68 else69 {70 //father71 pid_t rid = waitpid(id,NULL,0);72 if(rid < 0)73 printf("wait fail\n");74 }75 76 return 0;77 }78 79 int main()80 {81 while(1)82 {83 char usercommand[NUM];84 char* argv[SIZE];85 //1.打印提示符并且获取命令字符串86 int n = getUsercommand(usercommand,sizeof(usercommand));87 if(n <= 0) continue;//如果得到的是空串或者获取失败,不要往后执行88 //2.分割字符串89 commandSplit(usercommand, argv);90 //3.执行命令91 execute(argv); 92 }93 return 0;94 }
由于shell要一直运行, 所以要循环执行, 这里程序替换用execvp函数比较合适, 因为argv数组就是我们分割出的一个个命令的子串, argv[0]就是程序名, argv就是指令集. 父进程只进行wait即可.
此外, getUsercommand函数可以优化一下, 返回的是输入的指令的长度, 如果接收失败(返回值为-1或者返回值是0只打印了空行)就不需要往下执行了, 直接continue进行下一轮.
4.特殊处理
有一批命令, 不能让子进程执行, 必须让父进程自己执行, 这些命令叫内建命令.
1) cd指令
可以看到cd .. 之后并没有发生什么异常, 但是pwd之后发现路径没有发生变化.
我们为什么能在linux中进入某个目录, 就是因为我们改变了shell的工作目录. 每个进程都有自己的工作目录, 我们想让父进程的工作目录发生改变, 但是程序替换之后都是子进程在执行cd .., 改变的都是子进程的工作目录, 子进程改变完了又被回收了, 父进程完全没发生变化, 所以cd应该实现成内建命令。
79 void cd(const char* path)80 {81 chdir(path);82 }83 84 //1->yes,0->no85 int doBuildin(char* argv[])86 {87 if(strcmp(argv[0],"cd") == 0)88 {89 char* path = NULL;
W> 90 if(argv[1] == NULL) path = ".";91 else path = argv[1];92 cd(path);93 return 1;94 }95 else if(strcmp(argv[0],"ls")==0)96 {97 return 1;98 }99 return 0; 100 }101 102 int main()103 {104 while(1)105 {106 char usercommand[NUM];107 char* argv[SIZE];108 //1.打印提示符并且获取命令字符串109 int n = getUsercommand(usercommand,sizeof(usercommand));110 if(n <= 0) continue;//如果得到的是空串或者获取失败,不要往后执行111 //2.分割字符串112 commandSplit(usercommand, argv);113 //3.检查是不是内建命令,是的话直接执行114 n = doBuildin(argv);115 if(n) continue;//是内建命令不用往后执行了116 //4.执行命令117 execute(argv);118 }119 return 0;120 }
所以在执行命令前先检查是不是内建命令, 用返回值接收, 如果是就直接执行并返回1, continue不往下执行, 如果不是就返回0, 执行命令.
既然当前的工作目录改变了, 那么环境变量PWD也要改变:
chdir改变当前工作目录, getcwd获取当前的工作路径, sprintf将tmp中的内容输出到cwd中, putenv将cwd导入环境变量.
2) export命令
创建一个数组env储存要导入的环境变量, 设置size指向导入到第几个环境变量.
如果argv[1]是空就直接返回, 否则就导入环境变量, 注意不能直接把argv[1]导入进去, 因为argv[1]随着指令的输入时刻在变化, 需要开辟额外的空间去存储.
3)echo指令
4)ls指令
我们执行的ls指令中不同的文件都有不同的颜色,所以对于ls我们可以在分割命令的时候加上一个“--color=auto”.