自定义shell
提示生成→ 读取输入 →解析令牌→判断内建/外部→ 执行。内建命令修改 Shell 自身状态,不 fork;外部命令需要fork+exec,并妥善回收子进程。环境变量表是进程间信息传递的重要途径。重定向的本质是操控文件描述符表,且在exec之前完成。动手实现一个 Shell 是理解操作系统进程管理、文件描述符以及 Unix 哲学的最佳途径。希望你也能基于这份原型,修复其中的 bug,并添加自己梦寐
从零打造一个简易 Linux Shell:核心原理解析与代码实战
你是否好奇,每次在终端敲下 ls -l 并回车时,系统背后究竟发生了什么?Shell 作为用户与操作系统内核之间的桥梁,承担着命令解析、程序启动、环境管理等一系列复杂任务。本文将通过分析一个真实的 C++ 简易 Shell 实现,带你深入理解 Shell 的工作原理,并在代码解读的过程中指出常见陷阱和优化方向。
1. 项目总览:我们想实现什么?
这个简易 Shell 支持以下功能:
- 显示包含用户名、主机名、当前目录的命令行提示符;
- 读取用户输入的命令行;
- 解析命令行为独立的参数数组;
- 识别并执行内建命令(
cd,echo,export,alias等); - 通过
fork + execvp执行外部命令,并获取退出状态; - 基本的环境变量管理,支持从父 Shell 继承环境;
- (片段性)支持输入/输出重定向。
主程序结构清晰,采用经典的 “读取-解析-执行”循环,没有依赖任何第三方库。
2. 环境变量与工作目录:Shell 的“上下文”
Shell 之所以知道“你是谁”、“你在哪儿”,全靠环境变量。我们可以实现这几个基础辅助函数来告诉操作系统:
const char *GetUserName() {
const char *name = getenv("USER");
return name == NULL ? "None" : hostname;
}
const char *GetHostName() {
const char *hostname = getenv("HOSTNAME");
return name == NULL ? "None" : hostname;
}
const char *GetPwd() {
const char *pwd = getcwd(cwd, sizeof(cwd));
if(pwd != NULL) {
snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
putenv(cwdenv);
}
return pwd == NULL ? "None" : pwd;
}
我们可以利用DirName 函数尝试从完整路径中提取当前目录名
const std::string *DirName(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);
}
3. 命令行提示符:打造个性化的见面
提示符格式定义为 [用户名@主机名 目录]# ,通过 MakeCommandLine 拼装:
void MakeCommandLine(char cmd_prompt[], int size) {
snprintf(cmd_prompt, size, FORMAT,
GetUserName(), GetHostName(),
DirName(GetPwd()).c_str()); // 只打印当前目录
// 第二行被注释的代码为打印全路径,可按需选择
}
PrintCommandPrompt 负责将提示符输出并刷新缓冲区,保证在没有换行的情况下也能立即显示。
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt, sizeof(prompt));
printf("%s", prompt);
fflush(stdout);
}
4. 命令读取与解析:拆解用户意图
4.1 读取一行
GetCommandLine 使用 fgets 读取用户输入,然后去掉末尾的 \n:
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;
}
4.2 拆分为参数数组
CommandParse 使用 strtok 以空格为分隔符进行分割:
bool CommandParse(char *commandline) {
g_argc = 0;
g_argv[g_argc++] = strtok(commandline, SEP);
while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
g_argc--;
g_argv[g_argc] = NULL;
return g_argc > 0;
}
注意这里 strtok 会修改原字符串,所以传入的 commandline 不能是常量。
5. 内建命令:不需要 fork 的“自家事”
内建命令直接影响 Shell 进程自身状态,因此不能在子进程中执行。本实现主要支持两种内建命令。
5.1 cd 改变工作目录
bool Cd() {
if(g_argc == 1) {
std::string home = GetHome();
if(home.empty()) return true;
chdir(home.c_str());
} else {
std::string where = g_argv[1];
if(where == "~") { ... }
else { chdir(where.c_str()); }
}
return true;
}
cd - 返回上一级目录的功能还未实现,有兴趣的读者可以通过维护一个历史目录栈来完善。
5.2 echo 与退出码
echo $? 输出上一条命令的退出码,echo $VAR 输出环境变量值:
bool Echo() {
if(g_argc == 2) {
std::string opt = g_argv[1];
if(opt == "$?") {
std::cout << lastcode << std::endl;
lastcode = 0;
return true;
} 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;
}
return true;
}
return true;
}
6. 执行外部命令:fork 与 exec 的经典配合
当命令不是内建命令时,Execute 函数登场:
int Execute()
{
pid_t id = fork();
if(id == 0)
{
int fd = -1;
// 子进程检测重定向情况
if(redir == INPUR_REDIR)
{
fd = open(filename.c_str(), O_RDONLY);
if(fd < 0)exit(1);
up2(fd, 0);
close(fd);
}
else if(redir == OUTPUT_REDIR)
{
fd = open(filename,c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
if(fd < 0)exit(2);
up2(fd, 1);
close(fd);
}
else if(redir == APPEND_REDIR)
{
fd = open(filename,c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
if(fd < 0)exit(3);
up2(fd, 2);
close(fd);
}
else
{}
// child
execvp(g_argv[0], g_argv);
exit(1);
}
int status = 0;// 保存退出信息
// father
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
}
return 0;
}
进程替换 (execvp) 会用新程序镜像完全替代子进程的地址空间,因此重定向等操作必须在 execvp 之前完成,因为一旦替换成功,后续代码都不会执行。
重定向的解析可以放在 CommandParse 中,将 g_argv 中的重定向符号和文件名单独记录下来,并从 g_argv 中移除,确保后续 execvp 只看到真正的命令和参数。
7. 环境变量初始化:继承与本地管理
InitEnv 尝试从外部环境 environ 全局变量复制一份:
void InitEnv() {
extern char **environ;
memset(g_env, 0, sizeof(g_env));
// 错误:g_env = 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]);
}
一个健壮的 Shell 不应只复制父进程环境,还应支持 export 修改并重新同步给子进程。
8. 总结:
通过这份“充满教学点”的代码,我们可以提炼出 Shell 的核心骨架:
- 提示生成 → 读取输入 → 解析令牌 → 判断内建/外部 → 执行。
- 内建命令修改 Shell 自身状态,不 fork;外部命令需要
fork+exec,并妥善回收子进程。 - 环境变量表是进程间信息传递的重要途径。
- 重定向的本质是操控文件描述符表,且在
exec之前完成。
动手实现一个 Shell 是理解操作系统进程管理、文件描述符以及 Unix 哲学的最佳途径。希望你也能基于这份原型,修复其中的 bug,并添加自己梦寐以求的功能。完整可跑的 Shell 就在你的键盘之下,祝 Coding 愉快!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)