Linux【编写一个简单的shell】
目录
0.基本思路
1.打印出提示信息
2.获取用户的输入
3.将字符串拆分
5.TODO
4.TODO
6.给我们的命令添加颜色
7.支持别名
8.总结
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
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.总结
以下就是最终代码
#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));
}
}
}