目录
shell运行原理:通过让子进程执行命令,父进程等待,解析命令,即可完成对应的命令行解释器。
(子进程如果执行命令出错了,也不会影响父进程)
0.基本思路
命令行解释器一定是一个常驻内存的进程(不退出的程序)!
所以我们上来就执行while(1),也就是创建死循环
1.打印出提示信息
这里我们减少学习成本,直接粘贴和打印出来(其实这个打印出来的信息都是可以调用指定的接口获取到的)
当然我们也可以做一点小小的改动,就比方说下面这样
[root@localhost myshell]#
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//系统相关的头文件一般放在最后
#include <sys/wait.h>
int main()
{
while(1)
{
//1.打印出提示信息
//这里我们减少学习成本,直接粘贴和打印出来
printf("[root@localhost myshell]#");
//将提示符信息立马刷新出来,不然其信息一直都会呆在缓冲区内,不被打印出来
fflush(stdout);
sleep(10);
}
}
2.获取用户的输入
输入的是各种指令和选项:比方说"ls -a -l -i"
使用fgets,从特定的文件流中获取文件,大小是size,获取成功就是返回缓冲区的起始地址,获取失败就是NULL
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//系统相关的头文件一般放在最后
#include <sys/wait.h>
//定义一个大小为1024个字节的缓冲区
#define NUM 1024
char cmd_line[NUM];
int main()
{
while(1)
{
//1.打印出提示信息
//这里我们减少学习成本,直接粘贴和打印出来
printf("[root@localhost myshell]#");
//将提示符信息立马刷新出来,不然其信息一直都会呆在缓冲区内,不被打印出来
fflush(stdout);
//sizeof可以不带圆括号,直接求大小
//初始化我们的缓冲区,将我们的缓冲区也就是cmd_line中的全部内容(也就是大小为sizeof(cmd_line)的空间)全部初始化为'\0'
memset(cmd_line,'\0',sizeof cmd_line);
//2.获取用户的键盘输入
//第一个参数是将我们读取到的字符放入哦我们上面定义的缓冲区中,第二个参数是定义放入的字符的大小,第三个参数是指从输入流中读取
if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)
{
//如果读取到的是空,也就是出错了,就直接进行下一次循环,重新初始化
continue;
}
//将我们输入的内容打印出来
//但是我们输入的内容实际上最后带有一个\n
//ls -a -l -i\n
//我们需要将这个最后的\n给去除掉
//strlen求字符串长度的时候不包括后面的\0
//由于我们的下标是从0开始的,所以我们需要将cmd_line的长度-1的位置,也就是\n的位置置为\0
cmd_line[strlen(cmd_line)-1]='\0';
printf("echo:%s\n",cmd_line);
}
}
对于我们上面strlen(cmd_line)-1='\0'的小小的解释
#include <stdio.h>
#include <string>
int main()
{
char a[]="12345\n";
printf("%d\n",strlen(a));
a[sizeof a-1]='\0';
printf("%s\n",a);
}
3.将字符串拆分
"ls -a -l -i"转换成"ls","-a","-l","-i"的子字符串,我们需要对命令行字符串做解析工作
我们将字符串中的空格部分置为\0并且将每个子字符串的起始地址用指针指向,就得到了我们的打散的子字符串
把一个字符串打散成为多个子串,在c语言中有这样的接口吗?
第一个参数是你要打散的字符串,第二个参数是你要设置的分隔符,而且只会返回有效的子串
char *strtok(char *str, const char *delim);
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//系统相关的头文件一般放在最后
#include <sys/wait.h>
//定义一个大小为1024个字节的缓冲区
#define NUM 1024
//第一一个用于打散之后的字符串的个数的大小
#define SIZE 32
//宏定义分隔符
//由于我们下面的分隔符系统中在定义的时候是一个char*的,所以我们必须传入一个字符串,所以我们下面用的SEP必须要是双引号包裹起来的空格
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串,也就是将每一个打散之后的子串的起始地址保存在这个g_argv中
char *g_argv[SIZE];
int main()
{
while(1)
{
//1.打印出提示信息
//这里我们减少学习成本,直接粘贴和打印出来
printf("[root@localhost myshell]#");
//将提示符信息立马刷新出来,不然其信息一直都会呆在缓冲区内,不被打印出来
fflush(stdout);
//sizeof可以不带圆括号,直接求大小
//初始化我们的缓冲区,将我们的缓冲区也就是cmd_line中的全部内容(也就是大小为sizeof(cmd_line)的空间)全部初始化为'\0'
memset(cmd_line,'\0',sizeof cmd_line);
//2.获取用户的键盘输入
//第一个参数是将我们读取到的字符放入哦我们上面定义的缓冲区中,第二个参数是定义放入的字符的大小,第三个参数是指从输入流中读取
if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)
{
//如果读取到的是空,也就是出错了,就直接进行下一次循环,重新初始化
continue;
}
//将我们输入的内容打印出来
//但是我们输入的内容实际上最后带有一个\n
//ls -a -l -i\n
//我们需要将这个最后的\n给去除掉
//strlen求字符串长度的时候不包括后面的\0
//由于我们的下标是从0开始的,所以我们需要将cmd_line的长度-1的位置,也就是\n的位置置为\0
cmd_line[strlen(cmd_line)-1]='\0';
// printf("echo:%s\n",cmd_line);
//3.打散字符串
//我们将字符串中的空格部分置为\0并且将每个子字符串的起始地址用指针指向,就得到了我们的打散的子字符串
//第一个参数你要解析的子串,第二个参数是分隔符
g_argv[0]=strtok(cmd_line,SEP);//第一次调用,要传入原始字符串
int index=1;
//进行循环读取
// while(g_argv[index-1])
// {
// g_argv[index]= strtok(NULL,SEP);//第二次如果还要解析原始字符串,传入NULL,也就是第一个参数,第二个参数分隔符还是SEP
// index++;
// }
//先解析,再赋值,最后while解析g_argv,解析到最后,全部都解析完成的时候g_argv的值为null,while循环条件不满足,循环退出
while(g_argv[index++]= strtok(NULL,SEP));
//我们测试一下打散的操作成不成功
for(index=0;g_argv[index];index++)
{
printf("g_argv[%d]:%s\n",index,g_argv[index]);
}
}
}
5.创建子进程
第四步放到稍后再写。我们的第五步先进行创建子进程
子进程使用程序替换的接口excel来调用对应的程序
我们这里选择execvp因为这样我们就直接可以把我们的参数传进去,也就是我们打散之后的数据g_argv[],并且execvp还会自动从系统的路径中查找对应的程序
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//系统相关的头文件一般放在最后
#include <sys/wait.h>
#include <sys/types.h>
//定义一个大小为1024个字节的缓冲区
#define NUM 1024
//第一一个用于打散之后的字符串的个数的大小
#define SIZE 32
//宏定义分隔符
//由于我们下面的分隔符系统中在定义的时候是一个char*的,所以我们必须传入一个字符串,所以我们下面用的SEP必须要是双引号包裹起来的空格
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串,也就是将每一个打散之后的子串的起始地址保存在这个g_argv中
char *g_argv[SIZE];
int main()
{
while(1)
{
//1.打印出提示信息
//这里我们减少学习成本,直接粘贴和打印出来
printf("[root@localhost myshell]#");
//将提示符信息立马刷新出来,不然其信息一直都会呆在缓冲区内,不被打印出来
fflush(stdout);
//sizeof可以不带圆括号,直接求大小
//初始化我们的缓冲区,将我们的缓冲区也就是cmd_line中的全部内容(也就是大小为sizeof(cmd_line)的空间)全部初始化为'\0'
memset(cmd_line,'\0',sizeof cmd_line);
//2.获取用户的键盘输入
//第一个参数是将我们读取到的字符放入哦我们上面定义的缓冲区中,第二个参数是定义放入的字符的大小,第三个参数是指从输入流中读取
if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)
{
//如果读取到的是空,也就是出错了,就直接进行下一次循环,重新初始化
continue;
}
//将我们输入的内容打印出来
//但是我们输入的内容实际上最后带有一个\n
//ls -a -l -i\n
//我们需要将这个最后的\n给去除掉
//strlen求字符串长度的时候不包括后面的\0
//由于我们的下标是从0开始的,所以我们需要将cmd_line的长度-1的位置,也就是\n的位置置为\0
cmd_line[strlen(cmd_line)-1]='\0';
// printf("echo:%s\n",cmd_line);
//3.打散字符串
//我们将字符串中的空格部分置为\0并且将每个子字符串的起始地址用指针指向,就得到了我们的打散的子字符串
//第一个参数你要解析的子串,第二个参数是分隔符
g_argv[0]=strtok(cmd_line,SEP);//第一次调用,要传入原始字符串
int index=1;
//进行循环读取
// while(g_argv[index-1])
// {
// g_argv[index]= strtok(NULL,SEP);//第二次如果还要解析原始字符串,传入NULL,也就是第一个参数,第二个参数分隔符还是SEP
// index++;
// }
//先解析,再赋值,最后while解析g_argv,解析到最后,全部都解析完成的时候g_argv的值为null,while循环条件不满足,循环退出
while(g_argv[index++]= strtok(NULL,SEP));
//我们测试一下打散的操作成不成功
// for(index=0;g_argv[index];index++)
// {
// printf("g_argv[%d]:%s\n",index,g_argv[index]);
// }
//4.TODO
//5.fork()
pid_t id=fork();
if(id==0)//child
{
printf("下面功能让子进程执行的\n");
//ls -a -l -i,第一个参数g_argv[0]保存的就是我们的命令,后面的全部都是我们的参数
execvp(g_argv[0],g_argv);
exit(1);
}
//father
int status=0;
//阻塞式等待
pid_t ret= waitpid(id,& status,0);
if(ret>0)
{
printf("exit code:%d\n", WEXITSTATUS(status));
}
}
}
下面我们如果想要在我们的shell中删除内容的话,需要ctrl+删除 (mac的话是对应的control+delete)
我们再进行进一步的测试
nano mytest.c
写入下面的代码
#include <stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
直接ctrl+x退出
我们看到我们自己写的shell也可以运行我们在shell中编写的程序了
4.TODO
内置命令
我们观察到如果是使用系统默认的shell的话,我们在路径发生改变的时候,前面的提示信息也是会发生改变的
但是如果我们是用自己编写的shell执行上面的操作的话,路径是不会发生变化的。
按道理来讲,我们的路径应该回退。
但是因为这是我们自己的shell,无论是什么命令,都会去执行execl,所以无论是什么命令,都仅仅是去影响我们的子进程,并不会影响我们的父进程。但是我们想让我们的 shell的路径发生变化。
所以我们这里需要做一个工作,就是判断传入的命令,比方说cd这样的命令,不能创建子进程,而是交给父进程去执行。
内置命令:让父进程shell自己执行的命令,我们叫做内置命令,内建命令
内建命令本质上就是shell内部中的一个函数
这里我们调用chdir命令来改变我们的工作区
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//系统相关的头文件一般放在最后
#include <sys/wait.h>
#include <sys/types.h>
//定义一个大小为1024个字节的缓冲区
#define NUM 1024
//第一一个用于打散之后的字符串的个数的大小
#define SIZE 32
//宏定义分隔符
//由于我们下面的分隔符系统中在定义的时候是一个char*的,所以我们必须传入一个字符串,所以我们下面用的SEP必须要是双引号包裹起来的空格
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串,也就是将每一个打散之后的子串的起始地址保存在这个g_argv中
char *g_argv[SIZE];
int main()
{
while(1)
{
//1.打印出提示信息
//这里我们减少学习成本,直接粘贴和打印出来
printf("[root@localhost myshell]#");
//将提示符信息立马刷新出来,不然其信息一直都会呆在缓冲区内,不被打印出来
fflush(stdout);
//sizeof可以不带圆括号,直接求大小
//初始化我们的缓冲区,将我们的缓冲区也就是cmd_line中的全部内容(也就是大小为sizeof(cmd_line)的空间)全部初始化为'\0'
memset(cmd_line,'\0',sizeof cmd_line);
//2.获取用户的键盘输入
//第一个参数是将我们读取到的字符放入哦我们上面定义的缓冲区中,第二个参数是定义放入的字符的大小,第三个参数是指从输入流中读取
if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)
{
//如果读取到的是空,也就是出错了,就直接进行下一次循环,重新初始化
continue;
}
//将我们输入的内容打印出来
//但是我们输入的内容实际上最后带有一个\n
//ls -a -l -i\n
//我们需要将这个最后的\n给去除掉
//strlen求字符串长度的时候不包括后面的\0
//由于我们的下标是从0开始的,所以我们需要将cmd_line的长度-1的位置,也就是\n的位置置为\0
cmd_line[strlen(cmd_line)-1]='\0';
// printf("echo:%s\n",cmd_line);
//3.打散字符串
//我们将字符串中的空格部分置为\0并且将每个子字符串的起始地址用指针指向,就得到了我们的打散的子字符串
//第一个参数你要解析的子串,第二个参数是分隔符
g_argv[0]=strtok(cmd_line,SEP);//第一次调用,要传入原始字符串
int index=1;
//进行循环读取
// while(g_argv[index-1])
// {
// g_argv[index]= strtok(NULL,SEP);//第二次如果还要解析原始字符串,传入NULL,也就是第一个参数,第二个参数分隔符还是SEP
// index++;
// }
//先解析,再赋值,最后while解析g_argv,解析到最后,全部都解析完成的时候g_argv的值为null,while循环条件不满足,循环退出
while(g_argv[index++]= strtok(NULL,SEP));
//我们测试一下打散的操作成不成功
// for(index=0;g_argv[index];index++)
// {
// printf("g_argv[%d]:%s\n",index,g_argv[index]);
// }
//4.TODO内置命令
//内置命令:让父进程shell自己执行的命令,我们叫做内置命令,内建命令
//内建命令本质上就是shell内部中的一个函数
if(strcmp(g_argv[0],"cd")==0)//不让我们的子进程去执行cd命令,而是交给我们的父进程去完成
{
if(g_argv[1]!=NULL)
{
//将要切换的目标路径传进来
chdir(g_argv[1]);//cd ..
}
//进入下一次循环
continue;
}
//5.fork()
pid_t id=fork();
if(id==0)//child
{
printf("下面功能让子进程执行的\n");
//ls -a -l -i,第一个参数g_argv[0]保存的就是我们的命令,后面的全部都是我们的参数
execvp(g_argv[0],g_argv);
exit(1);
}
//father
int status=0;
//阻塞式等待
pid_t ret= waitpid(id,& status,0);
if(ret>0)
{
printf("exit code:%d\n", WEXITSTATUS(status));
}
}
}
我们观察到现在我们的shell的路径就会发生变化了 ,现在我们就实现了一个跨路径可执行程序的shell
shell执行的命令通常有两种
1.第三方提供的对应的在磁盘中有具体的二进制文件的可执行程序(这里的第三方就是你或者系统本身或者除了你和系统自带的第三方) (子进程)
2.shell内部自己实现的方法,由自己(父进程)来进行执行(因为有些命令就是要影响系统本身的,比方说cd,export)
(shell代表的其实就是用户,用户说想要切换到家目录下,就是想让shell切换到家目录,并不是让子进程切换到家目录下)
(export要键全局变量导给父进程,也就是shell,这样环境变量才能够被所有的子进程继承,否则如果仅仅是创建了呀一个子进程去改变了环境变量那么影响的就仅仅是那个执行的子进程了)
6.给我们的命令添加颜色
我们看到系统自己的ls是自带选项--color=auto的
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//系统相关的头文件一般放在最后
#include <sys/wait.h>
#include <sys/types.h>
//定义一个大小为1024个字节的缓冲区
#define NUM 1024
//第一一个用于打散之后的字符串的个数的大小
#define SIZE 32
//宏定义分隔符
//由于我们下面的分隔符系统中在定义的时候是一个char*的,所以我们必须传入一个字符串,所以我们下面用的SEP必须要是双引号包裹起来的空格
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串,也就是将每一个打散之后的子串的起始地址保存在这个g_argv中
char *g_argv[SIZE];
int main()
{
while(1)
{
//1.打印出提示信息
//这里我们减少学习成本,直接粘贴和打印出来
printf("[root@localhost myshell]#");
//将提示符信息立马刷新出来,不然其信息一直都会呆在缓冲区内,不被打印出来
fflush(stdout);
//sizeof可以不带圆括号,直接求大小
//初始化我们的缓冲区,将我们的缓冲区也就是cmd_line中的全部内容(也就是大小为sizeof(cmd_line)的空间)全部初始化为'\0'
memset(cmd_line,'\0',sizeof cmd_line);
//2.获取用户的键盘输入
//第一个参数是将我们读取到的字符放入哦我们上面定义的缓冲区中,第二个参数是定义放入的字符的大小,第三个参数是指从输入流中读取
if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)
{
//如果读取到的是空,也就是出错了,就直接进行下一次循环,重新初始化
continue;
}
//将我们输入的内容打印出来
//但是我们输入的内容实际上最后带有一个\n
//ls -a -l -i\n
//我们需要将这个最后的\n给去除掉
//strlen求字符串长度的时候不包括后面的\0
//由于我们的下标是从0开始的,所以我们需要将cmd_line的长度-1的位置,也就是\n的位置置为\0
cmd_line[strlen(cmd_line)-1]='\0';
// printf("echo:%s\n",cmd_line);
//3.打散字符串
//我们将字符串中的空格部分置为\0并且将每个子字符串的起始地址用指针指向,就得到了我们的打散的子字符串
//第一个参数你要解析的子串,第二个参数是分隔符
g_argv[0]=strtok(cmd_line,SEP);//第一次调用,要传入原始字符串
int index=1;
//6.给我们的shell设置颜色
if(strcmp(g_argv[0],"ls")==0)
{
g_argv[index++]=(char*)"--color=auto";
}
//进行循环读取
// while(g_argv[index-1])
// {
// g_argv[index]= strtok(NULL,SEP);//第二次如果还要解析原始字符串,传入NULL,也就是第一个参数,第二个参数分隔符还是SEP
// index++;
// }
//先解析,再赋值,最后while解析g_argv,解析到最后,全部都解析完成的时候g_argv的值为null,while循环条件不满足,循环退出
while(g_argv[index++]= strtok(NULL,SEP));
//我们测试一下打散的操作成不成功
// for(index=0;g_argv[index];index++)
// {
// printf("g_argv[%d]:%s\n",index,g_argv[index]);
// }
//4.TODO内置命令
//内置命令:让父进程shell自己执行的命令,我们叫做内置命令,内建命令
//内建命令本质上就是shell内部中的一个函数
if(strcmp(g_argv[0],"cd")==0)//不让我们的子进程去执行cd命令,而是交给我们的父进程去完成
{
if(g_argv[1]!=NULL)
{
//将要切换的目标路径传进来
chdir(g_argv[1]);//cd ..
}
//进入下一次循环
continue;
}
//5.fork()
pid_t id=fork();
if(id==0)//child
{
printf("下面功能让子进程执行的\n");
//ls -a -l -i,第一个参数g_argv[0]保存的就是我们的命令,后面的全部都是我们的参数
execvp(g_argv[0],g_argv);
exit(1);
}
//father
int status=0;
//阻塞式等待
pid_t ret= waitpid(id,& status,0);
if(ret>0)
{
printf("exit code:%d\n", WEXITSTATUS(status));
}
}
}
现在我们自己写的shell就是带颜色的。
7.支持别名
ll本质上就是ls的别名,但是我们的现在自己的shell是不支持ll的,也就是不支持别名
这里我们仅仅是让我们的shell支持ls的别名ll
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//系统相关的头文件一般放在最后
#include <sys/wait.h>
#include <sys/types.h>
//定义一个大小为1024个字节的缓冲区
#define NUM 1024
//第一一个用于打散之后的字符串的个数的大小
#define SIZE 32
//宏定义分隔符
//由于我们下面的分隔符系统中在定义的时候是一个char*的,所以我们必须传入一个字符串,所以我们下面用的SEP必须要是双引号包裹起来的空格
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串,也就是将每一个打散之后的子串的起始地址保存在这个g_argv中
char *g_argv[SIZE];
int main()
{
while(1)
{
//1.打印出提示信息
//这里我们减少学习成本,直接粘贴和打印出来
printf("[root@localhost myshell]#");
//将提示符信息立马刷新出来,不然其信息一直都会呆在缓冲区内,不被打印出来
fflush(stdout);
//sizeof可以不带圆括号,直接求大小
//初始化我们的缓冲区,将我们的缓冲区也就是cmd_line中的全部内容(也就是大小为sizeof(cmd_line)的空间)全部初始化为'\0'
memset(cmd_line,'\0',sizeof cmd_line);
//2.获取用户的键盘输入
//第一个参数是将我们读取到的字符放入哦我们上面定义的缓冲区中,第二个参数是定义放入的字符的大小,第三个参数是指从输入流中读取
if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)
{
//如果读取到的是空,也就是出错了,就直接进行下一次循环,重新初始化
continue;
}
//将我们输入的内容打印出来
//但是我们输入的内容实际上最后带有一个\n
//ls -a -l -i\n
//我们需要将这个最后的\n给去除掉
//strlen求字符串长度的时候不包括后面的\0
//由于我们的下标是从0开始的,所以我们需要将cmd_line的长度-1的位置,也就是\n的位置置为\0
cmd_line[strlen(cmd_line)-1]='\0';
// printf("echo:%s\n",cmd_line);
//3.打散字符串
//我们将字符串中的空格部分置为\0并且将每个子字符串的起始地址用指针指向,就得到了我们的打散的子字符串
//第一个参数你要解析的子串,第二个参数是分隔符
g_argv[0]=strtok(cmd_line,SEP);//第一次调用,要传入原始字符串
int index=1;
//6.给我们的shell设置颜色
if(strcmp(g_argv[0],"ls")==0)
{
g_argv[index++]=(char*)"--color=auto";
}
//7.让我们的shell支持别名
//这里我们仅仅是支持ll,也就是ls的别名
if(strcmp(g_argv[0],"ll")==0)
{
g_argv[0]=(char*)"ls";
g_argv[index++]=(char*)"-l";
g_argv[index++]=(char*)"--color=auto";
}
//进行循环读取
// while(g_argv[index-1])
// {
// g_argv[index]= strtok(NULL,SEP);//第二次如果还要解析原始字符串,传入NULL,也就是第一个参数,第二个参数分隔符还是SEP
// index++;
// }
//先解析,再赋值,最后while解析g_argv,解析到最后,全部都解析完成的时候g_argv的值为null,while循环条件不满足,循环退出
while(g_argv[index++]= strtok(NULL,SEP));
//我们测试一下打散的操作成不成功
// for(index=0;g_argv[index];index++)
// {
// printf("g_argv[%d]:%s\n",index,g_argv[index]);
// }
//4.TODO内置命令
//内置命令:让父进程shell自己执行的命令,我们叫做内置命令,内建命令
//内建命令本质上就是shell内部中的一个函数
if(strcmp(g_argv[0],"cd")==0)//不让我们的子进程去执行cd命令,而是交给我们的父进程去完成
{
if(g_argv[1]!=NULL)
{
//将要切换的目标路径传进来
chdir(g_argv[1]);//cd ..
}
//进入下一次循环
continue;
}
//5.fork()
pid_t id=fork();
if(id==0)//child
{
printf("下面功能让子进程执行的\n");
//ls -a -l -i,第一个参数g_argv[0]保存的就是我们的命令,后面的全部都是我们的参数
execvp(g_argv[0],g_argv);
exit(1);
}
//father
int status=0;
//阻塞式等待
pid_t ret= waitpid(id,& status,0);
if(ret>0)
{
printf("exit code:%d\n", WEXITSTATUS(status));
}
}
}
OK,现在已经成功执行了
8.环境变量
为了让我们的shell支持环境变量,也就是修改父进程的环境变量,然后子进程继承父进程的时候,这些环境变量也会作用于子进程
这里我们需要用的putenv,也就是添加一个环境变量,将对应的环境变量传给我们的函数就可以了。
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//系统相关的头文件一般放在最后
#include <sys/wait.h>
#include <sys/types.h>
//定义一个大小为1024个字节的缓冲区
#define NUM 1024
//第一一个用于打散之后的字符串的个数的大小
#define SIZE 32
//宏定义分隔符
//由于我们下面的分隔符系统中在定义的时候是一个char*的,所以我们必须传入一个字符串,所以我们下面用的SEP必须要是双引号包裹起来的空格
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串,也就是将每一个打散之后的子串的起始地址保存在这个g_argv中
char *g_argv[SIZE];
//写一个环境变量的buffer,用来测试
//cmd_line每一次都会被清空,我们的环境变量为了防止被清空我们这里定义一个buffer用来测试
char g_myval[64];
int main()
{
//8.创建一个全局变量指针
extern char**environ;
//0.命令行解释器:通过让子进程执行命令,父进程等待&&解析命令
while(1)
{
//1.打印出提示信息
//这里我们减少学习成本,直接粘贴和打印出来
printf("[root@localhost myshell]#");
//将提示符信息立马刷新出来,不然其信息一直都会呆在缓冲区内,不被打印出来
fflush(stdout);
//sizeof可以不带圆括号,直接求大小
//初始化我们的缓冲区,将我们的缓冲区也就是cmd_line中的全部内容(也就是大小为sizeof(cmd_line)的空间)全部初始化为'\0'
memset(cmd_line,'\0',sizeof cmd_line);
//2.获取用户的键盘输入
//第一个参数是将我们读取到的字符放入哦我们上面定义的缓冲区中,第二个参数是定义放入的字符的大小,第三个参数是指从输入流中读取
if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)
{
//如果读取到的是空,也就是出错了,就直接进行下一次循环,重新初始化
continue;
}
//将我们输入的内容打印出来
//但是我们输入的内容实际上最后带有一个\n
//ls -a -l -i\n
//我们需要将这个最后的\n给去除掉
//strlen求字符串长度的时候不包括后面的\0
//由于我们的下标是从0开始的,所以我们需要将cmd_line的长度-1的位置,也就是\n的位置置为\0
cmd_line[strlen(cmd_line)-1]='\0';
// printf("echo:%s\n",cmd_line);
//3.打散字符串
//我们将字符串中的空格部分置为\0并且将每个子字符串的起始地址用指针指向,就得到了我们的打散的子字符串
//第一个参数你要解析的子串,第二个参数是分隔符
//export myval=105 左侧的是命令,右侧的是环境变量
g_argv[0]=strtok(cmd_line,SEP);//第一次调用,要传入原始字符串
int index=1;
//6.给我们的shell设置颜色
if(strcmp(g_argv[0],"ls")==0)
{
g_argv[index++]=(char*)"--color=auto";
}
//7.让我们的shell支持别名
//这里我们仅仅是支持ll,也就是ls的别名
if(strcmp(g_argv[0],"ll")==0)
{
g_argv[0]=(char*)"ls";
g_argv[index++]=(char*)"-l";
g_argv[index++]=(char*)"--color=auto";
}
//进行循环读取
// while(g_argv[index-1])
// {
// g_argv[index]= strtok(NULL,SEP);//第二次如果还要解析原始字符串,传入NULL,也就是第一个参数,第二个参数分隔符还是SEP
// index++;
// }
//我们测试一下打散的操作成不成功
// for(index=0;g_argv[index];index++)
// {
// printf("g_argv[%d]:%s\n",index,g_argv[index]);
// }
//先解析,再赋值,最后while解析g_argv,解析到最后,全部都解析完成的时候g_argv的值为null,while循环条件不满足,循环退出
while(g_argv[index++]= strtok(NULL,SEP));
//上面的代码将我们全部的命令都解析完,然后我们下面再进行分析,不然我们下面的argv[1]就可能并没有被解析,然后就并不会进入下面添加环境变量的操作
//8.让我们的shell支持修改环境变量
//并且我们的环境变量不空我们才能将我们的环境变量导入
if(strcmp(g_argv[0],"export")==0&&g_argv[1]!=NULL)
{
//防止下一次导入的时候,将环境变量覆盖了
strcpy(g_myval,g_argv[1]);
//将新的环境变量导入到我们的shell当中
//然后它的环境变量在地址空间中是在它的栈的上面的命令行地址空间中,就被添加进去了
int ret=putenv(g_myval);
//看看环境变量有没有成功导入
if(ret==0)
{
printf("%s export success\n",g_argv[1]);
}
//当前我们的环境变量已经添加到系统中了
//将获取到的环境变量打印出来
// for(int i=0;environ[i];i++)
// {
// printf("%d:%s\n",i,environ[i]);
// }
continue;
}
//4.TODO内置命令
//内置命令:让父进程shell自己执行的命令,我们叫做内置命令,内建命令
//内建命令本质上就是shell内部中的一个函数
if(strcmp(g_argv[0],"cd")==0)//不让我们的子进程去执行cd命令,而是交给我们的父进程去完成
{
if(g_argv[1]!=NULL)
{
//将要切换的目标路径传进来
chdir(g_argv[1]);//cd ..
}
//进入下一次循环
continue;
}
//5.fork()
pid_t id=fork();
if(id==0)//child
{
printf("下面功能让子进程执行的\n");
printf("child,MYVAL:%s\n",getenv("MYVAL"));
printf("child,PATH:%s\n",getenv("PATH"));
//ls -a -l -i,第一个参数g_argv[0]保存的就是我们的命令,后面的全部都是我们的参数
execvp(g_argv[0],g_argv);
// execvpe(g_argv[0],g_argv,environ);
exit(1);
}
//father
int status=0;
//阻塞式等待
pid_t ret= waitpid(id,& status,0);
if(ret>0)
{
printf("exit code:%d\n", WEXITSTATUS(status));
}
}
}
这里在我们自己写的shell中删除时(contrl+delete)mac,windows(ctrl+backspace)
然后编写我们的测试程序
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("hello world\n");
printf("MYVAL=%s\n", getenv("MYVAL"));
return 0;
}
我们先用gcc mytesc.c将我们的test程序编译好,生成一个a.out文件,方便我们下面的测试
下面的代码中,我们先启动了我们自己编写的shell,然后设置了MYVAL=100,也就是在我们的父进程中设置了环境变量,然后我们执行了./a.out也就是我们的测试程序,我们就在下面的红框的部分看到了我们的子进程获得到了我们的MYVAL是100。
通过我们上面的测试,我们知道了我们在命令行上导入环境变量的本质就是我将你的命令拿出来,调用putenv将环境变量导入父进程,也就是当前shell的上下文当中,(那个空间是在栈区的上面,我们无法直接进行修改,一定要通过特定的接口进行维护)
然后我们的所有的子进程都会继承父进程的环境变量,所以我们的环境变量具有全局性。
不是说好的程序替换会替换代码和数据吗?那么我们刚刚的全局的g_myval[64]为什么没有被替换(也就是我们自己定义的一个数据缓冲区)?环境变量相关的数据会被替换吗?
不会!子进程进行程序替换的时候,并不是将所有的程序都替换掉,仅仅是将与子进程有关的代码和数据替换掉,但是与系统有关的环境变量的数据不会被替换,这就体现了环境变量的全局属性!
shell的环境变量又是从哪里来的呢?
我们所看到的环境变量,其实在我们的系统对应的配置文件中都是有的。
环境变量,是写在配置文件中的,当shell启动的时候后,是通过读取配置文件,获得的起始环境变量。
9.总结
以下就是最终代码
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//系统相关的头文件一般放在最后
#include <sys/wait.h>
#include <sys/types.h>
//定义一个大小为1024个字节的缓冲区
#define NUM 1024
//第一一个用于打散之后的字符串的个数的大小
#define SIZE 32
//宏定义分隔符
//由于我们下面的分隔符系统中在定义的时候是一个char*的,所以我们必须传入一个字符串,所以我们下面用的SEP必须要是双引号包裹起来的空格
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串,也就是将每一个打散之后的子串的起始地址保存在这个g_argv中
char *g_argv[SIZE];
//写一个环境变量的buffer,用来测试
//cmd_line每一次都会被清空,我们的环境变量为了防止被清空我们这里定义一个buffer用来测试
char g_myval[64];
int main()
{
//8.创建一个全局变量指针
extern char**environ;
//0.命令行解释器:通过让子进程执行命令,父进程等待&&解析命令
while(1)
{
//1.打印出提示信息
//这里我们减少学习成本,直接粘贴和打印出来
printf("[root@localhost myshell]#");
//将提示符信息立马刷新出来,不然其信息一直都会呆在缓冲区内,不被打印出来
fflush(stdout);
//sizeof可以不带圆括号,直接求大小
//初始化我们的缓冲区,将我们的缓冲区也就是cmd_line中的全部内容(也就是大小为sizeof(cmd_line)的空间)全部初始化为'\0'
memset(cmd_line,'\0',sizeof cmd_line);
//2.获取用户的键盘输入
//第一个参数是将我们读取到的字符放入哦我们上面定义的缓冲区中,第二个参数是定义放入的字符的大小,第三个参数是指从输入流中读取
if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)
{
//如果读取到的是空,也就是出错了,就直接进行下一次循环,重新初始化
continue;
}
//将我们输入的内容打印出来
//但是我们输入的内容实际上最后带有一个\n
//ls -a -l -i\n
//我们需要将这个最后的\n给去除掉
//strlen求字符串长度的时候不包括后面的\0
//由于我们的下标是从0开始的,所以我们需要将cmd_line的长度-1的位置,也就是\n的位置置为\0
cmd_line[strlen(cmd_line)-1]='\0';
// printf("echo:%s\n",cmd_line);
//3.打散字符串
//我们将字符串中的空格部分置为\0并且将每个子字符串的起始地址用指针指向,就得到了我们的打散的子字符串
//第一个参数你要解析的子串,第二个参数是分隔符
//export myval=105 左侧的是命令,右侧的是环境变量
g_argv[0]=strtok(cmd_line,SEP);//第一次调用,要传入原始字符串
int index=1;
//6.给我们的shell设置颜色
if(strcmp(g_argv[0],"ls")==0)
{
g_argv[index++]=(char*)"--color=auto";
}
//7.让我们的shell支持别名
//这里我们仅仅是支持ll,也就是ls的别名
if(strcmp(g_argv[0],"ll")==0)
{
g_argv[0]=(char*)"ls";
g_argv[index++]=(char*)"-l";
g_argv[index++]=(char*)"--color=auto";
}
//进行循环读取
// while(g_argv[index-1])
// {
// g_argv[index]= strtok(NULL,SEP);//第二次如果还要解析原始字符串,传入NULL,也就是第一个参数,第二个参数分隔符还是SEP
// index++;
// }
//我们测试一下打散的操作成不成功
// for(index=0;g_argv[index];index++)
// {
// printf("g_argv[%d]:%s\n",index,g_argv[index]);
// }
//先解析,再赋值,最后while解析g_argv,解析到最后,全部都解析完成的时候g_argv的值为null,while循环条件不满足,循环退出
while(g_argv[index++]= strtok(NULL,SEP));
//上面的代码将我们全部的命令都解析完,然后我们下面再进行分析,不然我们下面的argv[1]就可能并没有被解析,然后就并不会进入下面添加环境变量的操作
//8.让我们的shell支持修改环境变量
//并且我们的环境变量不空我们才能将我们的环境变量导入
if(strcmp(g_argv[0],"export")==0&&g_argv[1]!=NULL)
{
//防止下一次导入的时候,将环境变量覆盖了
strcpy(g_myval,g_argv[1]);
//将新的环境变量导入到我们的shell当中
//然后它的环境变量在地址空间中是在它的栈的上面的命令行地址空间中,就被添加进去了
int ret=putenv(g_myval);
//看看环境变量有没有成功导入
if(ret==0)
{
printf("%s export success\n",g_argv[1]);
}
//当前我们的环境变量已经添加到系统中了
//将获取到的环境变量打印出来
// for(int i=0;environ[i];i++)
// {
// printf("%d:%s\n",i,environ[i]);
// }
continue;
}
//4.TODO内置命令
//内置命令:让父进程shell自己执行的命令,我们叫做内置命令,内建命令
//内建命令本质上就是shell内部中的一个函数
if(strcmp(g_argv[0],"cd")==0)//不让我们的子进程去执行cd命令,而是交给我们的父进程去完成
{
if(g_argv[1]!=NULL)
{
//将要切换的目标路径传进来
chdir(g_argv[1]);//cd ..
}
//进入下一次循环
continue;
}
//5.fork()
pid_t id=fork();
if(id==0)//child
{
printf("下面功能让子进程执行的\n");
printf("child,MYVAL:%s\n",getenv("MYVAL"));
printf("child,PATH:%s\n",getenv("PATH"));
//ls -a -l -i,第一个参数g_argv[0]保存的就是我们的命令,后面的全部都是我们的参数
execvp(g_argv[0],g_argv);
// execvpe(g_argv[0],g_argv,environ);
exit(1);
}
//father
int status=0;
//阻塞式等待
pid_t ret= waitpid(id,& status,0);
if(ret>0)
{
printf("exit code:%d\n", WEXITSTATUS(status));
}
}
}
10.minishell支持重定向操作
查看下面这篇博文中的第七部分
这里的minishell的基本代码和上面都是一样的,只是添加了重定向操作的代码