/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}


一、全局配置与状态(代码最开头)
| basesize = 1024 | const int | 命令行缓冲区和 pwd 缓冲区大小 |
| argvnum = 64 | const int | gargv 最多存 64 个参数 |
| envnum = 64 | const int | 自维护环境表 genv 最多 64 项 |
| gargv[64] | static char*[] | 解析后的参数表,末尾必须 NULL(exec 族要求) |
| gargc | static int | 参数个数 |
| lastcode | static int | 上一条命令退出码,echo $? 就读它 |
| genv[64] | static char*[] | shell 自己维护的环境变量表(后面重点讲) |
| pwd[1024] / pwdenv[1024] | static char[] | 当前目录 / 拼好的 "PWD=xxx" 字符串 |
几个设计要点:
- 这些都是全局 static,生命周期等于整个程序,后续函数随便用。
- gargv 是 char* 数组,指向命令行缓冲区内部的各个单词(strtok 原地切出来的),不是拷贝。
- lastcode 是整个 shell 的状态核心——echo $? 的 $? 不是系统变量,就是打印这个变量。
二、提示符函数组(6 个函数,作用一条链)
GetUserName → GetHostName → GetPwd → LastDir → MakeCommandLine → PrintCommandLine
GetUserName / GetHostName:
getenv("USER")、getenv("HOSTNAME"),从进程环境表读用户名和主机名。
找不到返回 "None"。
注意 getenv 返回的指针是只读的(指向 environ 表内部),不要尝试修改。
GetPwd(含一个关键动作):
if (getcwd(pwd, sizeof(pwd)) == NULL) return "None";
snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
putenv(pwdenv);
- getcwd 是系统调用:问内核"我的当前工作目录在哪",结果写入 pwd。
- snprintf 拼出 "PWD=/home/xx" 存进全局 pwdenv。
- putenv(pwdenv) 把这个字符串写回进程的真实环境表(注意:写的是 pwd 里的,不是 genv,这就是后面的坑之一,详见第八节)。
| 头文件 | <unistd.h> | <stdlib.h> |
| 获取对象 | 当前工作目录(绝对路径) | 环境变量的值 |
| 结果存哪 | 你自己的缓冲区(或动态分配) | 进程环境块里的内部静态区 |
| 谁管生命周期 | 调用者 | 进程环境(后续调用可能覆盖) |
| 失败表现 | 返回 NULL + errno(ERANGE 缓冲区太小 / EACCES 无权限 / ENOENT 目录被删) | 返回 NULL,无 errno 区分 "不存在" |
| 对应的修改函数 | chdir() | setenv() / putenv() |
LastDir:
取最后一个 / 后面的部分做提示符目录显示(/home/xx → xx)。
MakeCommandLine:拼成 [user@host dir]# 。
PrintCommandLine:
printf("%s", MakeCommandLine().c_str());
fflush(stdout); // 必须 fflush,因为提示符没 \\n
这是难点:
printf 默认是行缓冲(有 \\n 才真正输出),提示符没有换行,所以必须手动 fflush,否则你输入命令时提示符还憋在缓冲区里没显示出来。
三、GetCommandLine:读一行
if (fgets(buf, size, stdin) == NULL) { printf("\\n"); return false; }
buf[strlen(buf) – 1] = '\\0'; // 去掉 fgets 读进来的 \\n
if (strlen(buf) == 0) return false;
- fgets 读到换行或缓冲区满为止,换行会保留在 buf 里,所以要手动把最后一位(\\n)换成 '\\0'。
- 三个返回值语义:
- 正常一行 → true
- 空行(直接回车,去 \\n 后为空)→ false,主循环 continue 重新打印提示符
- EOF(Ctrl+D)→ 打印换行、false——这里埋了个 bug,见第八节。
四、ParseCommandLine:strtok 拆词(难点①)
const char *sep = " ";
gargv[gargc++] = strtok(buf, sep); // 第一次调用传 buf
while ((gargv[gargc++] = strtok(NULL, sep)) != NULL)
;
gargc–;
strtok 的工作原理(这是最值得深挖的地方):
举例:
输入 ls -l /home,解析后内存里是 ls\\0-l\\0/home\\0,gargv = {"ls", "-l", "/home", NULL}。
最后那个 gargc– 是因为 while 循环在赋 NULL 之后又把 gargc 加了一次(循环条件先赋值后判断),所以要多退一格,但 NULL 已经写进数组——这正是 exec 族需要的结尾标记。
潜在崩溃点:
如果输入是纯空格(如 " "),第一个 strtok 返回 NULL,gargv[0]=NULL,后面 CheckAndExecBuiltCommand 里 strcmp(gargv[0], "exit") 会对 NULL 做 strcmp → 段错误。
五、环境变量:InitEnv / AddEnv(难点②:三套环境表)
这个程序里其实有三套环境,初学者最容易混淆:
| environ |
内核 + libc 维护的进程真实环境表 (extern char **environ,以 NULL 结尾) |
进程启动时由 exec 传入 | getenv / putenv 操作它 |
| genv | shell 自己 malloc 拷贝的一份 | InitEnv 从 environ 拷贝,export 追加 | 传给 execvpe 给子进程 |
| pwdenv | 全局静态数组 | GetPwd 每次拼 PWD=xxx 并 putenv | 只写回 environ,不进 genv |
InitEnv:
extern char **environ; // 声明:进程真实环境表
for (; environ[i] && i < envnum – 1; i++) {
genv[i] = (char *)malloc(strlen(environ[i]) + 1);
strcpy(genv[i], environ[i]);
}
genv[i] = NULL;
为什么要 malloc 拷贝而不是直接 genv[i] = environ[i]?
因为 putenv、setenv 可能会移动/改写 environ 表项,拷一份自己的副本才安全。这个 shell 后续要给子进程一份"自己说了算"的环境(用户 export 加进去的变量也要在),所以它维护自己的 genv。
AddEnv:
while (genv[i]) i++; // 找到末尾
genv[i] = malloc(strlen(item)+1);
strcpy(genv[i], item);
genv[++i] = NULL;
注意它没有检查 genv 是否已满 64 项——export 超过 64 次会数组越界写(内存破坏)。
六、ExecuteCommand:fork / exec / waitpid(难点③,全程序核心)
pid_t id = fork();
if (id < 0) { perror("fork"); lastcode = 1; return; }
if (id == 0) {
execvpe(gargv[0], gargv, genv); // 子进程
perror(gargv[0]); // exec 失败才会走到这
exit(127);
}
int status = 0;
waitpid(id, &status, 0); // 父进程
if (WIFEXITED(status))
lastcode = WEXITSTATUS(status);
else if (WIFSIGNALED(status))
lastcode = 128 + WTERMSIG(status);
else
lastcode = 100;
① fork():一次调用,两次返回
- fork 会复制当前进程(写时拷贝 COW:不修改数据时父子共享物理内存页,谁先改谁复制),创建子进程。
- 返回值语义:父进程得到子进程的 PID(>0);子进程得到 0;失败得 -1。
- 判断 id == 0 就是在问"我现在是不是子进程?"——这就是 fork 后父子代码分道扬镳的经典写法。
② execvpe:exec 家族里功能最全的一个 execvpe(文件名, argv, envp) 拆开看:
- exec:用新程序替换当前进程映像——代码段、数据段、堆栈全换,PID 不变。
- v:参数以 char*[](argv 数组)传入。
- p:自动按 PATH 环境变量搜索可执行文件,所以不用写 /bin/ls,写 ls 就行。
- e:用自定义环境 genv 作为新程序的环境表(不是默认的 environ)。
关键:
exec 成功不返回——新程序跑起来后,"子进程"这个身份就变成 ls 进程了,perror 和 exit(127) 那两行永远不会执行;
只有 exec 失败(比如命令不存在)才返回 -1,打印错误并 exit(127)。
127 是 shell 约定:"命令找不到"。
③ waitpid:父进程收尸
- waitpid(id, &status, 0) 阻塞等待指定子进程结束,回收其资源(僵尸进程回收)。status 是一个 int,退出信息被打包在特定位里,必须用宏拆开:
- WIFEXITED(status):是否正常退出(调用了 exit / main 返回)→ WEXITSTATUS 取低 8 位退出码。
- WIFSIGNALED(status):是否被信号杀死(如 kill、Ctrl+C 的 SIGINT)→ 128 + WTERMSIG(status),这是 shell 惯例:被 9 号信号杀就是 137。
- 拆出的结果存进全局 lastcode,供 echo $? 读取。
七、内建命令 CheckAndExecBuiltCommand
逐个看:
exit:
直接 exit(0) 终止整个进程。
为什么 shell 的 exit 必须是内建?
因为如果是外部程序,exit 只能结束那个子进程,shell 自己还活着。
cd:
if (gargc == 2) {
if (chdir(gargv[1]) != 0) perror("cd");
} else {
chdir(getenv("HOME") ? getenv("HOME") : "/");
}
lastcode = 0;
chdir 是系统调用,只改变当前进程(shell)自己的工作目录——这也是必须内建的原因:外部子进程改了目录跟 shell 无关。
注意 lastcode = 0 无条件执行,chdir 失败也是 0(缺陷)。
export:
AddEnv(gargv[1]),把 KEY=VALUE 追加进 genv 表。
下一轮 fork 出来的子进程就会带上这个变量——这就是环境变量"继承"的实现。
env:遍历打印 genv。
echo:
if (gargc == 2) {
if (gargv[1][0] == '$' && gargv[1][1] == '?')
printf("%d\\n", lastcode);
else
printf("%s\\n", gargv[1]);
} else {
printf("\\n");
}
- echo $? 就是打印 lastcode。
- 只支持单个参数:echo a b c 会走 else 只打印换行;echo $?x 因为第二个字符是 ? 也会被误判成 $?(缺陷)。
八、main 主循环 + 已知缺陷清单
int main() {
InitEnv(); // 启动时先拷贝环境
char cmd[basesize];
while (true) {
PrintCommandLine();
if (!GetCommandLine(cmd, basesize)) continue;
ParseCommandLine(cmd);
if (CheckAndExecBuiltCommand()) continue;
ExecuteCommand();
}
}
逻辑链:提示符 → 读 → 解析 → 内建?→ 是:continue 回到顶部;否:fork 执行外部命令 → 回到顶部。
这个教学版代码里隐藏着几个真实缺陷,值得你知道(面试常被问):
| 1 | Ctrl+D 死循环 | EOF 时 GetCommandLine 返回 false,main 里却 continue 回到循环顶部,下一次 fgets 又 EOF | 刷屏死循环,Ctrl+D 根本退不出去(应改为 break 或区分 EOF/空行) |
| 2 | 纯空格输入段错误 | strtok 返回 NULL,gargv[0]=NULL,随后 strcmp(NULL,…) | 直接崩 |
| 3 | PWD 不同步 | GetPwd 用 putenv 更新的是真实 environ,而传给子进程的是 genv(启动时拷贝的旧值) | cd 之后外部命令看到的 $PWD 仍是 shell 启动时的值 |
| 4 | argv 溢出 | ParseCommandLine 不检查 gargc < argvnum | 超 64 个参数越界写 |
| 5 | export 越界 | AddEnv 不检查 genv 容量 | export 超 64 次越界写 |
| 6 | echo a b c 只输出空行 | echo 只处理 gargc==2 | 多参数被吞 |
| 7 | cd 失败也 lastcode=0 | lastcode = 0 无条件执行 | $? 不反映真实失败 |
九、编译注意
- 编译命令:g++ -o mini_shell mini_shell.cpp -std=c++11
- execvpe 是 GNU 扩展,声明在 unistd.h 的 #ifdef __USE_GNU 块里。如果编译报 execvpe was not declared,需要加宏:g++ -o mini_shell mini_shell.cpp -std=c++11 -D_GNU_SOURCE
- 它依赖 Linux/Unix 环境(fork/exec/waitpid 都是 POSIX 系统调用),Windows 上无法直接编译运行。
十、函数复习
C 语言 `<string.h>` 中以 `str` 开头的函数(C 风格字符串):
表格
| 函数 | 作用 |
| — | — |
| `strlen(s)` | 求长度(不含 `\\0`) |
| `strcpy(dest, src)` | 复制字符串(不检查容量,危险) |
| `strncpy(dest, src, n)` | 复制最多 n 个字符 |
| `strcat(dest, src)` | 拼接(不检查容量,危险) |
| `strncat(dest, src, n)` | 拼接最多 n 个字符 |
| `strcmp(s1, s2)` | 比较(<0、0、>0) |
| `strncmp(s1, s2, n)` | 比较前 n 个字符 |
| `strchr(s, c)` | 正向找字符,返回指针,找不到返回 `NULL` |
| `strrchr(s, c)` | 反向找字符 |
| `strstr(s, sub)` | 找子串,返回子串首地址指针 |
| `strtok(s, delim)` | 按分隔符切分字符串 |
| `strdup(s)` | 复制一份新字符串(需 `free`) |
| `strspn(s, accept)` | 返回 s 开头连续属于 accept 的字符数 |
| `strcspn(s, reject)` | 返回 s 开头连续不属于 reject 的字符数 |
| `strerror(errno)` | 把错误码转成错误信息字符串 |
常用示例:
```
#include <string.h>
char s1[] = "hello";
char s2[20];
strcpy(s2, s1); // s2 = "hello"
strcat(s2, " world"); // s2 = "hello world"
if (strcmp(s1, "hello") == 0) { // 相等
}
char *p = strstr(s2, "wor"); // 指向 "world" 开头
char *c = strchr(s2, 'o'); // 指向第一个 'o'
// strtok 切分
char s[] = "apple,banana,cherry";
char *tok = strtok(s, ",");
while (tok) {
printf("%s\\n", tok); // apple / banana / cherry
tok = strtok(NULL, ",");
}
```
| printf | 标准输出 | — | 最常用 |
| fprintf | 指定文件流 | — | fprintf(stderr, …) 打错误 |
| sprintf | 字符缓冲区 | sprintf_s | 不检查容量,会溢出,慎用 |
| snprintf | 字符缓冲区(限长) | snprintf_s | 推荐,安全 |
| asprintf | 自动分配缓冲区 | — | GNU 扩展,自动 malloc,需 free |
| dprintf | 文件描述符 fd | — | POSIX,直接写 fd |
| sprintf_s | 字符缓冲区 | — | MSVC 安全版,验证参数 |
| snprintf_s | 字符缓冲区(限长) | — | MSVC 安全版 |
| vprintf | 标准输出 | — | 接受 va_list |
| vfprintf | 文件流 | — | 接受 va_list |
| vsprintf | 缓冲区 | vsprintf_s | 不检查容量 |
| vsnprintf | 缓冲区(限长) | vsnprintf_s | 接受 va_list 的 snprintf |
版本一:C语言
#include "myshell.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
// 提示符(真的是字符串)相关
static char username[32];
static char hostname[64];
static char cwd[256];
static char commandline[256];
// 命令行相关
static char *argv[64]; // 一般最多也就十几个
static int argc = 0;
static const char *sep = " ";
// 与退出码有关
static int lastcode;
// 与环境变量有关,应该由bash来维护,从系统配置文件读,直接从系统bash拷贝即可
static char *env[64];
static void GetUserName()
{
char *_username = getenv("USER"); // 获取环境变量
strcpy(username,(_username ? _username : "None"));
}
static void GetHostName()
{
char *_hostname = getenv("HOSTNAME"); // 获取环境变量
strcpy(hostname,(_hostname ? _hostname : "None"));
}
//static void GetCwdName() // 要详细处理,包括只保留"/"之后的路径
//{
// char *_cwd = getenv("PWD"); // 获取环境变量
// strcpy(cwd,(_cwd ? _cwd : "None"));
//}
static void GetCwdName() // 要详细处理,包括只保留"/"之后的路径
{
char _cwd[256];
getcwd(_cwd,sizeof(_cwd));
if(strcmp(_cwd,"/") == 0)
{
strcpy(cwd,_cwd);
}
else
{
int end = strlen(_cwd) – 1;
while(end >= 0)
{
if(_cwd[end] == '/')
{
// 如 /home/Alice
strcpy(cwd,&_cwd[end + 1]);
break;
}
end–;
}
}
}
void PrintPrompt()
{
GetUserName();
GetHostName();
GetCwdName();
printf("[%s@%s %s]# ",username,hostname,cwd);
fflush(stdout); // 直接刷新缓冲区
}
static void GetCommandLine()
{
if(fgets(commandline,sizeof(commandline),stdin) != NULL)
{
// 比如"abcd\\n" -> "abcd"
commandline[strlen(commandline) – 1] = 0;
// printf("debug:%s\\n",commandline);
}
}
static void ParseCommandLine()
{
// 清空
argc = 0;
memset(argv,0,sizeof(argv));
// 判空
if(strlen(commandline) == 0)
return;
// 解析 ls -a -l
argv[argc] = strtok(commandline,sep); // strtok分割字符串,变成几个子串
while((argv[++argc] = strtok(NULL,sep))); // argc的大小即子串个数
// // 调试信息注释掉
// printf("argc:%d\\n",argc);
// int i = 0;
// for(;argv[i];i++)
// {
// printf("argv[%d]:%s\\n",i,argv[i]);
// }
}
// 创建子进程
void Execute()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return;
}
else if(id == 0)
{
// printf("child running..\\n");
// 子进程
// 程序替换
execvp(argv[0],argv);
exit(1);
}
else{
// 父进程
int status = 0;
pid_t rid = waitpid(id,&status,0); // 得到状态
(void)rid;
lastcode = WEXITSTATUS(status);
}
}
// 1:yes
// 0:no
int CheckBuiltinAndExcute()
{
int ret = 0;
if(strcmp(argv[0],"cd") == 0)
{
// 内建命令
ret = 1;
if(argc == 2) // 有选项
{
chdir(argv[1]);
}
}
else if(strcmp(argv[0],"echo") == 0)
{
ret = 1;
if(argc == 2)
{
// echo $?
// echo $PATH
// echo "helloworld"
// echo helloworld
if(argv[1][0] == '$')
{
if(strcmp(argv[1],"$?") == 0)
{
printf("%d\\n",lastcode);
lastcode = 0; // 返回上一次的退出码
}
else
{
// env:环境变量
}
}
else
{
printf("%s\\n",argv[1]); // 打印字符串,不细分什么""或者没有""了
}
}
}
return ret;
}
void Bash()
{
while(1) // 软件都是死循环的
{
// 第1步:输出命令行
PrintPrompt();
// 第2步:等待用户输入,获取用户输入
// char commandline[256]; -> scanf()
// sleep(1); // 等待一会儿
GetCommandLine();
// 第3步:解析字符串,"ls -a -l" -> "ls" "-a" "-l"
ParseCommandLine();
if(argc == 0)
continue;
// 第4步:有些命令,cd、ench、env、export等命令,不应该让子进程执行,而应该让父进程自己执行,内建命令。bash内部的函数
if(CheckBuiltinAndExcute())
{
continue;
}
// 第5步:执行命令
Execute();
}
}
版本二:C++
/******************************************************************************
* mini_shell.cpp
* 一个最小可用的 Linux Shell:
* – 支持普通外部命令(ls / ps / whoami / pwd …)
* – 支持内建命令:cd / export / env / echo $? / exit
* – 支持环境变量继承与维护
* – 支持 echo $? 打印上一条命令退出码
* 编译:g++ -o mini_shell mini_shell.cpp -std=c++11
* 运行:./mini_shell
*****************************************************************************/
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
using namespace std;
/* ====================== 全局配置 ====================== */
const int basesize = 1024; // 命令行缓冲区大小
const int argvnum = 64; // argv 最多参数个数
const int envnum = 64; // 环境变量最多个数
/* ====================== 全局状态 ====================== */
static char *gargv[argvnum]; // 命令行参数表,末尾必须为 NULL
static int gargc = 0; // 参数个数
static int lastcode = 0; // 上一条命令的退出码(echo $? 用)
static char *genv[envnum]; // shell 自己维护的环境变量表
static char pwd[basesize]; // 当前工作目录
static char pwdenv[basesize];// "PWD=xxx" 字符串
/* ====================== 提示符相关 ====================== */
static string GetUserName() {
const char *name = getenv("USER");
return name ? name : "None";
}
static string GetHostName() {
const char *host = getenv("HOSTNAME");
return host ? host : "None";
}
static string GetPwd() {
if (getcwd(pwd, sizeof(pwd)) == NULL) return "None";
snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
putenv(pwdenv);
return pwd;
}
static string LastDir() {
string curr = GetPwd();
if (curr == "/" || curr == "None") return curr;
size_t pos = curr.rfind("/");
if (pos == string::npos) return curr;
return curr.substr(pos + 1);
}
static string MakeCommandLine() {
char line[basesize];
snprintf(line, basesize, "[%s@%s %s]# ",
GetUserName().c_str(),
GetHostName().c_str(),
LastDir().c_str());
return line;
}
static void PrintCommandLine() {
printf("%s", MakeCommandLine().c_str());
fflush(stdout); // 必须 fflush,因为提示符没 \\n
}
/* ====================== 读命令行 ====================== */
static bool GetCommandLine(char buf[], int size) {
if (fgets(buf, size, stdin) == NULL) {
printf("\\n"); // Ctrl+D 退出
return false;
}
buf[strlen(buf) – 1] = '\\0'; // 去掉 fgets 读进来的 \\n
if (strlen(buf) == 0) return false;
return true;
}
/* ====================== 解析命令行 ====================== */
static void ParseCommandLine(char buf[]) {
memset(gargv, 0, sizeof(gargv));
gargc = 0;
const char *sep = " ";
gargv[gargc++] = strtok(buf, sep);
while ((gargv[gargc++] = strtok(NULL, sep)) != NULL)
;
gargc–;
}
/* ====================== 环境变量操作 ====================== */
static void InitEnv() {
extern char **environ;
int i = 0;
for (; environ[i] && i < envnum – 1; i++) {
genv[i] = (char *)malloc(strlen(environ[i]) + 1);
strcpy(genv[i], environ[i]);
}
genv[i] = NULL;
}
static void AddEnv(const char *item) {
int i = 0;
while (genv[i]) i++;
genv[i] = (char *)malloc(strlen(item) + 1);
strcpy(genv[i], item);
genv[++i] = NULL;
}
/* ====================== 外部命令执行 ====================== */
static void ExecuteCommand() {
pid_t id = fork();
if (id < 0) { perror("fork"); lastcode = 1; return; }
if (id == 0) {
// 子进程:替换成外部程序
execvpe(gargv[0], gargv, genv);
perror(gargv[0]); // exec 失败才会走到这
exit(127);
}
// 父进程:阻塞等子进程
int status = 0;
waitpid(id, &status, 0);
if (WIFEXITED(status))
lastcode = WEXITSTATUS(status);
else if (WIFSIGNALED(status))
lastcode = 128 + WTERMSIG(status); // 被信号杀
else
lastcode = 100;
}
/* ====================== 内建命令 ====================== */
static bool CheckAndExecBuiltCommand() {
/* exit:退出 shell */
if (strcmp(gargv[0], "exit") == 0) { printf("bye~\\n"); exit(0); }
/* cd:改 shell 自己的工作目录 */
if (strcmp(gargv[0], "cd") == 0) {
if (gargc == 2) {
if (chdir(gargv[1]) != 0) perror("cd");
} else {
chdir(getenv("HOME") ? getenv("HOME") : "/");
}
lastcode = 0;
return true;
}
/* export KEY=VALUE */
if (strcmp(gargv[0], "export") == 0) {
if (gargc == 2) { AddEnv(gargv[1]); lastcode = 0; }
else lastcode = 1;
return true;
}
/* env:列环境变量 */
if (strcmp(gargv[0], "env") == 0) {
for (int i = 0; genv[i]; i++) printf("%s\\n", genv[i]);
lastcode = 0;
return true;
}
/* echo:支持 echo $? / echo xxx */
if (strcmp(gargv[0], "echo") == 0) {
if (gargc == 2) {
if (gargv[1][0] == '$' && gargv[1][1] == '?')
printf("%d\\n", lastcode);
else
printf("%s\\n", gargv[1]);
} else {
printf("\\n");
}
lastcode = 0;
return true;
}
return false; // 不是内建,去 fork
}
/* ====================== 主循环 ====================== */
int main() {
InitEnv();
char cmd[basesize];
while (true) {
PrintCommandLine();
if (!GetCommandLine(cmd, basesize)) continue;
ParseCommandLine(cmd);
if (CheckAndExecBuiltCommand()) continue;
ExecuteCommand();
}
return 0;
}
WIFEXITED 是 <sys/wait.h> 中的宏,判断子进程是否正常退出(通过 exit() 或 return),配合 wait() / waitpid() 使用:
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
pid_t pid = fork();
if (pid == 0) {
exit(42); // 子进程正常退出
}
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("正常退出,退出码 = %d\\n", WEXITSTATUS(status)); // 42
}
wait 状态宏全家桶:
| WIFEXITED(status) | 是否正常退出(exit/return) |
| WEXITSTATUS(status) | 正常退出时的退出码(仅 WIFEXITED 为真时用) |
| WIFSIGNALED(status) | 是否被信号杀死 |
| WTERMSIG(status) | 杀死它的信号编号(仅 WIFSIGNALED 为真时用) |
| WCOREDUMP(status) | 是否产生核心转储(非 POSIX 标准,部分系统有) |
| WIFSTOPPED(status) | 是否因信号暂停(配合 WUNTRACED) |
| WSTOPSIG(status) | 使其暂停的信号编号 |
| WIFCONTINUED(status) | 是否被 SIGCONT 恢复(配合 WCONTINUED) |
完整判断模板:
if (WIFEXITED(status)) {
printf("正常退出,码 %d\\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("被信号 %d 杀死\\n", WTERMSIG(status));
} else if (WIFSTOPPED(status)) {
printf("被信号 %d 暂停\\n", WSTOPSIG(status));
}
核心点:
status 是打包的位字段,不能直接比较,必须用宏解包。
判断 "是否正常退出" 看 WIFEXITED,取退出码看 WEXITSTATUS,两者通常成对出现。

网硕互联帮助中心




评论前必须登录!
注册