【Linux】模拟实现一个shell
接受每一个人的批评,可是保留你自己的判断。 ——莎士比亚
一段时间的没有更新是由于最近开学期间比较的忙,同时也是由于刚开学的几门课才学习的时候有点迷糊,需要在学校课堂上花的时间更多了,所以才没有更新的,求放过。
简单shell的实现
- 1、shell介绍
- 2、shell实现概括
- 3、shell实现困难
- 4、shell实现具体方式
- 4、1、main函数
- 4、2、MakeCommandLineAndPrint函数
- 4、3、GetUserCommand函数
- 4、4、SplitCommand函数
- 4、5、CheckBuildin函数
- 4、6、ExecuteCommand函数
- 5、总结
1、shell介绍
对于什么是shell问题来说,这是个好问题😊,但是其实如果你看过我之前的文章的话,应该能准确的理解什么是shell,如果想要看之前怎么介绍的话,就会到之前文章里看一看。这里的话就简单讲一下吧,shell简单点来说,就是一个你的老板的一个秘书,这里的老板也能够看作是内核,你想要让你的老板有什么行为的话,你的报告换句话说就是你得将你的命令行代码给到你的老板的秘书,也就是shell,会通过shell来帮助你去找到老板,但是并不是直接就能够找到,并且让他去执行,给到老板前,秘书也会自己考虑一下这个命令行的方式有没有什么不妥的地方,如果有的话也就不会直接麻烦操作系统,这样的话,既保证了内核的安全性,也保证了运行时候的效率,这里的效率提升就是因为能够秘书在接收到几次一样的请求之后能够不再去进行判断,直接否定。
2、shell实现概括
对于shell实现来说,每一次的命令行输入,都会对应着有着一段的运行结果。那对于这种方式来说,可以看作是一个在一个父进程的情况下,一个子进程在不断的执行不同的命令,或者换句话说是在不断的替换进程(其中的环境变量是从父进程传下来的)。
所以我们可以用进程替换的思想去实现一个shell进程(这里的这种进程要一直进行,这样才能够实现执行多次的命令行。
由于我们每次输入的命令行指令都是会被bash读到,然后寻找指定的命令行中提到的程序,然后执行相关的选项。就像这篇文章讲的那样,我们的程序中能够读取到我们输入的东西,所以为什么我们不能够利用这点来实现每次的命令行输入,将对应到进程替换成我们需要的进程,运行结束之后再退出来。
按照这样的方法的话,我就能够奠定了我们实现shell主要实现方向。
3、shell实现困难
1、对于shell来说,不仅仅是读取到我们输入到的命令行是什么,我们还需要在执行之前,每次都会有一段的前置的信息,这一段的前置消息就是,分别对应着用户名,主机名以及当前目录,所以第一个目标就是要解决基本信息的获取以及显示。
2、除此之外,我们还需要将读取到的命令行参数存放在数组之中,所以我们需要根据每一次的用户的命令字符串,切分为不同的字符串数组,其中的要求就是依据空格为分界符号。
3、拆分后,分别的放在一个字符串数组之中。然后进行进程替换,这里的进程替换,选择的函数是execvp,这个在之前的文章中讲述过具体的使用方法,不知道的可以回顾一下,这个进程替换的系统调用函数能够解决我们的问题。
4、当然如果我们知道内建命令,那么我们还需要额外的去实现内建命令构建的操作。
4、shell实现具体方式
4、1、main函数
首先构建一个main函数。
包含一下最主要的函数,最主要的需要实现的功能。
为了方便后续的使用,我们把512定义为一个SIZE,简单的认为这是一个大小的限制(就类似数组大小的限制)。
#define SIZE 512
int main()
{int quit = 0;while(!quit){// 1. 我们需要自己输出一个命令行MakeCommandLineAndPrint();// 2. 获取用户命令字符串char usercommand[SIZE];int n = GetUserCommand(usercommand, sizeof(usercommand));if(n <= 0) return 1;// 3. 命令行字符串分割. SplitCommand(usercommand, sizeof(usercommand));// 4. 检测命令是否是内建命令n = CheckBuildin();if(n) continue;// 5. 执行命令ExecuteCommand();}return 0;
}
4、2、MakeCommandLineAndPrint函数
让每一个命令行都打印出自己的相关的信息。这个函数也不需要传参,因为所有需要得到的都已经存在于环境变量中了。所以为了能够打印相关的信息,就要去读取。所以我们就需要去编写相关函数去编写读取的方法。
首先第一步是构建一个框架。
void MakeCommandLineAndPrint()
{char line[SIZE];const char *uswename=GetUserName();const char *hostname=GetHostName();const char *cwd=GetCwd(); SkipPath(cwd);snprintf(line,sizeof(line),"[%s@%s %s]> ",username,hostname,strlen(cwd)==1 ? "/":cwd+1);printf("%s",line);fflush(stdout);
}
其次就是去实现每一个函数的具体意义。
首先我们来看SkipPath
为什么这里会有一个SkipPath呢?难道说每次得到的还不是我们正常使用的cwd吗?那当然不是能够直接使用的啊。所以对于这个函数来说就是为了处理一开始得到的不是我们最终想要的结果。如果不知道原本是什么的话,其实简单说一下也就是从家目录到当前目录的所有的路径都在环境变量的cwd中。所以我们才需要进行额外的处理。为了能够不用多余的函数来增加我们shell的时间复杂度,并且为了能够不传指针就能够实现对于变量的改写,我们需要使用到宏。因为宏是一个能够在编译的时候就能在原本的位置中展开,这也就不会造成重新开栈,重新消耗空间,考虑形参和实参的关系。
#define SkipPath(p) do {p+=(strlen(p)-1); while(*p!='/')p--;} while(0)
这里单独的写出来do{}while,来包含主要的程序,主要的作用是为了防止出现优先级错误的情况。
其中的几个得到环境变量相关信息的函数本质上都是一样的。大概看看应该能够看懂。
const char *GetUserName()
{const char *name = getenv("USER");if(name == NULL) return "None";return name;
}
const char *GetHostName()
{const char *hostname = getenv("HOSTNAME");if(hostname == NULL) return "None";return hostname;
}
const char *GetCwd()
{const char *cwd = getenv("PWD");if(cwd == NULL) return "None";return cwd;
}
这样的话,就能够实现我们编写的shell的第一步了。
4、3、GetUserCommand函数
这个函数的话,是要读取用户输入的字符串,当然用户在输入的时候是有空格的,所以对于该函数,需要注意的是,这里不能够是直接使用scanf函数,而是要找到一个能够按照行来拿到字符串的函数。这样的话才能够保证不会因为存在空格反而不能读到正确的结果。
所以这个函数是什么呢?有没有比较好的一个接口呢?我的建议是选择一个char *fgets(char *s,int size,FILE *stream),如果能够 正确返回,那么返回s的起始位置的地址。如果返回错误,就返回NULL。建议使用这个文件流相关的知识,那是因为之后的文章中马上就要讲解有关于文件流的知识。其中的size指的是s的大小。并且输入的话,存放在的位置是在s中。==其中有一个不注意就会忘记的一点是,我们每次输入的时候按回车才能实现fgets真正的读完,所以说如果我们不干涉的话,在最后会有一个多余的回车。==所以我们需要进行改写,将函数内部传入命令行之后进行sizeof结尾置零操作。
对于这个函数传参的设计的话,应该是需要传入两个。
第一个参数是我们在main函数创建的一个专门存放命令行内容的usercommand数组,这是因为这个数组在读完数据之后还需要进行之后的操作,就比如说分割操作。
第二个参数就是我们用来得到这个字符串所占据的内存大小,因为在fgets函数使用的时候需要用到。
这样的话注意点,以及一些传参的设计都已经搞定了,下面就是真正的代码的实现。
#define ZERO '\0'
int GetUserCommand(char command[], size_t n)
{char *s = fgets(command, n, stdin);if(s == NULL) return -1;command[strlen(command)-1] = ZERO;return strlen(command);
}
4、4、SplitCommand函数
对于分割命令行参数的函数来说,我们需要像之前那样定义一个宏函数来帮助我们实现不用传参的操作吗?其实宏函数确实能够实现,但是对于学习阶段来说我们其实可以想一下,之前在介绍C语言中的字符串函数的时候,有一个函数其实能够刚好符合我们的要求。strtok函数,能够根据特定的字符来找到字符串中每一个字符的位置,如果只执行一次的话,找到的就是第一个要求的字符,如果接着执行的话,就会在第一个基础上往后找。根据函数的这个属性的话,我们就能够利用这个函数从前往后的一次寻找空格来自动帮我们分开字符串。当然找到了符合条件的情况下,就会返回从左到右的第一个子串,后续的会返回第一个结尾之后的第二个位置的子串。如果找不到符合条件的话,就返回NULL。
为什么就是需要我们去实现一个字符串分割为多个呢?那是因为无论未来我们是用什么样子的系统调用的程序替换都需要我们命令行输入的一个一个打散的,而不是整个一起的方式去读取。
其中的NUM是用来默认设置一个命令行参数的个数的,通常情况下来说一个指令后面加上的选项不会超过NUM默认的32个的,如果超过的话,可以自行修改NUM让其能够存放在gArgv[]之中
#define NUM 32
#define SEP " "
char *gArgv[NUM];
void SplitCommand(char command[], size_t n)
{(void)n;// "ls -a -l -n" -> "ls" "-a" "-l" "-n"gArgv[0] = strtok(command, SEP);int index = 1;while((gArgv[index++] = strtok(NULL, SEP))); // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束
}
这里定义的SEP我们需要找到的目标的位置是空格,但是这里非常容易错,那是因为strtok函数中的第二个参数是字符串而不是字符。
4、5、CheckBuildin函数
内建命令的特点就是不需要考虑当前环境或者是默认的配置的条件,在什么地方shell都能够运行出来相对于的结果。
对于现在的我来说我只认识两个内建命令。分别是cd命令,echo $?命令。这两个我在之前讲环境变量的时候讲述过了其特点。所以要想这两个命令的与众不同,肯定是在函数结构上的与众不同。就比如之前的一些命令的话会存在于bin目录之下,但是内建命令可能就直接存在程序之中,这样的话,不会受到环境的因素也能够实现相对应的指令。
所以根据内建命令的特点,我写了一个检查内建命令的函数,如果满足条件的话就会直接运行,不会先替换进程然后执行,这样就能够避免环境改变造成无法执行相关功能的问题。
函数的返回值设置为int类型,这样做的话能够判断是否用户输入的为内建命令,如果是内建命令的话,就会执行完,也就不会再去执行下一个的ExecuteCommand函数。避免了重复执行的错误。
char cwd[SIZE*2];
int lastcode = 0;
const char *GetHome()
{const char *home = getenv("HOME");if(home == NULL) return "/";return home;
}
void Cd()
{const char *path = gArgv[1];if(path == NULL) path = GetHome();//如果是空的话,会在直接返回家目录// path 一定存在chdir(path);// 刷新环境变量char temp[SIZE*2];getcwd(temp, sizeof(temp));snprintf(cwd, sizeof(cwd), "PWD=%s", temp);putenv(cwd); // OK
}int CheckBuildin()
{int yes = 0;const char *enter_cmd = gArgv[0];if(strcmp(enter_cmd, "cd") == 0){yes = 1;Cd();}else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0){yes = 1;printf("%d\n", lastcode);lastcode = 0;}return yes;
}
这里容易错的地方就是环境变量也是需要更新的,不能说我们进行了好几次的cd或者其他命令之后环境变量因为没有进行更新从而错误。
这样的话能够实现简单的内建命令。那我们该怎么去执行内建命令之外的命令呢?当然是使用进程替换!
4、6、ExecuteCommand函数
进程替换,那就是说在该函数中需要使用到fork()函数,并且还需要判断使用哪一个系统调用函数来确定传参条件。考虑之后还是使用execvp函数。下面是实现的代码。
void ExecuteCommand()
{pid_t id = fork();if(id < 0) Die();else if(id == 0){// childexecvp(gArgv[0], gArgv);exit(errno);}else{// fahterint status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);}}
}
5、总结
这样的话就简单的把一个shell的指令完全的自我实现了,其中当然也会有很多的不足的地方,但是基本上的内容都已经实现。希望读者能够在本篇文章的基础之上,学到更多,理解过多的关于shell编程的快乐,也希望能够通过该篇文章,给自己的学习路上添砖加瓦,做到更好。
相关文章:
【Linux】模拟实现一个shell
接受每一个人的批评,可是保留你自己的判断。 ——莎士比亚 一段时间的没有更新是由于最近开学期间比较的忙,同时也是由于刚开学的几门课才学习的时候有点迷糊,需要在学校课堂上花的时间更多了,所以才没有更新的,求放过…...
云原生数据库 PolarDB
简介:云原生数据库 PolarDB 是阿里云自研产品,在存储计算分离架构下,利用了软硬件结合的优势,为用户提供秒级弹性、高性能、海量存储、安全可靠的数据库服务。100%兼容MySQL和PostgreSQL生态,支持分布式扩展࿰…...
MobaXterm基本使用 -- 服务器状态、批量操作、显示/切换中文字体、修复zsh按键失灵
监控服务器资源 参考网址:https://www.cnblogs.com/144823836yj/p/12126314.html 显示效果 MobaXterm提供有这项功能,在会话窗口底部,显示服务器资源使用情况 如内存、CPU、网速、磁盘使用等: (完整窗口࿰…...
elastic Search 初步之向量检索的数据写入及检索查询
### Elasticsearch 向量检索实现方法方案 Elasticsearch 从 7.3 版本开始引入了向量检索功能,支持通过向量字段进行相似度搜索。以下是实现向量检索的步骤和方案,包括 Python 和 Java 版本的代码示例。 #### 1. 最低实现向量检索的 ES 版本 - **最低版本**: Elasticsearch …...
Tdesign TreeSelect 树形选择 多选
这里写自定义目录标题 小程序原生开发 Tdesign TreeSelect 树形选择 多选可以选择不同一级分类下的数据 小程序原生开发 Tdesign TreeSelect 树形选择 多选可以选择不同一级分类下的数据 TreeSelect 树形选择 在原demo基础上修改 const chineseNumber 一二三四五六七八九十.…...
Pygame中Sprite实现逃亡游戏5
在《Pygame中Sprite实现逃亡游戏4》中通过碰撞检测实现了玩家、飞龙与飞火之间的碰撞处理,基本上实现了逃亡功能。最后,实现这个逃亡游戏中文字提示的功能。 1 操作提示 当进入游戏后,会在玩家下方的位置给出操作提示,如图1所示…...
等保2.0数据库测评之达梦数据库测评
一、达梦数据库介绍 达梦数据库管理系统属于新一代大型通用关系型数据库,全面支持 ANSI SQL 标准和主流编程语言接口/开发框架。行列融合存储技术,在兼顾 OLAP 和 OLTP 的同时,满足 HTAP 混合应用场景。 本次安装环境为Windows10专业版操作…...
集成mcuboot后测试和验证的方法
本文介绍一些在实际项目中集成的 MCUboot后测试和验证的方法和步骤: 功能测试 启动测试 正常启动验证 : 多次上电启动设备,观察 MCUboot 是否能够正常加载并跳转到应用程序。检查启动过程中的日志输出(如果有)&#…...
Vulhub zico 2靶机详解
项目地址 https://download.vulnhub.com/zico/zico2.ova实验过程 将下载好的靶机导入到VMware中,设置网络模式为NAT模式,然后开启靶机虚拟机 使用nmap进行主机发现,获取靶机IP地址 nmap 192.168.47.1-254根据对比可知Zico 2的一个ip地址为…...
宠物医院微信小程序源码
文章目录 前言研究背景研究内容一、主要技术?二、项目内容1.整体介绍(示范)2.系统分析3.数据表信息4.运行截图5.部分代码介绍 总结 前言 随着当代社会科技的迅速发展,计算机网络时代正式拉来帷幕,它颠覆性的影响着社会…...
[教程]Crystal源码下载及编译
描述: 随着 Crystal Source 代码的更新,用于构建源代码和编译它们的指南已经过时,这导致了很多混淆和寻求帮助。 本指南将是一个完整的分步指南,从下载 Visual Studio 到启动到您的服务器。 此外,请确保下载此存储库中…...
【Android 14源码分析】WMS-窗口显示-流程概览与应用端流程分析
忽然有一天,我想要做一件事:去代码中去验证那些曾经被“灌输”的理论。 – 服装…...
双指针---(部分地更新)
双指针 复写零 给你一个长度固定的整数数组 arr ,请你将该数组中出现的每个零都复写一遍,并将其余的元素向右平移。 注意:请不要在超过该数组长度的位置写入元素。请对输入的数组 就地 进行上述修改,不要从函数返回任何东西。 …...
【Windows】自定义显示器的分辨率
背景 由于本人更新驱动导致2个显示器里面,有一个显示器的分辨率只剩下2个可以调节 这样就导致2个显示器分辨率不同,更新了多次驱动都修复不了,所以想着看能不能自定义分辨率 工具下载 显示器自定义分辨率工具 或者百度搜索 Custom Resolu…...
组播基础-2-IGMP协议
文章目录 IGMPIGMPv1IGMPv2IGMPv3IGMP总结IGMP Snooping IGMP 运行于主机和路由器之间 因特网组管理协议,TCP/IP 协议族中负责 IP 组播成员管理的协议,用来在接收者与其他直接相邻的组播路由器之间建立、维护组播组成员关系 负责组播成员管理…...
基于Springboot+Vue的视频点播系统设计与实现登录 (含源码数据库)
1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统中…...
执行力怎么培养?
执行力怎么培养? 并行:适合在初期养成习惯,不抱对结果的期望天才就是强迫症:适合中期修身:适合高级 并行:适合在初期养成习惯,不抱对结果的期望 在你开始做任何事情的时候,不要一开…...
Power apps:一次提交多项申请
1、添加一个Form,导入sharepoint列表,添加确认,继续,取消按钮 2、在页面的onvisible属性中添加 Set(applynumber,Last(付款申请表).申请编号1); #定义一个申请编号变量,每次申请,就将列表最后一个…...
Oracle数据库物理结构操作管理
实验步骤 (1)查询数据库初始化参数中参数名包含sga的参数的名称、值和描述信息。 SQL> select name,value,description from V$PARAMETER where name like %sga%; (2)设置sga_max_size的大小为1G SQL> alter system set sg…...
Python自然语言处理之spacy模块介绍、安装与常见操作案例
文章目录 spacy模块介绍安装spacy常见操作案例及代码1. 加载模型并处理文本2. 词性标注3. 命名实体识别4. 依存句法分析5. 可视化(在Jupyter Notebook中) spacy模块介绍 spacy是一个强大的Python库,用于自然语言处理(NLP…...
使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式
一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明:假设每台服务器已…...
【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
MongoDB学习和应用(高效的非关系型数据库)
一丶 MongoDB简介 对于社交类软件的功能,我们需要对它的功能特点进行分析: 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具: mysql:关系型数据库&am…...
oracle与MySQL数据库之间数据同步的技术要点
Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异,它们的数据同步要求既要保持数据的准确性和一致性,又要处理好性能问题。以下是一些主要的技术要点: 数据结构差异 数据类型差异ÿ…...
【Go】3、Go语言进阶与依赖管理
前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课,做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程,它的核心机制是 Goroutine 协程、Channel 通道,并基于CSP(Communicating Sequential Processes࿰…...
python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...
在Ubuntu中设置开机自动运行(sudo)指令的指南
在Ubuntu系统中,有时需要在系统启动时自动执行某些命令,特别是需要 sudo权限的指令。为了实现这一功能,可以使用多种方法,包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法,并提供…...
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...
