命令行解释器(Shell)是操作系统的"翻译官",它的核心工作流程可以抽象为:
循环 {
1. 显示提示符
2. 获取命令输入
3. 解析命令参数
4. 执行命令程序
}
本实现仅需200行C++代码,却能完整展现Shell的核心工作机制。让我们通过解剖麻雀的方式,逐步拆解这个微型Shell的实现过程。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
<unistd.h>
:提供POSIX系统调用接口<sys/wait.h>
:包含进程等待相关函数<cstring>
:字符串处理函数库#define MAXARGC 128
char *g_argv[MAXARGC]; // 参数指针数组
int g_argc = 0; // 参数计数器
设计思路:模拟命令行参数存储结构,与main函数的argc/argv
兼容
void PrintCommandPrompt() {
char prompt[COMMAND_SIZE];
// 格式化提示字符串
snprintf(prompt, sizeof(prompt), "[%s@%s %s]# ",
GetUserName(), GetHostName(), GetPwd());
printf("%s", prompt);
fflush(stdout);
}
关键技术点:
snprintf
的安全格式化:第二个参数指定缓冲区大小,防止溢出fflush(stdout)
:强制刷新输出缓冲区,确保立即显示getenv("USER")
:当前登录用户getenv("HOSTNAME")
:主机名称getenv("PWD")
:当前工作目录bool GetCommandLine(char *out, int size) {
if(!fgets(out, size, stdin)) return false;
out[strlen(out)-1] = 0; // 去除末尾换行符
return strlen(out) > 0;
}
安全输入要点:
fgets
替代gets
:指定最大读取长度\n
替换为\0
void CommandParse(char *commandline) {
g_argc = 0;
g_argv[g_argc++] = strtok(commandline, " "); // 首次分割
while((g_argv[g_argc++] = strtok(nullptr, " "))); // 持续分割
g_argc--; // 修正计数器
}
strtok
工作机制解析:
nullptr
继续处理原字符串\0
修改原字符串,返回每个token的起始地址示例解析过程:
输入:"ls -l /usr"
内存变化:
l s \0 - l \0 / u s r \0
^ ^ ^
g_argv[0] g_argv[1] g_argv[2]
int Execute() {
pid_t id = fork();
if(id == 0) { // 子进程
execvp(g_argv[0], g_argv);
exit(1); // exec失败时退出
}
// 父进程等待
waitpid(id, nullptr, 0);
return 0;
}
进程管理三剑客:
fork()
系统调用: execvp()
函数族: execvp("ls", ["ls","-l",nullptr])
v
表示参数以数组形式传递p
表示自动搜索PATH环境变量waitpid()
同步机制: 父进程
├── 代码段
├── 数据段
├── 堆
├── 栈
└── 子进程副本(fork后)
└── 被execvp替换为新程序
cd
命令:if(strcmp(g_argv[0], "cd") == 0) {
chdir(g_argv[1]);
return 1; // 跳过fork
}
exit
命令:if(strcmp(g_argv[0], "exit") == 0)
exit(0);
int pipefd[2];
pipe(pipefd); // 创建管道
dup2(pipefd[1], STDOUT_FILENO); // 重定向输出
if(命令以&结尾){
不执行waitpid
处理SIGCHLD信号
}
int fd = open(file, O_RDONLY);
dup2(fd, STDIN_FILENO);
/*
* 简易Shell模拟实现
* 功能:支持基本命令提示、命令解析与执行
* 实现机制:fork-exec模型配合环境变量操作
*/
#include <iostream> // 标准输入输出流
#include <cstdio> // C标准IO库
#include <cstring> // 字符串处理函数
#include <cstdlib> // 动态内存管理、环境变量等
#include <unistd.h> // POSIX系统调用(fork, exec等)
#include <sys/types.h> // 系统数据类型定义
#include <sys/wait.h> // 进程等待相关
#define COMMAND_SIZE 1024 // 命令缓冲区大小
#define FORMAT "[%s@%s %s]# " // 提示符格式模板
// ----------------- 全局数据结构定义 -----------------
#define MAXARGC 128 // 最大参数个数
char *g_argv[MAXARGC]; // 参数指针数组(兼容main函数参数格式)
int g_argc = 0; // 参数计数器
/* 环境变量获取函数组 */
// 获取当前用户名(从环境变量USER读取)
const char *GetUserName()
{
const char *name = getenv("USER");
return name == NULL ? "None" : name; // 环境变量不存在时返回默认值
}
// 获取主机名(从环境变量HOSTNAME读取)
const char *GetHostName()
{
const char *hostname = getenv("HOSTNAME");
return hostname == NULL ? "None" : hostname;
}
// 获取当前工作目录(从环境变量PWD读取)
const char *GetPwd()
{
const char *pwd = getenv("PWD");
return pwd == NULL ? "None" : pwd;
}
/* 路径处理函数(当前版本未启用)
* 功能:从完整路径提取当前目录名
* 示例:/home/user → user
*/
std::string DirName(const char *pwd)
{
#define SLASH "/"
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);
}
/* 生成命令提示符字符串
* 参数:
* cmd_prompt - 输出缓冲区
* size - 缓冲区大小(防溢出保护)
*/
void MakeCommandLine(char cmd_prompt[], int size)
{
// 使用snprintf安全格式化字符串
snprintf(cmd_prompt, size, FORMAT,
GetUserName(), // 当前用户
GetHostName(), // 主机名
GetPwd()); // 当前工作目录
}
/* 显示命令提示符 */
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt, sizeof(prompt)); // 生成提示字符串
printf("%s", prompt); // 输出提示符
fflush(stdout); // 强制刷新缓冲区(确保立即显示)
}
/* 获取用户输入命令
* 返回值:是否成功获取有效命令
* 参数:
* out - 输出缓冲区
* size - 缓冲区大小
*/
bool GetCommandLine(char *out, int size)
{
// 使用fgets安全读取输入(相比gets可防止缓冲区溢出)
char *c = fgets(out, size, stdin);
if(c == NULL) return false; // 读取失败(如EOF)
out[strlen(out)-1] = 0; // 去除末尾换行符(\n → \0)
return strlen(out) > 0; // 过滤空输入(直接回车)
}
/* 命令解析器(核心)
* 功能:将输入字符串分割为参数数组
* 示例:"ls -l /" → ["ls", "-l", "/", NULL]
*/
bool CommandParse(char *commandline)
{
#define SEP " " // 分隔符(支持扩展为多分隔符)
g_argc = 0; // 重置参数计数器
// 使用strtok进行字符串分割
g_argv[g_argc++] = strtok(commandline, SEP); // 首次调用需指定字符串
// 循环获取后续参数(注意strtok使用nullptr继续处理原字符串)
while((g_argv[g_argc++] = strtok(nullptr, SEP)));
g_argc--; // 修正计数器(因循环最后存入NULL指针)
return true;
}
/* 调试函数:打印解析后的参数列表 */
void PrintArgv()
{
for(int i = 0; g_argv[i]; i++) {
printf("argv[%d]->%s\n", i, g_argv[i]);
}
printf("argc: %d\n", g_argc);
}
/* 命令执行引擎(核心)
* 实现机制:fork-exec模型
* 返回值:执行状态(本实现始终返回0)
*/
int Execute()
{
pid_t id = fork(); // 创建子进程
if(id == 0)
{
// 子进程分支
// 执行程序替换(注意argv必须以NULL结尾)
execvp(g_argv[0], g_argv);
// 只有exec失败时会执行到这里
exit(1); // 非正常退出(错误码1)
}
// 父进程分支
pid_t rid = waitpid(id, nullptr, 0); // 阻塞等待子进程结束
(void)rid; // 消除未使用变量警告(实际应检查返回值)
return 0;
}
/* 主控流程 */
int main()
{
// 主循环:REPL(Read-Eval-Print Loop)模式
while(true)
{
// 1. 显示命令提示符
PrintCommandPrompt();
// 2. 获取用户输入
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline, sizeof(commandline)))
continue; // 跳过无效输入
// 3. 解析命令参数
CommandParse(commandline);
// PrintArgv(); // 调试用
// 4. 执行命令
Execute();
}
return 0; // 理论上不会执行到这里
}
GetUserName()
、GetHostName()
、GetPwd()
三剑客组成,通过getenv
系统函数获取环境变量值,为命令提示符提供数据支持MakeCommandLine()
配合PrintCommandPrompt()
,使用安全格式化函数snprintf
生成类似[user@host dir]#
的标准提示符GetCommandLine()
实现三步处理: fgets
防溢出)\n
→\0
)CommandParse()
使用strtok
进行字符串分割: nullptr
继续处理main()
函数兼容的argv
格式Execute()
实现经典fork-exec模型:while(true) {
显示提示 → 获取输入 → 解析命令 → 执行命令
}
NULL
继续处理原字符串\0
分割字符串,返回每个token的起始地址v
:参数以数组形式传递(需NULL结尾)p
:自动搜索PATH环境变量中的可执行文件if(strcmp(g_argv[0], "cd") == 0) {
chdir(g_argv[1]); // 实现目录切换
return; // 跳过fork-exec
}
int pipefd[2];
pipe(pipefd); // 创建管道
dup2(pipefd[1], STDOUT_FILENO); // 输出重定向
signal(SIGINT, [](int){ /* 处理Ctrl+C */ });
通过这个微型Shell的实现,我们掌握了以下核心技能:
getenv
的灵活使用
fork-exec-wait
黄金三角
为什么路径已经更换了但是前面的命令行提示符没有反应?
实际上是先变路径,然后变环境变量。需要shell自己去更新pwd
这些环境变量,然后就可以显示正常了