
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》
《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬 艾莉丝的简介:

文章目录
- [5 ~> 实现自主Shell命令行解释器](#5 ~> 实现自主Shell命令行解释器)
-
- [5.1 本文学习目标](#5.1 本文学习目标)
- [5.2 声明](#5.2 声明)
-
- [5.2.1 说明](#5.2.1 说明)
- [5.2.2 【鸟枪换炮】预告](#5.2.2 【鸟枪换炮】预告)
- [5.2.3 关于两种头文件包含方式:<>与""的区别](#5.2.3 关于两种头文件包含方式:<>与""的区别)
- [5.3 自主Shell命令行编辑器的实现原理](#5.3 自主Shell命令行编辑器的实现原理)
-
- [5.3.1 shell的一个典型的互动](#5.3.1 shell的一个典型的互动)
-
- [5.3.1.1 Shell的工作循环](#5.3.1.1 Shell的工作循环)
- [5.3.1.2 详细工作日志](#5.3.1.2 详细工作日志)
- [5.3.1.3 核心要点](#5.3.1.3 核心要点)
- [5.3.1.4 连续执行命令](#5.3.1.4 连续执行命令)
- [5.3.2 对Shell这一典型互动的总结](#5.3.2 对Shell这一典型互动的总结)
- [5.4 手搓自主Shell命令行解释器中的一些细节](#5.4 手搓自主Shell命令行解释器中的一些细节)
-
- [5.4.1 提示符(字符串)](#5.4.1 提示符(字符串))
- [5.4.2 static](#5.4.2 static)
- [5.4.3 fgets](#5.4.3 fgets)
- [5.4.4 strtok:切割](#5.4.4 strtok:切割)
-
- [1、strtok() 基本用法](#1、strtok() 基本用法)
- [2、示例步骤(输入 "ls -a -l")](#2、示例步骤(输入 "ls -a -l"))
- 3、代码关键行解析
- [4、strtok() 特性与注意事项](#4、strtok() 特性与注意事项)
- 5、调试输出结果示例
- 流程演示
- 总结
- [5.4.5 细节:五步走](#5.4.5 细节:五步走)
- [5.4.6 env](#5.4.6 env)
- [5.4.7 export](#5.4.7 export)
- [5.4.8 获取环境变量:LoadEnv](#5.4.8 获取环境变量:LoadEnv)
- [5.4.9 本地变量表](#5.4.9 本地变量表)
- [5.5 内建命令和普通命令](#5.5 内建命令和普通命令)
-
- [5.5.1 两者对比](#5.5.1 两者对比)
- [5.5.2 为什么需要区分内建命令?](#5.5.2 为什么需要区分内建命令?)
- [5.5.3 内建命令检查函数 CheckBuiltinAndExecute()](#5.5.3 内建命令检查函数 CheckBuiltinAndExecute())
- [5.5.4 which命令没查到的就是典型的内建命令------cd、echo、env也是内建,为什么能够在磁盘上用which指令查到?](#5.5.4 which命令没查到的就是典型的内建命令——cd、echo、env也是内建,为什么能够在磁盘上用which指令查到?)
- [5.6 指令别名支持、管道、重定向问题](#5.6 指令别名支持、管道、重定向问题)
- [5.7 Shell总结](#5.7 Shell总结)
- 本文代码演示
- 结尾

5 ~> 实现自主Shell命令行解释器
5.1 本文学习目标
| 目标编号 | 目标描述 |
|---|---|
| 5.1.1 | 要能处理普通命令 |
| 5.1.2 | 要能处理内建命令 |
| 5.1.3 | 要能帮助我们理解内建命令/本地变量/环境变量这些概念 |
| 5.1.4 | 要能帮助我们理解shell的运行原理 |
5.2 声明
5.2.1 说明
我们本文要实现的这个自主Shell命令行编辑器并没有实际的工程价值,我们之所以要学习、手搓这个自主Shell命令行编辑器,主要是想和大家一起重新理解我们之前已经学习了的Linux系统相关的知识点,比如理解"媒婆"、命令如何执行等等。
本次代码量并不多,第一版实际代码量 不到200行,充其量就相当于一个demo(演示)。
5.2.2 【鸟枪换炮】预告
艾莉丝马上就要更新到文件IO的部分啦,之前艾莉丝都是用CentOs版本进行实践和演示的,预计到IO部分,艾莉丝介绍时会使用Ubuntu版本的Linux操作系统 + VSCode进行一些实践和演示,可以说是"鸟枪换炮"了吧。

不仅是操作系统版本、编辑器要换,语言也要换啦!
之前一直是用C语言在编写代码,但是,等介绍完IO,艾莉丝也要切换到C++编码啦。
新武器库:CPP + Ubuntu + Vs Cdoe。
5.2.3 关于两种头文件包含方式:<>与""的区别
在C和C++编程中,头文件包含有两种形式:使用尖括号 < > 和双引号 " " 。它们的主要区别在于编译器搜索头文件的路径顺序不同。
尖括号 < >:
cpp
#include <stdio.h>
#include <iostream>
双引号 " ":
cpp
#include "myheader.h"
#include "../utils/helper.h"
| 特性 | <> |
"" |
|---|---|---|
| 搜索起点 | 系统目录 | 当前目录 |
| 包含范围 | 标准库/第三方库 | 项目文件 |
| 路径格式 | 系统路径 | 相对/绝对路径 |
| 性能 | 可能更快 (不搜当前目录) | 可能稍慢 |
| 可移植性 | 更好 | 需注意路径 |
简单记忆:
<>= 去系统那里找;
""= 先在附近找,找不到再去系统那里找。
5.3 自主Shell命令行编辑器的实现原理
我们都知道,真正的软件是 只要不退出就会一直运行,始终不结束的------死循环。
我们要实现自主Shell命令行编辑器,先要来考虑其实现原理。
5.3.1 shell的一个典型的互动
考虑下面这个与shell典型的互动:
bash
[root@localhost epoll]# ls
client.cpp readme.md server.cpp utility.h
[root@localhost epoll]# ps
PID TTY TIME CMD
3451 pts/0 00:00:00 bash
3514 pts/0 00:00:00 ps
用下图的时间轴表示事件发生的顺序,时间从左向右推移。代表 shell 的方块标识为"sh",并随时间从左向右移动。shell 读取用户输入的字符串"ls",接着创建一个新进程,在该进程中运行 ls 程序,并等待该进程结束。

我们将这个时间轴展开描述一下,即描述Shell的生命周期。
这就是Shell(媒婆)作为"命令调度中心"的完整生命周期:

我们可以打个相对比较恰当的比方:假设有这样一个餐厅后厨,我们"媒婆"Shell这里摇身一变,它的的生命周期就可以比喻成一位餐厅经理。
想象一下,Shell就是一个永不休息的餐厅前厅经理,它的工作就是循环处理顾客(用户)的点单(命令)。
5.3.1.1 Shell的工作循环
[等待点单] → [安排后厨] → [等待上菜] → [清理餐桌] → [回到等待]...

Shell工作循环这里的特点在于:
| 关键特点 | 描述 |
|---|---|
| 同步执行 | 默认情况下,Shell 会等待每个前台命令完成 |
| 进程隔离 | 每个命令在独立的进程中运行 |
| 状态维护 | Shell 维护环境变量、工作目录等状态 |
| 错误处理 | 捕获并显示命令执行结果和错误信息 |
5.3.1.2 详细工作日志
1. 待命状态 | 显示提示符 $
- 经理行为:站在前台,微笑,等待顾客。
- Shell 实质 :运行主循环,打印提示符,阻塞在
readline()等待输入。
2. 接收订单 | 读取命令 ls
- 经理行为:听到顾客说"一份意大利面"。
- Shell 实质 :从标准输入读取字符串
"ls\n",并解析其含义。
3. 安排后厨 | 调用 fork()
- 经理行为 :复制自己,生成一个和自己一模一样的"后厨经理分身"。
- Shell 实质:创建子进程。此时父子进程几乎完全相同,都准备执行"做意大利面"这个任务。
4. 执行烹饪 | 调用 exec()
- 经理行为 :分身经理瞬间变身 为意大利面主厨,并开始按菜谱烹饪。真身经理则回到前台。
- Shell 实质 :在子进程中,将进程内存映像替换为
/bin/ls的程序代码和数据,并开始执行。父进程 Shell 原样保留。
5. 待上菜 | 调用 wait()
- 经理行为 :真身经理在前台等待,直到后厨通知"菜好了"。
- Shell 实质:父进程进入睡眠(阻塞)状态,等待子进程终止的信号。
6. 菜品完成 | 子进程退出
- 经理行为 :主厨做完菜,消失(下班)。
- Shell 实质 :
ls程序运行结束,调用exit(),内核释放其大部分资源,留下一个"死亡凭证"(退出状态码)。
7. 清理与复盘 | 回收状态
- 经理行为:真身经理收到通知,查看菜品是否合格(退出状态),然后清理餐桌,准备迎接下一位顾客。
- Shell 实质 :父进程被唤醒,从
wait()返回,读取子进程的退出状态。之后继续循环,回到步骤 1。
5.3.1.3 核心要点
- Shell 永存:真身经理(父进程)永不亲自下厨,只负责调度和等待。
- 分身术 :
fork()创造执行机会。 - 变身术 :
exec()让机会变成具体的行动。 - 等待的艺术 :
wait()保证了秩序,让 Shell 可以同步地处理任务。
5.3.1.4 连续执行命令
bash
时间轴: t0 → t1 → t2 → t3 → t4 → t5 → t6 → t7 → t8
t0-t1: Shell 读取 "ls" 命令
t1-t2: 创建子进程,执行 ls
t2-t3: Shell 等待 ls 结束
t3-t4: ls 执行完毕,Shell 恢复
t4-t5: Shell 读取 "ps" 命令
t5-t6: 创建子进程,执行 ps
t6-t7: Shell 等待 ps 结束
t7-t8: ps 执行完毕,Shell 恢复
5.3.2 对Shell这一典型互动的总结

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序并等待这个进程结束。
所以要写一个shell,需要循环以下过程:
| 步骤 | 描述 |
|---|---|
| 1 | 获取命令行 |
| 2 | 解析命令行 |
| 3 | 建立一个子进程 (fork) |
| 4 | 替换子进程 (execvp) |
| 5 | 父进程等待子进程退出 (wait) |
根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了。
5.4 手搓自主Shell命令行解释器中的一些细节
5.4.1 提示符(字符串)
这一串提示符其实就是字符串:

我们要做的就是:获取用户名 + 主机名 + 工作目录。
这里艾莉丝暂时不 想用系统调用 (后面还是会用的),主要是想练习一下 前不久刚刚学习过的环境变量啦哈哈。
5.4.2 static
加上static之后,这些变量就是全局变量啦,父子进程共享数据。

在这段代码中,static 关键字用于修饰全局变量,它的作用主要有以下几个方面:
限制作用域:
-
当
static用于全局变量时,它会将变量的作用域限制在当前源文件(myshell.c)内; -
其他源文件即使使用
extern声明也无法访问这些变量; -
这相当于创建了文件作用域的私有变量。
变量示例说明:
c
// 这些变量只能在 myshell.c 文件中访问
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; // 私有:上一次命令的退出码
static char *env[64]; // 私有:环境变量数组
为什么使用 static?
| 概念 | 说明 |
|---|---|
| 信息隐藏/封装 | 这些变量是 shell 程序的内部状态,不希望其他文件(如 myshell.h 中声明的函数)直接修改这些内部变量,只有 myshell.c 中的函数可以访问和修改它们。 |
| 避免命名冲突 | 如果其他文件也定义了同名的全局变量,不会有冲突,因为 static 变量是文件私有的。 |
| 代码组织 | 明确哪些变量是本模块的"内部状态",提高代码的可维护性和可读性。 |
我们总结一下:
| 用途 | 说明 |
|---|---|
| 创建内部状态变量 | 用于创建 shell 程序的内部状态变量 |
| 实现模块化封装 | 实现模块化封装,隐藏实现细节 |
| 防止意外修改 | 防止其他文件意外修改这些关键数据 |
| 提高代码质量 | 提高代码的安全性和可维护性 |
5.4.3 fgets
c
fgets(commandline, sizeof(commandline), stdin)
-
从键盘读取输入到
commandline数组中; -
sizeof(commandline)= 256(根据之前定义); -
最多读取 255 个字符(留一个位置给 \0)。

这里fgets的优点有两点:
| 特性 | 说明 |
|---|---|
| 安全 | 限制读取长度,避免缓冲区溢出 |
| 保留换行符 | 如果读取到换行符 \n,会把它存入字符串 |
5.4.4 strtok:切割


1、strtok() 基本用法
| 项目 | 说明 |
|---|---|
| 函数原型 | char *strtok(char *str, const char *delim) |
| 首次调用 | strtok(commandline, " ") - 传入待分割字符串 |
| 后续调用 | strtok(NULL, " ") - 传入 NULL 继续分割 |
| 返回 | 分割出的子字符串,无更多时返回 NULL |
| 分隔符 | " "(空格),可定义其他字符如 "\t\n" |
| 原字符串 | 会被修改(分隔符替换为 \0) |
2、示例步骤(输入 "ls -a -l")
| 步骤 | 操作与结果 |
|---|---|
| 步骤1 | argv[0] = strtok("ls -a -l", " ") → "ls" |
| 步骤2 | argv[1] = strtok(NULL, " ") → "-a" |
| 步骤3 | argv[2] = strtok(NULL, " ") → "-l" |
| 步骤4 | argv[3] = strtok(NULL, " ") → NULL(结束) |
结果
argc = 3,argv = ["ls", "-a", "-l", NULL]
3、代码关键行解析
| 代码行 | 功能 | 说明 |
|---|---|---|
argc=0 |
重置参数计数器 | 每次解析前清零 |
memset(argv,0,...) |
清空参数数组 | 所有指针设为 NULL |
strlen(commandline)==0 |
检查空输入 | 用户只按回车时返回 |
argv[argc]=strtok(...) |
首次分割 | 获取第一个参数 |
while((argv[++argc]=strtok(...))) |
循环获取剩余参数 | 一行完成递增、分割、赋值 |
4、strtok() 特性与注意事项
| 特性 | 说明 | 注意 |
|---|---|---|
| 修改原串 | 分隔符处改为 \0 |
如需保留原串,先拷贝 |
| 静态变量 | 内部保存位置信息 | 非线程安全,用 strtok_r |
| 连续分隔符 | 多个分隔符视为一个 | 自动跳过 |
| 结尾处理 | 最后返回 NULL | 循环条件自然终止 |
5、调试输出结果示例
| 变量 | 值 |
|---|---|
| argc | 3 |
| argv[0] | "ls" |
| argv[1] | "-a" |
| argv[2] | "-l" |
| argv[3] | NULL |
流程演示

我们以表格的形式再理解一下:
| 步骤 | 操作 | 结果/说明 |
|---|---|---|
| 1 | 用户输入 | ls -a -l |
| 2 | fgets() 读取 | ls -a -l\n |
| 3 | 去除换行符 | ls -a -l |
| 4 | strtok() 分割 | 得到 ls, -a, -l |
| 5 | 存入 argv | argv[0]="ls", argv[1]="-a", argv[2]="-l" |
| 6 | 统计 argc | argc=3 |
总结
一句话总结就是:strtok() 按空格分割命令行,将 "命令 参数1 参数2" 变成数组 ["命令","参数1","参数2",NULL],并计数参数个数。
5.4.5 细节:五步走

5.4.6 env

演示结果如下所示:

5.4.7 export
export的变量可以被添加到环境变量表里面。

如下图所示:

5.4.8 获取环境变量:LoadEnv

我们之前定义了计数器,这里直接调用一下即可。
这样我们就获取到了环境变量。
5.4.9 本地变量表
如何理解本地变量?其实都是bash数据,都被子进程继承了,只不过我们看不到,也就是说,不带export的就到本地变量表了。
我们可以在这个shell里面再形成一张表,即本地变量表:

一句话概括两张表关系:
- 默认不带export,写到这个
local本地变量表里面; - 带了export,写到
env环境变量表里面
5.5 内建命令和普通命令

内建命令就是让Shell自己去维护自己解析的命令。我们之前说过:可以把内建命令当做bash内部的一个函数。
5.5.1 两者对比
内建命令(Builtin Command) 与 普通命令(External Command) 的表格对比如下:
| 对比维度 | 内建命令 (Builtin Command) | 普通命令 (External Command) |
|---|---|---|
| 执行方式 | 由 Shell 进程自身执行(不创建子进程) | 由于进程执行(通过 fork() + exec()) |
| 功能实现 | Shell 程序内部实现的功能函数 | 外部可执行文件 |
| 目的 | 影响 Shell 自身状态(如改变当前目录、设置环境变量等) | 执行独立程序 |
| 特点 | 执行速度快,无需创建新进程 | 需要创建新进程,有进程切换开销 |
| 示例 | cd、echo、env、export、exit 等 |
ls、grep、cat、python 等 |
5.5.2 为什么需要区分内建命令?
我们就以路径相关的指令:cd命令为例,存在cd路径无法回退的问题。
bash
# 如果是外部命令(错误):
bash$ cd /home/user # 子进程改变目录
bash$ pwd # 父进程目录没变,还在原地
# → /old/path
# 如果是内建命令(正确):
bash$ cd /home/user # 父进程改变目录
bash$ pwd # 父进程目录已变
# → /home/user
其它还有一些必须使用内建的情况,艾莉丝这里举一些例子:
-
export VAR=value: 设置环境变量(必须影响当前 Shell); -
exit: 退出 Shell 自身; -
source script.sh: 在当前 Shell 环境中执行脚本。
5.5.3 内建命令检查函数 CheckBuiltinAndExecute()
如下图所示:

上图中 CheckBuiltinAndExecute() 这个函数实现了两个内建命令:
cd 命令:
c
if(strcmp(argv[0],"cd") == 0) {
ret = 1; // 标记为内建命令
if(argc == 2) {
chdir(argv[1]); // 改变当前目录
}
}
-
使用
chdir()系统调用改变当前工作目录; -
必须在父进程中执行,因为子进程的目录改变不影响父进程。
echo 命令:
c
else if(strcmp(argv[0],"echo") == 0) {
ret = 1; // 标记为内建命令
if(argc == 2) {
if(argv[1][0] == '$') {
if(strcmp(argv[1],"$?") == 0) {
printf("%d\n",lastcode); // 打印上一次命令的退出码
lastcode = 0;
}
// 其他环境变量处理(未实现)
} else {
printf("%s\n",argv[1]); // 直接打印字符串
}
}
}
-
特殊处理
$?:打印上一个命令的退出状态码; -
其他情况直接输出参数。
5.5.4 which命令没查到的就是典型的内建命令------cd、echo、env也是内建,为什么能够在磁盘上用which指令查到?
which命令没查到的就是典型的内建命令。

其它的内建命令,像export,用which是查不到的,但是,cd、echo、env也是内建,为什么能够在磁盘上用which指令查到?

我们前面说内建命令是Shell自己实现的一个相当于成员函数的功能函数,由 Shell 进程自身执行(不创建子进程),那么就不可能能够用which指令查到,查到说明在磁盘上面存在这个文件。
这是因为Shell(比如bash)不仅是一个命令行解释器,同时也是一门语言------脚本语言。
之所以能够用which查到,是因为它们有两份,而且其中一份在磁盘上面真实存在!
5.6 指令别名支持、管道、重定向问题
像这些功能我们的shell已经可以做到了:

但是,别名、管道、重定向 这些都是我们现在的Shell没办法实现的,比如别名 ,举个例子,"ll"是别名,但是我们的Shell是识别不出来ll的:

别名 其实就是一个变量。
管道涉及到进程间通信,我们后面讲到了再介绍;重定向要麻烦一些,我们后面在文件部分再讨论,预计在下一篇博客就能够讲到啦。
其实在实现自主Shell命令行解释器中其它还能够解决,都没有特别吃力,唯独有一个------非常费劲:就是检查配置文件,这个不是我们现在能够办到的。
5.7 Shell总结
在继续学习新知识前,我们来思考函数和进程之间的相似性:
exec / exit就像call / return。
一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call / return系统进行通信。
这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间。如下图所示:

一个C程序可以fork / exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。
本文代码演示
1 自主Shell实现:版本一
myshell.h
c
1 #pragma once
2
3 #include <stdio.h>
4
5 void Bash();
myshell.c
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();
}
}
main.c
c
1 #include "myshell.h"
2
3 int main()
4 {
5 Bash();
6 return 0;
7 }
Makefile
c
1 mybash:myshell.c main.c
2 gcc -o $@ $^
3 .PHONY:clean
4 clean:
5 rm -f mybash
自主Shell版本一效果演示

2 自主Shell实现:版本二
myshell.h
c
1 #pragma once
2
3 #include <stdio.h>
4
5 void Bash();
myshell.c
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();
}
}
Makefile
c
1 mybash:myshell.c main.c
2 gcc -o $@ $^
3 .PHONY:clean
4 clean:
5 rm -f mybash
main.c
c
1 #include "myshell.h"
2
3 int main()
4 {
5 Bash();
6 return 0;
7 }
结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
【Linux进程控制(二)】进程程序替换详解:exec 函数族的使用与环境变量传递
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
