手把手带你手撕一个shell

 

                                                          🎬慕斯主页修仙—别有洞天 

                                                         ♈️今日夜电波:HEART BEAT—YOASOBI

                                                                2:20━━━━━━️💟──────── 5:35
                                                                    🔄   ◀️   ⏸   ▶️    ☰  

                                      💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍


目录

什么是shell?

怎么实现shell?

shell命令提示符的实现

创建子进程执行命令

内建命令的引入

内建命令的实现

shell的拼接

shell的总体代码


什么是shell?

        Shell是一种应用程序,它连接了用户和Linux内核,让用户能够更加高效、安全、低成本地使用Linux内核。 Shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。 它接收用户输入的命令并把它送入内核去执行。Shell并不是内核的一部分,而是一个建立在内核基础上的应用程序,与QQ、迅雷、Firefox等其它软件类似。

        说大白话:他就是一个进程!作为一个进程,他当然是可以实现的啦,那么我们就简简单单手撕一个简易的进程吧!

怎么实现shell?

shell命令提示符的实现

        Shell提示符是Linux系统中的一种表示形式,它出现在用户登录并启动终端模拟包或从Linux控制台登录后。它是用户与Shell进行交互的重要途径,提示符就象征着通往Shell的大门,用户可以在提示符处输入Shell命令。对于普通用户而言,Base shell的默认提示符就是一个美元符号"$",表示等待用户输入命令。如下:

[amazon@iZ7xvfrafhk3mf5qwrf2gxZ myshell]$

        要实现这个命令提示符,我们需要获取当前的用户、当前主机以及当前路径,因此根据之前所学的知识,写出以下的函数获取对应的数据:

        对于getenv()的回忆—主要用于搜索和返回环境变量的值。这个函数的参数是环境变量的名称,如果对应的环境变量存在,那么getenv函数就会返回一个指向该环境变量值的指针。

//获取用户名
const char* getUsername()
{const char* name = getenv("USER");if (name) return name;else return "none";
}//获取主机名
const char* getHostname()
{const char* hostname = getenv("HOSTNAME");if (hostname) return hostname;else return "none";
}//获取当前路径
const char* getCwd()
{const char* cwd = getenv("PWD");if (cwd) return cwd;else return "none";
}

        在得到这些数据后我们就可以组装出一个简易的命令行提示符了!再接收命令行输入的命令就可以完成基本的命令输入。需要注意的是:这里使用fgets是为了将空格也接收进来为什么return strlen(command)呢?这是判断是否有命令输入,如果没有输入,即只是传了一个回车,那么会返回一个0,后续主函数中会用于接收,通过continue跳过后续的函数(总体是一个死循环)。

//获取用户输入命令
int getUserCommand(char* command, int num)
{printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());char* r = fgets(command, num, stdin); // 最终你还是会输入\nif (r == NULL) return -1;// "abcd\n" "\n"command[strlen(command) - 1] = '\0'; // 为了将末尾的\n去掉,注意这样并不会越界 ,因为只要是输入都至少会有个\nreturn strlen(command);
}

创建子进程执行命令

        从上面的程序我们可知,存储的命令是整段的,对此我们需要进行命令的分割,用strtok按空格进行分割。从前面的知识中我们也知道:shell是创建子进程来让他执行任务的!对此,我们也创建一子进程,通过execvp的程序替换来执行对应的命令。而父进程需要执行的任务是waitpid等待子进程完成任务后储存对应的返回值,用于子进程任务完成怎么样的判断,对此shell的基本功能实现就完成了。但是还是需要改进的!

//按照空格分割命令
void commandSplit(char* in, char* out[])
{int argc = 0;out[argc++] = strtok(in, SEP);while (out[argc++] = strtok(NULL, SEP));}//总体运行
int execute(char* argv[])
{pid_t id = fork();if (id < 0) return -1;else if (id == 0) //child{//进程替换execvp(argv[0], argv); // cd ..exit(1);}else // father{int status = 0;//存储返回值,echo $?pid_t rid = waitpid(id, &status, 0);if (rid > 0) {lastcode = WEXITSTATUS(status);//存储返回值,echo $?}}return 0;
}

内建命令的引入

        由上图我们可知,当我们使用ls、pwd等等命令时自定义的shell是能够正常运行的,但是对于cd命令还有export是不能运行的。回过头想想,我们是怎么实现shell的呢?我们运用了进程的替换,我们替换了子进程中的进程而上面的shell中并没有实现!对此我们可以肯定的是:这些命令并不是外部的进程!这也引出了—内建命令。内建命令,如其名称所示,是由Shell自身提供的命令。这些命令已经和shell编译为一体,不需要借助外部程序文件来运行。这意味着它们执行速度更快,效率更高。通俗的讲,内建命令实际上就是shell中的一个函数!

内建命令的实现

        由于内建命令已经和shell编译为一体,不需要借助外部程序文件来运行,因此我们需要一一的实现对应的内建命令,这里就先实现三个,如果要接着实现可以接着else if 实现下去。详细解释见代码:

//获取家目录
char* homepath()
{char* home = getenv("HOME");if (home) return home;else return (char*)".";
}void cd(const char* path)//用于cd命令
{chdir(path);//用于改变当前工作目录char tmp[1024];//临时储存,但不能直接使用,需要全局变量,否则栈帧销毁也会销毁getcwd(tmp, sizeof(tmp));//获取当前工作目录的绝对路径sprintf(cwd, "PWD=%s", tmp); //输出到全局变量中,保证不会失效putenv(cwd);//改变或增加环境变量的内容
}// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char* argv[])
{if (strcmp(argv[0], "cd") == 0)//通过改变当前环境变量的内容从而改变->命令行提示符路径{char* path = NULL;if (argv[1] == NULL) path = homepath();//cd 回到家目录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]);//同理需要全局变量防止失效putenv(enval); // 改变环境变量return 1;}else if (strcmp(argv[0], "echo") == 0){if (argv[1] == NULL) {//echo 输出换行printf("\n");return 1;}if (*(argv[1]) == '$' && strlen(argv[1]) > 1) {//根据指令输出char* val = argv[1] + 1; // $PATH $?if (strcmp(val, "?") == 0){printf("%d\n", lastcode);lastcode = 0;}else {const char* enval = getenv(val);if (enval) printf("%s\n", enval);else printf("\n");}return 1;}else {//只是输出到屏幕printf("%s\n", argv[1]);return 1;}}else if (0) {}//接下来的内建命令return 0;
}

shell的拼接

        shell在Linux中是一直运行的,因此为一个死循环!,我们定义一个usercommand字符数组用于接收储存从命令行收到的初步命令,定义一个字符串数组来接收通过commandSplit分割后的字符串,接下来区分是否为内建命令,按照argv里面的命令进行执行程序替换或者执行内建命令。

int main()
{while (1) {char usercommand[NUM];char* argv[SIZE];// 1. 打印提示符&&获取用户命令字符串获取成功int n = getUserCommand(usercommand, sizeof(usercommand));if (n <= 0) continue;// 2. 分割字符串// "ls -a -l" -> "ls" "-a" "-l"commandSplit(usercommand, argv);// 3. check build-in commandn = doBuildin(argv);if (n) continue;// 4. 执行对应的命令execute(argv);}
}

shell的总体代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#define NUM 1024
#define SIZE 64
#define SEP " "char cwd[1024];
char enval[1024]; // for test
int lastcode = 0;//获取家目录
char* homepath()
{char* home = getenv("HOME");if (home) return home;else return (char*)".";
}//获取用户名
const char* getUsername()
{const char* name = getenv("USER");if (name) return name;else return "none";
}//获取主机名
const char* getHostname()
{const char* hostname = getenv("HOSTNAME");if (hostname) return hostname;else return "none";
}//获取当前路径
const char* getCwd()
{const char* cwd = getenv("PWD");if (cwd) return cwd;else return "none";
}//获取用户输入命令
int getUserCommand(char* command, int num)
{printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());char* r = fgets(command, num, stdin); // 最终你还是会输入\nif (r == NULL) return -1;// "abcd\n" "\n"command[strlen(command) - 1] = '\0'; // 为了将末尾的\n去掉,注意这样并不会越界 ,因为只要是输入都至少会有个\nreturn strlen(command);
}//按照空格分割命令
void commandSplit(char* in, char* out[])
{int argc = 0;out[argc++] = strtok(in, SEP);while (out[argc++] = strtok(NULL, SEP));}//总体运行
int execute(char* argv[])
{pid_t id = fork();if (id < 0) return -1;else if (id == 0) //child{//进程替换execvp(argv[0], argv); // cd ..exit(1);}else // father{int status = 0;//存储返回值,echo $?pid_t rid = waitpid(id, &status, 0);if (rid > 0) {lastcode = WEXITSTATUS(status);//存储返回值,echo $?}}return 0;
}void cd(const char* path)
{chdir(path);//用于改变当前工作目录char tmp[1024];//临时储存,但不能直接使用,需要全局变量,否则栈帧销毁也会销毁getcwd(tmp, sizeof(tmp));//获取当前工作目录的绝对路径sprintf(cwd, "PWD=%s", tmp); //输出到全局变量中,保证不会失效putenv(cwd);//改变或增加环境变量的内容
}// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char* argv[])
{if (strcmp(argv[0], "cd") == 0){char* path = NULL;if (argv[1] == NULL) path = homepath();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]);//同理需要全局变量防止失效putenv(enval); // ???return 1;}else if (strcmp(argv[0], "echo") == 0){if (argv[1] == NULL) {printf("\n");return 1;}if (*(argv[1]) == '$' && strlen(argv[1]) > 1) {char* val = argv[1] + 1; // $PATH $?if (strcmp(val, "?") == 0){printf("%d\n", lastcode);lastcode = 0;}else {const char* enval = getenv(val);if (enval) printf("%s\n", enval);else printf("\n");}return 1;}else {printf("%s\n", argv[1]);return 1;}}else if (0) {}return 0;
}int main()
{while (1) {char usercommand[NUM];char* argv[SIZE];// 1. 打印提示符&&获取用户命令字符串获取成功int n = getUserCommand(usercommand, sizeof(usercommand));if (n <= 0) continue;// 2. 分割字符串// "ls -a -l" -> "ls" "-a" "-l"commandSplit(usercommand, argv);// 3. check build-in commandn = doBuildin(argv);if (n) continue;// 4. 执行对应的命令execute(argv);}
}

 


                         感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o! 

                                       

                                                                        给个三连再走嘛~  

 

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

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

相关文章

解决Android Studio The path ‘X:\XXX‘ does not belong to a directory.

目录 前言 一、问题描述 二、解决方法 前言 在移动应用开发领域&#xff0c;Android Studio作为一款功能强大的集成开发环境&#xff0c;为开发人员提供了丰富的工具和功能。然而&#xff0c;在使用Android Studio的过程中&#xff0c;有时也会遇到各种各样的问题和错误。 &…

[Redis] Redisson实现分布式锁

实现分布式锁的方式有多种&#xff0c;例如基于数据库、Redis、ZooKeeper 等中间件来实现&#xff0c;它们通常依赖于这些中间件提供的事务特性&#xff0c;或者命令语义来达到分布式环境下的锁效果。例如&#xff0c;Redis 通过 SETNX 命令配合过期时间可实现一个简单的分布式…

0基础学习VR全景平台篇第134篇:720VR全景,云台调整节点

相机、云台和脚架全套设备组装完成后需要进行调校才能开始拍摄。这一节&#xff0c;我们将主要介绍云台调整的两个内容&#xff1a;对中心靶、调三点一线。&#xff08;后附调校原理&#xff09; 云台部件名称 一、调节准备 &#xff08;一&#xff09;对于安装好的云台 1.检…

clickhouse-client INSERT CSV/TSV时跳过错误行

clickhouse-client INSERT CSV/TSV时跳过错误行 在使用clickhouse-client向ck中导入csv文件时&#xff0c;当csv中有个别行数据格式错误时&#xff0c;整个文件就插入失败了&#xff0c;经常会导致丢数据。 经过一番搜索&#xff0c;发现ck提供了两个参数可以跳过错误行&#x…

三、C语言分支与循环知识点补充——随机数生成

本章分支结构的学习内容如下&#xff1a; 三、C语言中的分支与循环—if语句 (1) 三、C语言中的分支与循环—关系操作符 (2) 三、C语言中的分支与循环—条件操作符 与逻辑操作符(3) 三、C语言中的分支与循环—switch语句&#xff08;4&#xff09;分支结构 完 本章循环结构的…

java实验室预约管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 java servlet 实验室预约管理系统是一套完善的java web信息管理系统 系统采用serlvetdaobean&#xff08;mvc模式)&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数 据库&#xff0c;系统主要采用B/S模式开发。开发环境为T…

【触想智能】嵌入式工控一体机的特点与应用场景分析

嵌入式工控一体机是一种用于工业自动化控制的计算机设备&#xff0c;它将显示器、主机、键盘、鼠标等器件集成在一起&#xff0c;具有高可靠性、抗干扰能力强、易于维护等特点。 嵌入式工控一体机&#xff0c;有内嵌式和外嵌式两种&#xff0c;在社会生产中被广泛应用&#xff…

【194】PostgreSQL 14.5 编写SQL从身份证号中查找性别,并且更新性别字段。

假设有一张用户表 t_user &#xff0c;该表设计如下&#xff1a; id: character varying 主键 name: character varying 姓名 idcard: character varying 身份证号 gender: smallint 性别&#xff0c;女是0&#xff0c;男是1根据身份证号查找所有未填写…

stable diffusion 基础教程-文生图

置顶大模型插件资源链接 你如果没有魔法上网,请自取 百度云盘链接:链接:https://pan.baidu.com/s/1_xAu47XMdDNlA86ufXqAuQ?pwd=23wi 提取码:23wi 有疑问加微:mincarver 界面介绍 参数解释 参数解释Sampling method扩散去噪算法的采样模式,不同采样模式会带来不一样的效…

thinkadmin小程序用户登录,获取手机号

<?php namespace app\api\controller; use app\data\service\UserAdminService; use app\data\service\UserTokenService; use think\facade\D

C++_菱形继承(虚继承)

菱形继承 and 虚继承 菱形继承介绍菱形继承源码菱形继承运行结果 虚继承介绍虚继承源码虚继承运行结果 菱形继承介绍 本文主要介绍菱形继承基本操作(仅附源码 and 运行结果) 1.正常菱形继承 会产生 在孙子类 中产生两个 不同的基类 菱形继承逻辑图 菱形继承源码 #include<…

某金属加工公司的核心人才激励体系搭建项目纪实

【客户行业】金属加工行业 【问题类型】薪酬体系/激励体系 【客户背景】 某大型金属加工企业位于河北地区&#xff0c;成立于2000年&#xff0c;隶属于某大型有色金属集团&#xff0c;是一家集科研、开发、生产、销售于一体的国有企业&#xff0c;人员达到1000人。经过多年…

Linux内核--进程管理(九)Linux内核进程管理进程优先级

目录 一、引言 二、NICE值(静态优先级) 三、优先级和实时进程 ------>3.1、chrt ------>3.2、实时/非实时进程的区 四、O1调度 ------>4.1、多核平均负载指数 ------>4.2、O1调度器处理流程 五、CFS完全公平调度 ------>5.1、产生调度的时机 ------&…

微信小程序使用mqtt开发可以,真机不行

以下可以解决我的问题&#xff0c;请一步一步跟着做&#xff0c;有可能版本不一样就失败了 一、下载mqtt.js 前往蓝奏云 https://wwue.lanzouo.com/iQPdc1k50hpe 下载好后将.txt改为.js 然后放入项目里 二、连接mqtt const mqtt require(../../utils/mqtt.min); let cli…

关于“Python”的核心知识点整理大全64

目录 20.2.15 确保项目的安全 settings.py 20.2.16 提交并推送修改 20.2.17 创建自定义错误页面 1. 创建自定义模板 500.html settings.py settings.py 注意 views.py 20.2.18 继续开发 往期快速传送门&#x1f446;&#xff08;在文章最后&#xff09;&#xff1a…

LiveGBS流媒体平台GB/T28181常见问题-国标编号是什么设备编号和通道国标编号标记唯一的摄像头|视频|镜头通道

LiveGBS国标GB28181中国标编号是什么设备编号和通道国标编号标记唯一的摄像头|视频|镜头通道 1、什么是国标编号&#xff1f;2、国标设备ID和通道ID3、ID 统一编码规则4、搭建GB28181视频直播平台 1、什么是国标编号&#xff1f; 国标GB28181对接过程中&#xff0c;可能有的小…

自然语言处理4——深度学习驱动情感分析 - Python高级实践

写在开头 在当今数字化时代,大数据和自然语言处理(NLP)技术的蓬勃发展使得情感分析在企业和社交媒体等领域得到广泛应用。其中,深度学习作为NLP领域的一项重要技术,为情感分析任务的处理提供了强大的工具。本文将介绍深度学习在情感分析中的应用,并通过Python中主要的深…

2022年中职组“网络安全”赛项湖南省B-3——私钥泄漏

B-3&#xff1a;应用服务漏洞扫描与利用 任务环境说明&#xff1a;需要环境有问题可以加q 服务器场景&#xff1a;Server15服务器场景操作系统&#xff1a;未知&#xff08;关闭链接&#xff09; 使用命令nmap探测目标靶机的服务版本信息&#xff0c;将需要使用的参数作为FLA…

用通俗易懂的方式讲解:结合检索和重排序模型,改善大模型 RAG 效果明显

最近出现了在构建聊天机器人方面的应用浪潮&#xff0c;这主要得益于LlamaIndex 和 LangChain 这样的框架。许多这类应用都采用了用于检索增强生成&#xff08;RAG&#xff09;的标准技术栈&#xff0c;其中包括以下关键步骤&#xff1a; 向量存储库&#xff1a; 使用向量存储库…

若依前后台分离项目不想配nginx部署方案

若依前后台分离框架不使用Nginx部署项目 1、修改ResourcesConfig 文件 以下是完整文件 Configuration public class ResourcesConfig implements WebMvcConfigurer {Autowiredprivate RepeatSubmitInterceptor repeatSubmitInterceptor;Overridepublic void addResourceHand…