目录
在学习之前我们要认识到,Shell外壳中的命令行以及我们输入的指令都是字符串!
首先我们要创建两个文件:MyShell.c 与 makefile ,一个存储我们的 Shell 外壳,一个方便操作
其次,我们看一下 MyShell.c 中需要包含的头文件:
1 #include <stdio.h>
2 #include <unistd.h> //进程创建接口
3 #include <sys/types.h> //进程等待
4 #include <sys/wait.h> //进程等待
一、输出命令行
首先我们先来认识一下命令行,其中, Flash 是当前用户名、@ 后紧跟的是当前主机名、主机名空格后紧跟的是当前路径。所以由此我们知道,如果想打印出我们的命令行,我们至少要知道三个信息:1.用户名 2.主机名 3.当前路径
1.1 了解环境变量
这三个信息如何拿去呢?我们的操作系统中有自带的环境变量,在环境变量中存放着我们需要的三个信息。我们如何查我们的环境变量呢?
有三种方法:
1.命令行参数 -> [env] ,当我们在命令行输入指令,系统就会显示出一大串信息(这里有省略),其中就有我们需要的 [USER] [PWD] [HOSTNAME]
2.ENVIRON 等第三方提供的接口,这里不做详细介绍
3.使用C语言提供的接口函数,如 getenv() ,我们这里使用该方式,下面会详细介绍。
1.2 获取用户名、主机名、当前路径
现在我们就可以来编写我们的函数了。
char* GetUserName()
{
char* User = getenv("USER");
if (!User) return "None";
return User;
}
char* GetHostName()
{
char* Host = getenv("HOSTNAME");
if (!Host) return "None";
return Host;
}
char* GetCwd()
{
char* PWD = getenv("PWD");
if (!PWD) return "None";
return PWD;
}
void MakeCommandLine()
{
char* UserName = GetUserName();
char* HostName = GetHostName();
char* Cwd = GetCwd();
printf("[%s@%s %s]>\n", UserName, HostName, Cwd);
}
int main()
{
MakeCommandLine();
return 0;
}
我们还可以继续改进一下,使用缓冲区的概念
1.3 缓冲区改进MakeCommandLine
首先我们先宏定义一个缓冲区的大小,这里我设置为 256 。
#define SIZE 256
char Line[SIZE];//自定义缓冲区
其次我们再来学习一个函数 snprintf
使用 snprintf 就可以像我们自定义的缓冲区里写啦!
snprintf(Line, sizeof(Line), "[%s@%s %s]>", UserName, HostName, Cwd);
printf("%s\n", Line);
成品:
#define SIZE 256
//只有MakeCommandLine函数变化,只展示该函数
void MakeCommandLine()
{
char Line[SIZE];
char* UserName = GetUserName();
char* HostName = GetHostName();
char* Cwd = GetCwd();
snprintf(Line, sizeof(Line), "[%s@%s %s]>", UserName, HostName, Cwd);
printf("%s\n", Line);
}
但是还有一个细节,当我们想让程序慢一点结束时,使用 sleep 函数,就会发生神奇的一幕:
为什么没有立刻打印呢?原因是 stdout
的缓冲问题。printf
是行缓冲的,通常情况下,当遇到换行符('\n')时,缓冲区的内容会被送往 stdout
进行输出。然而在某些情况下,如果在调用 sleep
之前没有刷新缓冲区,那么输出可能会延迟直到缓冲区被刷新。
我们的解决方法是使用 fflush 函数!这将刷新(即清空并发送)包含 printf
输出的缓冲区,从而确保立即看到输出:
snprintf(Line, sizeof(Line), "[%s@%s %s]>", UserName, HostName, Cwd);
printf("%s\n", Line);
fflush(stdout);
sleep(5);
二、获取用户命令
首先,我们在输出命令行时,为了方便阅读,用 printf 输出 Line 时加了 '\n' ,我们现在要保证命令行和命令在同一行,所以现在我们要删除掉 '\n' 。
2.1 读取函数的选择
我们可以继续使用 scanf 函数来读取命令吗?按照我们输入的命令,如 [ls -a -l] ,他们都是以空格为分隔符,显然与我们的 sacnf 发生了冲突,而且无法控制每次输入命令的空格数量,所以我们不能使用 scanf 读取输入。
int GetUserCommand(char Command[], size_t n)
{
char* s = fgets(Command, n, stdin);
if (!s) return -1;
}
int main()
{
//输出命令行
MakeCommandLine();
//读取用户命令
char UserCommand[SIZE];
GetUserCommand(UserCommand, sizeof(UserCommand));
printf("echo : %s\n", UserCommand); //打印验证一下是否被读取
return 0;
}
2.2 细节优化
现在又来了一个细节问题:
fgets 按行读取,如果我们想执行就必须按 [回车] ,假设我们输入的是"Hello World",那么其读取到的就是"Hello World\n",再加上我们自己写的 printf 中的 '\n' 就变成了两行,下面我们进一步优化一下,只需要把命令的最后一个字符改成 '\0' 即可:
int GetUserCommand(char Command[], size_t n)
{
char* s = fgets(Command, n, stdin);
if (!s) return -1;
Command[strlen(Command) - 1] = '\0';
}
2.3 返回值
关于函数的返回值,为了与 [return -1] 区分开,也为了更好地执行命令,我们可以返回一下读取到的命令长度,这样当命令 [> 0] 时,我们再继续执行,否则直接退出:
int GetUserCommand(char Command[], size_t n)
{
char* s = fgets(Command, n, stdin);
if (!s) return -1;
Command[strlen(Command) - 1] = '\0';
return strlen(Command);
}
int main()
{
//输出命令行
MakeCommandLine();
//读取用户命令
char UserCommand[SIZE];
int n = GetUserCommand(UserCommand, sizeof(UserCommand));
(void)n;//暂时不搞,先强转一下,防止警告
return 0;
}
三、指令和选项分割
像我们上面使用的 []ls -a -l] ,它们是由指令和选项构成,所以我们如果要执行,肯定也要把读取用户传入的字符串分割成下面的形式, [ls] [-a] [-l] 。
3.1 strtok 函数
那么如何分割呢?这又要使用到C语言中的字符串函数 strtok 。
一说到分隔符,我们这里的分隔符显而易见的就是空格啦!由于我们的分割是把一个字符串分割成若干小字符串,所以我们就可以直接定义一个全局的数组,用来挨个存放这些小字符串:
但是要注意, strtok 函数使用的分隔符都是字符串,我们的空格不能设置为 ' ' ,而应该为 " "
#define SEP " "
3.2 分割实现
void SplitCommand(char Command[], size_t n)
{
(void)n;
argv[0] = strtok(Command, SEP);
int index = 1;
while ((argv[index++] = strtok(NULL, SEP)));//strtok如果无法分割,则返回NULL,此时argv最后一个元组直接被赋值为NULL且while循环结束
}
我们还可以通过打印来验证是否正确:
int i = 0;
for (; argv[i]; i++)
{
printf("%s ", argv[i]);
}
printf("\n");
四、执行命令
我们之前讲过 Bash 会创建子进程,为了保证安全,都是子进程在执行我们的命令,而我们今天要实现的也是使用子进程来帮助我们执行命令。
4.1 fork 方法
4.2 进程等待
我们之前说过,如果父进程先于子进程退出,子进程就会变成僵尸进程,为了避免这种影响,父进程可以通过进程等待的方式,回收子进程资源,获取子进程退出信息。
下面我们来介绍几个进程等待的方法:
4.3 进程替换
我们用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
有六种以 exec 开头的函数,统称为 exec 函数:
这些函数都可以用 man execl 查询:
4.4 程序编写
void ExecuteCommand(char Command[], size_t n)
{
pid_t id = fork();
if (id == 0)//子进程,执行命令
{
execvp(argv[0],argv);
}
else if (id > 0)//父进程
{
int status = 0;
waitpid(id, &status, 0);
}
else//fork创建失败,直接退出
{
exit(1);
}
}
下面我们来看一下完整代码以及效果图:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> //进程创建接口
#include <sys/types.h> //进程等待
#include <sys/wait.h> //进程等待
#define SIZE 256
#define NUM 16
#define SEP " "
char* argv[NUM];
char* GetUserName()
{
char* User = getenv("USER");
if (!User) return "None";
return User;
}
char* GetHostName()
{
char* Host = getenv("HOSTNAME");
if (!Host) return "None";
return Host;
}
char* GetCwd()
{
char* PWD = getenv("PWD");
if (!PWD) return "None";
return PWD;
}
void MakeCommandLine()
{
char Line[SIZE];
char* UserName = GetUserName();
char* HostName = GetHostName();
char* Cwd = GetCwd();
snprintf(Line, sizeof(Line), "[%s@%s %s]>", UserName, HostName, Cwd);
printf("%s", Line);
fflush(stdout);
}
int GetUserCommand(char Command[], size_t n)
{
char* s = fgets(Command, n, stdin);
if (!s) return -1;
Command[strlen(Command) - 1] = '\0';
return strlen(Command);
}
void SplitCommand(char Command[], size_t n)
{
(void)n;
argv[0] = strtok(Command, SEP);
int index = 1;
while ((argv[index++] = strtok(NULL, SEP)));
//strtok如果无法分割,则返回NULL,此时argv最后一个元组直接被赋值为NULL且while循环结束
}
void ExecuteCommand(char Command[], size_t n)
{
pid_t id = fork();
if (id == 0)//子进程,执行命令
{
execvp(argv[0],argv);
}
else if (id > 0)//父进程
{
int status = 0;
waitpid(id, &status, 0);
}
else//fork创建失败,直接退出
{
exit(1);
}
}
int main()
{
//输出命令行
MakeCommandLine();
//读取用户命令
char UserCommand[SIZE];
int n = GetUserCommand(UserCommand, sizeof(UserCommand));
(void)n;
printf("echo : %s\n", UserCommand);
//指令和选项分割
SplitCommand(UserCommand, sizeof(UserCommand));
//执行命令
ExecuteCommand(UserCommand, sizeof(UserCommand));
return 0;
}
五、程序优化
5.1 执行次数
虽然我们的 Shell 已经完成的有一点雏形了,但是怎么这个外壳只能使用一次呀?我们是不是要让他多执行几次呢?所以我们就要把这几个步骤都放到一个 while 循环中。
int main()
{
while(1)
{
//输出命令行
MakeCommandLine();
//读取用户命令
char UserCommand[SIZE];
int n = GetUserCommand(UserCommand, sizeof(UserCommand));
(void)n;
//指令和选项分割
SplitCommand(UserCommand, sizeof(UserCommand));
//执行命令
ExecuteCommand(UserCommand, sizeof(UserCommand));
}
return 0;
}
5.2 检测命令是否为内建命令
我们在我们的 Shell 中使用 cd 命令,但是我们的命令行无法进入某目录,这是为什么呢?
因为我们上面创建了子进程,我们的 cd 命令是让子进程执行的,和我们真正的 bash 没有关系,我们正确的做法是让父进程执行!
什么是内建命令呢?
下面我们来看看如何检测是否为内建命令。
虽然我们的 [cd] 已经可以使用,但是我们的命令行路径怎么不回退呢?
因为我们还要更改我们的环境变量!
所以我们此时在对我们的 cd 函数做修改:
void ExecuteCd()
{
const char* path = argv[1];
if (path == NULL) path = GetHome();
else chdir(path);
char tmp[SIZE];
getcwd(tmp, sizeof(tmp));
snprintf(cwd, sizeof(cwd), "PWD=%s", tmp);
putenv(cwd);
}
当然还有其他内建命令,方法都诸如此类。
5.3 子进程执行失败
我们直接在全局定义一个退出码 int lastcode ,然后在父进程中左对应的修改:
void ExecuteCommand(char Command[], size_t n)
{
pid_t id = fork();
if (id == 0)//子进程,执行命令
{
execvp(argv[0],argv);
}
else if (id > 0)//父进程
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
lastcode = WEXITSTATUS(status);
if (lastcode != 0) printf("%s:%s:%d\n", argv[0], strerror(lastcode), lastcode);
}
}
else//fork创建失败,直接退出
{
exit(1);
}
}
5.4 命令行路径更改
在我们的 XShell 中提供的 Shell 外壳,其命令行的路径都是相对路径,我们的 Shell 也可以改成这样,如下:
我们这里采用了宏函数,也可以使用正常函数,在使用宏函数是时,若程序是代码块,建议放在 [do while(0)] 中,如下:
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
同时在输出命令行中也调用该函数,如下:
void MakeCommandLine()
{
char Line[SIZE];
char* UserName = GetUserName();
char* HostName = GetHostName();
char* Cwd = GetCwd();
SkipPath(Cwd);
snprintf(Line, sizeof(Line), "[%s@%s %s]>", UserName, HostName, Cwd);
printf("%s", Line);
fflush(stdout);
}
六、完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> //进程创建接口
#include <sys/types.h> //进程等待
#include <sys/wait.h> //进程等待
#define SIZE 256
#define NUM 16
#define SEP " "
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
char* argv[NUM];
char cwd[SIZE];
int lastcode = 0;
char* GetUserName()
{
char* User = getenv("USER");
if (!User) return "None";
return User;
}
char* GetHostName()
{
char* Host = getenv("HOSTNAME");
if (!Host) return "None";
return Host;
}
char* GetCwd()
{
char* PWD = getenv("PWD");
if (!PWD) return "None";
return PWD;
}
void MakeCommandLine()
{
char Line[SIZE];
char* UserName = GetUserName();
char* HostName = GetHostName();
char* Cwd = GetCwd();
SkipPath(Cwd);
snprintf(Line, sizeof(Line), "[%s@%s %s]>", UserName, HostName, Cwd);
printf("%s", Line);
fflush(stdout);
}
int GetUserCommand(char Command[], size_t n)
{
char* s = fgets(Command, n, stdin);
if (!s) return -1;
Command[strlen(Command) - 1] = '\0';
return strlen(Command);
}
void SplitCommand(char Command[], size_t n)
{
(void)n;
argv[0] = strtok(Command, SEP);
int index = 1;
while ((argv[index++] = strtok(NULL, SEP)));
//strtok如果无法分割,则返回NULL,此时argv最后一个元组直接被赋值为NULL且while循环结束
}
const char* GetHome()
{
const char* home = getenv("HOME");
if (home == NULL) return "/";
return home;
}
void ExecuteCd()
{
const char* path = argv[1];
if (path == NULL) path = GetHome();
else chdir(path);
char tmp[SIZE];
getcwd(tmp, sizeof(tmp));
snprintf(cwd, sizeof(cwd), "PWD=%s", tmp);
putenv(cwd);
}
int CheckBuiltin()
{
int ch = 0;//默认非内建命令
const char* command = argv[0];
if (strcmp(command, "cd") == 0)
{
ch = 1;
ExecuteCd();
}
return ch;
}
void ExecuteCommand(char Command[], size_t n)
{
pid_t id = fork();
if (id == 0)//子进程,执行命令
{
execvp(argv[0],argv);
}
else if (id > 0)//父进程
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
lastcode = WEXITSTATUS(status);
if (lastcode != 0) printf("%s:%s:%d\n", argv[0], strerror(lastcode), lastcode);
}
}
else//fork创建失败,直接退出
{
exit(1);
}
}
int main()
{
while(1)
{
//输出命令行
MakeCommandLine();
//读取用户命令
char UserCommand[SIZE];
int n = GetUserCommand(UserCommand, sizeof(UserCommand));
(void)n;
//指令和选项分割
SplitCommand(UserCommand, sizeof(UserCommand));
//检查是否为内建命令
int ch = CheckBuiltin();
if (ch) continue;
//执行命令
ExecuteCommand(UserCommand, sizeof(UserCommand));
}
return 0;
}