目录
一、打印命令提示符和获取命令行命令字符串
1.1 设计
我们首先使用操作系统的bash看到了命令行提示符的组成为
[用户名@主机名 当前工作目录]$
,获取用户名、主机名和当前工作目录的函数在系统调用中都有,这里我们自己设计一个,这三个数据都是环境变量,我们可以通过getenv来获取到他们,获取后将他们按照操作系统bash的格式输出出来即可,通过下图我们可以发现,我们的当前工作目录与操作系统的有所区别,我们的当前工作目录是一条路径,可以通过裁剪得到与操作系统一样的效果,我这里为了区分与操作系统的区别,这里就不做裁剪了。
输出完命令行提示符后,就需要向bash中输入命令了,这里我们就需要一个输入函数来读取命令字符串,需要注意的是这里不能使用scanf函数,因为scanf函数不能读取空格之后的内容,可以选择gets/fgets函数来读取,当我们输入完命令字符串后需要按回车,那么获取到的字符串中也会获取到这个’\n’,所以我们还需要将这个’\n’处理掉。
#include<stdio.h>#include<string.h>#include<stdlib.h>#defineNUM1024constchar*getUsername(){char* username =getenv("USER");if(username)return username;elsereturn"none";}constchar*getHostname(){char* hostname =getenv("HOSTNAME");if(hostname)return hostname;elsereturn"none";}constchar*getCwd(){char* cwd =getenv("PWD");if(cwd)return cwd;elsereturn"none";}intmain(){char usercommand[NUM];printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());// scanf("%s",usercommand); scanf读到空格就会自动停止,所以这里不使用scanffgets(usercommand,sizeof(usercommand),stdin);// 读取字符串时,会自动获取到'\n',我们需要将这个'\n'去掉
usercommand[strlen(usercommand)-1]='\0';// 用于测试输入的字符串是否符合我们的预期printf("%s",usercommand);return0;}
1.2 封装
这里将打印命令行提示符与获取命令行字符串的工作统一封装到getUserCommand这个函数中。
intgetUserCommand(char* command ,int num){printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());// scanf("%s",usercommand); scanf读到空格就会自动停止,所以这里不使用scanf char* r =fgets(command,num,stdin);if(r ==NULL)return-1;// 读取失败返回-1 // 读取字符串时,会自动获取到'\n',我们需要将这个'\n'去掉
command[strlen(command)-1]='\0';// 由于读入字符串时,必定会有一个\n所以这么不可能会越界 // 用于测试输入的字符串是否符合我们的预期printf("%s",command);return1;}intmain(){char usercommand[NUM];getUserCommand(usercommand,NUM);return0;}
二、分割字符串
2.1 设计
当我们获取到了命令字符串后,需要将字符串以空格为分隔符将字符串分割为子字符串,并将每一个子字符串的地址存入到一个指针数组中,这里给出一个字符串被分割的例子:
"ls -l -a" -> "ls" "-l" "-a"
。我们可以使用strtok函数来将字符串分割,使用strtok函数处理同一个字符串时,第一次需要传入字符串的地址,后面再次调用则只需要传入NULL即可。
#include<stdio.h>#include<string.h>#include<stdlib.h>#defineNUM1024#defineSIZE64#defineSEP" "constchar*getUsername(){char* username =getenv("USER");if(username)return username;elsereturn"none";}constchar*getHostname(){char* hostname =getenv("HOSTNAME");if(hostname)return hostname;elsereturn"none";}constchar*getCwd(){char* cwd =getenv("PWD");if(cwd)return cwd;elsereturn"none";}intmain(){while(1){char usercommand[NUM];char* argv[SIZE];int x =getUserCommand(usercommand,NUM);if(x ==-1)continue;int argc =0;
argv[argc++]=strtok(usercommand,SEP);while(argv[argc++]=strtok(NULL,SEP));// strtok函数分割失败时会返回NULL,正好是我们字符串数组结尾所需要的 // 所以分割的方式可以使用赋值作为循环判断条件进行循序 // 若是大家不习惯可以使用下面这一种分割方式 // while(argv[argc++] = strtok(NULL,SEP)); // while(1) // { // argv[argc] = strtok(NULL,SEP); // if(argv[argc] == NULL) // break; // argc++; // } for(int i =0; argv[i]; i++){printf("%d : %s \n",i,argv[i]);}}return0;}
2.2 封装
voidSplitCommand(char* in ,char* out[]){int argc =0;
out[argc++]=strtok(in,SEP);while(out[argc++]=strtok(NULL,SEP));// strtok函数分割失败时会返回NULL,正好是我们字符串数组结尾所需要的 // 所以分割的方式可以使用赋值作为循环判断条件进行循序 // 若是大家不习惯可以使用下面这一种分割方式 // while(1) // { // out[argc] = strtok(NULL,SEP); // if(out[argc] == NULL) // break; // argc++; }// 用于测试字符串是否被分割 #ifdefdebug for(int i =0; out[i]; i++){printf("%d : %s \n",i,out[i]);}#endif}intmain(){while(1){char usercommand[NUM];char* argv[SIZE];int x =getUserCommand(usercommand,NUM);SplitCommand(usercommand,argv);}return0;}
三、执行指令
3.1 设计
将命令字符串分割后,就需要执行命令了,我们知道bash需要一直运行,这里添加一个循环让他一直运行,我们可以使用前面学习过的进程替换来执行命令,但是不能使用当前进程来进程替换,当前进程还需要继续运行,所以我们可以创建一个子进程来执行命令,由于我们将字符串分割为子字符串存储在了指针数组中,这里可以使用execvp函数来进行进场替换。
#include<stdio.h>#include<string.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>#defineNUM1024#defineSIZE64#defineSEP" "#definedebug1constchar*getUsername(){char* username =getenv("USER");if(username)return username;elsereturn"none";}constchar*getHostname(){char* hostname =getenv("HOSTNAME");if(hostname)return hostname;elsereturn"none";}constchar*getCwd(){char* cwd =getenv("PWD");if(cwd)return cwd;elsereturn"none";}intgetUserCommand(char* command ,int num){printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());// scanf("%s",usercommand); scanf读到空格就会自动停止,所以这里不使用scanf char* r =fgets(command,num,stdin);if(r ==NULL)return-1;// 读取失败返回-1 // 读取字符串时,会自动获取到'\n',我们需要将这个'\n'去掉
command[strlen(command)-1]='\0';// 由于读入字符串时,必定会有一个\n所以这么不可能会越界 // 用于测试输入的字符串是否符合我们的预期// printf("%s",command); return1;}voidSplitCommand(char* in ,char* out[]){int argc =0;
out[argc++]=strtok(in,SEP);while(out[argc++]=strtok(NULL,SEP));// strtok函数分割失败时会返回NULL,正好是我们字符串数组结尾所需要的 // 所以分割的方式可以使用赋值作为循环判断条件进行循序 // 若是大家不习惯可以使用下面这一种分割方式 // while(1) // { // out[argc] = strtok(NULL,SEP); // if(out[argc] == NULL) // break; // argc++; }#ifdefdebug for(int i =0; out[i]; i++){printf("%d : %s \n",i,out[i]);}#endif}intmain(){while(1){char usercommand[NUM];char* argv[SIZE];// 打印命令提示符和获取命令行命令字符串 int x =getUserCommand(usercommand,NUM);if(x <=0)continue;// 分割命令字符串SplitCommand(usercommand,argv);
pid_t id =fork();if(id <0)return-1;elseif(id ==0){execvp(argv[0],argv);// 若替换失败则子进程退出exit(-1);}else{
pid_t rid =wait(NULL);if(rid>0){};}}return0;}
3.2 封装
intexecute(char* argv[]){
pid_t id =fork();if(id <0)return-1;elseif(id ==0){execvp(argv[0],argv);// 若替换失败则子进程退出 exit(-1);}else{
pid_t rid =wait(NULL);if(rid>0){};}return0;}intmain(){while(1){char usercommand[NUM];char* argv[SIZE];// 打印命令提示符和获取命令行命令字符串 int x =getUserCommand(usercommand,NUM);if(x <=0)continue;// 分割命令字符串SplitCommand(usercommand,argv);// 执行命令execute(argv);}return0;}
四、处理內键命令的执行
当我们使用上面的代码执行命令时,发现大部分命令都可以被执行,但是例如cd、export、echo这样的内建命令却不能被执行,原因是内建命令是作用与bash的也就是这里的父进程,并且内建命令是bash的一部分,与常见命令不同,执行内建命令时不需要创建新的子进程,所以这些内建命令需要被特殊处理一下。将命令字符串分割后就判断当前命令是否为内建命令,是则直接执行内建命令,否则认定为常见命令向下继续执行。内建命令如何处理,这里就不多讲解,详细处理方法在下面的代码中有详细的注释,有兴趣的可以看一下。
#include<stdio.h>#include<string.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>#defineNUM1024#defineSIZE64#defineSEP" "// #define debug 1 // 由于环境变量PWD需要一直存在,这里定义一个全局变量来存储char cwd[1024];// 定义一个全局二维数组中,用于存储添加的环境变量char myenv[128][1024];// 记录二维数组中有多少个环境变量int cnt =0;int lastcode =0;// 记录退出码 char*getHomename(){char* homename =getenv("HOME");if(homename)return homename;elsereturn(char*)"none";}constchar*getUsername(){char* username =getenv("USER");if(username)return username;elsereturn"none";}constchar*getHostname(){char* hostname =getenv("HOSTNAME");if(hostname)return hostname;elsereturn"none";}constchar*getCwd(){char* cwd =getenv("PWD");if(cwd)return cwd;elsereturn"none";}intgetUserCommand(char* command ,int num){printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());// scanf("%s",usercommand); scanf读到空格就会自动停止,所以这里不使用scanf char* r =fgets(command,num,stdin);if(r ==NULL)return-1;// 读取失败返回-1// 读取字符串时,会自动获取到'\n',我们需要将这个'\n'去掉
command[strlen(command)-1]='\0';// 由于读入字符串时,必定会有一个\n所以这么不可能会越界// 用于测试输入的字符串是否符合我们的预期 // printf("%s\n",command); // 当命令行只输入回车时,则没有必要创建子进程来执行任务if(strlen(command)==0)return0;return1;}voidSplitCommand(char* in ,char* out[]){int argc =0;
out[argc++]=strtok(in,SEP);while(out[argc++]=strtok(NULL,SEP));// strtok函数分割失败时会返回NULL,正好是我们字符串数组结尾所需要的// 所以分割的方式可以使用赋值作为循环判断条件进行循序// 若是大家不习惯可以使用下面这一种分割方式//while(1) //{ // out[argc] = strtok(NULL,SEP); // if(out[argc] == NULL) // break; // argc++; //} #ifdefdebug for(int i =0; out[i]; i++){printf("%d : %s \n",i,out[i]);}#endif}intexecute(char* argv[]){
pid_t id =fork();if(id <0)return-1;elseif(id ==0){execvp(argv[0],argv);// 若替换失败则子进程退出exit(-1);}else{// sleep(1);int status =0;
pid_t rid =wait(&status);if(rid>0){
lastcode =WEXITSTATUS(status);}}return0;}voidcd(char* path){// 将当前的工作目录改为pathchdir(path);// tmp作为一个临时空间char tmp[1024];// 将当前工作目录写入到tmp中getcwd(tmp,sizeof(tmp));// 将PWD=与tmp进行组合,形成环境变量的格式存储到全局变量cwd sprintf(cwd,"PWD=%s",tmp);// 将cwd添加到环境变量中,覆盖掉原来的环境变量PWDputenv(cwd);}// 内键命令并执行1,非内键命令0intdobuildin(char* argv[]){if(strcmp(argv[0],"cd")==0){char* path =NULL;// cd命令后面没有添加路径,默认更改当前工作目录为家目录if(argv[1]==NULL)
path =getHomename();else
path = argv[1];cd(path);return1;}elseif(strcmp(argv[0],"export")==0){// 如果export后面没有内容则不做任何处理if(argv[1]==NULL)return1;else{// 将需要添加的环境变量保存到全局的二维数组中strcpy(myenv[cnt],argv[1]);// 将这个环境变量添加到进程的环境变量表中putenv(myenv[cnt++]);return1;}}elseif(strcmp(argv[0],"echo")==0){// 当echo后面没有内容时,默认输出回车if(argv[1]==NULL){printf("\n");return1;}// 当echo后面的字符串的第一个字符为$时// 就是查看进程的退出码或是环境变量的 elseif(*(argv[1])=='$'&&strlen(argv[1])>=2){// 输出退出码if(*(argv[1]+1)=='?'){printf("%d\n",lastcode);
lastcode =0;}else// 输出环境变量 {constchar* enval =getenv(argv[1]+1);if(enval){printf("%s",enval);}else{printf("\n");}}}// 不符合上面情况,通常就是将echo后面的字符串直接输出else{printf("%s",argv[1]);}return1;}elseif(0){}return0;}intmain(){while(1){char usercommand[NUM];char* argv[SIZE];// 打印命令提示符和获取命令行命令字符串 int x =getUserCommand(usercommand,NUM);if(x <=0)continue;SplitCommand(usercommand,argv);
x =dobuildin(argv);if(x ==1)continue;execute(argv);}return0;}
五、重定向(本文章所有代码)
写完前面的代码后,发现这个代码并不能解决重定向的问题,没了解重定向的最好看一下后面一篇文章了解一下重定向是什么,在分割命令字符串之前,我们需要判断这个命令字符串是否需要进行重定向,需要重定向则需要对字符串进行处理,例如
"ls -l -a > fortest.txt" -> "ls -l -a" 重定向类型 "fortest.txt"
,我们会得到三个部分,命令字符串、重定向类型、和文件名,在代码定义四种重定向类型,无重定向、输入重定向、输出重定向和追加重定向,默认情况下是无重定向,定义一个全局变量存储重定向类型,定义一个全局指针来指向文件名,然后我们针对不同的重定向类型使用dup2函数进行不同的处理,详细不同重定向的处理过程请查看代码中重定向的一部分。
#include<stdio.h>#include<string.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>#include<sys/stat.h>#include<fcntl.h>#include<ctype.h>#defineNUM1024#defineSIZE64#defineSEP" "// #define debug 1 #defineNoneRedir0#defineInputRedir1#defineOutputRedir2#defineAppendRedir3int redir = NoneRedir;char* filename =NULL;// 由于环境变量PWD需要一直存在,这里定义一个全局变量来存储char cwd[1024];// 定义一个全局二维数组中,用于存储添加的环境变量char myenv[128][1024];// 记录二维数组中有多少个环境变量int cnt =0;int lastcode =0;// 记录退出码 char*getHomename(){char* homename =getenv("HOME");if(homename)return homename;elsereturn(char*)"none";}constchar*getUsername(){char* username =getenv("USER");if(username)return username;elsereturn"none";}constchar*getHostname(){char* hostname =getenv("HOSTNAME");if(hostname)return hostname;elsereturn"none";}constchar*getCwd(){char* cwd =getenv("PWD");if(cwd)return cwd;elsereturn"none";}intgetUserCommand(char* command ,int num){printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());// scanf("%s",usercommand); scanf读到空格就会自动停止,所以这里不使用scanfchar* r =fgets(command,num,stdin);if(r ==NULL)return-1;// 读取失败返回-1// 读取字符串时,会自动获取到'\n',我们需要将这个'\n'去掉
command[strlen(command)-1]='\0';// 由于读入字符串时,必定会有一个\n所以这么不可能会越界// 用于测试输入的字符串是否符合我们的预期 // printf("%s\n",command); // 当命令行只输入回车时,则没有必要创建子进程来执行任务if(strlen(command)==0)return0;return1;}voidSplitCommand(char* in ,char* out[]){int argc =0;
out[argc++]=strtok(in,SEP);while(out[argc++]=strtok(NULL,SEP));#ifdefdebug for(int i =0; out[i]; i++){printf("%d : %s \n",i,out[i]);}#endif}intexecute(char* argv[]){
pid_t id =fork();if(id <0)return-1;elseif(id ==0){int fd =0;if(redir == InputRedir){
fd =open(filename,O_RDONLY);dup2(fd,0);}elseif(redir == OutputRedir){
fd =open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);dup2(fd,1);}elseif(redir == AppendRedir){
fd =open(filename,O_WRONLY|O_CREAT|O_APPEND,0666);dup2(fd,1);}else{// do nothing}execvp(argv[0],argv);// 若替换失败则子进程退出exit(-1);}else{// sleep(1);int status =0;
pid_t rid =wait(&status);if(rid>0){
lastcode =WEXITSTATUS(status);}}return0;}voidcd(char* path){chdir(path);char tmp[1024];// char* tmp = getenv("PWD");getcwd(tmp,sizeof(tmp));sprintf(cwd,"PWD=%s",tmp);putenv(cwd);}// 内键命令并执行1,非内键命令0intdobuildin(char* argv[]){if(strcmp(argv[0],"cd")==0){char* path =NULL;if(argv[1]==NULL)
path =getHomename();else
path = argv[1];cd(path);return1;}elseif(strcmp(argv[0],"export")==0){if(argv[1]==NULL)return1;else{strcpy(myenv[cnt],argv[1]);putenv(myenv[cnt++]);return1;}}elseif(strcmp(argv[0],"echo")==0){if(argv[1]==NULL){printf("\n");return1;}elseif(*(argv[1])=='$'&&strlen(argv[1])>=2){// 输出退出码if(*(argv[1]+1)=='?'){printf("%d\n",lastcode);
lastcode =0;}else// 输出环境变量 {constchar* enval =getenv(argv[1]+1);if(enval){printf("%s",enval);}else{printf("\n");}}}else{printf("%s",argv[1]);}return1;}elseif(0){}return0;}// 用于跳过空格#defineSkipSpace(pos)do{while(isspace(*pos)) pos++;}while(0)// 判断是否为重定向voidCheckRedir(char command[]){char* start = command;char* end = command +strlen(command);while(start < end){// 输入重定向if(*end =='<'){*end ='\0';
filename = end +1;SkipSpace(filename);
redir = InputRedir;}elseif(*end =='>'){// 追加重定向if(*(end-1)=='>'){*(end-1)='\0';
filename = end +1;SkipSpace(filename);
redir = AppendRedir;}// 输出重定向else{*end ='\0';
filename = end +1;SkipSpace(filename);
redir = OutputRedir;}}
end--;}}intmain(){while(1){// 初始默认为非重定向
redir = NoneRedir;
filename =NULL;char usercommand[NUM];char* argv[SIZE];// 打印命令提示符和获取命令行命令字符串 int x =getUserCommand(usercommand,NUM);if(x <=0)continue;// 判断是否为重定向CheckRedir(usercommand);// 分割命令行 "ls -a -l" -> "ls" "-a" "-l"SplitCommand(usercommand,argv);// 判断是否为內键命令
x =dobuildin(argv);if(x ==1)continue;// 执行指令execute(argv);}return0;}
结尾
如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹
版权归原作者 是阿建吖! 所有, 如有侵权,请联系我们删除。