Linux编写一个极简版本的Shell
Linux编写一个极简版本的Shell
📟作者主页:慢热的陕西人
🌴专栏链接:Linux
📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言
本博客主要内容在Linux环境下,简易实现了一个Shell,顺便讲解和实现了一些内建命令
文章目录
- Linux编写一个极简版本的Shell
- ①读取命令行
- ②父子进程框架
- ③切割命令行
- ④子进程借用分割的结果来替换程序
- ⑤优化:
- ⑥内建命令(重要)
首先我们观察到:
bash的命令行提示符:[用户名@主机名 当前目录]
[mi@lavm-5wklnbmaja demo1]

所以我们无限循环去打印这个命令行提示符
#include<stdio.h>
#include<unistd.h>
int main()
{ while(1) { printf("[xupt@my_machine currpath]#"); //这里因为我们不能加换行,所以得刷新缓冲区 fflush(stdout); sleep(1); } return 0;
}
运行效果:

①读取命令行
接下来我们就要获取命令输入的命令行参数:
我们创建一个字符数组用来专门存放用户输入的命令行
#define MAX 1024 //因为命令行最长支持到1024
char commondstr[MAX] = {0};
我们用fgets来获取命令行
fgets(commondstr, sizeof(commondstr), stdin);
我们测试一下:
结果正常,但是我们的命令重新被打印的时候多打印了一个换行符,因为fgets读取了换行符,并且存储到了commondstr中了.
[mi@lavm-5wklnbmaja demo1]$ ./myshell
[xupt@my_machine currpath]#ls -a
ls -a
解决方案:
commondstr[strlen(commondstr) - 1] = '\0';//处理fget获取了换行符的问题
运行结果:

②父子进程框架
这个时候我们就需要用到子进程了,因为执行命令行的时候需要用到程序替换,那么如果我们用父进程的话,直接就全崩掉了。
每次输入命令,都把命令交给子进程去执行,而父进程去等待子进程就好了:
pid_t id = fork(); assert(id >= 0); (void) id; //和上面的处理原因一样 if(id == 0) { //child } int status = 0; waitpid(id, &status, 0);
在子进程执行之前,我们先要将用户输入进来的命令行进行拆分
③切割命令行
切割的原理很简单,我们只需要把命令行中间的空格变成\0即可。
ls -a -l ----> ls\0-a\0-l;
这个时候我们要引入一个C库提供的函数strtok,它是一个专门用来分隔字符串的函数。
我们需要封装一下这个函数来达到为我们分割命令行的目的:
注意strtok函数第二次切割的时候只需要传入NULL即可。
int split(char* commondstr, char* argv[])
{ assert(commondstr); assert(argv); argv[0] = strtok(commondstr, SEP); int i = 1; while((argv[i++] = strtok(NULL, SEP)));
// {
// argv[i] = strtok(NULL, SEP);
// if(argv[i] == NULL) break;
// i++;
// } //表示切割成功 return 0;
}
main函数内部这样去调用分割函数
int n = split(commondstr, argv);//等于0表示切割成功if(n != 0) continue;//DebugPrint(argv);
我们再设计一个函数来打印我们切割的结果,查看我们切割的结果是否正确:
void DebugPrint(char* argv[])
{for(int i = 0; argv[i]; ++i){printf("%d : %s\n", i, argv[i]);}
}
运行结果:

④子进程借用分割的结果来替换程序
因为我们用split函数将命令行分装到argv字符串指针数组内部了,所以我们只能用带v的加载函数。
另外因为我们不能固定路径,所以我们也只能用带p的。
所以综上:我们的加载函数就选择到了execvp函数:
在子进程内部调用:
if(id == 0) { //child execvp(argv[0], argv); exit(0); }
那么这时候我们在运行一下:

⑤优化:
我们看到我们在用bash提供的ls的时候,它产生的结果是带有颜色的。

但是我们自己实现的简易Shell是没有颜色的,那么这到底是为什么?
我们which ls查看一下,原来系统在ls后边面追加了一个参数--color==auto;

那么我们也可以对我们的简易Shell进行一些优化让他支持这样的显示:
我们只需要在代码中特判一下即可:
if(strcmp(argv[0], "ls") == 0) { //先找到末尾 int pos = 0; while(argv[pos]) pos++; //追加color参数 argv[pos] = (char*)"--color=auto"; //安全处理 pos++; argv[pos] = NULL; }
运行效果:

⑥内建命令(重要)
(1)内建命令的概念:
—>首先我们先明确一下内建命令/内置命令的概念,就是让我们bash自己执行的命令,我们称之为内建命令/内置命令。
(2)cd命令
当我们在我们的简易Shell中切换目录时:
我们发现不论我们怎么切换目录,结果都是目录没有变化,**原因是我们是在子进程中运行这些命令行的,**进程具有独立性。其实我们切换目录是切换了子进程的目录,但是父进程也就是我们pwd显示的目录却没有任何变化,并且这里其实pwd的也是子进程的当前目录,但是因为子进程在执行完cd命令后,就被exit了。当我们再执行pwd的时候是一个新的子进程在帮我们完成这个命令,因为我们之前cd没有改变父进程的当前目录,那么新创建的子进程的目录也就变成了和父进程一样的,所以看起来我们就是没有改变当前目录一样。

所以这里的cd命令,我们要在父进程中交给一个函数chdir()来让我们的bash来执行:
代码:
//当我们输入cd命令的时候 if(strcmp(argv[0], "cd") == 0) { if(argv[1] != NULL) chdir(argv[1]); continue; }
运行结果:

(3)export命令
此外不止我们的cd,包括我们当时去在bash中执行我们的export添加环境变量的时候,实际上是添加到我们的bash内部的,那么如果我们的简易Shell去把这个命令交给我们的子进程去执行了,那么就不太合适了,应该让我们的父进程自行去执行这个命令!
所以我们依旧采用内建命令的方式:
//当我们输入export命令时 if(strcmp(argv[0], "export") == 0) { //我们把这个环境变量存储在我们自己设定的数组内部 if(argv[1] != NULL) { strcpy(myenv[env_index], argv[1]); //再将数组内部的环境变量放到父进程的环境变量中 putenv(myenv[env_index++]); } }
我们尝试测试一下:

最终我们找到了

但是我们的env打印的好像是子进程的环境变量,这似乎不是我们想要的,我们应该想要的是父进程的环境变量,所以我们再做一下处理:
我们自行实现一个函数去打印我们的环境变量:
void PrintEnv(){extern char **environ;for(int i = 0; environ[i]; ++i){printf("%d:%s\n",i, environ[i]);}}//当我们查看环境变量的时候if(strcmp(argv[0], "env") == 0){PrintEnv();continue; }
运行效果:


所以其实我们之前学习的几乎所有的环境变量,相关的命令都是内建命令。
我们在将echo支持成内建命令:
//当我们echo的时候if(strcmp(argv[0], "echo") == 0){//先确认一下echo后面第一个跟的是$if(argv[1][0] == '$'){char* env_ret = getenv(argv[1] + 1); if(env_ret != NULL){printf("%s=%s\n", argv[1] + 1, env_ret);}}continue;}
运行结果:

既然支持了环境变量的查询,我们再来顺便支持一下进程退出码的支持,也就是我们的echo $?
//当我们echo的时候 if(strcmp(argv[0], "echo") == 0) { //先确认一下echo后面第一个跟的是$ if(argv[1][0] == '$') { if(argv[1][1] == '?') { printf("%d\n", last_exit); continue; } else { char* env_ret = getenv(argv[1] + 1); if(env_ret != NULL) printf("%s=%s \n", argv[1] + 1, env_ret); } } int status = 0;pid_t ret = waitpid(id, &status, 0);if(ret > 0){ last_exit = WEXITSTATUS(status);//last_exit我们放在main函数里但不要放在循环里,他要长期保留。}
测试结果:

⑦代码汇总:
#include<stdio.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>//因为命令行最长支持到1024
#define MAX 1024
//限制最多切割为64段
#define ARGC 64#define SEP " "int split(char* commondstr, char* argv[])
{assert(commondstr);assert(argv);argv[0] = strtok(commondstr, SEP);int i = 1;while((argv[i++] = strtok(NULL, SEP)));
// {
// argv[i] = strtok(NULL, SEP);
// if(argv[i] == NULL) break;
// i++;
// }//表示切割成功return 0;
}void PrintEnv()
{extern char **environ;for(int i = 0; environ[i]; ++i){printf("%d:%s\n",i, environ[i]);}
}void DebugPrint(char* argv[])
{for(int i = 0; argv[i]; ++i){ printf("%d : %s\n", i, argv[i]);}
}int main()
{int last_exit = 0; //存储上一个进程的退出码int env_index = 0; //环境变量数组的下标char myenv[32][64];while(1){//每次进来都初始化一下char commondstr[MAX] = {0};char* argv[ARGC] = {NULL};printf("[xupt@my_machine currpath]#");fflush(stdout);//这里因为我们不能加换行,所以得刷新缓冲区char* s = fgets(commondstr, sizeof(commondstr), stdin); assert(s);(void)s;//保证在release发布的时候,因为assert去掉,而导致s没有被使用过而产生的告警,什么都没做,充当一次使用commondstr[strlen(commondstr) - 1] = '\0'; //解决了fgets读入换行符的问题int n = split(commondstr, argv);//等于0表示切割成功if(n != 0) continue;//DebugPrint(argv);//当我们输入export命令时if(strcmp(argv[0], "export") == 0){//我们把这个环境变量存储在我们自己设定的数组内部if(argv[1] != NULL)strcpy(myenv[env_index], argv[1]); //再将数组内部的环境变量放到父进程的环境变量中putenv(myenv[env_index++]);}}//当我们查看环境变量的时候if(strcmp(argv[0], "env") == 0){PrintEnv();continue; }//当我们echo的时候if(strcmp(argv[0], "echo") == 0){//先确认一下echo后面第一个跟的是$if(argv[1][0] == '$'){ if(argv[1][1] == '?'){printf("%d\n", last_exit); continue;}else{char* env_ret = getenv(argv[1] + 1);if(env_ret != NULL) printf("%s=%s \n", argv[1] + 1, env_ret);}}continue;}//当我们输入cd命令的时候if(strcmp(argv[0], "cd") == 0){if(argv[1] != NULL) chdir(argv[1]);continue;} //当我们输入ls命令的时候if(strcmp(argv[0], "ls") == 0){//先找到末尾int pos = 0;while(argv[pos]) pos++;//追加color参数argv[pos] = (char*)"--color=auto";//安全处理pos++;argv[pos] = NULL;}pid_t id = fork();assert(id >= 0);(void) id; //和上面的处理原因一样if(id == 0) {//childexecvp(argv[0], argv);exit(0);}int status = 0;pid_t ret = waitpid(id, &status, 0);if(ret > 0){last_exit = WEXITSTATUS(status);} // printf("%s\n", commondstr);} return 0;
}
到这本篇博客的内容就到此结束了。
如果觉得本篇博客内容对你有所帮助的话,可以点赞,收藏,顺便关注一下!
如果文章内容有错误,欢迎在评论区指正

相关文章:
Linux编写一个极简版本的Shell
Linux编写一个极简版本的Shell 📟作者主页:慢热的陕西人 🌴专栏链接:Linux 📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言 本博客主要内容在Linux环境下ÿ…...
亚马逊云AI应用科技创新下的Amazon SageMaker使用教程
目录 Amazon SageMaker简介 Amazon SageMaker在控制台的使用 模型的各项参数 pytorch训练绘图部分代码 Amazon SageMaker简介 亚马逊SageMaker是一种完全托管的机器学习服务。借助 SageMaker,数据科学家和开发人员可以快速、轻松地构建和训练机器学习模型&#…...
Eigen:旋转向量(Angle-Axis)转换为四元素和旋转矩阵
0. 在固定欧拉角系下。 绕固定系旋转,旋转的先后顺序为X、Y、Z。当然也支持XYZ的任意顺序旋转。 1. 转为四元素 Eigen::Quaterniond q Eigen::AngleAxisd(yaw, Eigen::Vector3d::UnitZ()) *Eigen::AngleAxisd(pitch, Eigen::Vector3d::UnitY()) *Eigen::AngleAxi…...
C#8.0本质论第十二章--泛型
C#8.0本质论第十二章–泛型 C#通过泛型来促进代码重用,在词义上等价于C模板。 在泛型编程中,数据类型也是一种参数。 12.1如果C#没有泛型 为object的方法使用值类型时,“运行时”将自动对它进行装箱,获取值类型的实例时则需要…...
Python与ArcGIS系列(七)自动化打印地图
目录 0 简述1 获取可用打印机列表2 打印地图3 导出地图至PDF4 导出地图至图像0 简述 本篇介绍如何利用arcpy实现获取可用打印机列表、打印地图、导出地图至PDF和图像。 1 获取可用打印机列表 通过arcpy提供的ListPrinterNames()函数可以生成可用的打印机列表。 import arcpy.m…...
基于STM32单片机抢答器设计
**单片机设计介绍, 基于STM32单片机抢答器设计-Proteus仿真 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于STM32单片机的抢答器设计可以用于教育和培训场景中的抢答游戏或考试环节。以下是一个基本的介绍设计步骤…...
冯·诺伊曼体系结构--操作系统
文章目录 1.认识冯诺依曼系统1.1约翰冯诺依曼1.2冯诺依曼结构1.3存储器的读写速度1.4对冯诺依曼结构的认识1.5冯诺依曼结构在生活中的演示 2.操作系统--“搞管理”的软件2.1概念2.2OS存在的意义2.3管理的方式2.4系统调用和库函数概念 1.认识冯诺依曼系统 1.1约翰冯诺依曼 1.2冯…...
IDEA插件开发--持久化配置信息方案
这里写自定义目录标题 配置信息持久化存储保存配置文件的方式每种方式的实现方案1.PropertiesComponent:2.PersistentStateComponent:3.Project Settings:4.外部文件: 5.数据库:6.加密数据:7,自定义配置文件…...
Vscode禁止插件自动更新
由于电脑的vscode版本不是很新。2022.10月份的版本1.7.2,电脑vscode的python插件装的也是2022年4月份的某个版本,但插件经常自动更新,导致python代码无法Debug,解决办法: 点设置,搜autoUpdate, 把红色框选成无...
Zookeeper篇---第六篇
系列文章目录 文章目录 系列文章目录一、请简述Zookeeper的选主流程二、为什么Zookeeper集群的数目,一般为奇数个?三、知道Zookeeper监听器的原理吗?一、请简述Zookeeper的选主流程 Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做…...
mysql数据库存储过程之游标(光标cursor)
游标是用来存储查询结果集的数据类型,在存储过程和函数中可以使用游标对结果集进行循环的处理。游标的使用包括游标的声明、open、fetch和close。 一、语法。 #声明游标 declare 游标名称 cursor for 查询语句; #开启游标 open 游标名称; #获取游标记录 fetch 游标…...
「帝国风暴兵」加入 The Sandbox,推出真实的全新人物化身系列和体验!
我们很高兴宣布与流行文化中最具标志性的娱乐品牌 Shepperton 设计工作室的「帝国风暴兵」达成合作伙伴关系。这一合作标志着该科幻品牌首次进入元宇宙,让风暴兵的粉丝们以全新的方式体验「帝国风暴兵」。 在这个体验中,玩家将置身于帝国风暴兵的营地&am…...
asp.net员工管理系统VS开发sqlserver数据库web结构c#编程包括出差、请假、考勤
一、源码特点 asp.net员工管理系统是一套完善的web设计管理系统(主要包括出差、请假、考勤基础业务管理),系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为vs2010 ,数据库为sqlserver2008&a…...
C++套接字库sockpp介绍
sockpp是一个开源、简单、现代的C套接字库,地址为:https://github.com/fpagliughi/sockpp,最新发布版本为0.8.1,license为BSD-3-Clause。目前支持Linux、Windows、Mac上的IPv4、IPv6和Unix域套接字。其它*nix和POSIX系统只需很少的…...
Mac M2开发环境安装
持续更新 brew 安装 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"JAVA多版本环境 ## 终端下载安装 brew install --cask temurin8 brew install --cask temurin11 brew install --cask temurin17## vim ~/.…...
Linux各种版本安装详细步骤和root密码破解
文章目录 VMware新建虚拟机硬件设置设置虚拟网络挂载ISO文件 root密码破解 VMware新建虚拟机 硬件设置 设置虚拟网络 编辑>虚拟网络编辑器>VMnet8(NAT模式) 挂载ISO文件 加电>开启次虚拟机 第二项可以检查挂载上来的iso文件是否完整没有破坏 磁盘分区 选自定义分…...
Netty - 回顾Netty高性能原理和框架架构解析
文章目录 概述JDK 原生 NIO 程序的问题Why Netty使用场景Related ProjectsNetty 高性能设计I/O 模型【阻塞 I/O】:【I/O 复用模型】【基于 Buffer】 线程模型事件驱动模型Reactor 线程模型Netty的线程模型异步处理 Netty框架的架构设计功能特性模块组件Bootstrap、S…...
uni-app——項目day01
配置uni-app開發環境 uni-app快速上手 | uni-app官网 创建项目 图中四个划线就是要配置的地方. 选择vue2还是vue3看个人选择。 目录结构 但是现在新版本创建的项目已经没有components目录了,需要自己创建。 项目运行到微信开发者工具 使用git管理项目 node-mod…...
【Java、MongoDB】程序控制非关系数据库
步骤: (1)连接 连接字符串 (2)CRUD 类与接口 解析 (3)maven管理方法 依赖 <dependency><groupId>org.mongodb</groupId><artifactId>mongodb-driver-legacy<…...
MySQL查询时间处理相关函数与方法实践笔记
1. 实践案例 在查询mysql数据库获取数据时,有这样一个需求:按每30分钟分组获取电量数据,形成1天48个数据点。 方法一: select hour(a.CreateTime) 时点,case when MINUTE(a.CreateTime)<30 then 1 else 2 end 半小时,sum(a…...
基于算法竞赛的c++编程(28)结构体的进阶应用
结构体的嵌套与复杂数据组织 在C中,结构体可以嵌套使用,形成更复杂的数据结构。例如,可以通过嵌套结构体描述多层级数据关系: struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...
云原生核心技术 (7/12): K8s 核心概念白话解读(上):Pod 和 Deployment 究竟是什么?
大家好,欢迎来到《云原生核心技术》系列的第七篇! 在上一篇,我们成功地使用 Minikube 或 kind 在自己的电脑上搭建起了一个迷你但功能完备的 Kubernetes 集群。现在,我们就像一个拥有了一块崭新数字土地的农场主,是时…...
【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
MySQL用户和授权
开放MySQL白名单 可以通过iptables-save命令确认对应客户端ip是否可以访问MySQL服务: test: # iptables-save | grep 3306 -A mp_srv_whitelist -s 172.16.14.102/32 -p tcp -m tcp --dport 3306 -j ACCEPT -A mp_srv_whitelist -s 172.16.4.16/32 -p tcp -m tcp -…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...
人工智能--安全大模型训练计划:基于Fine-tuning + LLM Agent
安全大模型训练计划:基于Fine-tuning LLM Agent 1. 构建高质量安全数据集 目标:为安全大模型创建高质量、去偏、符合伦理的训练数据集,涵盖安全相关任务(如有害内容检测、隐私保护、道德推理等)。 1.1 数据收集 描…...
Vue ③-生命周期 || 脚手架
生命周期 思考:什么时候可以发送初始化渲染请求?(越早越好) 什么时候可以开始操作dom?(至少dom得渲染出来) Vue生命周期: 一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个…...
comfyui 工作流中 图生视频 如何增加视频的长度到5秒
comfyUI 工作流怎么可以生成更长的视频。除了硬件显存要求之外还有别的方法吗? 在ComfyUI中实现图生视频并延长到5秒,需要结合多个扩展和技巧。以下是完整解决方案: 核心工作流配置(24fps下5秒120帧) #mermaid-svg-yP…...
实战设计模式之模板方法模式
概述 模板方法模式定义了一个操作中的算法骨架,并将某些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的前提下,重新定义算法中的某些步骤。简单来说,就是在一个方法中定义了要执行的步骤顺序或算法框架,但允许子类…...
TJCTF 2025
还以为是天津的。这个比较容易,虽然绕了点弯,可还是把CP AK了,不过我会的别人也会,还是没啥名次。记录一下吧。 Crypto bacon-bits with open(flag.txt) as f: flag f.read().strip() with open(text.txt) as t: text t.read…...
