云计算百科
云计算领域专业知识百科平台

【Linux】 自主shell编写巩固复习篇

/* 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,这就是后面的坑之一,详见第八节)。
维度getcwdgetenv
头文件 <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 的工作原理(这是最值得深挖的地方):

  • 第一次调用 strtok(buf, " "):从 buf 开头找第一个非空格字符,记下单词起点;再从那里往后找空格,找到就把空格替换成 '\\0',返回单词起点指针。同时,strtok 内部用一个静态指针记住当前位置(停在刚才那个 '\\0' 之后)。
  • 后续调用 strtok(NULL, " "):不传新字符串,strtok 从内部记住的位置继续切下一个单词。
  • 连续空格自动跳过——所以 ls -l 和 ls -l 结果一样。
  • 它直接修改原字符串:buf 里的空格被改写成 '\\0',于是 buf 被原地拆成好几段,gargv[i] 只是指向这些段的首地址。
  • 举例:

            输入 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,两者通常成对出现。


    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Linux】 自主shell编写巩固复习篇
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!