一、打印命令提示符和获取命令行命令字符串
1.1 设计
我们首先使用操作体系的bash看到了命令行提示符的组成为[用户名@主机名 当前工作目次]$,获取用户名、主机名和当前工作目次的函数在体系调用中都有,这里我们自己设计一个,这三个数据都是环境变量,我们可以通过getenv来获取到他们,获取后将他们按照操作体系bash的格式输出出来即可,通过下图我们可以发现,我们的当前工作目次与操作体系的有所区别,我们的当前工作目次是一条路径,可以通过裁剪得到与操作体系一样的效果,我这里为了区分与操作体系的区别,这里就不做裁剪了。
输出完命令行提示符后,就必要向bash中输入命令了,这里我们就必要一个输入函数来读取命令字符串,必要注意的是这里不能使用scanf函数,因为scanf函数不能读取空格之后的内容,可以选择gets/fgets函数来读取,当我们输入完命令字符串后必要按回车,那么获取到的字符串中也会获取到这个’\n’,所以我们还必要将这个’\n’处理掉。
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #define NUM 1024
- const char* getUsername()
- {
- char* username = getenv("USER");
- if(username)
- return username;
- else
- return "none";
- }
- const char* getHostname()
- {
- char* hostname = getenv("HOSTNAME");
- if(hostname)
- return hostname;
- else
- return "none";
- }
- const char* getCwd()
- {
- char* cwd = getenv("PWD");
- if(cwd)
- return cwd;
- else
- return "none";
- }
- int main()
- {
- char usercommand[NUM];
- printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());
-
- // scanf("%s",usercommand); scanf读到空格就会自动停止,所以这里不使用scanf
- fgets(usercommand,sizeof(usercommand),stdin);
- // 读取字符串时,会自动获取到'\n',我们需要将这个'\n'去掉
- usercommand[strlen(usercommand)-1] = '\0';
- // 用于测试输入的字符串是否符合我们的预期
- printf("%s",usercommand);
- return 0;
- }
复制代码 1.2 封装
这里将打印命令行提示符与获取命令行字符串的工作统一封装到getUserCommand这个函数中。
- int getUserCommand(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);
-
- return 1;
- }
- int main()
- {
- char usercommand[NUM];
-
- getUserCommand(usercommand,NUM);
- return 0;
- }
复制代码 二、分割字符串
2.1 设计
当我们获取到了命令字符串后,必要将字符串以空格为分隔符将字符串分割为子字符串,并将每一个子字符串的地址存入到一个指针数组中,这里给出一个字符串被分割的例子:"ls -l -a" -> "ls" "-l" "-a"。我们可以使用strtok函数来将字符串分割,使用strtok函数处理同一个字符串时,第一次必要传入字符串的地址,后面再次调用则只必要传入NULL即可。
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #define NUM 1024
- #define SIZE 64
- #define SEP " "
- const char* getUsername()
- {
- char* username = getenv("USER");
- if(username)
- return username;
- else
- return "none";
- }
- const char* getHostname()
- {
- char* hostname = getenv("HOSTNAME");
- if(hostname)
- return hostname;
- else
- return "none";
- }
- const char* getCwd()
- {
- char* cwd = getenv("PWD");
- if(cwd)
- return cwd;
- else
- return "none";
- }
- int main()
- {
- 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]);
- }
- }
- return 0;
- }
复制代码 2.2 封装
- void SplitCommand(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++;
- }
-
- // 用于测试字符串是否被分割
- #ifdef debug
- for(int i = 0 ; out[i] ; i++)
- {
- printf("%d : %s \n",i,out[i]);
- }
- #endif
- }
-
- int main()
- {
- while(1)
- {
- char usercommand[NUM];
- char* argv[SIZE];
-
- int x = getUserCommand(usercommand,NUM);
-
- SplitCommand(usercommand,argv);
- }
- return 0;
- }
复制代码 三、执行指令
3.1 设计
将命令字符串分割后,就必要执行命令了,我们知道bash必要一直运行,这里添加一个循环让他一直运行,我们可以使用前面学习过的进程替换来执行命令,但是不能使用当前进程来进程替换,当前进程还必要继承运行,所以我们可以创建一个子进程来执行命令,由于我们将字符串分割为子字符串存储在了指针数组中,这里可以使用execvp函数来进行进场替换。
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/wait.h>
- #define NUM 1024
- #define SIZE 64
- #define SEP " "
- #define debug 1
- const char* getUsername()
- {
- char* username = getenv("USER");
- if(username)
- return username;
- else
- return "none";
- }
- const char* getHostname()
- {
- char* hostname = getenv("HOSTNAME");
- if(hostname)
- return hostname;
- else
- return "none";
- }
- const char* getCwd()
- {
- char* cwd = getenv("PWD");
- if(cwd)
- return cwd;
- else
- return "none";
- }
- int getUserCommand(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);
-
- return 1;
- }
- void SplitCommand(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++;
- }
-
-
- #ifdef debug
- for(int i = 0 ; out[i] ; i++)
- {
- printf("%d : %s \n",i,out[i]);
- }
- #endif
- }
- int main()
- {
- 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;
- else if(id == 0)
- {
- execvp(argv[0],argv);
- // 若替换失败则子进程退出
- exit(-1);
- }
- else
- {
- pid_t rid = wait(NULL);
- if(rid>0){};
- }
- }
- return 0;
- }
复制代码 3.2 封装
- int execute(char* argv[])
- {
- pid_t id = fork();
- if(id < 0) return -1;
- else if(id == 0)
- {
- execvp(argv[0],argv);
- // 若替换失败则子进程退出
- exit(-1);
- }
- else
- {
- pid_t rid = wait(NULL);
- if(rid>0){};
- }
- return 0;
- }
- int main()
- {
- while(1)
- {
- char usercommand[NUM];
- char* argv[SIZE];
- // 打印命令提示符和获取命令行命令字符串
- int x = getUserCommand(usercommand,NUM);
-
- if(x <= 0) continue;
- // 分割命令字符串
- SplitCommand(usercommand,argv);
- // 执行命令
- execute(argv);
- }
- return 0;
- }
复制代码 四、处理內键命令的执行
当我们使用上面的代码执行命令时,发现大部分命令都可以被执行,但是比方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>
-
- #define NUM 1024
- #define SIZE 64
- #define SEP " "
- // #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;
- else
- return (char*)"none";
- }
- const char* getUsername()
- {
- char* username = getenv("USER");
- if(username)
- return username;
- else
- return "none";
- }
- const char* getHostname()
- {
- char* hostname = getenv("HOSTNAME");
- if(hostname)
- return hostname;
- else
- return "none";
- }
- const char* getCwd()
- {
- char* cwd = getenv("PWD");
- if(cwd)
- return cwd;
- else
- return "none";
- }
- int getUserCommand(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)
- return 0;
- return 1;
- }
- void SplitCommand(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++;
- //}
- #ifdef debug
- for(int i = 0 ; out[i] ; i++)
- {
- printf("%d : %s \n",i,out[i]);
- }
- #endif
- }
- int execute(char* argv[])
- {
- pid_t id = fork();
-
- if(id < 0) return -1;
- else if(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);
- }
-
- }
- return 0;
- }
- void cd(char* path)
- {
- // 将当前的工作目录改为path
- chdir(path);
- // tmp作为一个临时空间
- char tmp[1024];
- // 将当前工作目录写入到tmp中
- getcwd(tmp,sizeof(tmp));
- // 将PWD=与tmp进行组合,形成环境变量的格式存储到全局变量cwd
- sprintf(cwd,"PWD=%s",tmp);
- // 将cwd添加到环境变量中,覆盖掉原来的环境变量PWD
- putenv(cwd);
- }
- // 内键命令并执行1,非内键命令0
- int dobuildin(char* argv[])
- {
- if(strcmp(argv[0],"cd") == 0)
- {
- char* path = NULL;
- // cd命令后面没有添加路径,默认更改当前工作目录为家目录
- if(argv[1] == NULL)
- path = getHomename();
- else
- path = argv[1];
- cd(path);
- return 1;
- }
- else if(strcmp(argv[0],"export") == 0)
- {
- // 如果export后面没有内容则不做任何处理
- if(argv[1] == NULL)
- return 1;
- else
- {
- // 将需要添加的环境变量保存到全局的二维数组中
- strcpy(myenv[cnt],argv[1]);
- // 将这个环境变量添加到进程的环境变量表中
- putenv(myenv[cnt++]);
- return 1;
- }
- }
- else if(strcmp(argv[0],"echo") == 0)
- {
- // 当echo后面没有内容时,默认输出回车
- if(argv[1] == NULL)
- {
- printf("\n");
- return 1;
- }
- // 当echo后面的字符串的第一个字符为$时
- // 就是查看进程的退出码或是环境变量的
- else if(*(argv[1]) == '$' && strlen(argv[1]) >= 2)
- {
- // 输出退出码
- if(*(argv[1]+1) == '?')
- {
- printf("%d\n",lastcode);
- lastcode = 0;
- }
- else // 输出环境变量
- {
- const char* enval = getenv(argv[1]+1);
- if(enval)
- {
- printf("%s",enval);
- }
- else
- {
- printf("\n");
- }
- }
- }
- // 不符合上面情况,通常就是将echo后面的字符串直接输出
- else
- {
- printf("%s",argv[1]);
- }
- return 1;
- }
- else if(0){}
- return 0;
- }
- int main()
- {
- 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);
- }
- return 0;
- }
复制代码 五、重定向(本文章所有代码)
写完前面的代码后,发现这个代码并不能解决重定向的问题,没了解重定向的最悦目一下后面一篇文章了解一下重定向是什么,在分割命令字符串之前,我们必要判断这个命令字符串是否必要进行重定向,必要重定向则必要对字符串进行处理,比方"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>
-
- #define NUM 1024
- #define SIZE 64
- #define SEP " "
- // #define debug 1
-
- #define NoneRedir 0
- #define InputRedir 1
- #define OutputRedir 2
- #define AppendRedir 3
-
- int 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;
- else
- return (char*)"none";
- }
- const char* getUsername()
- {
- char* username = getenv("USER");
- if(username)
- return username;
- else
- return "none";
- }
- const char* getHostname()
- {
- char* hostname = getenv("HOSTNAME");
- if(hostname)
- return hostname;
- else
- return "none";
- }
- const char* getCwd()
- {
- char* cwd = getenv("PWD");
- if(cwd)
- return cwd;
- else
- return "none";
- }
- int getUserCommand(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)
- return 0;
- return 1;
- }
- void SplitCommand(char* in , char* out[])
- {
- int argc = 0;
- out[argc++] = strtok(in,SEP);
- while(out[argc++] = strtok(NULL,SEP));
-
- #ifdef debug
- for(int i = 0 ; out[i] ; i++)
- {
- printf("%d : %s \n",i,out[i]);
- }
- #endif
- }
- int execute(char* argv[])
- {
- pid_t id = fork();
- if(id < 0) return -1;
- else if(id == 0)
- {
- int fd = 0;
- if(redir == InputRedir)
- {
- fd = open(filename,O_RDONLY);
- dup2(fd,0);
- }
- else if(redir == OutputRedir)
- {
- fd = open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);
- dup2(fd,1);
- }
- else if(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);
- }
-
- }
- return 0;
- }
- void cd(char* path)
- {
- chdir(path);
- char tmp[1024];
- // char* tmp = getenv("PWD");
- getcwd(tmp,sizeof(tmp));
- sprintf(cwd,"PWD=%s",tmp);
- putenv(cwd);
- }
- // 内键命令并执行1,非内键命令0
- int dobuildin(char* argv[])
- {
- if(strcmp(argv[0],"cd") == 0)
- {
- char* path = NULL;
- if(argv[1] == NULL)
- path = getHomename();
- else
- path = argv[1];
- cd(path);
- return 1;
- }
- else if(strcmp(argv[0],"export") == 0)
- {
- if(argv[1] == NULL)
- return 1;
- else
- {
- strcpy(myenv[cnt],argv[1]);
- putenv(myenv[cnt++]);
- return 1;
- }
- }
- else if(strcmp(argv[0],"echo") == 0)
- {
- if(argv[1] == NULL)
- {
- printf("\n");
- return 1;
- }
- else if(*(argv[1]) == '$' && strlen(argv[1]) >= 2)
- {
- // 输出退出码
- if(*(argv[1]+1) == '?')
- {
- printf("%d\n",lastcode);
- lastcode = 0;
- }
- else // 输出环境变量
- {
- const char* enval = getenv(argv[1]+1);
- if(enval)
- {
- printf("%s",enval);
- }
- else
- {
- printf("\n");
- }
- }
- }
- else
- {
- printf("%s",argv[1]);
- }
- return 1;
- }
- else if(0){}
- return 0;
- }
- // 用于跳过空格
- #define SkipSpace(pos) do{while(isspace(*pos)) pos++;}while(0)
- // 判断是否为重定向
- void CheckRedir(char command[])
- {
- char* start = command;
- char* end = command + strlen(command);
- while(start < end)
- {
- // 输入重定向
- if(*end == '<')
- {
- *end = '\0';
- filename = end + 1;
- SkipSpace(filename);
- redir = InputRedir;
- }
- else if(*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--;
- }
- }
- int main()
- {
- 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);
- }
- return 0;
- }
复制代码 末了
如果有什么建议和疑问,或是有什么错误,各人可以在批评区中提出。
希望各人以后也能和我一起进步!! |