【Linux】C语言模拟实现shell命令行(程序替换原理)
目录
一、自动化构建工具(makefile)
二、输出提示符
三、获取用户输入的数据
四、将用户输入的指令字符串进行分割:
五、执行用户输入的命令
六、发现cd命令用不了(内建命令)
原因在于:
七、处理内建命令cd:
八、存在一个小问题:
八、处理内建命令export
九、获取最近一次进程的退出码
十、处理内建命令echo
十一、让ls指令输出的内容带上颜色
十二、完整代码
上一章节我们学习了程序替换,现在我们就可以通过程序替换来模拟实现shell命令行;
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家
点击跳转到网站
一、自动化构建工具(makefile)
myprocess:myshell.c gcc -o $@ $^ -g -std=c99 .PHONY:clean clean: rm -f myprocess
二、输出提示符
我们在命令行终端处时,一般会有这个输出提示符:
#include
#include //获取主机名 const char* HostName() { char* hostname = getenv("HOSTNAME"); if(hostname)return hostname; else return "None"; } //获取当前登录的用户名 const char* UserName() { char* username = getenv("USER"); if(username)return username; else return "None"; } //获取当前工作目录 const char* CurrentWorkDir() { char* currentworkdir = getenv("PWD"); if(currentworkdir)return currentworkdir; else return "None"; } int main() { //输出提示符 printf("[%s@%s %s]$ ",UserName(),HostName(),CurrentWorkDir()); return 0; }
三、获取用户输入的数据
void Interactive(char out[],int size) { fgets(out,SIZE,stdin);//stdin是标准输入流,意思就是从键盘获取数据保存到commandline中 out[strlen(out)-1] = ' ';//因为fgets会读取换行符,所以这步我们去掉换行。 } int main() { //输出提示符 printf("[%s@%s %s]$ ",UserName(),HostName(),CurrentWorkDir()); //获取用户输入的命令 char commandline[SIZE]; Interactive(commandline,SIZE); printf("test:%s ",commandline); return 0; }
四、将用户输入的指令字符串进行分割:
//对字符串进行分割 void Split(char in[]) { int i = 0; argv[i++] = strtok(in,SEP);//对字符串commandline以空格作为分隔符进行切割,"ls -a -l" while(argv[i++] = strtok(NULL,SEP));//进行第二次切割时,strtok第一个参数需要传入NULL。 } int main() { //1、输出提示符 printf("[%s@%s %s]$ ",UserName(),HostName(),CurrentWorkDir()); //2、获取用户输入的命令 char commandline[SIZE]; Interactive(commandline,SIZE); printf("test:%s ",commandline); //3、对命令行字符串进行切割 Split(commandline); return 0; }
五、执行用户输入的命令
执行命令我们是用程序替换的原理,去执行对应的命令,而程序替换过后,就不会再执行之后的代码,为了避免这一点,所以我们创建子进程取进行程序替换。
void Execute() { //因为程序替换后,不会在执行之后的代码,所以这里创建子进程去执行最合适 pid_t id = fork(); if(id == 0) { //子进程通过程序替换执行命令 execvp(argv[0],argv); exit(1); } //父进程进行等待 pid_t rid = waitpid(id,NULL,0); printf("run done,rid: %d ",rid); } int main() { //1、输出提示符 printf("[%s@%s %s]$ ",UserName(),HostName(),CurrentWorkDir()); //2、获取用户输入的命令 char commandline[SIZE]; Interactive(commandline,SIZE); printf("test:%s ",commandline); //3、对命令行字符串进行切割 Split(commandline); //4、执行分割好的命令 Execute(); return 0; }
这样我们就能运行起来单次命令了,所以我们套个while循环,就能循环输入了:
int main() { while (1) { // 1、输出提示符 printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir()); // 2、获取用户输入的命令 char commandline[SIZE]; Interactive(commandline, SIZE); printf("test:%s ", commandline); // 3、对命令行字符串进行切割 Split(commandline); // 4、执行分割好的命令 Execute(); } return 0; }
六、发现cd命令用不了(内建命令)
当我们使用cd命令时,发现没有起作用,比如cd -,没有返回上一次的工作目录:
原因在于:
有些命令不应该让子进程去执行的,而是应该由shell自己去执行,就不如上述的cd命令,这种命令叫内建命令。
所以我们在执行命令之前应该要先处理内建命令
七、处理内建命令cd:
char* Home() { return getenv("HOME"); } int BuildinCmd() { int ret = 0; //检查是否为内建命令,是 1,否 0 if(strcmp("cd",argv[0]) == 0) { //执行 ret = 1; char* target = argv[1]; if(!target) target = Home();//如果只输入cd,则argv[1]的值为0,则会进入if语句,默认跳转到Home()工作目录 //通过系统调用chdir,改变当前工作目录 chdir(target); //虽然具体的工作目录变了,但是命令行提示符中工作目录我们没有实时更新,所以还没有变 //此时需要处理一下,修改PWD环境变量,这样下次循环时,命令行提示符获取的就是当前路径。 snprintf(pwd,SIZE,"PWD=%s",target); putenv(pwd); } return ret; } int main() { while (1) { // 1、输出提示符 printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir()); // 2、获取用户输入的命令 char commandline[SIZE]; Interactive(commandline, SIZE); // 3、对命令行字符串进行切割 Split(commandline); // 4、处理内建命令 int n = BuildinCmd(); if(n)continue; // 5、执行分割好的命令 Execute(); } return 0; }
八、存在一个小问题:
问题如下图:
该现象的原因在于,用户输入指令cd ..后target字符串的内容就变成了"..",后续调用chdir,因为chdir是系统调用,内部是处理了"."和".."的,所以这里会正常执行,但后面调用snprintf函数时,pwd的值就变成了"..",这样putenv改变的环境变量的内容就变成了"..",下次循环命令行提示的地方读取到的环境变量值就为".."。
因为在这之前,我们已经用chdir函数改变了当前工作目录了,所以我们后面使用一个接口叫:getcwd(),该接口返回的就是当前工作目录,用该接口的返回传给getenv,这样环境变量就能正常修改了
int BuildinCmd() { int ret = 0; //检查是否为内建命令,是 1,否 0 if(strcmp("cd",argv[0]) == 0) { //执行 ret = 1; char* target = argv[1]; if(!target) target = Home();//如果只输入cd,则argv[1]的值为0,则会进入if语句,默认跳转到Home()工作目录 //通过系统调用chdir,改变当前工作目录 chdir(target); char temp[1024]; getcwd(temp,1024); //虽然具体的工作目录变了,但是命令行提示符中工作目录我们没有实时更新,所以还没有变 //此时需要处理一下,修改PWD环境变量,这样下次循环时,命令行提示符获取的就是当前路径。 snprintf(pwd,SIZE,"PWD=%s",temp); putenv(pwd); } return ret; }
八、处理内建命令export
export是用来导入新的环境变量的,也是一个内建命令,因为只有将新环境变量导入给自己,这样才能被子进程继承下去。
直接else if 接着判断即可:
九、获取最近一次进程的退出码
十、处理内建命令echo
else if(strcmp("echo",argv[0]) == 0) { ret = 1; if(argv[1] == NULL) { printf(" "); } else{ if(argv[1][0] == '$')//$用于查看环境变量的值 { if(argv[1][1] == '?')//$?:查看进程退出码 { printf("%d ",lastcode); lastcode = 0; } else { char* e = getenv(argv[1]+1);//echo $PWD if(e) printf("%s ",e); } } else{ //如果不是以$开头,则正常打印内容 printf("%s ",argv[1]); } } }
十一、让ls指令输出的内容带上颜色
十二、完整代码
#include
#include #include #include #include #include #define SIZE 1024 #define MAX_ARGC 64 #define SEP " " // 全局进程 char *argv[MAX_ARGC]; //工作目录 char pwd[SIZE]; //环境变量 char env[SIZE]; //进程退出信息 int lastcode = 0; // 获取主机名 const char *HostName() { char *hostname = getenv("HOSTNAME"); if (hostname) return hostname; else return "None"; } // 获取当前登录的用户名 const char *UserName() { char *username = getenv("USER"); if (username) return username; else return "None"; } // 获取当前工作目录 const char *CurrentWorkDir() { char *currentworkdir = getenv("PWD"); if (currentworkdir) return currentworkdir; else return "None"; } // 获取用户输入的命令 void Interactive(char out[], int size) { fgets(out, SIZE, stdin); // stdin是标准输入流,意思就是从键盘获取数据保存到commandline中 out[strlen(out) - 1] = ' '; // 因为fgets会读取换行符,所以这步我们去掉换行。 } // 对字符串进行分割 void Split(char in[]) { int i = 0; argv[i++] = strtok(in, SEP); // 对字符串commandline以空格作为分隔符进行切割,"ls -a -l" while (argv[i++] = strtok(NULL, SEP)); // 进行第二次切割时,strtok第一个参数需要传入NULL。并且最后会填入NULL if(strcmp(argv[0], "ls") == 0) { argv[i - 1] = "--color";//即在字符串末尾加上--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); } // printf("run done,rid: %d ", rid); } char* Home() { return getenv("HOME"); } int BuildinCmd() { int ret = 0; //检查是否为内建命令,是 1,否 0 if(strcmp("cd",argv[0]) == 0) { //执行 ret = 1; char* target = argv[1]; if(!target) target = Home();//如果只输入cd,则argv[1]的值为0,则会进入if语句,默认跳转到Home()工作目录 //通过系统调用chdir,改变当前工作目录 chdir(target); char temp[1024]; getcwd(temp,1024); //虽然具体的工作目录变了,但是命令行提示符中工作目录我们没有实时更新,所以还没有变 //此时需要处理一下,修改PWD环境变量,这样下次循环时,命令行提示符获取的就是当前路径。 snprintf(pwd,SIZE,"PWD=%s",temp); putenv(pwd); } else if(strcmp("export",argv[0]) == 0) { ret = 1; if(argv[1]) { strcpy(env,argv[1]); putenv(env); } } else if(strcmp("echo",argv[0]) == 0) { ret = 1; if(argv[1] == NULL) { printf(" "); } else{ if(argv[1][0] == '$')//$用于查看环境变量的值 { if(argv[1][1] == '?')//$?:查看进程退出码 { printf("%d ",lastcode); lastcode = 0; } else { char* e = getenv(argv[1]+1);//echo $PWD if(e) printf("%s ",e); } } else{ //如果不是以$开头,则正常打印内容 printf("%s ",argv[1]); } } } return ret; } int main() { while (1) { // 1、输出提示符 printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir()); // 2、获取用户输入的命令 char commandline[SIZE]; Interactive(commandline, SIZE); // 3、对命令行字符串进行切割 Split(commandline); // 4、处理内建命令 int n = BuildinCmd(); if(n)continue; // 5、执行分割好的命令 Execute(); } return 0; }