本文逐模块、逐函数解析你提交的 Shell 源码(包含提示符、命令读取、解析、内建命令、环境变量、重定向与执行流程),用行为级说明帮助读者完全理解每一行代码在运行时做了什么。
GetCommandLine、CommandPrase)
cd、echo、export)
Initenv)
RedirCheck 与 TrimSpace)
Excute:fork、dup2、execvp、waitpid)
main)的执行流程与行为
Shell 是一个简单的命令行解释器,核心功能包括:
[user@host dirname]#)
<、>、>>)
argv(strtok)
cd、echo、export
fork()→子进程 dup2 重定向 → execvp() 执行 → 父进程 waitpid() 回收并记录退出码
下面按模块逐一解析。
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]#"
#define MAXARGC 128
char cwd[1024];
char cwdenv[2024];
char* g_argv[MAXARGC];
int g_argc = 0;
int lastcode = 0; // 记录最后一次命令退出码
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
int redir = NONE_REDIR;
std::string filename;cwd / cwdenv:存放当前工作目录和构造出的 PWD=... 字符串(后续用 putenv)。
g_argv / g_argc:全局保存当前命令的 argv/argc,便于 CheckAndExcBuiltin、Excute 使用。
lastcode:保存上一次执行外部命令的退出码(供 echo $? 查询)。
g_env:用来保存从父进程继承的环境变量字符串副本,并在初始化时 putenv 到当前进程环境中。
redir / filename:记录当前命令的重定向类型与目标文件名。
这些函数为提示符和环境变量准备信息。
Getusername()const char* Getusername(){
const char* name = getenv("USER");
return name == NULL ? "None" : name;
}USER 获取用户名;若缺失返回 "None"。
Gethostname()const char* Gethostname() {
static char hostname[256];
if (gethostname(hostname, sizeof(hostname)) == 0) return hostname;
else return "None";
}gethostname() 系统调用获得主机名(比从环境变量取更可靠)。
GetPwd()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;
}getcwd() 获取当前目录到 cwd,并把 PWD=... 字符串写入 cwdenv,再 putenv(cwdenv) 把 PWD 导出到当前进程环境。
GetHome()const char* GetHome(){
const char* home = getenv("HOME");
return home == NULL ? "" : home;
}HOME 环境变量的值(如果存在)。
DirName(const char* pwd)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);
}/home/hu/test → test),用于提示符显示当前目录名。
函数 MakeCommandLine 与 PrintCommandPrompt:
void MakeCommandLine(char cmd_prompt[], int size) {
snprintf(cmd_prompt, size, FORMAT,
Getusername(), Gethostname(), DirName(GetPwd()).c_str());
}
void PrintCommandPrompt(){
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt, sizeof(prompt));
printf("%s", prompt);
fflush(stdout);
}FORMAT = "[ %s@%s %s ]#",生成类似 [hu@host dir]# 的提示符并打印到标准输出(立即 flush)。
GetCommandLinebool 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;
}fgets() 读取一行(包含换行),去掉换行并忽略空行。
false 表示没有有效输入(例如按 Ctrl+D 或空行)。
CommandPrasebool CommandPrase(char* commandline){
g_argc = 0;
g_argv[g_argc++] = strtok(commandline, " ");
while ((bool)(g_argv[g_argc++] = strtok(nullptr, " ")));
g_argc--;
return g_argc > 0 ? true : false;
}strtok 以单个空格 " " 为分隔符依次拆分 token 并把指针放入 g_argv。
g_argv 最后会以 NULL 结尾。(g_argc 最后被修正为实际个数)
strtok 忽略。
g_argv[0] 是命令名。
PrintArgv调试函数,打印当前 g_argv 列表与 g_argc。
cd、echo、export)这些命令在父进程中执行(非 fork 执行),因为它们需要影响 Shell 本身的状态或环境。
Cd()cd 没参数或 cd ~:切换到 HOME(通过 GetHome())。
cd -:尝试读取 OLDPWD 并切换到上一次目录(函数中会打印并 chdir(old_pwd))。
cd path:调用 chdir(path)。
Cd() 基于 g_argv[1] 的值决定行为并调用 chdir() 执行切换。
Echo()echo $?:打印 lastcode(上一次外部命令退出码)。
echo $VAR:打印环境变量 VAR 的值(通过 getenv)。
echo 普通字符串:逐参数输出并在参数间加入空格,最后换行。
g_argc 为 1 时不输出任何内容(你的代码先判 g_argc > 1)。
Export()(导出环境变量)environ 中的所有已导出环境变量(declare -x ... 格式)。
=(如 VAR=val),用 setenv(key,val,1) 设定并导出;
=(如 export VAR),取当前 getenv(VAR) 的值(若存在)并用 setenv(VAR, val? val: "", 1) 导出(若不存在则导出空值)。
setenv 来确保变量加入当前进程环境。
Initenv()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++] = (char*)"TEST=test";
g_env[g_envs] = NULL;
for (int i = 0; g_env[i]; i++){
putenv(g_env[i]);
}
}environ(父进程环境)复制每一项到 g_env(通过 malloc 分配内存并 strcpy 内容)。
"TEST=test" 这一项。
putenv(g_env[i]),把这些字符串(格式 KEY=VAL)加入到当前进程的环境表中。
运行结果/行为:
TEST=test。
RedirCheck 与 TrimSpace这部分处理命令行中的重定向符号 <、>、>>,并从命令字符串中分离出目标文件名。
TrimSpacevoid TrimSpace(char cmd[], int &end){
while(isspace(cmd[end])){
end++;
}
}end 前移,跳过空白字符,直到遇到第一个非空白字符。注意它改变的是索引 end,并期望后续由 filename=commandline+end 使用。
RedirCheck核心流程(按代码逻辑):
redir=NONE_REDIR; filename.clear();
start = 0; end = strlen(commandline)-1;
while(end > start):
<:将 commandline[end++] = 0;(在该位置写 \0 截断命令),调用 TrimSpace,设置 redir=INPUT_REDIR,filename = commandline + end,break。
>:检测 commandline[end-1] == '>' 判断是 >>(追加)还是单 >(覆盖)。相应地在命令串中写入 \0 来截断,调整 end 位置,TrimSpace,设置 redir 为 APPEND_REDIR 或 OUTPUT_REDIR,把 filename 指向 commandline+end。
end-- 继续向左扫描。
行为说明:
'\0'),并把文件名部分(跳过空格后的内容)记录到 filename。
CommandPrase 时,命令字符串已被截断,g_argv 不会包含重定向与文件名(因为这些字符已被置 \0)。
Excute()int Excute(){
pid_t id = fork();
if (id == 0) {
// child: 处理重定向
if (redir == INPUT_REDIR) {
int fd = open(filename.c_str(), O_RDONLY);
if (fd < 0) exit(1);
dup2(fd, 0);
close(fd);
} else if (redir == OUTPUT_REDIR) {
int fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (fd < 0) exit(2);
dup2(fd, 1);
close(fd);
} else if (redir == APPEND_REDIR) {
int fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
if (fd < 0) exit(2);
dup2(fd, 1);
close(fd);
}
// 执行命令替换(execvp)
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;
}行为详解:
fork():父子复制地址空间(写时复制),父进程继续;子进程负责把自己设置好并 exec 外部程序。
open 目标文件(只读)→ dup2(fd,0) 把 stdin 重定向到该文件描述符 → close(fd)。
open(..., O_CREAT|O_WRONLY|O_TRUNC,0666) → dup2(fd,1) 把 stdout 指向文件。
O_APPEND 打开并 dup2 到 1。
execvp(g_argv[0], g_argv):用 PATH 搜索并执行命令;若成功,当前进程映像被替换,不再返回到这段代码;若失败,execvp 返回并子进程执行 exit(1).
waitpid(id, &status, 0) 等待子进程结束;若 waitpid 返回成功,用 WEXITSTATUS(status) 取出子进程退出码写入 lastcode。
退出码含义(按你实现的约定):
open 失败而 exit(1) 或 exit(2),父进程会把对应的退出码收集到 lastcode。
execvp 找不到命令,子进程 exit(1)(或更复杂的失败场景),父进程的 lastcode 反映这个退出码。
main)的执行流程与行为主函数核心逻辑(精简版):
Initenv();
while(true) {
PrintCommandPrompt();
if (!GetCommandLine(commandline, sizeof(commandline))) continue;
RedirCheck(commandline);
printf("redir:%d,filename:%s\n", redir, filename.c_str());
if (!CommandPrase(commandline)) continue;
if (CheckAndExcBuiltin()) continue;
Excute();
}逐步行为说明:
Initenv(),把继承的环境变量放到 g_env 并 putenv。
[user@host dir]#。
fgets),去掉换行,忽略空行。
RedirCheck 分离重定向并记录 redir、filename,并在主循环中打印 redir:..., filename:... 便于观察命令解析结果。
CommandPrase 将截断后的命令行分割成 g_argv 参数列表。
g_argv[0] 是 cd/echo/export 等内建命令,通过 CheckAndExcBuiltin 在父进程直接执行并回到循环(不 fork)。
Excute():fork 出子进程,子进程处理重定向并 execvp 外部命令,父进程等待子结束并记录退出码到 lastcode。
注意(代码行为观察):
RedirCheck/CommandPrase 的重复调用(在你提交代码里这一对出现了两次)。因此在实际运行中会执行这些解析两次,且会打印两次 redir:...,filename:...。这是代码的实际行为——解释器会把同一输入按相同顺序处理两次(打印两次解析信息),然后继续后续逻辑。
下面给出几个典型的交互示例,展示命令从输入到执行的行为:
cd、echo输入:
[hu@host test]# cd ..行为:
GetCommandLine 读取 "cd ..".
RedirCheck 没有找到重定向,redir = NONE_REDIR。
CommandPrase 解析为 g_argv[0] = "cd", g_argv[1] = "..".
CheckAndExcBuiltin() 识别 cd,调用 Cd() 在父进程 chdir(".."),并返回到主循环(不 fork)。
输入:
[hu@host test]# echo $HOME行为:
g_argv[0] = "echo", g_argv[1] = "$HOME"
Echo() 分析 $HOME,调用 getenv("HOME") 并输出结果。
输入:
[hu@host test]# ls -l > out.txt行为:
RedirCheck 从字符串尾部发现 >,截断命令,把 filename = "out.txt",并把 redir = OUTPUT_REDIR。
redir:2,filename:out.txt(如代码所示会打印两遍)。
CommandPrase 得到 g_argv = {"ls","-l",NULL}。
CheckAndExcBuiltin() 返回 false → Excute():
fork() 子进程
out.txt(O_CREAT|O_WRONLY|O_TRUNC,0666),dup2(fd,1) 把标准输出重定向为文件
execvp("ls", g_argv) 执行 ls -l,输出写入 out.txt
waitpid 等待子结束并取出退出码到 lastcode
输入:
[hu@host test]# ./myfile log1.txt(若 myfile 实现 open argv[1] 并 dup2(fd,0) 的那类) 行为:
g_argv[0] = "./myfile", g_argv[1] = "log1.txt"
Excute() 中子进程会执行 open("log1.txt",O_RDONLY)、dup2(fd,0)、execvp("./myfile", g_argv)。注意这里你的 Shell 也支持 cmd < file 形式:若用户输入 cmd < file,RedirCheck 会把 filename 置为 file,子进程在 Excute 中执行 dup2(fd,0),然后 execvp(g_argv[0], g_argv)。
[user@host dir]# 时,GetPwd() 已经把 PWD 写入到环境中(putenv(cwdenv))。
g_argv,以便外部命令 execvp 使用。
cd、export),外部命令通过 fork/exec 执行并通过 waitpid 获取退出码以供 $? 查询。
dup2),不会污染父进程的 stdin/stdout,这保证了 Shell 的持续可用性。
environ 继承变量并放到 g_env,通过 putenv 与 setenv 建立在当前进程中的环境视图。
RedirCheck 与 CommandPrase 在主循环中出现了重复调用语句(会导致两次打印 redir 信息)。这是代码在运行时实际表现出来的行为。
Echo()、Export()、Cd() 都通过读取/修改环境或调用 chdir() 在父进程完成(内建命令的标准做法)。
Initenv() 将父进程环境复制到 g_env 并通过 putenv 导入当前进程,程序增加了一个 TEST=test 条目。
Excute() 对重定向失败使用硬编码退出状态(如 exit(1) 或 exit(2)),再由父进程的 WEXITSTATUS 读取并放入 lastcode。
这份源码实现了一个结构清晰、功能完整的迷你 Shell:提示符、行读取、解析、内建命令、环境处理、重定向与 fork/exec 执行都具备。通过 g_argv 全局数组、redir/filename 状态和 lastcode 的设计,shell 在父进程与子进程职责分工上实现了典型行为:内建命令直接生效、外部命令在子进程受重定向影响并通过 execvp 执行。