自行编写一个简单的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.mzph.cn/news/210188.shtml

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

相关文章

C#基础面试题集

C#基础 1. 简述值类型和引用类型有什么区别2. C# String类型比 stringBuilder 类型的优势是什么?3.面向对象的三大特点4.请简述private&#xff0c;public&#xff0c;protected&#xff0c;internal的区别5.结构体和类6.请描述Interface与抽象类之间的不同7.在类的构造函数前…

go自带rpc框架生产环境使用demo

基础使用 序列化使用自带gob协议 server package mainimport ("net""net/rpc" )// 定义一个handler结构体 type HelloService struct { }// 定义handler方法,大小写&#xff0c;参数&#xff0c;返回值都是固定的&#xff0c;否则无法注册 func (receiv…

数据库事务:保障数据一致性的基石

目录 1. 什么是数据库事务&#xff1f; 1.1 ACID特性解析 2. 事务的实现与控制 2.1 事务的开始和结束 2.2 事务的隔离级别 3. 并发控制与事务管理 3.1 并发控制的挑战 3.2 锁和并发控制算法 4. 最佳实践与性能优化 4.1 事务的划分 4.2 批处理操作 5. 事务的未来发展…

SpringSecurity6 | 自定义登录页面

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; Java从入门到精通 ✨特色专栏&#xf…

高工氢电年会 | 未势能源解超朋博士受邀出席并做主题演讲

12月4日&#xff0c;以“战略重构 商业觉醒”为主题的2023高工氢电年会在深圳举办&#xff0c;未势能源副总裁解超朋博士受邀出席开幕式论坛&#xff0c;以《把握机遇、直面挑战&#xff0c;迎接氢车规模化推广时代》为主题发表演讲&#xff0c;并参与圆桌论坛研讨。 氢势已来&…

【Linux】resolv.conf 文件

resolv.conf resolv.conf 文件 是 DNS 的 client 端使用的文件&#xff0c;用于设置 DNS 服务器的 ip 地址以及 DNS 域名&#xff0c;还可以配置域名搜索顺序等等。主要包含如下关键字&#xff1a;nameserver、domain、search、sortlist、options。设置的格式都是 关键字空格 …

管理类联考——数学——真题篇——按知识分类——数据

文章目录 排列组合2023真题&#xff08;2023-05&#xff09;-数据分析-排列组合-组合-C运算-至少-需反面思考真题&#xff08;2023-08&#xff09;-数据分析-排列组合-相邻不相邻-捆绑法插空法-插空法注意空位比座位多1个&#xff0c;是用A&#xff1b;捆绑法内部排序用A&#…

2023中国(海南)国际高尔夫旅游文化博览会 暨国际商界峰层·全球华人高尔夫精英巡回赛 全国颍商自贸港行盛大启幕

2023中国&#xff08;海南&#xff09;国际高尔夫旅游文化博览会&#xff08;以下简称“海高博”&#xff09;暨全国颍商走进海南自贸港于12月7-9日在海口观澜湖盛大开幕。该活动由中国国际贸易促进委员会海南省委员会、海南省旅游和文化广电体育厅主办&#xff0c;中国国际商会…

最新版本11.17的YOLOv8加入注意力方法

本文基于11.17版本,以往版本略有不同,可查看改进YOLOv8,教你YOLOv8如何添加20多种注意力机制进行参考 放入注意力代码,以biformer注意力为例 import torch import torch.nn as nn import torch.nn.functional as Fdef position(H, W, is_cuda=

探索 Python 中链表的实现:从基础到高级

# 更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 链表是一种基础的数据结构&#xff0c;它由一系列节点组成&#xff0c;每个节点都包含数据和指向下一个节点的引用。在Python中&#xff0c;可以使用类来实现链表&#xff0c;本文将介绍如何实现链表&#xff…

【密码学引论】认证

认证是许多应用系统中安全保护的第一道设防认证和加密的区别&#xff1a;加密用来确保数据的保密性&#xff0c;而认证用来确保报文发送者和接受者的真实性和报文的完整性。认证和数字签名的区别&#xff1a; 认证总是基于某种收发双方共享的保密数据来认证被鉴别对象的真实性&…

关于linux开机自启动

1、系统启动流程 2、 init、 inittab、 init.d、 rcx.d /etc/inittab是Linux系统中的一个配置文件&#xff0c;用于定义系统的运行级别和相应的操作。其语法格式如下&#xff1a; 标签&#xff1a;运行级别&#xff1a;操作&#xff1a;进程 label:runlevel:action:process下面…

PHP基础 - 注释变量

一. 语言开始标识 在PHP中,文件的开头需要使用语言开始标识来指定该文件是PHP代码。标识通常为"<?php",也可以是"<?",但建议使用"<?php"以确保代码的兼容性和可读性。 <?php // PHP代码从这里开始写 二. PHP注释 注释是用…

【Python】Faker库详解:创建测试数据轻而易举

Python Faker库详解&#xff1a;创建测试数据轻而易举 在软件开发和测试过程中&#xff0c;通常需要大量的测试数据来模拟真实环境。Python的Faker库为开发者提供了一个方便、灵活且强大的工具&#xff0c;用于生成各种虚构数据。本文将深入介绍Faker库&#xff0c;演示其基本…

十一、了解分布式计算

1、什么是&#xff08;数据&#xff09;计算&#xff1f; 2、分布式(数据)计算 &#xff08;1&#xff09;概念 顾名思义&#xff0c;分布式计算&#xff0c;即以分布式的形式完成数据的统计&#xff0c;得到需要的结果。 分布式数据计算&#xff0c;顾名思义&#xff0c;就是…

c语言选择排序总结(详解)

选择排序cpp文件项目结构截图 项目cpp文件截图 项目具体代码截图 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <math.h> #include <iostream> #include <string.h> #include <time.h> #include &…

Java发展史

Java发展史 Java历史的几个重要节点&#xff1a; 1990&#xff0c;Sun公司启动了一个名为“绿色计划”的项目&#xff0c;由James Gosling等领导&#xff0c;目标是开发一种可以在机顶盒等消费性电子产品上运行的程序架构。这个计划产生了Java的前身&#xff1a;Oak&#xff…

看图学源码 之 ConcurrentHashMap put、get、remove、clear、扩容等相关方法的源码分析

ConcurrentHashMap 引入 HashMap 的弊端 多线程下是不安全的&#xff0c;是有死循环的 JDK1.7 的时候会使用头插法将新的节点增加到头部&#xff0c;那么就会造成链表翻转成为了闭环&#xff0c;就是所谓的死循环。 JDK1.8之后使用的是尾插法&#xff0c;因此不会造成环形链…

京东数据运营:京东API接口有哪些?京东数据如何采集调用?

市场分析对于电商品牌来说非常重要&#xff0c;它可以帮助电商品牌更好地理解市场&#xff0c;把握市场机会&#xff0c;以及制定有效的产品定价、产品营销策略等等。 结合市场中可以帮助品牌方做市场分析的电商数据分析工具——鲸参谋电商数据分析平台&#xff0c;我们一起具体…

在Mac上安装Windows应用程序的简便方法:CrossOver for Mac

对于许多Mac用户来说&#xff0c;有时候他们可能需要使用一些只有在Windows上才能找到的应用程序。以前&#xff0c;解决这个问题的方法是通过安装Windows虚拟机或使用双系统来在Mac上运行Windows应用程序。但这些方法需要额外的硬件资源和时间来配置&#xff0c;并且可能会导致…