跳到主要内容自定义 Shell 实现原理与代码解析 | 极客日志Shell / Bash算法
自定义 Shell 实现原理与代码解析
介绍如何在 Linux 环境下使用 C/C++ 语言自定义编写一个简易 Shell 解释器。内容涵盖 Shell 的运行原理(fork/exec/wait)、命令行提示符打印、参数获取与解析、内建命令(cd/echo)实现以及进程控制。通过实例代码展示了如何模拟 Bash 行为,帮助读者深入理解操作系统进程管理与环境变量机制。
FlinkHero2 浏览 目标
Shell 是一种用于与操作系统交互的命令行界面程序。它充当用户和操作系统内核之间的中介,通过用户输入的命令来执行操作,提供与操作系统的互动。
具体来说,Shell 可以做以下几件事:
- 命令解释和执行:Shell 接受用户输入的命令,解析并传递给操作系统的内核执行。
- 脚本编程:允许用户将一系列命令写入一个文件中,形成脚本自动化任务。
- 交互式环境:提供一个交互式环境,用户可以在其中执行命令、查看输出等。
我们在 Linux 中最常用的 shell 是 Bash。今天我们将在 Bash 上实现一个自定义的 Shell,主要进行命令解释和执行,以贯通进程管理等知识。
实现目标:
- 能处理普通命令
- 能处理内建命令
- 帮助理解内建命令、本地变量、环境变量概念
- 帮助理解 Shell 的运行原理
运行原理
Shell 的典型互动流程如下:Shell 从用户读入字符串后建立一个新的子进程,然后在子进程中运行指定程序并等待子进程结束,结束后再读入下一行。
因此,实现自定义 Shell 需要循环下列过程:
- 获取命令行
- 解析命令行
- 建立一个子进程(fork)
- 替换子进程(exec)
- 父进程等待子进程退出(wait)
实现
我们实现的 Shell 较为简单,采用 C/C++ 混编,主体以 C 语言为主,在一个 .cc 文件中进行编写。
3.1 打印命令行提示符
Shell 总是会先打印出命令行提示符,然后处于阻塞状态等待用户输入。提示符由 [用户名@主机名 当前工作目录]$ 组成,可以从环境变量中获取。
#include <cstdlib>
const char* GetPwd() {
const char* pwd = getenv("PWD");
return pwd == NULL ? "None" : pwd;
}
const char* GetHostName() {
const char* hostname = getenv("HOSTNAME");
return hostname == ? : hostname;
}
{
* user = ();
user == ? : user;
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- 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
NULL
"None"
const char* GetUser()
const
char
getenv
"USER"
return
NULL
"None"
#define SLASH "/"
std::string GetPwdDir(const char* pwd) {
std::string dir = pwd;
if (dir == SLASH) return SLASH;
auto pos = dir.rfind(SLASH);
if (pos == std::string::npos) return "BUG";
return dir.substr(pos + 1);
}
#define FORMAT "[%s@%s %s]# "
#define CMDLINE_MAX 1024
void MakeCommandPrompt(char* out, int size) {
snprintf(out, size, FORMAT, GetUser(), GetHostName(), GetPwdDir(GetPwd()).c_str());
}
void PrintCommandPrompt() {
char prompt[CMDLINE_MAX];
MakeCommandPrompt(prompt, CMDLINE_MAX);
printf("%s", prompt);
fflush(stdout);
}
3.2 获取命令行参数
我们将获取命令行参数的返回值设置为 bool 类型,可以在直接输入回车时重新进入循环。
#include <cstdio>
bool GetCommandLine(char* out, int size) {
char* c = fgets(out, size, stdin);
if (c == NULL) return false;
out[strlen(out) - 1] = 0;
if (strlen(out) == 0) return false;
return true;
}
3.3 命令行解析
将命令行参数以空格为分隔符,填入到命令行参数表中:
#define DELIM " "
#define ARGV_MAX 1024
char* g_argv[ARGV_MAX];
int g_argc = 0;
bool CommandParse(char* commandline) {
g_argc = 0;
g_argv[g_argc++] = strtok(commandline, DELIM);
if (g_argv[0] == NULL) return false;
while ((bool)(g_argv[g_argc++] = strtok(nullptr, DELIM)));
g_argc--;
return true;
}
3.4 执行命令
通过创建一个子进程,让子进程进行程序替换来执行对应的命令。选择 execvp 函数:
void Execute() {
pid_t id = fork();
if (id < 0) {
perror("fork false:");
exit(1);
} else if (id == 0) {
execvp(g_argv[0], g_argv);
exit(1);
}
int status;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0) lastcode = WEXITSTATUS(status);
}
3.5 内建命令
内建命令是 Shell 直接提供的命令,无需调用外部程序。在执行命令前,需要先判断命令是否为内建命令。
bool IsBuiltInCommand() {
if (!strcmp(g_argv[0], "cd")) {
CommandCd();
return true;
} else if (!strcmp(g_argv[0], "echo")) {
CommandEcho();
return true;
}
return false;
}
3.5.1 cd
支持 cd - 返回上次所在目录,cd ~ 返回家目录,cd 返回根目录。
void CommandCd() {
int ret;
std::string old_dir = get_current_dir_name();
if (g_argc == 1) {
std::string home = GetHome();
if (home.empty()) exit(1);
ret = chdir(home.c_str());
} else {
std::string where = g_argv[1];
if (where == "-") {
ret = chdir(GetOldpwd());
} else if (where == "~") {
ret = chdir(GetHome());
} else {
ret = chdir(where.c_str());
}
}
if (ret == -1) {
perror("cd");
lastcode = 1;
} else {
lastcode = 0;
setenv("PWD", get_current_dir_name(), 1);
setenv("OLDPWD", old_dir.c_str(), 1);
}
}
3.5.2 echo
支持打印字符、查看环境变量值以及 $? 查看上一个程序的退出码。
int lastcode = 0;
void CommandEcho() {
if (g_argc == 1) {
printf("\n");
} else {
std::string opt = g_argv[1];
if (opt == "$?") {
std::cout << lastcode << std::endl;
} else if (opt[0] == '$') {
std::string env_name = opt.substr(1);
const char* env_value = getenv(env_name.c_str());
if (env_value) std::cout << env_value << std::endl;
} else {
std::cout << opt << std::endl;
}
}
lastcode = 0;
}
小结
自定义 Shell 大致完成。对比真实 Shell 还有差距,但编写目的是巩固进程、函数、环境变量等知识。
进程与函数有相似性:exec/exit 就像 call/return。Linux 鼓励将这种在拥有私有数据的函数间通信的模式扩展到程序之间。
源码
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
#define CMDLINE_MAX 1024
#define FORMAT "[%s@%s %s]# "
#define DELIM " "
#define SLASH "/"
#define ARGV_MAX 1024
char* g_argv[ARGV_MAX];
int g_argc = 0;
#define ENV_MAX 1024
char* g_env[ENV_MAX];
int g_envs;
int lastcode = 0;
void InitEnv() {
extern char** environ;
memset(g_env, 0, sizeof(g_env));
g_envs = 0;
for (int i = 0; environ[i]; i++) {
g_env[i] = (char*)malloc(strlen(environ[i]) + 1);
strcpy(g_env[i], environ[i]);
g_envs++;
}
g_env[g_envs] = NULL;
for (int i = 0; g_env[i]; i++) {
putenv(g_env[i]);
}
environ = g_env;
}
const char* GetPwd() {
const char* pwd = getenv("PWD");
return pwd == NULL ? "None" : pwd;
}
const char* GetOldpwd() {
const char* oldpwd = getenv("OLDPWD");
return oldpwd == NULL ? "None" : oldpwd;
}
const char* GetHome() {
const char* home = getenv("HOME");
return home == NULL ? "None" : home;
}
const char* GetHostName() {
const char* hostname = getenv("HOSTNAME");
return hostname == NULL ? "None" : hostname;
}
const char* GetUser() {
const char* user = getenv("USER");
return user == NULL ? "None" : user;
}
std::string GetPwdDir(const char* pwd) {
std::string dir = pwd;
if (dir == SLASH) return SLASH;
auto pos = dir.rfind(SLASH);
if (pos == std::string::npos) return "BUG";
return dir.substr(pos + 1);
}
void MakeCommandPrompt(char* out, int size) {
snprintf(out, size, FORMAT, GetUser(), GetHostName(), GetPwdDir(GetPwd()).c_str());
}
void PrintCommandPrompt() {
char prompt[CMDLINE_MAX];
MakeCommandPrompt(prompt, CMDLINE_MAX);
printf("%s", prompt);
fflush(stdout);
}
bool GetCommandLine(char* out, int size) {
char* c = fgets(out, size, stdin);
if (c == NULL) return false;
out[strlen(out) - 1] = 0;
if (strlen(out) == 0) return false;
return true;
}
bool CommandParse(char* commandline) {
g_argc = 0;
g_argv[g_argc++] = strtok(commandline, DELIM);
if (g_argv[0] == NULL) return false;
while ((bool)(g_argv[g_argc++] = strtok(nullptr, DELIM)));
g_argc--;
return true;
}
void Execute() {
pid_t id = fork();
if (id < 0) {
perror("fork false:");
exit(1);
} else if (id == 0) {
execvp(g_argv[0], g_argv);
exit(1);
}
int status;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0) lastcode = WEXITSTATUS(status);
}
void CommandCd() {
int ret;
std::string old_dir = get_current_dir_name();
if (g_argc == 1) {
std::string home = GetHome();
if (home.empty()) exit(1);
ret = chdir(home.c_str());
} else {
std::string where = g_argv[1];
if (where == "-") {
ret = chdir(GetOldpwd());
} else if (where == "~") {
ret = chdir(GetHome());
} else {
ret = chdir(where.c_str());
}
}
if (ret == -1) {
perror("cd");
lastcode = 1;
} else {
lastcode = 0;
setenv("PWD", get_current_dir_name(), 1);
setenv("OLDPWD", old_dir.c_str(), 1);
}
}
void CommandEcho() {
if (g_argc == 1) {
printf("\n");
} else {
std::string opt = g_argv[1];
if (opt == "$?") {
std::cout << lastcode << std::endl;
} else if (opt[0] == '$') {
std::string env_name = opt.substr(1);
const char* env_value = getenv(env_name.c_str());
if (env_value) std::cout << env_value << std::endl;
} else {
std::cout << opt << std::endl;
}
}
lastcode = 0;
}
bool IsBuiltInCommand() {
if (!strcmp(g_argv[0], "cd")) {
CommandCd();
return true;
} else if (!strcmp(g_argv[0], "echo")) {
CommandEcho();
return true;
}
return false;
}
int main() {
InitEnv();
while (true) {
PrintCommandPrompt();
char commandline[CMDLINE_MAX];
if (!GetCommandLine(commandline, CMDLINE_MAX)) continue;
if (!CommandParse(commandline)) continue;
if (IsBuiltInCommand()) continue;
Execute();
}
return 0;
}