【打造你自己的Shell:编写定制化命令行体验】(三):https://developer.aliyun.com/article/1425823
此时确实获取了用户输入的字符串,但是为什么中间还多了一个空行呢?是不是因为我们打印输出的时候带了一个换行符呢?不是,这里有一个空行,说明我们换行了两次,这是因为用户在输入ls -a -l时还敲了一下回车,加上打印输出的时候带了一个换行符,所以这里会打印一个空行,那我们想要解决就要读取到用户的换行符,这里我们可以将\n修改为\0。
运行结果:
现在我们把上面的功能封装一下,命名为交互。
void interactive(char out[], int size) { //输出提示符并获取用户输入的命令字符串"ls -a -l" getInfo(); printf("[%s@%s %s]$",info[0],info[1],info[2]); //获取用户输入的字符串 //scanf("%s",commandline); fgets(out,size,stdin); //将\n修改成\0 out[strlen(out) - 1] = '\0';//也能处理空串的情况 } int main() { //1.打印命令行提示符,获取用户输入的命令字符串 char commandline[SIZE]; interactive(commandline, SIZE); return 0; }
现在我们获取了用户输入的命令字符串,现在我们想要执行它,就必须要分隔字符串,这里可以使用strtok函数。
#include <string.h> char *strtok(char *str, const char *delim);
str
是要拆分的字符串,第一次调用时传入要拆分的字符串,之后传入NULL
表示继续使用上一次拆分的字符串。delim
是用来指定分隔符的字符串,即根据哪些字符来拆分字符串。
strtok
函数返回一个指向拆分后的子字符串的指针,如果没有找到子字符串,则返回 NULL
。每次调用 strtok
都返回拆分后的一个子字符串,直到没有更多的子字符串为止。注意,strtok
在每次调用时会修改原始字符串,需要使用 NULL
继续拆分。
这里我们来解释一下我们上面的写法,第一次strtok分隔的字符【ls】存放到了argv[0]的位置,随后i就变为1,因为strtok在每次调用时会修改原始字符串,需要使用NULL继续拆分,随后进入while循环,argv[1]的位置就存放了【-a】,argv[2]的位置存放了【-l】,随后继续自增,然后此时就没有找到字符,由于这里是等号【=】,就将NULL返回给了argv[3],此时while循环的条件就不满足,循环也就退出了。
运行结果:
此时我们就分隔了我们的用户输入的命令行字符串了,这里我们也封装一下,命名为切割。
void Split(char in[]) { int i = 0; argv[i++] = strtok(in, " "); while(argv[i++] = strtok(NULL, " "));//这里写成=,不是== } int main() { //1.打印命令行提示符,获取用户输入的命令字符串 char commandline[SIZE]; interactive(commandline, SIZE); //2.对命令行字符串进行分隔 Split(commandline); int i = 0; for(i = 0; argv[i]; ++i) { printf("argv[%d]:%s\n", i, argv[i]); } return 0; }
现在我们这里就要执行我们的命令了,这里我们不能直接调用我们的程序替换接口,而应该创建一个子进程让它去执行命令。
运行结果:
现在我们对执行过程也做一下封装,命名execute。
void Execute() { pid_t id = fork(); if(id == 0) { //子进程执行命令 execvp(argv[0],argv); exit(1); } //父进程回收子进程 waitpid(id,NULL,0); } int main() { //1.打印命令行提示符,获取用户输入的命令字符串 char commandline[SIZE]; interactive(commandline, SIZE); //2.对命令行字符串进行分隔 Split(commandline); //3.执行命令 Execute(); return 0; }
但是我们发现我们的命令行只能做一次执行,所以这里我们想要多次循环就要使用循环。
运行结果:
此时我们的命令行解释器shell就可以一直运行,但是当我们运行cd命令为什么就跑不起来呢?
cd 是一个特殊的命令,它用于改变当前工作目录。然而,cd 命令通常需要由父进程直接来执行,而不能通过创建子进程的方式来运行。如果你的命令行解释器是通过创建子进程来执行用户输入的命令,那么 cd 命令可能无法按预期工作,因为它改变的是子进程的工作目录,而不影响父进程的工作目录。每个子进程都有自己的工作目录,改变其中一个不会影响其他进程。为了使 cd 命令在你的 shell 中生效,你可能需要在父进程中执行该命令,而不是创建一个子进程来执行。这样,cd 命令就能够改变你的 shell 进程的当前工作目录。
int BuildCmd() { int ret = 0; //1.检查是否是内键命令,是1,否0 if(strcmp(argv[0],"cd") == 0) { ret = 1; //执行cd命令 char* target = argv[1];//cd xxx or cd if(!target) target = getenv("HOME"); chdir(target);//更改当前进程的工作目录 } return ret; } int main() { while(1) { //1.打印命令行提示符,获取用户输入的命令字符串 char commandline[SIZE]; interactive(commandline, SIZE); //2.对命令行字符串进行分隔 Split(commandline); //3.处理内键命令 - 由父进程执行 int n = BuildCmd(); if(n) continue; //4.执行命令 Execute(); } return 0; }
运行结果:
上面已经可以做到目录的切换,但是还是有一点问题,首先是空串的处理,其次是命令行提示符哪里工作目录没有改变,这里是因为环境变量没有变化,虽然当前进程的工作目录发生了改变,但是我们的环境变量并没有更新。
运行结果:
但是我们发现我们这里的上级目录..没有处理好,在这里需要使用我们的getcwd函数。
我们先来看一下关于getcwd的使用。
运行结果:
默认情况下,我们的子进程会继承父进程的环境变量,随后我们更改当前工作目录为上级目录,便就能用getcwd获取到该目录,而不是我们的..了。
运行结果:
随后我们导入一下环境变量,进而用env查询一下,可是我们没有查到,为什么呢?
这是因为我们是在子进程中执行了export,只在子进程导入了环境变量,而后通过 env
在父进程中查询,是看不到这个环境变量的。所以导入环境变量这个过程我们也要让父进程执行。
我们运行后导入我们的环境变量确实存在了,但是当我们再次执行其他命令的时候,再次env查询环境变量却有没有了,为什么呢?这是因为我们argv指向是用户输入的commandline分隔的字串,当我们执行后续指令,此时用户输入的命令就变化了,argv所指向的分隔的字串也发生了变化,此时环境变量就会被一直覆盖,所以我们就需要一个数组保存之前的导入的环境变量。
此时我们的环境变量就不会被覆盖了,然后我们再来执行我们的echo命令,发现也不能执行,说明echo也是内键命令,所以我们的代码也要修改,我们先来看看echo的特点。
echo # 输出空行 echo $? # 输出退出码 echo $HOME # 输出环境变量 echo "" # 输出字符串
代码实现:
else if(strcmp(argv[0],"echo") == 0) { ret = 1; if(argv[1] == NULL) { printf("\n");//输出空行 } else { if(argv[1][0] == '$') { if(argv[1][1] == '?') { printf("%d\n",lastcode);//输出退出码 lastcode = 0; } else { char* e = getenv(argv[1] + 1); if(e) printf("%s\n",e); } } else { printf("%s\n",argv[1]);//输出字符串 } } }
运行结果:
现在我们的shell功能已经齐全,但是我们发现没有颜色区别,我们再来修改一下
运行结果:
此时我们就拥有了我们的颜色。
但是此时我们的输出重定项和管道都不能使用,我们后面再添加。
整体shell实现代码:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #define SIZE 1024 #define MAX_ARGC 64 const char* info[3]; char* argv[MAX_ARGC];//保存分隔字串 char pwd[SIZE];//更新环境变量 char env[SIZE];//环境变量表 int lastcode = 0; void getInfo() { char* user = getenv("USER"); if(user) info[0] = user; else info[0] = "None"; char* hostname = getenv("HOSTNAME"); if(hostname) info[1] = hostname; else info[1] = "None"; char* pwd = getenv("PWD"); if(pwd) info[2] = pwd; else info[2] = "None"; } int interactive(char out[], int size) { //输出提示符并获取用户输入的命令字符串"ls -a -l" getInfo(); printf("[%s@%s %s]$ ",info[0],info[1],info[2]); //获取用户输入的字符串 //scanf("%s",commandline); fgets(out,size,stdin); //将\n修改成\0 out[strlen(out) - 1] = '\0';//也能处理空串的情况 return strlen(out); } void Split(char in[]) { int i = 0; argv[i++] = strtok(in, " "); while(argv[i++] = strtok(NULL, " "));//这里写成=,不是== if(strcmp(argv[0],"ls") == 0) { argv[i-1] = (char*)"--color"; argv[i] = NULL; } } void Execute() { pid_t id = fork(); if(id == 0) { //子进程执行命令 execvp(argv[0],argv); exit(1); } //父进程回收子进程 int status = 0; pid_t rid = waitpid(id,&status,0); if(rid == id) lastcode = WEXITSTATUS(status);//获取进程退出码 } int BuildCmd() { int ret = 0; //1.检查是否是内键命令,是1,否0 if(strcmp(argv[0],"cd") == 0) { ret = 1; //执行cd命令 char* target = argv[1];//cd xxx or cd if(!target) target = getenv("HOME"); chdir(target);//更改当前进程的工作目录 char temp[1024]; getcwd(temp,1024);//使用绝对路径 snprintf(pwd,SIZE,"PWD=%s",temp);//更新环境变量 putenv(pwd); } else if(strcmp(argv[0],"export") == 0) { ret = 1; if(argv[1]) { strcpy(env,argv[1]); putenv(env);//导入环境变量 } } else if(strcmp(argv[0],"echo") == 0) { ret = 1; if(argv[1] == NULL) { printf("\n");//输出空行 } else { if(argv[1][0] == '$') { if(argv[1][1] == '?') { printf("%d\n",lastcode);//输出退出码 lastcode = 0; } else { char* e = getenv(argv[1] + 1); if(e) printf("%s\n",e); } } else { printf("%s\n",argv[1]);//输出字符串 } } } return ret; } int main() { while(1) { //1.打印命令行提示符,获取用户输入的命令字符串 char commandline[SIZE]; int n = interactive(commandline, SIZE); if(!n) continue; //2.对命令行字符串进行分隔 Split(commandline); //3.处理内键命令 - 由父进程执行 n = BuildCmd(); if(n) continue; //4.执行命令 Execute(); } return 0; }