【 Linux入门 】之 手搓 命令行解释器 bash(带源码)
- 目的
- 基本结构
- 提取输入命令
- fgets的使用
- 命令初步处理
- 命令的本质
- 创建子进程
- 重要知识补充
- 进程替换
- 命令处理
- 简单 bash 完成及演示
- 优化bash
- ls颜色输出颜色
- 实现cd命令
- ecport 命令
- env
- echo $
- echo $?
目的
主要目的在于进一步了解 Linux 系统下使用进程相关的系统调用 及 shell 工作的基本原理
本篇文章适合有一定C语言基础,及基本了解 Linux操作 和 Linux进程同学编写
为减少废话,我基本不会解释简单语句以及所有函数用法,我相信大家既然要写这个命令行解释器 bash对语法等相关知识肯定是有了一定了解
最终目的实现一个基本能用的bash
主要内容:
- 获取命令行
- 解析命令行
- 建立一个子进程(fork)
- 替换子进程(execvp)
- cd / ecport / env / echo 等特殊命令单独处理,使其运行在父进程中
-
- 父进程等待子进程退出(waitpid)
基本结构
首先在Linux命令行上我们是可以不断输入的,所以其一定是一个循环,在这我们就设置一个死循环吧,最后程序退出使用 CTRL + C 退出即可
如下:
运行效果
如下输出了我想要的命令行提示字符串,想要不一样的自己可以改printf中的内容
由于是死循环,所以最后输入CTRL + C 退出程序
注意:在做下一步时上述的的sleep(1)及命令行提示字符串后面的 \n 可以删掉了,这些只是测试用的
提取输入命令
在命令行输入的命令需先存入数组,供后续处理,可打印测试自己是否将命令存入了数组,但后续需删掉打印代码
运行如下
fgets的使用
通过man手册查询得知上述用到的 fgets资料如下
从流中读取最多一个小于size的字符,并将它们存储到s指向的内存中,读取到EoF或换行符后停止。且换行符也会被存到s指向的内存中,最后会在最后一个数据后一位填充字符串终止符(\0)
fgets()读取成返回s,失败返回NULL
所以读取命令行之后需要检查一下fgets()是否读取失败
命令初步处理
上面说到,fgets()会把\n也存到字符串中。刚好在命令行输入时就会用回车表示输入结束
所以我加了第19行处理了一下
没处理之前,printf中并未加换行符但打印出的代码却自动换了,说明存命令输入的数组末尾确实存有\n不然不会自动换行
测试如下:
处理之后,打印出的代码不会自动换行了。
命令的本质
Linux中执行命令的本质就是进程,进程就是执行某个程序,所以执行命令就是执行程序
但是命令基本都是以子进程的形式进行,官方bash 在执行命令时也是多以子进程形式进行的,这样能保证bash的稳定性
子进程的好处:子进程执行命令时出错不会影响父进程并且在发生错误后还会将错误返回给父进程,父进程只需要报错即可
创建子进程
上面讲述了,为保证bash的稳定性可用子进程来执行命令,我也采用这一方法
这次代码,增加了两个头文件
怎加代码如下
重要知识补充
说明一下:其实我们平时在命令行执行的 ls -l 其实 ls 就是程序文件名 -l 是程序参数表示我们要怎么执行。
如 ls 等命令程序文件的路径都是在默认环境中的,在默认环境路径下的程序直接输入程序文件名即可运行,但不在默认环境路径下的程序需 ./ 执行
进程替换
上面讲述了执行命令就是执行特定的程序,
但是要执行其他程序我们需要进行进程替换,这里我用的进程替换为execvp()介绍如下:
参数中字符串 file是程序文件名如 ls , argv 是以NULL结尾的指针数组,argv 里存的是 程序文件名,程序参数等 ,execvp()只有错误时才会返回-1
命令处理
上述讲述要执行进程替换execvp需传程序名文件名 ,及以NULL结尾的指针数组
我们想要的:
char* argv[] = {"ls","-l",NULL};
execvp(argv[0],argv);
但是咱读取命令行得到的是这样的如在命令行输入:ls -l
实际得到的:
char* command = {"ls -l"};
所以咱得处理一下所获得的字符串,并用指针数组储存起来
处理思路:
1 将command 数组中数据以空格为分割,将数据存入指针数组argv中
2 最后在 argv 数组有效元素末尾添加NULL
字符串分割我用的是这个函数
strtok()函数的作用是:将一个字符串分解为0个或多个非空标记的序列。在第一次调用strtok()时,要解析的字符串应该在str中指定。在后续的每个应该解析相同字符串的调用中,str必须为NULL。
delim参数指定了一组字节,用于分隔parsedl字符串中的标记。
如果找到了指定分割符则返回其分割后的字符串,注意每次只能找一个分割符并返回一个字符串,失败返回NULL
则代码如下编写
处理字符串的分割函数
主函数增加了五句
编辑好后,直接退出vim编译一下运行即可执行ls pwd ps 命令 还能创建文件 等
简单 bash 完成及演示
优化bash
ls颜色输出颜色
测试发现自己写的bash,ls输出的文件列表没有配色
查询得知 ls 只是别名其调用时实际时 ls - -color=auto,所以实际是系统bash在调用ls时同时也调用了配色方案
在自己的bash中查询 ls 没有带 --color=auto
经测试在命令行结尾 添加–color=auto即可调用配色方案
但每次执行 ls 都要手动加上 --color=auto那太麻烦了吧,所以咱直接优化代码
添加代码如下
再执行就已经有了颜色搭配
实现cd命令
在自己编写的bash中执行 cd 发现并不作用,这是因为 cd 也在子进程执行了执行完cd后子进程又推出了,改变的是子进程当前目录,但咱们的bash作为父进程并没有改变
改进代码使 cd 命令在 bash 进程中运行
改进前,咱先了解一下一个函数
chdir()将调用进程的当前工作目录更改为path中指定的目录,如果成功,返回0。出错时,返回-1
所以咱要改变当前进程工作目录直接调用chdir即可,但咱需要先筛选出 cd 命令
查找函数
编辑好后退出vim,编译运行即可正常使用cd命令
ecport 命令
ecport是定义环境变量,env 是查询环境变量
和上述cd一样如果不做特殊处理,ecport和env命令也是被子进程运行了,细想我们自己写的bash每次执行命令都会创建子进程执行,假设先执行 ecport 程序开辟子进程定义 ecport命令 后进程就退出了(进程被销毁ecport定义的环境变量也没了)
之后又想执行 env 命令,程序开辟子进程执行了env命令肯定是查询不到刚刚定义的环境变量,因为他们在执行命令时都不是一个进程,不可能被查询到
但是只要ecport是在父进程运行的,子进程就会继承父进程的环境变量,也就能查询到了
这里就不演示了,直接修改代码吧
思路:ecport 和cd命令一样需要特殊处理,执行在父进程下
添加环境变量的函数如下
putenv()函数的作用是:添加或更改环境变量的值。参数字符串的形式为name=value。如果name在环境中不存在,则将string添加到环境中。如果name存在,则将环境中name的值更改为value。string所指向的字符串成为环境的一部分,因此改变字符串也改变了环境。
putenv()函数成功时返回零,如果发生错误则返回非零。如果发生错误,则设置errno来指示错误的原因
添加代码如下
但是经测试env还是查询不到刚刚定义的变量,环境列表很长我就不截全了
这是因为在bash中定义的环境变量,需要自己去维护存储环境变量的内存,保证其不被覆盖且一直存在
所以咱在putenv()之前得把要声明的环境变量存储到一个在当前进程结束前不会被覆盖的位置。
更改后代码如下:
将之前的putenv命令等代码放到main函数中
这样就改完啦
env
env是用于查看环境变量的命令,由于之前我没有对env命令进行处理所以现在执行env肯定是被执行在子进程中的
但是我们在执行env的时候,是想看自己的环境变量还是子进程的环境变量呢?
不用质疑肯定是自己的,所以env命令也需要特殊处理
首先在int find_command(char* argv[])函数中加入红框中代码,用于筛选env命令
主函数也有所改变,两个 if 语句相较于之前被调换了上下位置
跟改完后,编译运行即可。打印出来的每个环境变量之前有编号是因为我的打印命令自己加的
echo $
这个命令也需要特殊处理 ,要不然也会去提取子进程中的内容。使用该命令时用户肯定是想要查看当前进程内容的
在int find_command(char* argv[])函数中加入红框中的内容即可对echo$命令做处理
执行例如:
echo $PATH命令
echo $USER命令
echo $?
这个命令是提取进程退出码,但目前我的My_bash并未实现这一功能。执行后居然是打印说明功能缺失了。
需要特殊处理一下
思路:在筛选出echo命令后在筛选$后面紧跟的?
于是在之前筛选echo命令的if中加入了一个判断是否是?号,为打印退出码函数也怎加了一个退出码的形参。
主函数也有改变,怎加了一个存储退出码的变量
怎加括号中内容,提取退出码
函数传参也多传了一个退出码
测试如下,已经能正常输出退出码
小伙伴们,到这我就演示完成了可能还有其他功能没有实现,需要自己扩展哦
下面是本人源码
#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>//sleep的头文件#include<sys/types.h>
#include<sys/wait.h>#define max 100int DisposeStr(char* _command ,char* _argv[])//字符串分割处理
{int i = 0;_argv[i] = strtok(_command," ");if(_argv[i] == NULL){ return -1; }//如果一个字符串都没有直接返回while(_argv[i++])//当等于NULL退出循环{ _argv[i] = strtok(NULL," ");//连续调用用NULL}if(strcmp(_argv[0] , "ls") == 0)//如果输入的是ls命令{_argv[--i] =(char*)"--color=auto";//在NULL位置改为 --color=auto_argv[++i] = NULL;//在最后以NULL结尾}return 0;
}int find_command(char* argv[], int quit)
{if(strcmp(argv[0],"echo")==0)//匹配env命令{const char* pp = NULL;if(argv[1][0] == '$')//保证其是 echo $.....{pp = getenv(argv[1]+1);//$之后的字符串if(argv[1][1] == '?'){printf("%d\n", quit );}//提取$后面是否是?else if(pp != NULL){ printf("%s=%s\n",argv[1]+1 , pp) ; }else{return 1;}//匹配失败}else{return 1;}//匹配失败return 0;//执行结束返回0}if(strcmp(argv[0],"cd")==0)//匹配cd命令{int i = chdir(argv[1]);if(i == -1){printf("%s\n",strerror(2));//cd执行出错则报错}return 0;//执行结束返回0} if(strcmp(argv[0],"env")==0)//匹配env命令{int i = 0;extern char** environ;for(i = 0 ; environ[i]; i++ ){printf("%d:%s\n",i ,environ[i]);//循环打印环境变量}return 0;//执行结束返回0}return 1;//返回1代表没匹配上对应指令,需要执行else
}int main()
{char export_str[30][100] = {{0}};int port = 0;int _quit = 0;//储存退出码while(1){char* argv[10] = {NULL};//初始化为NULLchar command[100] = {0};//将数组初始化\0printf("[ZhuGeBin made the bash]$");//命令行提示符char* tmp = fgets(command, 100 , stdin);//从输入流中输入到command数组中assert(tmp);//确保命令读取成功(void)tmp;//保证编译不报错command[strlen(command)-1] = '\0';//将字符串末尾的\n去掉int cur = DisposeStr(command , argv);//字符串分割处理if(cur == -1){continue ;}//如果输入空字符串重新输入if(strcmp("export",argv[0])==0)//匹配exprot命令{strcpy(export_str[port++] , argv[1]);//存储环境变量putenv(export_str[port-1]);//添加环境变量continue;}else if(find_command(argv , _quit)==0) { }//查找命令else{pid_t it = fork();//创建子进程if(it == 0){int i = execvp(argv[0],argv);printf("%s\n",strerror(i));exit(1);}int status = 0;//需初始化为0int cur = waitpid(it , &status , 0);//阻塞式等待子进程if(cur> 0 )//等待成功{_quit = WEXITSTATUS(status);}}}return 0;
}
相关文章:

【 Linux入门 】之 手搓 命令行解释器 bash(带源码)
目的基本结构提取输入命令fgets的使用命令初步处理命令的本质创建子进程重要知识补充进程替换命令处理简单 bash 完成及演示优化bashls颜色输出颜色实现cd命令ecport 命令envecho $echo $?目的 主要目的在于进一步了解 Linux 系统下使用进程相关的系统调用 及 shel…...
【运维】运维常用命令
shell大全读取文件每一行内容文件是否存在数组定义和循环取值变量循环流程控制语句:case判断数值相等/大于/小于判断字符串相等awk求和、平均、最大、最小sed用法exprbc计算器读取文件每一行内容 while read line doecho $line done < a.txt文件是否存在 if [ …...
MYSQL常用命令大全
文章目录 基本语句链接数据库显示已有数据库创建数据库选择数据库显示数据库中的表显示当前数据库的版本信息,链接用户名删除数据库创建表表 增加将查询结果插入到新表中:表 删除表 修改表 查询in子查询between ~ and ~ 模糊查询模糊查询regexp中的OR:多个信息查询同义词:删…...
锚点定位方案
一 背景知识: 1.1 #号的作用 #代表网页中的一个位置。其右面的字符,就是该位置的标识符。比如,http://www.example.com/index.html#print 就代表网页index.html的print位置。浏览器读取这个URL后,会自动将print位置滚动至可视区域。 为网页…...

Flink学习--第一章 初识Flink
Flink是Apache基金会旗下的一个开源大数据处理框架,如今已被很多人认为是大数据实时处理的方向和未来,许多公司也都在招聘和储备掌握Flink技术的人才。 1.1 Flink的源起和设计理念 Flink起源于一个叫作Stratosphere的项目,它是由3所地处柏林的…...

电脑技巧:常见的浏览器内核介绍
浏览器是大家日常使用电脑必备的软件,网上查资料、听音乐、办公等等,都不离不开浏览器给我们提供的方便,今天小编来给大家介绍一下常见的浏览器内核,一起来学习一下吧!1、Chromium 内核Google Chrom内核:统…...

【数据分析之道①】字符串
文章目录专栏导读1、字符串介绍2、访问字符串中的值3、字符串拼接4、转义字符5、字符串运算符6、字符串格式化7、字符串内置函数专栏导读 ✍ 作者简介:i阿极,CSDN Python领域新星创作者,专注于分享python领域知识。 ✍ 本文录入于《数据分析之…...

网络安全之防火墙
目录 网络安全之防火墙 路由交换终归结底是联通新设备 防御对象: 定义: 防火墙的区域划分: 包过滤防火墙 --- 访问控制列表技术 --- 三层技术 代理防火墙 --- 中间人技术 --- 应用层 状态防火墙 --- 会话追踪技术 --- 三层、四层 UTM …...

STM32之点亮一个LED小灯(轮询法)
目录 一、初始化GPIO口 二、按键点亮LED灯(轮询法) 一、初始化GPIO口 1、点亮LED小灯前,需要先初始化GPIO口 HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init) GPIO_TypeDef *GPIOx: //指初始化GPIO…...

pandas读CSV、读JSON、Excel
学习让我快乐 pandas的数据读取基本操作 pandas是Python中非常流行的数据处理库,它提供了许多强大的工具来读取、处理和分析数据。在本文中,我们将介绍pandas中的一些基本数据读取操作。 读取CSV文件 CSV文件是最常见的数据文件格式之一,p…...

企业站项目
企业站项目 一、项目实现结果 该项目共分为七大类:头部区域(logo图片、输入框)、导航区域、轮播图区域、内容区域、市场项目区域、产品中心区域、尾部区域 如图所示: http://企业站项目源码http://xn--vhquvo17e18gllbz7h2v9d …...

STM32开发(九)STM32F103 通信 —— I2C通信编程详解
文章目录一、基础知识点二、开发环境三、STM32CubeMX相关配置四、Vscode代码讲解GPIO模拟I2C代码SHT30相关代码main函数中循环代码五、结果演示方式一、示波器分析I2C数据方式2、通过Modbus将获取到的数据传到PC上一、基础知识点 本实验通过I2C通信获取SHT30温湿度值ÿ…...

手撕数据结构—栈
Tips不得不再次提一下这个语法问题,当数组创建的时候,进行初始化的时候,分为全部初始化或者说部分初始化,对于不完全初始化而言,剩下的部分就全部默认为零。现在比如说你想对整型数组的1万个元素把它全部变成-1&#x…...
【java刷题】排序子序列
这里写目录标题问题描述解决思路实现代码问题描述 牛牛定义排序子序列为一个数组中一段连续的子序列,并且这段子序列是非递增或者非递减排序的。牛牛有一个长度为n的整数数组A,他现在有一个任务是把数组A分为若干段排序子序列,牛牛想知道他最少可以把这个数组分为几段排序子序…...

Springboot怎么快速集成Mybatis和thymeleaf?
前言有时候做方案,需要模拟一些业务上的一些场景来验证方案的可行性,基本上每次都是到处百度如何集成springbootmybatisthymeleaf这些东西的集成平时基本上一年也用不了一次,虽然比较简单,奈何我真得记不住详细的每一步࿰…...
shell常见面试题一
(1)、set //查看系统变量 (2)、chsh -s /bin/zsh test //修改用户登录shell (3)、2>&1 //标准错误重定向到标准输出 &> //同样可以将标准错误重定向到标准输出 如下: ls test.…...

python如何快速采集美~女视频?无反爬
人生苦短 我用python~ 这次康康能给大家整点好看的不~ 环境使用: Python 3.8 Pycharm mou歌浏览器 mou歌驱动 —> 驱动版本要和浏览器版本最相近 <大版本一样, 小版本最相近> 模块使用: requests >>> pip install requests selenium >>> pip …...

kali内置超好用的代理工具proxychains
作者:Eason_LYC 悲观者预言失败,十言九中。 乐观者创造奇迹,一次即可。 一个人的价值,在于他所拥有的。所以可以不学无术,但不能一无所有! 技术领域:WEB安全、网络攻防 关注WEB安全、网络攻防。…...

Java栈和队列·下
Java栈和队列下2. 队列(Queue)2.1 概念2.2 实现2.3 相似方法的区别2.4 循环队列3. 双端队列 (Deque)3.1 概念4.java中的栈和队列5. 栈和队列面试题大家好,我是晓星航。今天为大家带来的是 Java栈和队列下 的讲解!😀 继上一个讲完的栈后&…...

b01lers CTF web 复现
warmup 按照提示依次 base64 加密后访问,可以访问 ./flag.txt,也就是 Li9mbGFnLnR4dA 。 from base64 import b64decode import flaskapp flask.Flask(__name__)app.route(/<name>) def index2(name):name b64decode(name)if (validate(name))…...
【Java学习笔记】Arrays类
Arrays 类 1. 导入包:import java.util.Arrays 2. 常用方法一览表 方法描述Arrays.toString()返回数组的字符串形式Arrays.sort()排序(自然排序和定制排序)Arrays.binarySearch()通过二分搜索法进行查找(前提:数组是…...

【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...

深度学习习题2
1.如果增加神经网络的宽度,精确度会增加到一个特定阈值后,便开始降低。造成这一现象的可能原因是什么? A、即使增加卷积核的数量,只有少部分的核会被用作预测 B、当卷积核数量增加时,神经网络的预测能力会降低 C、当卷…...

用机器学习破解新能源领域的“弃风”难题
音乐发烧友深有体会,玩音乐的本质就是玩电网。火电声音偏暖,水电偏冷,风电偏空旷。至于太阳能发的电,则略显朦胧和单薄。 不知你是否有感觉,近两年家里的音响声音越来越冷,听起来越来越单薄? —…...

如何更改默认 Crontab 编辑器 ?
在 Linux 领域中,crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用,用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益,允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...
离线语音识别方案分析
随着人工智能技术的不断发展,语音识别技术也得到了广泛的应用,从智能家居到车载系统,语音识别正在改变我们与设备的交互方式。尤其是离线语音识别,由于其在没有网络连接的情况下仍然能提供稳定、准确的语音处理能力,广…...

Kubernetes 节点自动伸缩(Cluster Autoscaler)原理与实践
在 Kubernetes 集群中,如何在保障应用高可用的同时有效地管理资源,一直是运维人员和开发者关注的重点。随着微服务架构的普及,集群内各个服务的负载波动日趋明显,传统的手动扩缩容方式已无法满足实时性和弹性需求。 Cluster Auto…...

如何做好一份技术文档?从规划到实践的完整指南
如何做好一份技术文档?从规划到实践的完整指南 🌟 嗨,我是IRpickstars! 🌌 总有一行代码,能点亮万千星辰。 🔍 在技术的宇宙中,我愿做永不停歇的探索者。 ✨ 用代码丈量世界&…...
记一次spark在docker本地启动报错
1,背景 在docker中部署spark服务和调用spark服务的微服务,微服务之间通过fegin调用 2,问题,docker容器中服务器来后,注册中心都有,调用服务也正常,但是调用spark启动任务后报错,报错…...