本期我们利用之前学过的知识,写一个shell命令行程序


目录

一、初始代码

二、使用户输入的ls指令带有颜色分类

三、解决cd指令后用户所在路径不变化问题

3.1 chdir函数

四、关于环境变量的问题


一、初始代码

#include#include#include#include#include#include#define MAX 1024#define ARGc 512void shift_commend(char** s, char* c, int n){assert(s);assert(c);int num = 0;*s = c;++s;while (num < n){if (*(c + num) == ' ')//将每个空格字符替换为'\0',再将选项的首地址存入s{*(c + num) = '\0';if (*(c + num + 1) != ' ')//防止用户输入多个空字符{*s = c + 1 + num;++s;}}++num;}}int main(){while (1){printf("[%s@MyShell]#", getenv("USER"));fflush(stdout);char comment[MAX] = { 0 };char* p = fgets(comment, sizeof(comment), stdin);assert(p);//assert函数在release版本下会被编译器删去(void)p;//防止在release版本下assert函数被删除导致p指针从未被使用而报错comment[strlen(comment) - 1] = '\0';//去除fgets函数最后获取的\n字符char* s[ARGc] = { NULL };shift_commend(s, comment, strlen(comment));//分割输入的命令行字符串,将每一个选项的地址存入指针数组spid_t id = fork();if (id == 0){execvp(*s, s);exit(1);}int status = 0;waitpid(id, &status, 0);}}

上述代码我们实现了一个基本的shell命令行程序,其实现基本思路为:

使用fgets函数获取用户输入的命令行,获取后切除字符串最后的换行符(用户输入指令后必须使用换行符来输入缓冲区中,而fgets并不会自动切除其换行符),然后将输入的指令进行切割(将指令与选项一个个切割开,方便传入execvp函数(例如fgets函数获取到用户输入的“ls -a -l”,经过切割会变成“ls/0-a/0-l/0”))。最后再创建子进程,用子进程调用execvp函数,传入用户指令最后实现shell命令行程序。

运行效果:

我们拿xshell来对比一下我们自己实现的shell命令行:

咦?xshell的ls指令有颜色变化,为什么我们自己实现的shell就没有呢?

这是因为xshell下的ls指令多了一行颜色配置:–color=auto

如果我们要想自己的shell命令行的ls指令有颜色分类的话,我们只需要在切割用户输入的指令后判断其是否为ls指令,如果是,在该指针数组的最后一项加上”–color=auto”字符串即可:

二、使用户输入的ls指令带有颜色分类

#include#include#include#include#include#include#define MAX 1024#define ARGc 512void shift_commend(char** s, char* c, int n){assert(s);assert(c);int num = 0;*s = c;++s;while (num < n){if (*(c + num) == ' ')//将每个空格字符替换为'\0',再将选项的首地址存入s{*(c + num) = '\0';if (*(c + num + 1) != ' ')//防止用户输入多个空字符{*s = c + 1 + num;++s;}}++num;}}int main(){while (1){printf("[%s@MyShell]#", getenv("USER"));fflush(stdout);char comment[MAX] = { 0 };char* p = fgets(comment, sizeof(comment), stdin);assert(p);//assert函数在release版本下会被编译器删去(void)p;//防止在release版本下assert函数被删除导致p指针从未被使用而报错comment[strlen(comment) - 1] = '\0';//去除fgets函数最后获取的\n字符char* s[ARGc] = { NULL };shift_commend(s, comment, strlen(comment));//分割输入的命令行字符串,将每一个选项的地址存入指针数组sif (strcmp(s[0], "ls") == 0)//使用户输入的ls指令带有颜色分类{int n = 0;while (*(s + n)){++n;}*(s + n) = (char*)"--color=auto";}pid_t id = fork();if (id == 0){execvp(*s, s);exit(1);}int status = 0;waitpid(id, &status, 0);}}

运行效果:

三、解决cd指令后用户所在路径不变化问题

上面代码还有一个问题,在我们使用cd指令后,再使用pwd来查看所在路径时,会发现没有任何变化:

这是因为当用户输入cd指令后,通过子进程调用的execvp函数来执行cd指令,改变的是子进程的目录路径和父进程压根没有关系!

所以当类似cd这样要改变父进程环境的指令(内建指令),就要父进程自己来执行,我们需要特殊判断做特殊处理:

3.1 chdir函数

chdir函数(包含在头文件unistd.h中)可以改变进程所在路径:

我们可以向path传入想要进入的路径,这样子chdir函数就会自动帮我们改变当前进程的所处路径:

#include#include#include#include#include#include#define MAX 1024#define ARGc 512void shift_commend(char** s, char* c, int n){assert(s);assert(c);int num = 0;*s = c;++s;while (num < n){if (*(c + num) == ' ')//将每个空格字符替换为'\0',再将选项的首地址存入s{*(c + num) = '\0';if (*(c + num + 1) != ' ')//防止用户输入多个空字符{*s = c + 1 + num;++s;}}++num;}}int main(){while (1){printf("[%s@MyShell]#", getenv("USER"));fflush(stdout);char comment[MAX] = { 0 };char* p = fgets(comment, sizeof(comment), stdin);assert(p);//assert函数在release版本下会被编译器删去(void)p;//防止在release版本下assert函数被删除导致p指针从未被使用而报错comment[strlen(comment) - 1] = '\0';//去除fgets函数最后获取的\n字符char* s[ARGc] = { NULL };shift_commend(s, comment, strlen(comment));//分割输入的命令行字符串,将每一个选项的地址存入指针数组sif (strcmp(s[0], "cd") == 0)//cd指令直接使用chdir函数改变父进程所在路径{if(s[1]!=NULL)chdir(s[1]);continue;}if (strcmp(s[0], "ls") == 0)//使用户输入的ls指令带有颜色分类{int n = 0;while (*(s + n)){++n;}*(s + n) = (char*)"--color=auto";}pid_t id = fork();if (id == 0){execvp(*s, s);exit(1);}int status = 0;waitpid(id, &status, 0);}}

四、关于环境变量的问题

在xshell中我们可以使用export导入自定义的环境变量,但是在我们自实现的代码中还没有这个功能,下面我们来实现一下:

使用我们在往期博客中介绍过导入环境变量的函数:putenv

#include #include#include#include#include#include#define MAX 1024#define ARGc 512void shift_commend(char** s, char* c, int n){assert(s);assert(c);int num = 0;*s = c;++s;while (num < n){if (*(c + num) == ' ')//将每个空格字符替换为'\0',再将选项的首地址存入s{*(c + num) = '\0';if (*(c + num + 1) != ' ')//防止用户输入多个空字符{*s = c + 1 + num;++s;}}++num;}}int main(){while (1){printf("[%s@MyShell]#", getenv("USER"));fflush(stdout);char comment[MAX] = { 0 };char* p = fgets(comment, sizeof(comment), stdin);assert(p);//assert函数在release版本下会被编译器删去(void)p;//防止在release版本下assert函数被删除导致p指针从未被使用而报错comment[strlen(comment) - 1] = '\0';//去除fgets函数最后获取的\n字符char* s[ARGc] = { NULL };shift_commend(s, comment, strlen(comment));//分割输入的命令行字符串,将每一个选项的地址存入指针数组sif (strcmp(s[0], "cd") == 0)//cd指令直接使用chdir函数改变父进程所在路径{if(s[1]!=NULL)chdir(s[1]);continue;}if (strcmp(s[0], "export") == 0)//导入用户自定义的环境变量{if (s[1] != NULL)putenv(s[1]);continue;}if (strcmp(s[0], "ls") == 0)//使用户输入的ls指令带有颜色分类{int n = 0;while (*(s + n)){++n;}*(s + n) = (char*)"--color=auto";}pid_t id = fork();if (id == 0){execvp(*s, s);exit(1);}int status = 0;waitpid(id, &status, 0);}}

运行结果:

这张图不太好看,博主仔细找过,在子进程调用env指令查找环境变量时,并没有看到导入的环境变量“Myenv=100”

这是为什么呢?子进程的环境变量不应该继承父进程的嘛?

这点没错,子进程确实基础了父进程的环境变量,我们再来仔细看看可以看到,在我们导入环境变量后,再一次调用env指令,屏幕上在最后多打印了一行空行。为什么会这样?

因为使用putenv函数导入环境变量时,该函数只是把形参所获取到的地址添加到进程中的环境变量表中了,我们仔细看看代码中存储环境变量的是一个变量s[1],当我们下一次在调用其他指令时该地址的数据就发生了变化!也就是说自定义环境变量是由我们自己维护的,我们应该将其放在一个不会被覆盖的空间里

下面我们改写一下代码:

#include #include#include#include#include#include#define MAX 1024#define ARGc 512void shift_commend(char** s, char* c, int n){assert(s);assert(c);int num = 0;*s = c;++s;while (num < n){if (*(c + num) == ' ')//将每个空格字符替换为'\0',再将选项的首地址存入s{*(c + num) = '\0';if (*(c + num + 1) != ' ')//防止用户输入多个空字符{*s = c + 1 + num;++s;}}++num;}}int main(){char user_env[32][256];//存储自定义环境变量int env_num = 0;while (1){printf("[%s@MyShell]#", getenv("USER"));fflush(stdout);char comment[MAX] = { 0 };char* p = fgets(comment, sizeof(comment), stdin);assert(p);//assert函数在release版本下会被编译器删去(void)p;//防止在release版本下assert函数被删除导致p指针从未被使用而报错comment[strlen(comment) - 1] = '\0';//去除fgets函数最后获取的\n字符char* s[ARGc] = { NULL };shift_commend(s, comment, strlen(comment));//分割输入的命令行字符串,将每一个选项的地址存入指针数组sif (strcmp(s[0], "cd") == 0)//cd指令直接使用chdir函数改变父进程所在路径{if(s[1]!=NULL)chdir(s[1]);continue;}if (strcmp(s[0], "export") == 0)//导入用户自定义的环境变量{if (s[1] != NULL){strcpy(*user_env, s[1]);//将用户输入的环境变量复制到自定义存储空间里putenv(user_env[env_num++]);}continue;}if (strcmp(s[0], "ls") == 0)//使用户输入的ls指令带有颜色分类{int n = 0;while (*(s + n)){++n;}*(s + n) = (char*)"--color=auto";}pid_t id = fork();if (id == 0){execvp(*s, s);exit(1);}int status = 0;waitpid(id, &status, 0);}}

运行效果:

但是这还不够,我们最终是用子进程调用env来打印环境变量的,但是不排除子进程因为要进行某些操作会修改环境变量,所以最保险的方法还是自己实现一个打印父进程的环境变量的函数:

void show_env(){extern char** environ;//使用environ前先声明int i = 0;for (i; environ[i]; ++i){printf("environ[%d]:%s\n", i, environ[i]);}}

检测到env指令时调用一下该函数即可:

#include #include#include#include#include#include#define MAX 1024#define ARGc 512void shift_commend(char** s, char* c, int n){assert(s);assert(c);int num = 0;*s = c;++s;while (num < n){if (*(c + num) == ' ')//将每个空格字符替换为'\0',再将选项的首地址存入s{*(c + num) = '\0';if (*(c + num + 1) != ' ')//防止用户输入多个空字符{*s = c + 1 + num;++s;}}++num;}}void show_env(){extern char** environ;//使用environ前先声明int i = 0;for (i; environ[i]; ++i){printf("environ[%d]:%s\n", i, environ[i]);}}int main(){char user_env[32][256];//存储自定义环境变量int env_num = 0;while (1){printf("[%s@MyShell]#", getenv("USER"));fflush(stdout);char comment[MAX] = { 0 };char* p = fgets(comment, sizeof(comment), stdin);assert(p);//assert函数在release版本下会被编译器删去(void)p;//防止在release版本下assert函数被删除导致p指针从未被使用而报错comment[strlen(comment) - 1] = '\0';//去除fgets函数最后获取的\n字符char* s[ARGc] = { NULL };shift_commend(s, comment, strlen(comment));//分割输入的命令行字符串,将每一个选项的地址存入指针数组sif (strcmp(s[0], "cd") == 0)//cd指令直接使用chdir函数改变父进程所在路径{if(s[1]!=NULL)chdir(s[1]);continue;}if (strcmp(s[0], "export") == 0)//导入用户自定义的环境变量{if (s[1] != NULL){strcpy(*user_env, s[1]);//将用户输入的环境变量复制到自定义存储空间里putenv(user_env[env_num++]);}continue;}if (strcmp(s[0], "env") == 0)//直接打印父进程环境变量{show_env();continue;}if (strcmp(s[0], "ls") == 0)//使用户输入的ls指令带有颜色分类{int n = 0;while (*(s + n)){++n;}*(s + n) = (char*)"--color=auto";}pid_t id = fork();if (id == 0){execvp(*s, s);exit(1);}int status = 0;waitpid(id, &status, 0);}}

运行效果:

从上面的实现过程我们可以得出一个结论:其实我们之前学习到的几乎所有的环境变量命令,都是内建命令!


这就是本期博客的全部内容,如有纰漏还请各位大佬指点~