跳到主要内容Linux 进程控制:自主 Shell 命令行解释器实现 | 极客日志Shell / Bash
Linux 进程控制:自主 Shell 命令行解释器实现
在 Linux 系统中自主实现 Shell 命令行解释器的过程。主要涵盖搭建基本框架、导入环境变量表、输出提示符、获取用户输入、解析命令字符串以及执行命令的逻辑。重点区分了内建命令(如 cd、echo)与普通命令的执行方式,内建命令由父进程直接处理,普通命令通过 fork 创建子进程并 exec 替换执行。完整代码展示了从环境加载到命令循环执行的实现细节。
不知所云366 浏览 一、搭建基本框架
- 首先 bash 是一个进程,而且是一个需要一直运行的进程,所以主函数一定是一个死循环
while(1){}。
int main()
{
Loadenv();
char command_line[MAXSIZE] = {0};
while(1)
{
PrintCommandLine();
if(0 == GetCommand(command_line, sizeof(command_line)))
{
continue;
}
ParseCommand(command_line);
if(CheckBuiltinExecute() > 0)
continue;
ExecuteCommand();
}
return 0;
}
- 首先就是要导入环境变量表,之后进入 while 循环,不断执行输出命令行字符串,获取用户输入字符串,解析字符串,和执行命令这几步。
1.0 导入环境变量表
int genvc = 0;
char* genv[MAXARGS];
void Loadenv()
{
extern char** environ;
(; environ[genvc]; genvc++)
{
genv[genvc] = (*)(()*);
(genv[genvc], environ[genvc]);
}
genv[genvc] = ;
( i = ; genv[i]; i++)
{
(, i, genv[i]);
}
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
for
char
malloc
sizeof
char
4096
strcpy
NULL
for
int
0
printf
"genv[%d]: %s\n"
1.1 输出 shell 命令行函数 PrintCommandLine()
- 在我们什么都不输入的时候,bash 会输出这样一串字符串,这个字符串可以拆分为
[用户名@主机名 当前文件的名字]$/#。其中 $ 代表普通用户,# 代表 root 用户。
- 我们就可以根据这个格式来写出这个 shell 命令行的函数 PrintCommandLine()。其中用户名、主机名以及当前文件的名字都可以用从环境变量表里面进行获取,需要用到的函数是 getenv()。
std::string rfindDir(const std::string &p)
{
if(p == "/")
return p;
const std::string psep = "/";
auto pos = p.rfind(psep);
if(pos == std::string::npos)
return std::string();
return p.substr(pos+1);
}
const char* GetUsername()
{
char* name = getenv("USER");
if(name == NULL)
return "None";
return name;
}
const char* GetHostname()
{
char* hostname = getenv("HOSTNAME");
if(hostname == NULL)
return "None";
return hostname;
}
const char* GetPwd()
{
char* pwd = getenv("PWD");
if(pwd == NULL)
return "None";
return pwd;
}
void PrintCommandLine()
{
printf("[%s@%s %s]#", GetUsername(), GetHostname(), rfindDir(GetPwd()).c_str());
fflush(stdout);
}
- 打印出这样一串 shell 命令行,由于后面需要用户来输入命令,所以不能加换行符
\n,但是不加 \n 的话这串字符就只能一直呆在缓冲区里面,所以需要 fflush 函数来刷新一下缓冲区。
1.2 获取用户输入字符串函数 GetCommand()
- 获取字符串的时候,就会卡住,也就是卡在打印出的命令行后面。
- 用来获取用户输入的字符串函数有很多,比如 scanf,fgets,这里我们用 fgets 读取一整行,因为 scanf 遇到空格就不会读取了,这不符合我们的要求,我们要把空格一起读取到。
#define MAXSIZE 128
int GetCommand(char* commandline, int size)
{
if(NULL == fgets(commandline, size, stdin))
return 0;
commandline[strlen(commandline)-1] = '\0';
return strlen(commandline);
}
- 首先我在 while 循环外定义了一个 command_line 命令行数组用来存储用户输入的命令行,它的大小定为 128 字节。
- 之后是 Getcommand 函数,这个函数通过 fgets 函数读取用户所输入的命令行。由于用户在每次命令行输入之后都会摁一下回车键,这个回车键也会被当成字符读取到 commandline 数组里面,所以这里需要做一些处理,让 commandline[strlen(commandline) - 1] = '\0',手动将最后一个回车字符置为结束符'\0'。
- 如果用户没有输入,那么 fgets 读取失败 Getcommand 函数的返回值就是 0,会执行 continue 语句,不会执行接下来的代码,重新进入循环。
1.3 解析字符串函数
- 首先是解析之后的字符串要放在哪里,根据空格进行分割后的字符串就是一个个的命令行参数,当然是要放在命令行参数表里面,我设置一个 char* 类型的数组 gargv 来进行存储。同时还有记录命令行参数个数的 gargc 变量。
strtok 函数
- 再就是用来解析字符串的 strtok 函数,它的第一个参数是需要进行切割的字符串,第二个参数是分隔符字符串。它的返回值是切割后的目标字符串首字母地址。
- 切割下第一个目标字符串第一个参数传入需要进行切割的字符串 str,如果要接着进行切割的话第一个参数就不能也传入 str 了,而是需要传入 NULL。这样 strtok 函数才会知道是需要接着进行上一次的切割,否则它仍旧会切割下 str 的第一个目标字符串。
- strtok 函数支持传入多个分隔符,比如传入 " #!" 它就会按照空格、#、! 这三个字符作为分隔符来对传入的字符串进行切割。
- 至于为什么 strtok 跟其他函数不同,可以继续进行上一次函数的切割,这是因为它的实现运用了 static 变量。
#define MAXARGS 32
int gargc = 0;
char* gargv[MAXARGS];
const char* gsep = " ";
void ParseCommand(char* commandline)
{
gargc = 0;
memset(gargv, 0, sizeof(gargv));
gargv[0] = strtok(commandline, gsep);
while((gargv[++gargc] = strtok(NULL, gsep)));
}
while ((gargv[++gargc] = strtok(NULL, gsep))); 代码逻辑是 strtok 先切割返回一个值,再把这个值存入 gargv[++gargc] 里面,再检测这个值。
- 代码还有一点,那就是 strtok 函数在没有可以切割的字符串了之后会返回 NULL,这个 NULL 会存入 gargv[++gargc] 里面,gargc 自增 1,所以 gargc 的个数不会少,而是刚刚好。
- 每次进行切割的时候都要把原来的 gargv 也就是命令行参数列表清空,gargc 命令行参数个数清零。
1.4 执行命令函数
1. 普通命令 vs 内建命令
- 像是 ls 这样的二进制文件,一般需要通过创建子进程然后让子进程进行程序切换来完成调用。这种就叫做普通命令。
- 而 cd 和 echo 这种,前者所要改变的是当前 bash 的路径而不是子进程的路径所以不能通过子进程进行程序切换来完成调用,不然改变的只是子进程的路径,而不会影响到它的父进程 bash 的路径。而 echo 有一个作用 $? 可以打印出上一个所执行的二进制文件的退出码。这个文件是由一般子进程执行的,谁能获得子进程的退出码?只能是父进程。像是这种不能够由子进程通过程序替换来执行的命令,需要由 Shell 自行执行的命令就叫做内建命令。
- 内建命令是 Shell 自身实现的功能(无独立二进制文件),无需通过「创建子进程 + 程序替换」执行,直接在 Shell 进程内运行,这是与普通命令的核心区别。
2. 父进程执行内建命令函数 CheckBuiltinExecute()
if(CheckBuiltinExecute() > 0)
continue;
- 在让子进程执行命令之前,首先需要判断该命令是否为内建命令,如果是内建命令就让自主 Shell 运行,不是则通过创建子进程 + 程序替换执行。
- 内建命令有很多,这里只实现 cd 和 echo 命令。如果是需要父进程执行的命令则返回 1,执行后续的 continue 语句,如果是要子进程执行的命令则返回 0,会继续执行接下来的代码。
cd
- 需要切换 Shell 的路径,需要用到函数 chdir。
chdir 函数和 getcwd 函数
-
使用 chdir 函数需要包含头文件 unistd.h 头文件。
-
这个函数的作用是更改当前所执行这个函数的进程的路径。会将当前进程的工作路径修改为传入的 path。
-
成功则返回 0,失败则返回 -1。
-
chdir 用于修改进程的工作目录,而 getcwd 用于获取进程当前的工作目录,执行成功则返回指向 buf 的指针,buf 存储的是以'\0'为结尾的绝对路径字符串。
-
用户使用 cd 时传入的第二个命令行参数就是我们的目标路径,所以只需要给 chdir 函数传入 gargv[1] 就好。
-
但是会存在一个问题,那就是通过 chdir 修改 Shell 的路径,系统不会自动给我们修改环境变量里面的 PWD,这会导致我们的 Shell 打印的命令行字符串后面的路径不会改变。所以这里我们需要手动的更改一下环境变量 PWD。
char cwd[MAXSIZE];
if(strcmp(gargv[0], "cd") == 0)
{
if(gargc == 2)
{
chdir(gargv[1]);
char pwd[1024];
getcwd(pwd, sizeof(pwd));
snprintf(cwd, sizeof(cwd), "PWD=%s", pwd);
putenv(cwd);
return 1;
}
}
echo
- 需要处理 echo $ 这种内建命令特殊情况和 echo 语句这种普通情况,后面的普通情况交给子进程去处理就好,我们处理前面的特殊情况。
int lastcode = 0;
else if(strcmp(gargv[0], "echo") == 0)
{
if(gargc == 2)
{
if(gargv[1][0] == '$')
{
if(strcmp(gargv[1]+1, "?") == 0)
{
printf("%d\n", lastcode);
}
else if(strcmp(gargv[1]+1, "PATH") == 0)
{
printf("%s\n", getenv("PATH"));
}
lastcode = 0;
return 1;
}
}
}
- 解释一下 gargv[1] + 1,这个 gargv[1] 它实际上是一个指针,指向这个 gargv[1] 存储的字符串的首字母,+1 这个指针就会往后移动一个字节,就会跳过'$'字符,指向它后面的字符来作为新字符串的首字符。
- 然后新增全局变量 lastcode,这个变量是用来记录上一个命令执行完之后的退出码。Shell 进程执行成功之后也要更新 lastcode 为 0。
3. 子进程执行普通命令函数 ExecuteCommand()
int ExecuteCommand()
{
pid_t id = fork();
if(id == 0)
{
execvp(gargv[0], gargv);
exit(0);
}
else if(id < 0)
return -1;
else
{
int status = 0;
pid_t sid = waitpid(id, &status, 0);
if(sid > 0) lastcode = WEXITSTATUS(status);
}
return 0;
}
- 也就是创建子进程然后让子进程进行程序切换执行命令,父进程就等待回收子进程,这样父进程就能够得到子进程的退出码,最后如果回收成功则更新退出码。创建子进程成功则返回 0,创建失败则返回 -1。
二、完整代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<iostream>
#include<string>
#define MAXSIZE 128
#define MAXARGS 32
int gargc = 0;
char* gargv[MAXARGS];
const char* gsep = " ";
int genvc = 0;
char* genv[MAXARGS];
char cwd[MAXSIZE];
int lastcode = 0;
void Loadenv()
{
extern char** environ;
for(; environ[genvc]; genvc++)
{
genv[genvc] = (char*)malloc(sizeof(char)*4096);
strcpy(genv[genvc], environ[genvc]);
}
genv[genvc] = NULL;
for(int i = 0; genv[i]; i++)
{
printf("genv[%d]: %s\n", i, genv[i]);
}
}
std::string rfindDir(const std::string &p)
{
if(p == "/")
return p;
const std::string psep = "/";
auto pos = p.rfind(psep);
if(pos == std::string::npos)
return std::string();
return p.substr(pos+1);
}
const char* GetUsername()
{
char* name = getenv("USER");
if(name == NULL)
return "None";
return name;
}
const char* GetHostname()
{
char* hostname = getenv("HOSTNAME");
if(hostname == NULL)
return "None";
return hostname;
}
const char* GetPwd()
{
char* pwd = getenv("PWD");
if(pwd == NULL)
return "None";
return pwd;
}
void PrintCommandLine()
{
printf("[%s@%s %s]#", GetUsername(), GetHostname(), rfindDir(GetPwd()).c_str());
fflush(stdout);
}
int GetCommand(char* commandline, int size)
{
if(NULL == fgets(commandline, size, stdin))
return 0;
commandline[strlen(commandline)-1] = '\0';
return strlen(commandline);
}
void ParseCommand(char* commandline)
{
gargc = 0;
memset(gargv, 0, sizeof(gargv));
gargv[0] = strtok(commandline, gsep);
while((gargv[++gargc] = strtok(NULL, gsep)));
}
int CheckBuiltinExecute()
{
if(strcmp(gargv[0], "cd") == 0)
{
if(gargc == 2)
{
chdir(gargv[1]);
char pwd[1024];
getcwd(pwd, sizeof(pwd));
snprintf(cwd, sizeof(cwd), "PWD=%s", pwd);
putenv(cwd);
}
return 1;
}
else if(strcmp(gargv[0], "echo") == 0)
{
if(gargc == 2)
{
if(gargv[1][0] == '$')
{
if(strcmp(gargv[1]+1, "?") == 0)
{
printf("%d\n", lastcode);
}
else if(strcmp(gargv[1]+1, "PATH") == 0)
{
printf("%s\n", getenv("PATH"));
}
lastcode = 0;
return 1;
}
}
}
return 0;
}
int ExecuteCommand()
{
pid_t id = fork();
if(id == 0)
{
execvp(gargv[0], gargv);
exit(0);
}
else if(id < 0)
return -1;
else
{
int status = 0;
pid_t sid = waitpid(id, &status, 0);
if(sid > 0) lastcode = WEXITSTATUS(status);
}
return 0;
}
int main()
{
Loadenv();
char command_line[MAXSIZE] = {0};
while(1)
{
PrintCommandLine();
if(0 == GetCommand(command_line, sizeof(command_line)))
{
continue;
}
ParseCommand(command_line);
if(CheckBuiltinExecute() > 0)
continue;
ExecuteCommand();
}
return 0;
}