自行编写一个简单的shell!

本文旨在编写一个简单的shell外壳程序!功能类似于shell的一些基本操作!虽然不能全部实现shell的一些功能!但是通过此文章,自己写一个简单的shell程序也是不成问题!并且通过此文章,可以让读者对linux中一些环境变量等基本概念有更深的理解!希望读完本篇文章能对读者有一定的收获!文末会附带自己编写shell的源码!


好的废话少说,正文开始!

首先我们先来看一下linux中的shell长什么样子!

这是其shell刚启动的时候的样子!其外貌就是一个中括号内部加上一系列的东西!其实当我们认真观察,不难发现,里面包括的就是“用户名“+“@”+“主机名字”+“当前工作路径!”那么发现了此规律之后我们不难实现此描述框!那么接下来我们就着手与这些描述框的实现!

linux描述框的实现!

其中要想获得我们的用户名!我们其实可以通过环境变量进行获取,那么该如何获取环境变量的值呢?这里就不得不引进一个获得环境变量的值的函数了!

getenv(“USER”)

通过查询man手册,我们可以发现getenv()函数只需要传递一个参数即可!那么此参数是什么呢?其实此参数就是我们想要获得环境变量的值的名字!所以要想获得用户名,我们可以直接使用getenv(USER),即可获得我们想要的用户名!我们可以验证一下USER对应的环境变量是否真的是我们所要的环境变量名!我们可以通过echo $USER  此命令来判断是否真的是我们想要的用户名!

不难看出,USER对应的环境变量确实是我们的用户名!


getenv(“HOSTNAME”)

既然有了用户名,那么我们的主机名如何获得呢?思路还是调用getenv(HOSTNAME)操作!获取主机名!同样的也可以通过echo命令进行验证!这里就不再累赘了!


getenv(“PWD”)

最后再来获取我们的当前工作目录!也是调用getenv函数!同样的可以通过echo命令进行验证!


那么这些基本的环境变量都出来了,我们是否可以通过上述思路来创建一个简单的描述框呢?

代码如下:

其中这里为了方便起见,直接将各个函数进行封装!保证代码的健壮性!

其中还需要扩充的几点有:

为了区别与系统的shell,我们在描述框后面加上一个#以区分系统的$ !这样我们的描述框已经基本实现了!

获取用户指令以及将其分割!

那么基本的描述框已经实现了!我们还需要做的一点就是获取用户输入的指令!那么如何获取用户的指令呢?思路很简单:定义一个数组,然后将用户输入的字符放到数组中即可!!那么能否用scanf函数呢?答案是肯定不行!因为用户输入的指令一般都是指令+选项!其中指令和选项之间都是有着空格来间隔区分的!那么应该如何获取用户的输入呢?答案很简单,用fgets函数即可,那么接下来我们就来介绍一下fgets函数的用法!!

fgets函数

 通过查询man手册可以看出,其中fgets函数中有三个参数,第一个是就是缓冲区即(将要被写到哪里的地址!)第二个参数表示此缓冲区的大小!第三个参数是用哪些流进行写入!一般第三个参数我们都选择(stdin标准输入流)进行写入!

既然介绍了fgets函数的用法,那么我们就知道我们需要创建一个数组来存放即将要写入的数据!数组的大小自己来定义即可!

那么用户的指令获取成功之后,我们需要将用户的指令进行打散然后利用execvp进行替换即可!那么如何进行打散这段字符串呢?这里就不得不引进我们C语言中的strtok函数了!

strtok()函数!

查询man手册可以得知,strtok有两个参数!其中第一个参数是将要打散的原字符串,第二个字符串指的是用于打散的标记符都有哪些。

返回值:第一次调用,返回标记符第一次出现的位置,然后并将标记符转化为\0,此时会记住此位置!然后再次使用的使用第一个参数只需要传NULL指针即可!如果最终不可再进行分割的时候,返回值就会返回NULL!这样就可以将原字符串进行打散!我们的目的是想要将其打散放在一个数组中,方便之后使用!所以我们还需要自己再定义一个指针数组用于存放分割后的各个字符串!

通过以上的思路,我们就可以将用户命令和将命令打散此功能进行实现了!

代码如下:

其中第60行是将最后的\n转化为\0,防止其进行跳行!!其中在commandSplit函数中,我们还设置了条件宏!用于检查我们的代码是否将原字符串进行正确的打断!如果最后不想要打印出分割后的字符串,可以将宏定义取消即可!其中char*out[]表示打散后的数组!!char *in 表示的是原字符串!spint是一个宏定义用来标明分割字符串都有哪些,这里分割符只要空格!

至此,描述框和获取用户指令都已经实现了!

完成进程替换!

那么我们如何将用户的指令转化为shell的操作呢?这里就得引进进程替换的概念!我们需要将进程进行替换来让他执行我们想让他执行的代码!

那么进程替换有很多中调用方式?我们应该选择那种呢?其实很简单!我们已经将用户的命令行进行打断分散处理了,所以我们完全可以根据v的特性来进行选择,又因为我们并不知道用户以后需要输入的指令,所以我们也不知道其指令所在路径,所以我们就可以使用execvp这个系统调用来进行进程替换!其中v我们已经有了!p默认为我们提供了路径,所以用户的指令肯定是存放在v[0]上的!所以我们的进程替换就可以写出来了!

需要注意的是,我们要进行进程替换的时候,一定不要让我们的父进程进行替换!因为一旦父进程进行替换的时候,如果进程挂掉了,那么我们的shell不就是结束了么,所以我们可以使用fork来创建子进程来进行进程替换,而父进程只需要等待子进程退出,回收其资源即可!

下面来看一下进程替换的代码!

至此,进程替换的指令也可以实现了,我们自定义的shell程序也能实现ls  top pwd 等操作了!但是对于其他命令我们自定义的shell程序却不能正确的执行了!例如cd命令,还有export命令!这是为什么呢?这就不得不引进内建命令了!

内建命令

何为内建命令呢?内建命令就是这些命令只能由bash自己执行!而不能让子进程进行执行!那么我们常见的linux中有哪些命令是内建命令呢,下面就来简单的介绍几个内建命令,并且在我们自定义的shell中实现这些内建命令!!

cd命令!!

其中cd是一种常见的内建命令!这个指令只能交付给父进程自己执行,而不能交付给子进程让子进程执行!因为cd指的就是改变当前的路径,如果交给子进程进行执行,那么父进程的路径将不会修改!那么该如何进行编写我们shell中的cd命令呢?

代码如下:

其中cd主要进行的操作就是将当前的工作目录进行修改!那么如何修改当前的工作目录呢?这里就不得不引进chdir这个系统调用了!

chdir()

其中chdir函数只有一个参数,这个参数代表的是将要修改的路径!我们只需要定义一个字符数组,然后将我们要修改的路径存放到此数组中,然后将此数组就进行传递即可完成改变当前的路径!其中还需要将当前的环境变量PWD也进行修改!创建一个临时数组和全局数组,全局用于存放环境变量的值!然后将修改后的环境变量的值写入到全局数组中!最后再将环境变量进行同步!只需要调用putenv就可以将环境变量进行修改!

export命令!

还有一个常见的内建命令就是export,那么什么是export呢?export命令就是将我们定义的变量导入到环境变量之中!下面来看一下如何实现我们自己shell的export命令!

代码如下:

其中我们需要定义一个全局变量的数组用于存放我们的环境变量的值!如果我们使用的是局部变量的话!就会导致每次用户输入命令的时候,我们不更新环境变量,其环境变量就会自动消失!这是因为局部变量的局部性!所以定义一个全局变量是最为合适的!但是此代码也有一个小bug,就是当再导入一个新的环境变量的时候,之前的那个环境变量就会消失!

既然环境变量也能导出了,那么我们总得知道是否真正的将其导出了,这里就得引出了echo命令了,因为此命令也是内建命令,所以也得交给我们的父进程自己执行!下面就来写一下关于echo命令的代码!

ehco命令!

其中echo命令简单分为三个功能!第一个是回显出退出码!第二个是显示出环境变量的值!第三个就是普通的回显字符串!

对于第一个回显错误码:我们只需要判断其分割后的第二个字符串是否是“$”即可!然后根据$后面跟的字符即可判断出来,如果$后面跟的是"?"字符的话,那就是显示出退出码的信息!如果“$”后面跟的不是"?"而是一个字符串!那么就是显示出其环境变量的值!最后如果连"$"字符都没有的话,那就是简单的回显字符串了!

这就是echo命令实现的简单逻辑了!

但是我们写的shell还有大多数功能没有实现,比如本地变量的存储,以及重定向的操作!对于本地变量的存储,我们可以用malloc在堆内申请空间来存储变量中的值,对于重定向!我们可以利用dup函数进行重定向的操作!下面来看一下简单的检查是否有重定向的函数吧!

重定向:

其中SkipSpace是一个宏,其作用就是跳过空格的!我们检查是否有重定向,顺便也能将文件名与命令相分割,然后在execu函数体内进行重定向的操作!

这样我们的shell也能支持重定向的功能了!

至此我们自己的shell已经初步完成了,它能完成一些简单的操作!!希望读完本文,读者也尝试写一下shell的实现!

下面将源码附在下面!

源码

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
#define Size 50
#define NUM 1024
#define spint " "
//#define debug 1#define NOredir 0
#define AppendRedir 3
#define InputRedir  1
#define OutputRedir 2#define SkipSpace(pos) do{ while(isspace(*pos)) pos++; }while(0)char *filename=NULL;
int redir=NOredir;
int lastcode=0;
char enval[1024];
char cwd[1024];
// char eni[1024];
const char* getUser()
{char* user=getenv("USER");if(user){return user;}else{return "none";}}const char*getHost()
{char *host=getenv("HOSTNAME");if(host){return host;}else{return "none";}
}char*gethome()
{char *pwd=getenv("PWD");if(pwd){return pwd;}else{return "none";}
}int getcommand(char*command,int n)
{printf("[%s@%s %s]#",getUser(),getHost(),gethome());char*r=fgets(command,n,stdin);if(r==NULL) return 0 ;command[strlen(command)-1]='\0';return 1;
}void commandSplit(char *in,char *out[])
{int argc=0;out[argc++] =strtok(in,spint);while(out[argc++]=strtok(NULL,spint));
#ifdef debug int i=0;for(i=0;out[i];i++){// printf("%d:%s\n",i,out[i]);printf("%s\n",out[i]);}// printf("\n");
#endif
}//只需要将用户的命令行数组指令传递过来即可!
int execute(char* argv[])
{pid_t rit=fork();if(rit==0){int fd=0;if(redir==InputRedir){fd = open(filename, O_RDONLY); // 差错处理我们不做了dup2(fd, 0);}else if(redir==OutputRedir){fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);dup2(fd, 1);}else if(redir==AppendRedir){fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);dup2(fd, 1);}else{//do nothing}//子进程!用于进程切换!而不是让父进程bash直接自己运行!//其中进程替换直接用execvp函数即可,因为我们有了用户的命令行了!execvp(argv[0],argv);exit(0);//如果替换失败就会退出!负责代表进程替换成功!}else{int status=0;//父进程!只需要等待子进程退出即可!pid_t ret=waitpid(rit,&status,0);if(ret==rit){// printf("wait success\n");lastcode = WEXITSTATUS(status);// printf("%d",lastcode);//  return 0;}}return 0;
}void cd(const char*path)
{chdir(path);char tem[1024];getcwd(tem,sizeof(tem));sprintf(cwd,"PWD=%s",tem);putenv(cwd);}
//检查是否为内建命令并执行!
int dobuildin(char*argv[])
{//cd命令!if(strcmp(argv[0],"cd")==0){char *path=NULL;if(argv[1]==NULL)  {path=gethome(); }else path=argv[1];cd(path);return 1;}else if(strcmp(argv[0],"export")==0){ if(argv[1]==NULL) return 1; strcpy(enval,argv[1]);//  strcpy(envir,argv[1]);// putenv(envir);//此处需要用全局变量数组来存储env 因为一旦使用局部变量的时候,会随着用户输入的指令putenv(enval);//此处需要用全局变量数组来存储env 因为一旦使用局部变量的时候,会随着用户输入的指令//环境变量会消失!return 1;}else if(strcmp(argv[0],"echo")==0){//与系统中的echo保持一致!if(argv[1]==NULL){printf("\n");return 1;}if(*(argv[1])=='$'&&strlen(argv[1])>1){char *val=argv[1]+1;if(strcmp(val,"?")==0){printf("%d\n",lastcode);lastcode=0;}else{char *enval=getenv(val);if(enval) printf("%s\n",enval);else{printf("\n");}// return 1;}return 1;}else{printf("%s\n",argv[1]);return 1;}// return 1;}else if(0){}return 0;
}void checkRedir(char usercommand[], int len)
{// ls -a -l > log.txt// ls -a -l >> log.txtchar *end = usercommand + len - 1;char *start = usercommand;while(end>start){if((*end) == '>'){if(*(end-1) == '>'){*(end-1) = '\0';filename = end+1;SkipSpace(filename);redir = AppendRedir;break;}else{*end = '\0';filename = end+1;SkipSpace(filename);redir = OutputRedir;break;}}else if(*end == '<'){*end = '\0';filename = end+1;SkipSpace(filename); // 如果有空格,就跳过redir = InputRedir;break;}else{end--;}}
}int main()
{while(1){char userCommand[NUM];char* argv[Size];//显示框架!获取用户输入的指令!int n= getcommand(userCommand,sizeof(userCommand));// if(n==0) continue;// printf("%s")//将用户的命令进行切割!checkRedir(userCommand,strlen(userCommand));commandSplit(userCommand,argv);//判断命令是否为内建命令1!int k=dobuildin(argv);if(k) continue;//创建子进程用于进行进程替换!execute(argv);}// return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/213717.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

爱智EdgerOS之深入解析如何应用爱智的视频流模块完成拉流

一、ONVIF 规范和常见视频流传输协议 ① ONVIF 规范 随着视频监控产业链的成熟&#xff0c;市面上陆陆续续出现了各式各样的网络摄像设备&#xff0c;这些设备都需要通讯协议才能进行数据传输。早期厂商都采用私有协议&#xff0c;但是现在厂商分工明确&#xff0c;有的负责生…

【python交互界面】实现动态观察图像在给定HSV范围的区域显示

HSV颜色空间 与RGB颜色空间相比&#xff0c;HSV颜色空间更适合进行颜色分析和提取特定颜色的目标。在HSV空间中&#xff0c;颜色信息被分布在不同的通道上&#xff0c;使我们能够更准确地定义颜色的范围&#xff0c;并使用阈值操作轻松地分离出我们感兴趣的区域部分。 HSV三个通…

个人博客网站如何实现https重定向(301)到http

对于个人网站站注册比较少的&#xff0c;服务器配置不是很好的&#xff0c;没必要https,https跳转到http是要时间的&#xff0c;会影响网站打开的速度。免费的https每年都要更换。个人博客网站https有一段时间了&#xff0c;而且很多页面都有收录排名&#xff0c;现在已去掉htt…

记录一次vscode markdown的图片路径相关插件学习配置过程

插件及说明查找过程 csdn搜索markdown图片路径&#xff0c;找到关于这一款插件的回答。打开vscode拓展搜索Paste Image这款插件&#xff0c;看到下载量挺高的&#xff0c;应该不赖。 点击仓库,进入该插件开源的github仓库,查看README文件阅读说明. 淡然在Vscode 插件项目下的细…

云原生系列1

1、虚拟机集群环境准备 VirtualBox类似vmware的虚拟化软件&#xff0c;去官网https://www.virtualbox.org/下载最新版本免费的&#xff0c;VirtualBox中鼠标右ctrl加home跳出鼠标到wins中。 VirtualBox安装步骤 https://blog.csdn.net/rfc2544/article/details/131338906 cent…

【小红书运营指南1】赛道选择 + 账号运营全周期

小红书运营指南1 写在最前面11.23标签一级标签二级标签 网络资源整理1. 赛道选择近2年小红书女性人群画像 2. 基础认知阶段3. 账号启动阶段4. 选题规划阶段5. 爆款打造阶段6. 涨粉变现阶段涨粉变现阶段粉丝发展阶段 写在最前面 最近做的一个项目调研&#xff0c;调研和实际有一…

Redis部署-哨兵模式

目录 redis sentinel相关名词 redis sentinel架构 故障转移流程 基于docker搭建redis哨兵 准备工作 搭建过程 模拟主节点宕机,观察哨兵节点的工作流程 哨兵重新选取主节点的流程 1.主观下线 2.客观下线 3.哨兵节点推举出一个leader节点 4.leader选举完毕,leader挑选…

高项备考葵花宝典-项目进度管理输入、输出、工具和技术(中,很详细考试必过)

项目进度管理的目标是使项目按时完成。有效的进度管理是项目管理成功的关键之一&#xff0c;进度问题在项目生命周期内引起的冲突最多。 小型项目中&#xff0c;定义活动、排列活动顺序、估算活动持续时间及制定进度模型形成进度计划等过程的联系非常密切&#xff0c;可以视为一…

Appium获取toast方法封装

一、前置说明 toast消失的很快&#xff0c;并且通过uiautomatorviewer也不能获取到它的定位信息&#xff0c;如下图&#xff1a; 二、操作步骤 toast的class name值为android.widget.Toast&#xff0c;虽然toast消失的很快&#xff0c;但是它终究是在Dom结构中出现过&…

用23种设计模式打造一个cocos creator的游戏框架----(十)迭代器模式

1、模式标准 模式名称&#xff1a;迭代器模式 模式分类&#xff1a;行为型 模式意图&#xff1a;提供一种方法顺序访问一个聚合对象中的各个元素&#xff0c;且不需要暴露该对象的内部表示. 结构图&#xff1a; ​ 适用于&#xff1a; 1、当你需要遍历一个复杂的数据结构…

Orcal数据库Schema理解、表分区理解

目录 1 Schema1.1 Orcal数据库示例1.2 MySQL数据库示例 2 Orcal表分区2.1 创建表分区2.2 查看表分区2.3 查看指定分区数据 此前未了解过Schema的概念&#xff0c;仅知道Orcal数据库比较侧重这个概念&#xff0c;搜遍全网都&#xff0c;都是啰哩吧嗦的搬抄定义&#xff0c;特此在…

微信小程序基础bug

1.苹果11手机小程序请求数据不显示 设置-》隐私-》分析与改进-》开启 ”与开发者共享“ 2.<navigator>组件回退delta不成功 tabBar 页面是不能实现后退的效果的. 因为, 当我们跳转到 tabBar 页面&#xff0c;会关闭其他所有非tabBar 页面,所以当处于 tabBar 页面时, 无…

MistralAI开源全球首个(可能)基于MoE(Mixture of Experts)技术的大模型:预训练下载链接全球直发,但实测表现似乎一般!

本文来自DataLearnerAI官方网站&#xff1a; MistralAI开源全球首个&#xff08;可能&#xff09;基于MoE&#xff08;Mixture of Experts&#xff09;技术的大模型&#xff1a;预训练下载链接全球直发&#xff0c;但实测表现似乎一般&#xff01; | 数据学习者官方网站(Datal…

在AWS Lambda上部署EC2编译的FFmpeg工具——自定义层的方案

大纲 1 确定Lambda运行时环境1.1 Lambda系统、镜像、内核版本1.2 运行时1.2.1 Python1.2.2 Java 2 环境准备2.1 创建EC2实例 3 编译FFmpeg3.1 连接EC2 4 编译5 上传S3存储桶5.1 创建S3桶5.2 创建IAM策略5.3 创建IAM角色5.4 EC2关联角色5.5 修改桶策略5.6 打包并上传 6 创建Lamb…

Java多线程并发(二)

四种线程池 Java 里面线程池的顶级接口是 Executor&#xff0c;但是严格意义上讲 Executor 并不是一个线程池&#xff0c;而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。 newCachedThreadPool 创建一个可根据需要创建新线程的线程池&#xff0c;但是在以前…

Android : Xui- RecyclerView+BannerLayout 轮播图简单应用

实例图&#xff1a; 1.引用XUI http://t.csdnimg.cn/Wb4KR 2.创建显示图片布局 banner_item.xml <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:app"…

【1】一文读懂PyQt简介和环境搭建

目录 1. PyQt简介 1.1. Qt 1.2. PyQt 1.3. 关于PyQt和PySide 2. 通过pip安装PyQt5 3. 无法运行处理 4. VSCode配置PYQT插件 PyQt官网:Riverbank Computing | Introduction 1. PyQt简介 PyQt是一套Python的GUI开发框架,即图形用户界面开发框架。 Python中经常使用的GU…

【微服务】spring循环依赖深度解析

目录 一、循环依赖概述 1.2 spring中的循环依赖 二、循环依赖问题模拟 2.1 循环依赖代码演示 2.2 问题分析与解决 2.2.1 使用反射中间容器 三、spring循环依赖问题解析 3.1 spring中的依赖注入 3.1.1 field属性注入 3.1.2 setter方法注入 3.1.3 构造器注入 3.2 spri…

uniapp 微信小程序连接蓝牙卡死 uni.onNeedPrivacyAuthorization

解决方法&#xff0c;需要同意隐私保护协议&#xff0c;否则不能开启蓝牙权限和定位权限&#xff0c;会导致连接蓝牙失败

C++ 对象的初始化和清理:构造函数和析构函数

目录 构造函数和析构函数 构造函数 析构函数 构造函数的分类及调用 括号法 显示法 隐式转换法 拷贝构造函数的调用时机 使用一个已经创建完毕的对象来初始化一个新对象 值传递的方式给函数参数传值 以值方式返回局部对象 构造函数调用规则 初始化列表 类对象作…