目录
[1. 引言](#1. 引言)
[2. Shell 的工作流程(主循环)](#2. Shell 的工作流程(主循环))
[3. 全局数据结构设计](#3. 全局数据结构设计)
[4. 命令行提示符的生成](#4. 命令行提示符的生成)
[4.1 获取用户名、主机名](#4.1 获取用户名、主机名)
[4.2 获取当前工作目录(并更新环境变量 PWD)](#4.2 获取当前工作目录(并更新环境变量 PWD))
[4.3 提取最后一级目录名](#4.3 提取最后一级目录名)
[4.4 组装提示符](#4.4 组装提示符)
[5. 命令读取与解析](#5. 命令读取与解析)
[5.1 读取一行输入](#5.1 读取一行输入)
[5.2 解析命令行(拆分为参数数组)](#5.2 解析命令行(拆分为参数数组))
[6. 内建命令详解](#6. 内建命令详解)
[6.1 cd 命令(支持 cd、cd ~、cd -、cd 路径)](#6.1 cd 命令(支持 cd、cd ~、cd -、cd 路径))
[6.2 echo 命令(支持普通字符串、?、环境变量)](#6.2 echo 命令(支持普通字符串、?、环境变量))
[6.3 其他内建命令(扩展点)](#6.3 其他内建命令(扩展点))
[6.4 内建命令的调度](#6.4 内建命令的调度)
[7. 外部命令的执行(fork + execvp + waitpid)](#7. 外部命令的执行(fork + execvp + waitpid))
[8. 环境变量的初始化](#8. 环境变量的初始化)
[9. 完整运行示例](#9. 完整运行示例)
[10. 总结与扩展思考](#10. 总结与扩展思考)
[10.1 已实现的核心功能](#10.1 已实现的核心功能)
[10.2 可进一步扩展的功能](#10.2 可进一步扩展的功能)
[10.3 知识巩固](#10.3 知识巩固)
1. 引言
Shell 是用户与Linux内核之间的桥梁。它不断读取用户输入的命令,解析后执行------普通命令通过创建子进程运行,内建命令则由Shell自身处理。理解Shell的工作原理,是掌握进程创建、进程等待、程序替换、环境变量管理等核心概念的最佳实践。
本文将基于您提供的完整代码,逐行剖析一个功能完备的迷你Shell,包括:
-
命令行提示符的自定义(支持
[user@hostname current_dir]#格式) -
命令的读取、解析、参数拆分
-
内建命令的实现:
cd(含cd -、cd ~)、echo $?、echo $变量 -
外部命令的执行(
fork + execvp + waitpid) -
退出码
$?的维护 -
环境变量的初始化与继承
-
程序整体结构与执行流程
该Shell虽然简洁,但已具备最基本的交互能力,可以作为理解bash等复杂Shell的起点。
2. Shell 的工作流程(主循环)
一个Shell的核心是无限循环,每一步处理一个命令行:
c
int main()
{
InitEnv(); // 初始化环境变量
while (1) {
PrintCommandPrompt(); // 1. 显示提示符
GetCommandLine(); // 2. 获取用户输入
CommandParse(); // 3. 解析命令(拆分为argc/argv)
if (CheckAndExecBuiltin()) // 4. 内建命令处理
continue;
Execute(); // 5. 外部命令执行(fork + exec)
}
return 0;
}
以下将逐一深入每个模块。
3. 全局数据结构设计
c
#define COMMAND_SIZE 1024
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc = 0;
#define MAX_ENVS 100
char* g_env[MAX_ENVS];
int g_envs = 0;
int lastcode = 0; // 保存上一条命令的退出码(对应 $?)
char LAST_PATH[PATH_MAX]; // 用于 cd - 保存上一个路径
-
g_argv/g_argc:解析命令行后得到的参数数组和参数个数,供execvp使用。 -
g_env:Shell自身维护的环境变量表(从父Shell继承,可扩展)。 -
lastcode:每次执行外部命令后,通过WEXITSTATUS获取子进程退出码,供echo $?使用。 -
LAST_PATH:实现cd -时需要记住切换前的路径。
4. 命令行提示符的生成
我们希望提示符格式为 [user@hostname current_dir]#,其中 current_dir 只显示路径的最后一个目录名(如 /home/user/project 显示为 project)。
4.1 获取用户名、主机名
c
const char* GetUserName() {
const char* name = getenv("USER");
return name == NULL ? "None" : name;
}
const char* GetHostName() {
const char* hostname = getenv("HOSTNAME");
return hostname == NULL ? "None" : hostname;
}
4.2 获取当前工作目录(并更新环境变量 PWD)
c
const char* GetPwd() {
const char* pwd = getcwd(cwd, sizeof(cwd)); // 系统调用,获取绝对路径
if (pwd != NULL) {
snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
putenv(cwdenv); // 更新环境变量 PWD
}
return pwd == NULL ? "None" : pwd;
}
注意:
chdir后必须同步更新PWD,否则子进程会得到错误的当前目录。这里通过putenv直接修改环境变量。
4.3 提取最后一级目录名
c
std::string DirName(const char* pwd) {
std::string dir = pwd;
if (dir == "/") return "/";
auto pos = dir.rfind('/');
if (pos == std::string::npos) return "BUG?";
return dir.substr(pos + 1);
}
4.4 组装提示符
c
void MakeCommandLine(char cmd_prompt[], int size) {
snprintf(cmd_prompt, size, "[%s@%s %s]# ",
GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
}
void PrintCommandPrompt() {
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt, sizeof(prompt));
printf("%s", prompt);
fflush(stdout);
}
5. 命令读取与解析
5.1 读取一行输入
c
bool GetCommandLine(char* commandline, int size) {
char* c = fgets(commandline, size, stdin);
if (c == NULL) return false; // EOF 或错误
commandline[strlen(commandline) - 1] = 0; // 去掉末尾换行符
if (strlen(commandline) == 0) return false;
return true;
}
5.2 解析命令行(拆分为参数数组)
使用 strtok 按空格分隔,填充 g_argv,并维护 g_argc:
c
bool CommandParse(char* commandline) {
g_argc = 0;
g_argv[g_argc++] = strtok(commandline, " ");
while ((bool)(g_argv[g_argc++] = strtok(nullptr, " ")));
g_argc--; // 最后一个 NULL 不计入 argc
return g_argc > 0;
}
strtok会修改原字符串,因此不能直接传入只读字符串。这里每次读取到commandline数组后即可安全拆分。
6. 内建命令详解
内建命令必须由Shell自身执行,不能通过子进程(否则无法改变Shell自身状态,如 cd 无效)。
6.1 cd 命令(支持 cd、cd ~、cd -、cd 路径)
c
bool Cd() {
if (g_argc == 1) { // 无参数 -> 回到 HOME
std::string home = GetHome();
if (home.empty()) return true;
strcpy(LAST_PATH, GetPwd());
chdir(home.c_str());
} else {
std::string where = g_argv[1];
if (where == "-") {
char NOW_PATH[PATH_MAX];
strcpy(NOW_PATH, GetPwd());
chdir(LAST_PATH);
strcpy(LAST_PATH, NOW_PATH);
} else if (where == "~") {
std::string home = GetHome();
if (home.empty()) return true;
strcpy(LAST_PATH, GetPwd());
chdir(home.c_str());
} else {
char NOW_PATH[PATH_MAX];
strcpy(NOW_PATH, GetPwd());
if (chdir(where.c_str()) == 0) {
strcpy(LAST_PATH, NOW_PATH);
}
}
}
return true;
}
关键点:
-
切换目录使用
chdir系统调用,它影响的是Shell自身(父进程)的当前目录。 -
cd -需要在切换前保存当前路径到LAST_PATH,然后切换到旧的LAST_PATH,再更新LAST_PATH为之前的当前路径。 -
cd ~或cd无参数时回到HOME目录。 -
每次成功
chdir后,下次调用GetPwd会通过getcwd重新获取并更新PWD环境变量,因此cd后提示符中的目录会自动变化。
6.2 echo 命令(支持普通字符串、$?、$环境变量)
c
void Echo() {
if (g_argc == 2) {
std::string opt = g_argv[1];
if (opt == "$?") {
std::cout << lastcode << std::endl;
lastcode = 0;
} 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(实际bash中不会清空,但这里简化)。 -
$PATH之类:提取变量名,用getenv获取环境变量的值并输出。 -
普通字符串直接输出。
6.3 其他内建命令(扩展点)
代码中预留了 export 和 alias 的框架,但未完整实现。一个完整的Shell还应支持:
-
export VAR=value:向g_env添加变量,并调用putenv使其生效。 -
alias:命令别名。
6.4 内建命令的调度
c
bool CheckAndExecBuiltin() {
std::string cmd = g_argv[0];
if (cmd == "cd") { Cd(); return true; }
if (cmd == "echo") { Echo(); return true; }
// if (cmd == "export") { ... return true; }
return false;
}
如果匹配到内建命令,返回 true,主循环中 continue 跳过后续的 Execute;否则进入外部命令执行。
7. 外部命令的执行(fork + execvp + waitpid)
这是普通命令(如 ls、ps、grep)的执行方式:
c
int Execute() {
pid_t id = fork();
if (id == 0) {
// 子进程
execvp(g_argv[0], g_argv);
exit(1); // exec 失败才会走到这里
}
// 父进程阻塞等待
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0) {
lastcode = WEXITSTATUS(status); // 保存退出码
}
return 0;
}
注意:
-
execvp会根据PATH环境变量自动搜索可执行文件,无需写全路径。 -
子进程
exec成功后不会返回,失败则调用exit(1),父进程通过waitpid获取其退出码,存储到lastcode。 -
父进程必须等待,否则子进程会变成僵尸进程。
8. 环境变量的初始化
Shell启动时需要继承父Shell(如bash)的环境变量,以便后续子进程能够获得正确的环境。
c
void InitEnv() {
extern char** environ; // 全局环境变量数组,由启动时设置
memset(g_env, 0, sizeof(g_env));
g_envs = 0;
// 1. 复制父Shell的环境变量
for (int i = 0; environ[i]; i++) {
g_env[i] = (char*)malloc(strlen(environ[i]) + 1);
strcpy(g_env[i], environ[i]);
g_envs++;
}
// 2. 添加一个测试变量(可选)
g_env[g_envs++] = (char*)"HAHA=for_test";
g_env[g_envs] = NULL;
// 3. 将 g_env 中的变量全部导入到当前进程的环境
for (int i = 0; g_env[i]; i++) {
putenv(g_env[i]);
}
// 4. 可选:修改全局 environ 指向,但 putenv 已经生效
// environ = g_env;
}
关键点:
-
extern char **environ是程序启动时从父进程继承的完整环境变量数组。 -
先复制到
g_env,再逐个putenv,确保子进程exec后能获得这些环境变量。 -
如果后续实现
export内建命令,则需要修改g_env并再次调用putenv或直接操作environ。
9. 完整运行示例
编译运行该Shell后,交互效果如下(假设可执行文件名为 myshell):
bash
$ ./myshell
[user@host project]# ls -l
total 8
-rwxr-xr-x 1 user user 12345 Dec 1 10:00 myshell
[user@host project]# cd ..
[user@host home]# pwd
/home/user
[user@host home]# cd -
[user@host project]# pwd
/home/user/project
[user@host project]# echo $?
0
[user@host project]# export MYVAR=hello # 暂未实现,但可扩展
[user@host project]# echo $MYVAR
hello
[user@host project]# ls notexist
ls: cannot access 'notexist': No such file or directory
[user@host project]# echo $?
2
10. 总结与扩展思考
10.1 已实现的核心功能
-
提示符动态显示用户名、主机名、当前目录名
-
命令行读取、解析、参数拆分
-
内建命令
cd(含cd -、cd ~)和echo(含$?,$VAR) -
外部命令通过
fork+execvp+waitpid执行 -
退出码维护
-
环境变量继承
10.2 可进一步扩展的功能
-
管道
|:需要连接多个子进程的输入输出,涉及pipe、dup2。 -
输入/输出重定向
><>>:在子进程exec前使用open+dup2。 -
后台运行
&:父进程不调用wait,而是将子进程放入后台列表。 -
作业控制 :
jobs、fg、bg、信号处理。 -
完整的
export和alias实现。 -
命令历史记录 (通过
readline库或自己维护数组)。
10.3 知识巩固
该迷你Shell完整地应用了:
-
进程控制(
fork/exec/wait) -
环境变量管理(
getenv/putenv/environ) -
字符串处理(
strtok,snprintf) -
文件路径操作(
chdir/getcwd/rfind)
通过亲手实现这样一个Shell,您将彻底理解命令行解释器的底层奥秘,为后续学习系统编程、网络编程、并发服务器等打下坚实基础。
附录:完整代码(已整理)
您提供的代码已经非常完整,可以直接编译运行。使用时注意:
-
编译命令:
g++ -o myshell myshell.cpp -std=c++11 -
运行:
./myshell
代码中
export部分尚未实现,读者可自行补全作为练习。
本博客完