跳到主要内容Linux 环境下 Bash Shell 模拟实现 | 极客日志Shell / Bash
Linux 环境下 Bash Shell 模拟实现
通过 C 语言系统调用模拟 Bash Shell 功能。主要步骤包括:利用环境变量获取用户名、主机名和路径以显示提示符;使用 fgets 替代 scanf 读取包含空格的完整命令并去除换行符;利用 strtok 函数分割命令字符串为参数数组;最后通过 fork 创建子进程,使用 execvpe 执行命令,父进程通过 waitpid 等待子进程结束。实现了基本的命令行交互与执行流程。
链路追踪3 浏览 在学习了进程控制之后(进程创建、终止、等待、替换),我们可以来实现 bash 的简单模拟。
什么是 shell/bash?
shell/bash 是操作系统的外壳程序,负责帮助用户进行指令的执行;拿到用户的命令后交给操作系统,再将结果返回给用户。shell/bash 本质上也是一个可执行程序。
要模拟实现 bash,我们要实现的功能主要有三个:
提示词,显示包括用户名和主机名等信息;获取用户的输入,并能将命令和选项分割提取;将获取的命令交给操作系统执行并返回结果。
获取用户名
我们可以看待 bash 的每行命令前都会显示当前用户名和主机名:
可以看到是 [用户名@主机名 当前路径] 的格式,用户名和主机名是存储于环境变量中的。我们可以使用 env 查看环境变量。
用户名存放于环境变量 USER 中;主机名存放于环境变量 HOSTNAME 中;当前路径存放于 PWD 中。因此我们需要使用 getenv 函数来获取环境变量。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
const char* getusername() {
return getenv("USER");
}
const char* gethostname() {
return getenv("HOSTNAME");
}
const char* getpwd() {
return getenv("PWD");
}
int main() {
return 0;
}
之后我们仿照 bash 的格式,添加上 [];为了美观我们可以 define 重新定义一下:
#define LEFT "【"
#define RIGHT "】"
# LABLE
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 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
define
"¥"
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),getpwd());
用户输入
输入
bash 的一个重要功能就是要获取用户输入的指令。所以,下面我们来实现用户输入的部分。我们单独创建一个 userInput 的函数来实现这个功能。
#define Input_size 1024
char userline[Input_size];
之后让用户进行输入,并将输入的内容存放到该数组中。我们是否可以使用 scanf 函数来进行输入呢?先直接在 main 函数中测试:
我们再加一行测试代码,打印出数组的内容,看是否将用户输入的内容存入数组:
printf("echo:%s\n",userline);
这是因为 scanf 函数在读取到空格时就会终止。所以我们不能使用 scanf 函数来实现。
这个地方我们采用 fgets 函数。我们使用 man 命令查看关于它的说明:
它的头文件为 stdio.h。如果读取成功则返回对应的字符串地址,失败则返回 NULL。我们可以看到它有三个参数,第一个表示我们需要将文件放入哪个缓存区;第二个表示这个缓存区有多大;第三个则表示文件对象。
Linux 系统会默认为我们打开三个输入输出流,分别为 stdin, stdout, stderr。我们直接从 stdin 中读取即可。所以,我们的输入代码如下:
void userInput(char* uline,int size) {
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),getpwd());
char* s = fgets(uline,size,stdin);
}
int main() {
char userline[Input_size];
userInput(userline,sizeof(userline));
printf("echo:%s\n",userline);
return 0;
}
不读取换行符
不过,我们可以看到显示的结果后面还有一个空行,这是因为我们在输入命令之后还要敲换行符,而这个换行符也会被读取,所以存放到数组中的最后一个元素是我们最后输入的换行符。
那么,怎么去掉这个空行,即不让数组读取到这个换行符呢?
因为换行符是存储在数组中的,我们通过数组下标的方式找到这个换行符替换为终止符即可。
void userInput(char* uline,int size) {
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),getpwd());
char* s = fgets(uline,size,stdin);
assert(s);
uline[strlen(uline)-1] = '\0';
}
持续使用
我们现在的代码执行一次后就退出了,而 bash 是可以一直使用的。所以,我们还需要实现循环使用的功能,让程序成为一个死循环,只要不退出就会一直使用:
int main() {
char userline[Input_size];
while(1) {
userInput(userline,sizeof(userline));
printf("echo:%s\n",userline);
}
return 0;
}
字符串分割
不过,我们现在虽然能够获取输入的字符串,但是是整体的一整行;我们的一个命令是由多个字符串组成的,例如 ls -a -l ;是由三个字符串组成的,我们该怎样将它们分割开来?
第一个参数表示要分割的子串;第二个参数表示分割符(默认为空格)。其返回值为分割出来的子串。不过调用一次,该函数只能分割出一个子串。所以如果想要全部分割,就需要循环去调用它。
int divideString(char userline[],char* argv[]) {
int i = 0;
argv[i++] = strtok(userline,DELIM);
while(argv[i++] = strtok(NULL,DELIM));
return i-1;
}
while(1) {
userInput(userline,sizeof(userline));
printf("echo:%s\n",userline);
int argc = divideString(userline,argv);
if(argc == 0) continue;
for(int i = 0;argv[i];i++)
printf("[%d]:%s\n",i,argv[i]);
}
执行命令
另外一个重要的功能就是将获取到的命令交给操作系统去执行,并返回结果。而这个流程不就是整个进程控制的内容吗?
int main() {
extern char** environ;
int i = 0;
char* argv[ARGC_SIZE];
char userline[Input_size];
while(1) {
userInput(userline,sizeof(userline));
printf("echo:%s\n",userline);
int argc = divideString(userline,argv);
if(argc == 0) continue;
for(int i = 0;argv[i];i++)
printf("[%d]:%s\n",i,argv[i]);
pid_t id = fork();
if(id<0) {
perror("fork failed");
continue;
} else if(id == 0) {
execvpe(argv[0],argv,environ);
exit(199);
} else {
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
printf("等待成功\n");
}
}
return 0;
}
完整代码
通过上面的实现,我们已经基本完成了 bash 的模拟,可以获取用户输入的指令并交给操作系统执行。完整代码如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
#define LEFT "【"
#define RIGHT "】"
#define LABLE "¥"
#define DELIM " "
#define Input_size 1024
#define ARGC_SIZE 32
#define EXIT_CODE 199
const char* getusername() {
return getenv("USER");
}
const char* gethostname() {
return getenv("HOSTNAME");
}
const char* getpwd() {
return getenv("PWD");
}
void userInput(char* uline,int size) {
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),getpwd());
char* s = fgets(uline,size,stdin);
assert(s);
uline[strlen(uline)-1] = '\0';
}
int divideString(char userline[],char* argv[]) {
int i = 0;
argv[i++] = strtok(userline,DELIM);
while(argv[i++] = strtok(NULL,DELIM));
return i-1;
}
int main() {
extern char** environ;
int i = 0;
char* argv[ARGC_SIZE];
char userline[Input_size];
while(1) {
userInput(userline,sizeof(userline));
printf("echo:%s\n",userline);
int argc = divideString(userline,argv);
if(argc == 0) continue;
for(int i = 0;argv[i];i++)
printf("[%d]:%s\n",i,argv[i]);
pid_t id = fork();
if(id<0) {
perror("fork failed");
continue;
} else if(id == 0) {
execvpe(argv[0],argv,environ);
exit(199);
} else {
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
printf("等待成功\n");
}
}
return 0;
}