【Linux】教你用进程替换制作一个简单的Shell解释器
本章的代码可以访问这里获取。
由于程序代码是一体的,本章在分开讲解各部分的实现时,代码可能有些跳跃,建议在讲解各部分实现后看一下源代码方便理解程序。
制作一个简单的Shell解释器
- 一、观察Shell的运行状态
- 二、简单的Shell解释器制作原理
- 1、获取命令行
- 2、解析命令行
- 3、创建子进程 进行程序替换 父进程等待
- 4、实际运行
- 二、对简单的内建命令进行处理
- 1、给ls命令加上色彩
- 2、支持cd命令
- 3、支持export命令
- 4、支持env命令
- 5、支持echo命令
一、观察Shell的运行状态
我们想要制作一个简单的Shell解释器,需要先观察Shell是怎么运行的,根据Shell的运行状态我们再去进行模拟实现。
我们可以先考虑下面的指令与Shell的互动:

我们仔细进行分析可以发现,Shell执行上面的命令时,可以被理解为下面的过程。

当然上面的命令都是普通命令,所以Shell都是通过创建子进程的方式来执行的,对于一些内建命令(Shell自己去执行命令)我们现在还不考虑,在后面的部分我们再进行进一步的讨论内建命令应该怎么去处理。
二、简单的Shell解释器制作原理
通过观察Shell的运行状态,我们知道然后Shell读取新的一行输入,建立一个新的子进程,在这个子进程中运行程序并等待这个进程结束。
所以要写一个shell,需要循环以下过程:
- 获取命令行
- 解析命令行
- 建立一个子进程(
fork) - 替换子进程(
execvp),执行替换后的程序 - 父进程等待子进程退出(
wait)
1、获取命令行
我们在在Shell中输入的命令本质上就是输入一个字符串,因此我们想要获取命令行,可以先创建一个字符数组commandstr,然后使用C语言的fgets函数从键盘中进行读取数据到字符数组里面,这样我们就获取了一个命令行了。
注意:
- 这里不能使用
scanf函数 ,这里的命令会包含空格,会导致scanf读取不到完整的数据。fgets函数会将我们输入的命令时的最后一个的\n符也给读取到字符数组内,我们需要特殊处理将\n进行用\0进行覆盖
//这里包含的头文件是我们整个程序需要用到的所有头文件
#include<stdio.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>//这里的N用于定义字符数组的大小
#define N 128int main()
{//存储命令行的字符数组char commandstr[N] = "";//Shell要一直运行接受命令,所以这里必须是死循环!while(1){//模拟Shell的提示符printf("[hong@machine MiniShell]# ");//从标准输入流中读取字符串char* s = fgets(commandstr, sizeof(commandstr), stdin);assert(s); //判断fgets是否读取成功//处理\n 示例字符串:ls -a -l\n\0commandstr[strlen(commandstr) - 1] = '\0';}
2、解析命令行
虽然我们通过前一步已经拿到命令行,但是我们还不能直接使用,因为我们拿到的字符串中间可能有许多空格以及一些其他的问题,我们还需要将命令行的字符进行切割提取出我们想要的子串,这样才符合程序替换函数的要求。例如:将 ls -a -l提取成 ls ,-a , -l。
对于字符串的切割,我们可以使用C语言提供的strtok函数,由于切割以后我们的字符串从一个变成了多个,因此我们需要用一个字符串指针数组argv,存储每一部分切割后的首地址,同时这个argv也可以直接传递给execvp函数进行程序替换了。
//在全局域中 定义切割符
#define SEP " "//main函数的外部 定义一个命令行切割函数
int split(char commandstr[], char* argv[])
{assert(commandstr);assert(argv);//第一次切割argv[0] = strtok(commandstr, SEP);if(argv[0] == NULL){//返回 -1表示异常退出return -1; }//循环切割int i = 1;while((argv[i++] = strtok(NULL, SEP)));return 0;
}//main函数内部,while循环上面定义切割后的字符指针数组
char* argv[N] ={NULL};//while循环内部//切割字符串 例如将"ls -a -l " 变为 "ls" "-a" "-l"int n = split(commandstr, argv);if(n == -1){//切割失败就终止本次循环continue;}

3、创建子进程 进行程序替换 父进程等待
创建子进程而我们可以使用fork函数进行创建,创建完以后进程的执行流由一个变成了两个,我们在子进程中进行程序替换可以使用execvp命令,同时我们的argv[0]就是程序名,argv中存储的就是命令按照什么方式进行执行。
最后我们的父进程可以在外面进行阻塞等待,然后获取子进程的退出码和退出信息。
//main函数内部,while循环上面定义退出码变量int last_status = 0;//while循环内部//创建子进程,进行命令处理pid_t id = fork();assert(id >= 0);if(id == 0){//child processexecvp(argv[0], argv); //如果执行到这里说明程序替换失败 exit(-1);}//父进程等待子进程int status;int pid = waitpid(id, &status, 0); //等待成功就提取退出码信息if(pid >= 0){last_status = WEXITSTATUS(status);}}return 0;
4、实际运行
我们可以执行 ls pwd ps -axj命令 看一看效果。

二、对简单的内建命令进行处理
我们知道内建命令是让Shell自己执行的命令,而不是让子进程执行的命令,例如cd命令就是内建命令,因为我们要改变的是Shell自己的工作目录,而不是子进程的工作目录,类似的命令还有export env echo命令。
由于上面我们写的程序执行命令时都是交给子进程去做的,所以我们上面写的程序是没有办法执行内建命令的,或者说能执行内建命令,但不是我们想要的结果或目的。
所以接下来我们要对这个简单的Shell进行改造,让它能够执行一些简单的内建命令,还有刚刚我们的ls命令没有色彩,我们也要进行一些修改。
1、给ls命令加上色彩
在真正的Shell中我们执行的ls命令其实是ls --color=auto,ls被我们真正的Shell进行了起别名。

我们在运行我们自己制作的Shell时也可以加上--color=auto。
//此段代码应该在切割字符串之后//argv[0]就是我们的命令名
if(strcmp(argv[0], "ls") == 0){int pos = 0;//寻找指针数组的结尾while(argv[pos++]);//在NULL位置加上 --color=autoargv[pos - 1] = "--color=auto";//将后一个位置置空argv[pos] = NULL;}
这样以后我们在我们自己制作的Shell中执行ls命令时也会由颜色了!
2、支持cd命令
对于cd命令如果让父进程进行执行,我们可以调用系统调用chdir我们只需要传递一个参数:路径字符串,当执行成功时会返回0,执行失败会返回-1,并设置错误码。

//此段代码应该在ls添加颜色之后else if(strcmp(argv[0], "cd") == 0){//argv[1]里面存放的是路径字符串if(argv[1] == NULL){printf("没有正确的路径!\n");//设置错误码last_status = -1;continue;}//执行系统调用改变父进程的工作目录chdir(argv[1]);continue;}
3、支持export命令
export命令可以将一个本地变量加入到环境变量表中,我们让我们自己制作的Shell完成expoprt命令可以用C语言提供的函数putenv函数,但是在向环境变量表加入新的环境变量时,我们要维护好我们加入到环境变量,这个环境变量不能够被轻易的覆盖,否则环境变量表在找我们的环境变量时就会找不到,所以我们还要创建一个我们自己维护的二维数组。
//在全局域中定义
// 自己维护的二维数组最多能向环境变量表几个自定义的环境变量
#define MAX 64//main函数内部,while循环上面定义
//指向下一个要添加的环境变量的位置int env_index = 0;
//要维护的二维数组char envstr[MAX][N];//此段代码应该在ls添加颜色之后else if(strcmp(argv[0], "export") == 0){//声明putenv函数否则会编译器会有警告extern int putenv(char *string); //argv[1]位置应该是环境变量if(argv[1] == NULL){printf("没有输入变量!\n");last_status = -1;continue;}//将argv[1]位置的环境变量,拷贝到env_str中,否则下一次解析的命令会覆盖环境变量strcpy(envstr[env_index], argv[1]);//将环境变量导入环境变量表putenv(envstr[env_index++]);}
4、支持env命令
对于env命令我们只需要写一个打印环境变量表的函数就能完成此命令了。
//main函数的外部 定义一个打印环境变量表的函数void showEnv()
{extern char** environ;int i = 0;while(environ[i]){printf("%d : %s\n", i, environ[i++]);}
}//此段代码应该在ls添加颜色之后else if(strcmp(argv[0], "env") == 0){showEnv();continue;}
5、支持echo命令
echo命令可以用于打印环境变量,也可以打印退出码,这取决于$后面是不是?是?我们就可以打印last_status,不是我们就用getenv命令拿到环境变量的内容。
//此段代码应该在ls添加颜色之后
else if(strcmp(argv[0], "echo") == 0){if(*argv[1] == '$'){if(*(argv[1] + 1) == '?'){printf("process exit code %d\n", last_status);continue;}else{char* str = getenv(argv[1] + 1);printf("%s\n",str);continue;}}}
相关文章:
【Linux】教你用进程替换制作一个简单的Shell解释器
本章的代码可以访问这里获取。 由于程序代码是一体的,本章在分开讲解各部分的实现时,代码可能有些跳跃,建议在讲解各部分实现后看一下源代码方便理解程序。 制作一个简单的Shell解释器 一、观察Shell的运行状态二、简单的Shell解释器制作原理…...
onMeasure里如何重置只有1个子view一行满屏, 若有多个自适应一行
onMeasure里如何重置只有1个子view一行满屏, 若有多个自适应一行 可以尝试在 onMeasure 方法中重写 measureChildWithMargins 或 measureChild 方法来实现这个需求。 对于只有一个字的 View,我们可以把它的宽度设为屏幕宽度,高度设为最大高度,这样这个 View 就会占满一整行…...
Postman创建项目 对接口发起请求处理
查看本文之前 您需要理解了解 Postman 的几个简单工作区 如果还没有掌握 可以先查看我的文章 简单认识 Postman界面操作 那么 掌握之后 我们就可以正式来开启我们的接口测试 我们先选择 Collections 我们点上面这个加号 多拉一个项目出来 然后 我们选我们刚加号点出来的项目…...
在Vue3项目中js-cookie库的使用
文章目录 前言1.安装js-cookie库2.引入、使用js-cookie库 前言 今天分享一下在Vue3项目中引入使用js-cookie。 1.安装js-cookie库 js-cookie官网 安装js-cookie,输入 npm i js-cookie安装完成可以在package.json中看到: 安装以后,就可…...
【论文笔记】Attention和Visual Transformer
Attention和Visual Transformer Attention和Transformer为什么需要AttentionAttention机制Multi-head AttentionSelf Multi-head Attention,SMA TransformerVisual Transformer,ViT Attention和Transformer Attention机制在相当早的时间就已经被提出了&…...
独立IP服务器和共享IP服务器有什么区别
在选择一个合适的服务器时,最常见的选择是共享IP服务器和独立IP服务器。尽管两者看起来很相似,但它们有着很大的不同。本文将详细介绍共享IP服务器和独立IP服务器的不同之处,以及如何选择适合您需求的服务器。 一、什么是共享IP服务器? 共享…...
Java8
Java8 (一)、双列集合(二)、Map集合常用api(三)、Map集合的遍历方式(四)、HashMap(五)、LinkedHashMap(六)、TreeMap(七&a…...
nn.conv1d的输入问题
Conv1d(in_channels, out_channels, kernel_size, stride1, padding0, dilation1, groups1, biasTrue) in_channels(int) – 输入信号的通道。在文本分类中,即为词向量的维度out_channels(int) – 卷积产生的通道。有多少个out_channels,就需要多少个1维…...
js判断是否为null,undefined,NaN,空串或者空对象
js判断是否为null,undefined,NaN,空串或者空对象 这里写目录标题 js判断是否为null,undefined,NaN,空串或者空对象特殊值nullundefinedNaN空字符串("")空对象(…...
Java每日一练(20230501)
目录 1. 路径交叉 🌟🌟 2. 环形链表 🌟🌟 3. 被围绕的区域 🌟🌟 🌟 每日一练刷题专栏 🌟 Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏…...
从零开始学习Web自动化测试:如何使用Selenium和Python提高效率?
B站首推!2023最详细自动化测试合集,小白皆可掌握,让测试变得简单、快捷、可靠https://www.bilibili.com/video/BV1ua4y1V7Db 目录 引言: 一、了解Web自动化测试的基本概念 二、选择Web自动化测试工具 三、学习Web自动化测试的…...
fastdfs环境搭建
安装包下载路径 libfastcommon下载地址:https://github.com/happyfish100/libfastcommon/releasesFastDFS下载地址:https://github.com/happyfish100/fastdfs/releasesfastdfs-nginx-module下载地址:https://github.com/happyfish100/fastdf…...
有什么牌子台灯性价比高?性价比最高的护眼台灯
由心感叹现在的孩子真不容易,学习压力比我们小时候大太多,特别是数学,不再是简单的计算,而更多的是培养学生其他思维方式,有时候我都觉得一年级数学题是不是超纲了。我女儿现在基本上都是晚上9点30左右上床睡觉&#x…...
信息系统项目管理师 第9章 项目范围管理
1.管理基础 1.产品范围和项目范围 产品范围:某项产品、服务或成果所具有的特征和功能。根据产品需求来衡量。 项目范围:包括产品范围,是为交付具有规定特性与功能的产品、服务或成果而必须完成的工作。项目管理计划来衡量 2.管理新实践 更加注重与商业分析师一起…...
【Android入门到项目实战-- 8.2】—— 使用HTTP协议访问网络
目录 一、使用HttpURLConnection 1、使用Android的HttpURLConnection步骤 1)获取HttpURLConnection实例 2)设置HTTP请求使用的方法 3)定制HTTP请求,如连接超时、读取超时的毫秒数 4)调用getInputStream()方法获取返回的输入流 5)关闭HTTP连接 2、…...
Go官方指南(五)并发
Go 程 Go 程(goroutine)是由 Go 运行时管理的轻量级线程。 go f(x, y, z) 会启动一个新的 Go 程并执行 f(x, y, z) f, x, y 和 z 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。 Go 程在相同的地址空间中运行,…...
VS快捷键大全 | 掌握这些快捷键,助你调试快人一步
欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,滤波估计、多传感器信息融合,机器学习,人工智能等相关领域的知识和…...
【刷题】203. 移除链表元素
203. 移除链表元素 一、题目描述二、示例三、实现方法1-找到前一个节点修改next指向方法2-不是val的尾插重构 总结 203. 移除链表元素 一、题目描述 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val val 的节点,并返回 新…...
C++11学习- CPU多核与多线程、并行与并发
随着计算机编程频繁使用,关于CPU的处理性能的讨论从未停止过,由于我最近在学习多线程相关的知识,那么就来理一理CPU的核心问题。 一、线程与进程 业解释 线程是CPU调度和分配的基本单位,可以理解为CPU只看得到线程; …...
docker登录harbor、K8s拉取镜像报http: server gave HTTP response to HTTPS client
docker登录harbor、K8s拉取镜像报http: server gave HTTP response to HTTPS client 当搭建完docker私有仓库后,准备docker login http://ip:端口 登录时会包如下错误 当我们使用docker私有仓库中的镜像在K8s集群中部署应用时会包如下错误 以上错误根据报错信息可…...
未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...
CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...
Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...
C# 类和继承(抽象类)
抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...
深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南
🚀 C extern 关键字深度解析:跨文件编程的终极指南 📅 更新时间:2025年6月5日 🏷️ 标签:C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言🔥一、extern 是什么?&…...
自然语言处理——循环神经网络
自然语言处理——循环神经网络 循环神经网络应用到基于机器学习的自然语言处理任务序列到类别同步的序列到序列模式异步的序列到序列模式 参数学习和长程依赖问题基于门控的循环神经网络门控循环单元(GRU)长短期记忆神经网络(LSTM)…...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
tomcat入门
1 tomcat 是什么 apache开发的web服务器可以为java web程序提供运行环境tomcat是一款高效,稳定,易于使用的web服务器tomcathttp服务器Servlet服务器 2 tomcat 目录介绍 -bin #存放tomcat的脚本 -conf #存放tomcat的配置文件 ---catalina.policy #to…...
