本期我们利用之前学过的知识,写一个shell命令行程序
目录
一、初始代码
二、使用户输入的ls指令带有颜色分类
三、解决cd指令后用户所在路径不变化问题
3.1 chdir函数
四、关于环境变量的问题
#include
#include
#include
#include
#include
#include
#define MAX 1024
#define ARGc 512
void 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));//分割输入的命令行字符串,将每一个选项的地址存入指针数组s
pid_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"字符串即可:
#include
#include
#include
#include
#include
#include
#define MAX 1024
#define ARGc 512
void 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));//分割输入的命令行字符串,将每一个选项的地址存入指针数组s
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);
}
}
运行效果:
上面代码还有一个问题,在我们使用cd指令后,再使用pwd来查看所在路径时,会发现没有任何变化:
这是因为当用户输入cd指令后,通过子进程调用的execvp函数来执行cd指令,改变的是子进程的目录路径和父进程压根没有关系!
所以当类似cd这样要改变父进程环境的指令(内建指令),就要父进程自己来执行,我们需要特殊判断做特殊处理:
chdir函数(包含在头文件unistd.h中)可以改变进程所在路径:
我们可以向path传入想要进入的路径,这样子chdir函数就会自动帮我们改变当前进程的所处路径:
#include
#include
#include
#include
#include
#include
#define MAX 1024
#define ARGc 512
void 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));//分割输入的命令行字符串,将每一个选项的地址存入指针数组s
if (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 512
void 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));//分割输入的命令行字符串,将每一个选项的地址存入指针数组s
if (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 512
void 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));//分割输入的命令行字符串,将每一个选项的地址存入指针数组s
if (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 512
void 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));//分割输入的命令行字符串,将每一个选项的地址存入指针数组s
if (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);
}
}
运行效果:
从上面的实现过程我们可以得出一个结论:其实我们之前学习到的几乎所有的环境变量命令,都是内建命令!
这就是本期博客的全部内容,如有纰漏还请各位大佬指点~