文章目录
- 1. 了解命令行解释器
- 1.1 命令行解释器是什么?
- 1.2 我们为什么要尝试手写一个命令行解释器?
- 2. 命令行解释器的实现
- 2.1 打印提示符
- 2.2 获取用户输入
- 2.3 创建子进程并进行进程程序替换
- 2.4 内建命令
1. 了解命令行解释器
1.1 命令行解释器是什么?
命令行解释器是一种软件程序,它接收来自用户的命令行输入,并将其解释和执行。它允许用户通过键入特定的命令和参数来与计算机进行交互和控制。
命令行解释器通常提供一个命令行界面,用户可以在该界面中输入命令,并根据命令的要求执行相应的操作。它是与计算机进行交互的一种文本界面,相比于图形用户界面,命令行界面更加灵活和高效。
而这样的软件程序我们常常称其为Shell。
1.2 我们为什么要尝试手写一个命令行解释器?
操作系统的进程控制包括进程创建,进程终止,进程等待,进程程序替换等。单单学习这些概念,我们无法真正地理解它们。一个简易的命令行解释器恰好就是这些概念的应用场景,通过自己手写一个简单的命令行解释器,我们可以更好地理解这些概念。
2. 命令行解释器的实现
2.1 打印提示符
上图是Linux云服务器中的命令行解释器,可以看到,我们每次在键入命令时,前面都有一串字符,标识我们的用户,主机,路径等等。
所以,我们如果要想实现一个命令行解释器,首先得把这一串提示符打印出来。
注意:这里为了和系统自带的提示符更相似,末位是不加’\n’的。但是如果不加的的话,就会造成缓冲区不会刷新的问题。所以printf之后我们应该主动刷新缓冲区。
2.2 获取用户输入
获取用户输出,在这里我们使用的是fgets函数。
第一个参数是用于读取文本的字符数组的指针。
第二个参数是最大读取的字符数(包括换行符和空字符),防止溢出。
第三个参数是要读取的文件流,通常是stdin。
首先我们定义一个宏NUM表示最大输出的字符数,然后定义一个全局数组lineCommand用来存储用户的输入。
这样用户输出的字符串,都保存在了lineCommand中了。
但是,我们要对用户的输入进行解释的话,首先要对字符串进行分割。
比如用户输入"ls -a -l",我们要把它分割成"ls" “-a” “-l”,根据分割成的子串从而对用户的输入进行解释并执行!
我们定义宏和字符指针数组,用于保存分割之后的字符串。
字符串分割,我们用的是strtok函数。
第一个参数是要进行分割的字符串,第二个参数为用于指定分割子字符串的分隔符字符串。
strtok函数会根据第二个参数指定的分隔符,将第一个参数拆分为多个子字符串,并以此返回每个子字符串的指针。
每次调用strtok函数时,它会返回一个指向下一个子字符串的指针,在后续调用中,将NULL作为第一个参数传递给strtok函数,以便继续上一次返回位置的下一个字符开始查找下一个字符串。
2.3 创建子进程并进行进程程序替换
Linux操作系统中,常用的Shell是bash,bash通常执行命令往往创建一个子进程再进程程序替换为指定的命令去执行(也有不是的情况,后面再说),这样做的好处就是一旦出现问题,由于进程的独立性,只有子进程会出问题,而bash进程不会受到任何影响。
我们自己写的命令行解释器也是这种思路。
主进程创建子进程,并使用execvp函数进行进程程序替换,最后父进程回收子进程的资源。为了获取子进程的退出信息,定义两个全局变量lastCode和lastSig。
至此,Shell就写的差不多了。但是我们使用Shell总不能每次就输入一次命令就退出吧,Shell本质是一个死循环,我们也要将我们的代码放入一个死循环中。
下面看看基本的运行效果
有个小细节,当我们在执行系统命令ls的时候是有颜色的,但是在我们自己写的Shell中却没有颜色,下面进行一些修改,让ls在我们自己写的Shell中也有颜色。
再来试一下
这样就有颜色了。
2.4 内建命令
什么是内建命令?
内建命令是指直接内置在操作系统的命令行解释器(如Shell)中的命令,而不是作为外部可执行文件存在。
通俗点说,这个命令不存在于磁盘上,执行它时Shell也不需要再去创建子进程,而是直接在Shell中运行。
常见的内建命令有cd和echo,下面我们尝试实现。
cd命令
现在我们还没有将cd命令设置为内建命令,看看结果是怎样的。
可以发现,在cd之后,用pwd查看当前路径,发现当前路径是没有发生变化的。
Linux下一切皆文件,正在运行的myshell进程本质上也是一个文件。我们查看这个文件内的内容,找到cwd和exe。cwd保存了myshell的工作路径,当在myshell中pwd时查看到的就是这个路径,exe保存了myshell在磁盘上的路径。
每次cd的时候,要创建子进程进行cd,子进程进程cd那么也只会改变子进程的cwd,由于进程具有独立性,它不会影响父进程!
如果我们要改变父进程的工作路径,不能创建子进程!
在父进程中对工作路径进行修改,用的是chdir函数。
传入要改成的路径即可。
echo命令
使用echo命令,有时候我们要打印本地变量的值。如果创建子进程的话,子进程不能继承Shell内定义的本地变量,那么echo也就不能打印本地变量的值了。并且,echo命令相对简单,不涉及复杂的操作,我们也没必要专门去创建一个子进程去执行它。
至此,自制简易Shell已全部完毕。下面给出全部的代码!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#define NUM 1024 // 用户输入的最长长度
#define OPT_NUM 64 // 分割后的字符串的最长长度
char lineCommand[NUM]; // 用于存放用户输入的字符串
char *myargv[OPT_NUM]; // 保存分割之后的字符串
int lastCode = 0;
int lastSig = 0;
int main()
{
while (1)
{
// 输出提示符
printf("[用户名@主机名 当前路径]$ ");
fflush(stdin); // 清空缓冲区
// 获取用户输入
// 第二个参数为什么要减一?要留下一个'\0'的位置!
fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
// 但是这样会造成用户输入之后出现一个空行的问题,原因就是把'\n'也读取到了
// 清除最后的'\n'
lineCommand[strlen(lineCommand) - 1] = '\0';
// 将用户的输入字符串进行分割
myargv[0] = strtok(lineCommand, " "); // 以空格作为分隔符
int i = 1; // i为子字符串的个数
if (myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
{
myargv[i++] = (char*)"--color=auto";
}
while (myargv[i++] = strtok(NULL, " "))
;
// cd内建命令
if (myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
{
if (myargv[1] != NULL) chdir(myargv[1]);
continue;
}
// echo内建命令
if (myargv[0] != NULL && strcmp(myargv[0], "echo") == 0)
{
// 打印上一次运行结果或者具体的文本内容
if (myargv[1] != NULL && strcmp(myargv[1], "$?") == 0)
printf("%d %d\n", lastCode, lastSig);
else
printf("%s\n", myargv[1]);
continue;
}
// 创建子进程并进行进程程序替换
pid_t id = fork(); // 创建子进程
if (id == 0)
{
// 子进程
execvp(myargv[0], myargv);
exit(1);
}
int status = 0;
waitpid(id, &status, 0); // 父进程阻塞式地等待子进程,并获取子进程退出信息
}
return 0;
}