当前位置: 首页 > news >正文

迷你版Shell:源码详解与行为解析

前言

本文逐模块、逐函数解析你提交的 Shell 源码(包含提示符、命令读取、解析、内建命令、环境变量、重定向与执行流程),用行为级说明帮助读者完全理解每一行代码在运行时做了什么。


目录(快速导航)

  1. 概览

  2. 全局变量与常量

  3. 辅助函数(用户名、主机名、PWD、Home、目录名)

  4. 提示符生成与打印

  5. 获取命令行与解析(GetCommandLineCommandPrase

  6. 内建命令(cdechoexport

  7. 环境变量初始化(Initenv

  8. 重定向解析(RedirCheckTrimSpace

  9. 外部命令执行(Excuteforkdup2execvpwaitpid

  10. 主循环(main)的执行流程与行为

  11. 运行示例(典型交互)

  12. 小结:运行时画面与全局状态


1. 概览

Shell 是一个简单的命令行解释器,核心功能包括:

  • 显示格式化 Prompt([user@host dirname]#

  • 读取一行用户输入

  • 解析重定向(<>>>

  • 将命令按空格切分为 argvstrtok

  • 执行内建命令:cdechoexport

  • 对外部命令使用 fork()→子进程 dup2 重定向 → execvp() 执行 → 父进程 waitpid() 回收并记录退出码

下面按模块逐一解析。


2. 全局变量与常量(代码片段含义)

#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,便于 CheckAndExcBuiltinExcute 使用。

  • lastcode:保存上一次执行外部命令的退出码(供 echo $? 查询)。

  • g_env:用来保存从父进程继承的环境变量字符串副本,并在初始化时 putenv 到当前进程环境中。

  • redir / filename:记录当前命令的重定向类型与目标文件名。


3. 辅助函数(用户名、主机名、PWD、Home、目录名)

这些函数为提示符和环境变量准备信息。

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/testtest),用于提示符显示当前目录名。


4. 提示符生成与打印

函数 MakeCommandLinePrintCommandPrompt

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

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

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. 内建命令(cdechoexport

这些命令在父进程中执行(非 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()

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. 重定向解析:RedirCheckTrimSpace

这部分处理命令行中的重定向符号 <>>>,并从命令字符串中分离出目标文件名。

TrimSpace

void TrimSpace(char cmd[], int &end){while(isspace(cmd[end])){end++;}
}
  • 把指针 end 前移,跳过空白字符,直到遇到第一个非空白字符。注意它改变的是索引 end,并期望后续由 filename=commandline+end 使用。

RedirCheck

核心流程(按代码逻辑):

  1. 初始化:redir=NONE_REDIR; filename.clear();

  2. start = 0; end = strlen(commandline)-1;

  3. 从字符串尾向前扫描 while(end > start)

    • 若遇到 <:将 commandline[end++] = 0;(在该位置写 \0 截断命令),调用 TrimSpace,设置 redir=INPUT_REDIRfilename = commandline + endbreak

    • 若遇到 >:检测 commandline[end-1] == '>' 判断是 >>(追加)还是单 >(覆盖)。相应地在命令串中写入 \0 来截断,调整 end 位置,TrimSpace,设置 redirAPPEND_REDIROUTPUT_REDIR,把 filename 指向 commandline+end

    • 否则 end-- 继续向左扫描。

行为说明:

  • 以从右向左扫描方式寻找最后出现的重定向符号,找到后把原命令字符串从该位置截断(写 '\0'),并把文件名部分(跳过空格后的内容)记录到 filename

  • 在后续 CommandPrase 时,命令字符串已被截断,g_argv 不会包含重定向与文件名(因为这些字符已被置 \0)。


9. 外部命令执行: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 打开并 dup21

  • 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)的执行流程与行为

主函数核心逻辑(精简版):

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();
}

逐步行为说明:

  1. 启动时调用 Initenv(),把继承的环境变量放到 g_envputenv

  2. 每次循环:

    • 打印提示符 [user@host dir]#

    • 读取一行命令(fgets),去掉换行,忽略空行。

    • 调用 RedirCheck 分离重定向并记录 redirfilename,并在主循环中打印 redir:..., filename:... 便于观察命令解析结果。

    • 再用 CommandPrase 将截断后的命令行分割成 g_argv 参数列表。

    • 如果 g_argv[0]cd/echo/export 等内建命令,通过 CheckAndExcBuiltin 在父进程直接执行并回到循环(不 fork)。

    • 否则调用 Excute()fork 出子进程,子进程处理重定向并 execvp 外部命令,父进程等待子结束并记录退出码到 lastcode

注意(代码行为观察)

  • 主循环里有两段 RedirCheck/CommandPrase 的重复调用(在你提交代码里这一对出现了两次)。因此在实际运行中会执行这些解析两次,且会打印两次 redir:...,filename:...。这是代码的实际行为——解释器会把同一输入按相同顺序处理两次(打印两次解析信息),然后继续后续逻辑。


11. 运行示例(典型交互)

下面给出几个典型的交互示例,展示命令从输入到执行的行为:

示例 A:执行内建命令 cdecho

输入:

[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.txtO_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 < fileRedirCheck 会把 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(如 cdexport),外部命令通过 fork/exec 执行并通过 waitpid 获取退出码以供 $? 查询。

  • 重定向行为:重定向只在子进程中生效(通过 dup2),不会污染父进程的 stdin/stdout,这保证了 Shell 的持续可用性。

  • 环境变量:Shell 启动时从 environ 继承变量并放到 g_env,通过 putenvsetenv 建立在当前进程中的环境视图。


附:代码里值得注意的运行行为(事实陈述)

  • RedirCheckCommandPrase 在主循环中出现了重复调用语句(会导致两次打印 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 执行。

http://www.dtcms.com/a/351199.html

相关文章:

  • 【Linux 34】Linux-主从复制
  • 嵌入式学习日记(34)HTTP协议
  • 支持向量机核心知识总结
  • 读懂支持向量机(SVM)
  • CI/CD 全链路实践:从 Git 基础到 Jenkins + GitLab 企业级部署
  • Flask 之上下文详解:从原理到实战
  • IDEA-Maven和Tomcat乱码问题
  • 2025改版:npm 新淘宝镜像域名地址
  • Uniapp(Vue2)Api请求封装
  • 企业级集群部署gpmall商城:MyCat+ZooKeeper+Kafka 环境部署与商城应用上线流程
  • VxWorks 核心数据结构详解 【消息队列、环形缓冲区、管道、FIFO、双缓冲区、共享内存】
  • Debian Buster 软件源失效问题
  • 在分布式环境下正确使用MyBatis二级缓存
  • 虚拟滚动优化——js技能提升
  • zookeeper-保姆级配置说明
  • http与https配置
  • 使用分流电阻器时的注意事项--PCB 设计对电阻温度系数的影响
  • Ubuntu 虚拟机配置 Git 并推送到Gitee
  • 低代码如何颠覆企业系统集成传统模式?快来一探究竟!
  • 两数之和,leetCode热题100,C++实现
  • 2025年视觉、先进成像和计算机技术论坛(VAICT 2025)
  • LeetCode热题100--108. 将有序数组转换为二叉搜索树--简单
  • 【Lua】题目小练11
  • Ansible 自动化运维工具:介绍与完整部署(RHEL 9)
  • 【软考论文】论领域驱动开发方法(DDD)的应用
  • CentOS 7服务器初始化全攻略:从基础配置到安全加固
  • AI应用--接口测试篇
  • Maya绑定基础:驱动关键帧的使用
  • C# .NET支持多线程并发的压缩组件
  • 视频创作者如何用高级数据分析功能精准优化视频策略