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…...
Admin.Net中的消息通信SignalR解释
定义集线器接口 IOnlineUserHub public interface IOnlineUserHub {/// 在线用户列表Task OnlineUserList(OnlineUserList context);/// 强制下线Task ForceOffline(object context);/// 发布站内消息Task PublicNotice(SysNotice context);/// 接收消息Task ReceiveMessage(…...
连锁超市冷库节能解决方案:如何实现超市降本增效
在连锁超市冷库运营中,高能耗、设备损耗快、人工管理低效等问题长期困扰企业。御控冷库节能解决方案通过智能控制化霜、按需化霜、实时监控、故障诊断、自动预警、远程控制开关六大核心技术,实现年省电费15%-60%,且不改动原有装备、安装快捷、…...

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...
TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案
一、TRS收益互换的本质与业务逻辑 (一)概念解析 TRS(Total Return Swap)收益互换是一种金融衍生工具,指交易双方约定在未来一定期限内,基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南 在数字化营销时代,邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天,我们将深入解析邮件打开率、网站可用性、页面参与时…...

ABAP设计模式之---“简单设计原则(Simple Design)”
“Simple Design”(简单设计)是软件开发中的一个重要理念,倡导以最简单的方式实现软件功能,以确保代码清晰易懂、易维护,并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计,遵循“让事情保…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...

【从零学习JVM|第三篇】类的生命周期(高频面试题)
前言: 在Java编程中,类的生命周期是指类从被加载到内存中开始,到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。本文会深入探寻类的生命周期,让读者对此有深刻印象。 目录 …...
GitHub 趋势日报 (2025年06月06日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...
4. TypeScript 类型推断与类型组合
一、类型推断 (一) 什么是类型推断 TypeScript 的类型推断会根据变量、函数返回值、对象和数组的赋值和使用方式,自动确定它们的类型。 这一特性减少了显式类型注解的需要,在保持类型安全的同时简化了代码。通过分析上下文和初始值,TypeSc…...