前言
本文逐模块、逐函数解析你提交的 Shell 源码(包含提示符、命令读取、解析、内建命令、环境变量、重定向与执行流程),用行为级说明帮助读者完全理解每一行代码在运行时做了什么。
目录(快速导航)
-
概览
-
全局变量与常量
-
辅助函数(用户名、主机名、PWD、Home、目录名)
-
提示符生成与打印
-
获取命令行与解析(
GetCommandLine
、CommandPrase
) -
内建命令(
cd
、echo
、export
) -
环境变量初始化(
Initenv
) -
重定向解析(
RedirCheck
与TrimSpace
) -
外部命令执行(
Excute
:fork
、dup2
、execvp
、waitpid
) -
主循环(
main
)的执行流程与行为 -
运行示例(典型交互)
-
小结:运行时画面与全局状态
1. 概览
Shell 是一个简单的命令行解释器,核心功能包括:
-
显示格式化 Prompt(
[user@host dirname]#
) -
读取一行用户输入
-
解析重定向(
<
、>
、>>
) -
将命令按空格切分为
argv
(strtok
) -
执行内建命令:
cd
、echo
、export
-
对外部命令使用
fork()
→子进程dup2
重定向 →execvp()
执行 → 父进程waitpid()
回收并记录退出码
下面按模块逐一解析。
2. 全局变量与常量(代码片段含义)
cpp
#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
:记录当前命令的重定向类型与目标文件名。
3. 辅助函数(用户名、主机名、PWD、Home、目录名)
这些函数为提示符和环境变量准备信息。
Getusername()
cpp
const char* Getusername(){
const char* name = getenv("USER");
return name == NULL ? "None" : name;
}
- 从环境变量
USER
获取用户名;若缺失返回"None"
。
Gethostname()
cpp
const char* Gethostname() {
static char hostname[256];
if (gethostname(hostname, sizeof(hostname)) == 0) return hostname;
else return "None";
}
- 使用
gethostname()
系统调用获得主机名(比从环境变量取更可靠)。
GetPwd()
cpp
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()
cpp
const char* GetHome(){
const char* home = getenv("HOME");
return home == NULL ? "" : home;
}
- 返回
HOME
环境变量的值(如果存在)。
DirName(const char* pwd)
cpp
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
),用于提示符显示当前目录名。
4. 提示符生成与打印
函数 MakeCommandLine
与 PrintCommandPrompt
:
cpp
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)。
5. 获取命令行与解析
GetCommandLine
cpp
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;
}
-
使用
fgets()
读取一行(包含换行),去掉换行并忽略空行。 -
返回
false
表示没有有效输入(例如按 Ctrl+D 或空行)。
CommandPrase
cpp
bool 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
。
6. 内建命令(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
来确保变量加入当前进程环境。
7. 环境变量初始化:Initenv()
cpp
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
)加入到当前进程的环境表中。
运行结果/行为:
- 初始化时,当前进程获得与父 shell 相同的环境变量(以字符串副本形式),并增加了
TEST=test
。
8. 重定向解析:RedirCheck
与 TrimSpace
这部分处理命令行中的重定向符号 <
、>
、>>
,并从命令字符串中分离出目标文件名。
TrimSpace
cpp
void 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
)。
9. 外部命令执行:Excute()
cpp
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
反映这个退出码。
10. 主循环(main
)的执行流程与行为
主函数核心逻辑(精简版):
cpp
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:...
。这是代码的实际行为------解释器会把同一输入按相同顺序处理两次(打印两次解析信息),然后继续后续逻辑。
11. 运行示例(典型交互)
下面给出几个典型的交互示例,展示命令从输入到执行的行为:
示例 A:执行内建命令 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")
并输出结果。
示例 B:外部命令 + 输出重定向
输入:
[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
-
示例 C:输入重定向
输入:
[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)
。
12. 小结:运行时画面与全局状态
-
Prompt :当显示
[user@host dir]#
时,GetPwd()
已经把PWD
写入到环境中(putenv(cwdenv)
)。 -
命令分离 :你的解析流程先做重定向分离、再 token 化;命令参数保存在
g_argv
,以便外部命令execvp
使用。 -
内建 vs 外部 :内建命令在父进程执行,直接影响 Shell(如
cd
、export
),外部命令通过fork/exec
执行并通过waitpid
获取退出码以供$?
查询。 -
重定向行为 :重定向只在子进程中生效(通过
dup2
),不会污染父进程的stdin/stdout
,这保证了 Shell 的持续可用性。 -
环境变量 :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
执行。