【Linux笔记】自定义一个简单的shell

一、命令行解释器shell的原理

我们已经知道Linux给我们提供了一系列由exec开头的系统调用接口,可以让我们在自己所写的程序中调用各种指令或者我们自己写的其他程序:

而我们的shell命令行解释器也是接收用户输入的指令,然后执行:

那我们在自己所写的程序中执行一次指令,是不是就类似于一个只执行了一次的shell?

而如果我们自己写一个程序可以不断地接收用户输入的指令并执行,那是不是就等价于一个简易的shell?

所以,命令行解释器shell的原理其实就是一个死循环程序,它不断地接收用户输入的指令并执行对应的程序,直到用户退出shell。

二、先搭好大致的框架

1、先获取用户信息并打印出提示符

既然要模拟式下一个shell,那我们也要模拟的像点样子,我们平时在shell命令行中打印命令的时候,在命令的前面都有一个长长的提示符:

这个长长的提示符主要是提示一些用户的信息,包括用户名和主机名和当前所在的工作目录。

那我们也需要先获取一下。

这些用户名、主机名和工作目录其实都是一些环境变量,所以我们可以通过getenv系统调用获得:

而用户名这些信息,其实在系统的环境变量中都有:

所以要打印出这些信息其实并不难,我们直接在环境变量中获取就行了:

效果也是如预料之中的:

2、获取用户输入指令并分割成字符串数组

解决打印提示符的工作接下来就应该接收用户输入的命令了,需要注意的是我们平时在命令行中输入各种指令的时候,例如“ls -a -l”,都是会带一些空格的。所以我们不能直接使用scanf接收,因为scanf是默认遇到空格就结束的了,所以scanf最多只能接收到第一个ls。

所以我们就要使用另一个更适合的接口——fgets:

它的功能其实就是从一个输入流中读取数据,写入一个缓冲区中,我们可以从stdin(键盘)中读取数据,然后定义一个字符串,将数据保存到字符串中:

其结果也是符合我们的预期的:

获取完用户输入的指令之后我们要有干什么呢?

我们最后的目的是要执行指令的啊,我们先来考虑一下我们该使用哪一个程序题换接口才更方便呢?

因为我们现在是在自己写的程序里面去执行指令,而程序并不知道我们所输入的指令的路径在哪里,所以我们肯定是选择自带路径的,即带‘p’的。而可变参数列表又只适用于手动传参,所以我们可能也得选择带‘v’的。

所以最优的选择就是:

所以这就需要我们将用户输入的字符串以空格为分隔符,分割进一个字符串数组中。

而C语言也有这样的接口,能让我们对一个字符串以一个分隔符分割,然后放入到一个字符数组中,那就是strtok:

现在来回一下,这个strtok的第一次使用和后面的使用是不一样的,第一次使用我们需要传递的是字符串数组的起始位置,而之后传递就只需要传第一个NULL即可。

因为strtok一旦失败就返回NULL,所以我们可以像下面这样写:

运行结果:

3、执行指令

有了上面的准备工作,我们就终于可以来执行指令了。

我们观察到,在shell中执行一个指令后其实是执行完就退出了的:

既然是执行完就退出,那我们就肯定不能让我们的父进程执行,而是应该让子进程来执行,子进程执行完就直接退出,而父进程则负责回收子进程:

运行结果:

所以我们现在就成功的调用到了我们系统的指令了,而现在它只执行了一次,我们只需要将之前所写的逻辑放入到一个死循环中,就可以让我们自定义的shell一直运行了:

运行结果:

至此,我们自定义shell的雏型也就完成了,它可以执行很多我们系统的指令,也不会退出,已经满足了shell命令行的大部分功能了。

但还有一些指令,是现阶段的myshell不能完成的,比如我们可以试着运行一下cd命令:

我们会发现,myshell在执行完cd命令后,路径并没有发生改变,也就是说不能完成cd的任务。

这是因为像cd这样的命令,它是一个“内建命令”

三、处理“内建命令”

1、什么是内建命令

在Linux中,有一些命令是一定要父进程来执行的,不能由子进程来执行,这些命令就被称为“内建命令”。

就拿上面所提到的“cd”命令来说,它的本质是程序的工作目录发生了改变,之后执行任何指令都是在这个工作目录下执行。那它就必定不能交给子进程来执行,因为子进程一执行就退了,所以就算子进程的工作目录改变了也没用。

所以"cd"命令一定要是父进程执行。

Linux中其实有很多的内建命令,今天我们实现的是一个简易的shell,所以我这里只实现三个:cd、export,echo。

2、cd

执行cd命令是改变当前程序的工作目录,所以我们先要来认识一个接口:

这个接口就是用来改变当前进程的工作目录的,谁调用chdir,谁的工作目录就发生改变。

所以我们要做的就是让父进程来调用这个chdir。

我们可以封装一个函数来判断当前命令是否是内建命令如果是则执行并返回1,如果不是则返回0。

然后我们在创建子进程之前先判断一下就行了:

之后我们的cd内建命令就可以正常执行了:

但是这里还有一个问题,也就是虽然我们工作目录的确是改变了,但是我们提示符里面的工作目录却并没有改变:

原因在于我们在打印提示符的时候获取的工作目录实在环境变量里获取的:

而我们这里只是改变了工作目录,并没有对环境变量做更改,所以它每一次获取到的都是一样的。

如果想要让提示符内的路径也发生改变,那我们还得要先认识一个接口:

这个接口的作用就是将当前进程所在的绝对路径获取,并放入一个缓冲区内。

所以我们可以创建一个全局的cwd,每次改变path的时候,就获取一次当前进程的绝对路径,然后将获取到的路径放入cwd中,然后再将cwd导入到系统的环境变量表中:

做完这些工作之后,我们提示符里面的路径就也会发生改变了:

为什么cwd一定要用全局变量呢?这是因为环境变量的获取一定要有一个源头,如果cwd只是局部变量,那么子进程一退出,局部变量就被销毁了。那我们之后再查询env的时候,就查不到对应的环境变量了。

4、export

就像上面所说到的,环境变量在查询的时候一定要有一个“源头”,所以我们要导出的环境变量就一定不能存储在一个临时的空间里面。

所以我们要为我们写的myshell创建一个全局的环境变量表:

然后我们在导入环境变量的同时,把要导入的环境变量加入到我们创建的环境变量表中即可:

这样,我们导的环境变量就不会消失了,而且也能导入多个:

3、echo

这个echo就有很多情况要分了,如果echo后面跟的是“$+一个环境变量”,我们需要去环境变量表中查询出这个环境变量然后打印出来,如果后面跟的是一个字符串,那我们直接打印出这个字符串即可,而如果我们后面跟的是“$?”,那我们要打印的是最近一个程序结束时的退出码,这个也是我们等下需要特殊处理的东西。

打印字符串或者打印环境变量其实很好处理,如果是字符串那我们就直接打印好了,如果是环境变量那我们就用getenv获取后再打印出来:

运行结果:

对于退出码我们可以创建一个全局的变量latcode,默认设为0,然后在每次子进程结束后,父进程使用waitpid回收子进程的状态时将lastcode赋值即可:

运行结果:

四、整体代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/types.h>char enval[100][100];
int env_num = 0;
int lastcode = 0; // 记录最近一个进程退出是的退出码,默认为0char cwd[1024]; 
const char* getUserName() {const char *username = getenv("USER");if (username) {return username;}return "none";
}const char* getHostName() {const char* hostname = getenv("HOSTNAME");if (hostname) {return hostname;}return "none";
}const char* getPwd() {const char* pwd = getenv("PWD");if (pwd) {return pwd;}return "none";
}
// 处理内建命令
// 成功执行返回1,失败返回0
int dobuildin(char *argv[]) {if (strcmp(argv[0], "cd") == 0) {char *path = NULL;if (NULL == argv[0]) {// 如果后面没有跟路径,就让路径默认为'.'即当前目录path = ".";} else {path = argv[1];}chdir(path);// 获取当前进程的绝对路径char temp[1024];getcwd(temp, sizeof(temp));// 将temp写入cwd中sprintf(cwd, "PWD=%s", temp);// 将cwd中的环境变量导入到系统的环境变量表中putenv(cwd);return 1;} else if (strcmp(argv[0], "export") == 0) {if (argv[1] == NULL) {return 1;}strcpy(enval[env_num], argv[1]);putenv(enval[env_num]); // 注意这里要导入的是enval[env_num],而不能是argv[1]env_num++;return 1;} else if (strcmp(argv[0], "echo") == 0) {if (argv[1] == NULL) {printf("\n");return 1;}if (argv[1][0] == '$' && strlen(argv[1]) > 1) {if (argv[1][1] == '?') {// 打印上一个进程的退出码printf("%d\n", lastcode);// 因为内建命令执行时总是成功的,所以这里直接将lastcode设成0就行lastcode = 0; } else {// 打印环境变量char *val = argv[1] + 1;char *reval = getenv(val);if (reval == NULL) {printf("\n");return 1;}printf("%s\n", reval);}} else {// 表示是字符串printf("%s\n", argv[1]);}return 1;}return 0;}int main() {char usercommand[1024];while (1) {char *argv[100] = { NULL };int argc = 0;printf("[%s@%s %s]¥ ", getUserName(), getHostName(), getPwd());char *r = fgets(usercommand, sizeof(usercommand), stdin);if (NULL == r || strlen(usercommand) == 0) {continue;}usercommand[strlen(usercommand) - 1] = '\0';// 分割用户输入的指令argv[argc++] = strtok(usercommand, " ");while (argv[argc++] = strtok(NULL, " "));// 检查是否是内建命令并执行int res = dobuildin(argv);if (res) {continue; // 如果成功执行就不用再往后执行了}// 执行指令pid_t id = fork();if (0 == id) {// childint n = execvp(argv[0], argv);if (-1 == n) {printf("-myShell: %s: command not found\n", argv[0]);}exit(1);} else {// fatherint status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0) {lastcode = WEXITSTATUS(status);} else {return -1;}}}return 0;
}

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

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

相关文章

腾讯滑块(1-13,js逆向)

前言&#xff1a;之前打算写的猿人学比赛题系列因为种种原因耽搁了&#xff0c;主要还是比完赛之后热情就少了很多&#xff0c;看到评论区有人说做了这么久才做出一题&#xff0c;这里需要狡辩一下&#xff0c;我虽然菜但是还没到那种地步&#xff0c;比赛两天时间里我跟队友是…

CH341 SPI方式烧录BK7231U

CH341是一个USB总线的转接芯片&#xff0c;通过USB总线提供异步串口、打印口、并口以及常用的2线和4线等同步串行接口。 BK7231U Wi-Fi SOC芯片&#xff0c;内嵌处理器。1. 符合802.11b/g/n 1x1协议 2. 17dBm 输出功率3. 支持20/40 MHz带宽和STBC 4. 支持Wi-Fi STA、AP、…

ftp安装与配置 云服务器 CentOS7

1、FTP的安装 #安装 yum install -y vsftpd#设置开机启动 systemctl enable vsftpd.service#启动 systemctl start vsftpd.service#停止 systemctl stop vsftpd.service#查看状态 systemctl status vsftpd.service 2、配置FTP #修改前先进行备份文件 cp /etc/vsftpd/vsftpd…

腾讯云怎么领取免费云服务器?

腾讯云免费服务器申请入口 https://curl.qcloud.com/FJhqoVDP 免费服务器可选轻量应用服务器和云服务器CVM&#xff0c;轻量配置可选2核2G3M、2核8G7M和4核8G12M&#xff0c;CVM云服务器可选2核2G3M和2核4G3M配置&#xff0c;腾讯云服务器网txyfwq.com分享2024年最新腾讯云免费…

弟12章 1 网络编程

文章目录 网络协议概述 p164TCP协议与UDP协议的区别 p165 网络协议概述 p164 ipv4&#xff1a;十进制点分制 ipv6&#xff1a;十六进制冒号分隔 TCP协议与UDP协议的区别 p165 tcp协议的三次握手&#xff1a;

行为型设计模式——备忘录模式

备忘录模式 备忘录模式提供了一种状态恢复的实现机制&#xff0c;使得用户可以方便地回到一个特定的历史步骤&#xff0c;当新的状态无效或者存在问题时&#xff0c;可以使用暂时存储起来的备忘录将状态复原&#xff0c;很多软件都提供了撤销&#xff08;Undo&#xff09;操作…

【教程】微信小程序如何拍摄图片及视频并上传到后台进行存储

需求分析 在微信小程序中需要使用手机拍摄照片以及视频上传到后台进行进一步的操作&#xff0c;需要解决以下两个问题&#xff1a; 微信小程序如何拍摄照片及视频如何将拍摄的照片及视频上传到后台进行存储 解决方案 前端开发&#xff1a;微信小程序原生 后端开发&#xf…

sentinel熔断与限流

文章目录 一、sentinel简介Sentinel 是什么&#xff1f;Sentinel安装 二、sentinel整合工程新建cloudalibaba-sentinel-service8401微服务引入依赖yml配置主启动类添加EnableDiscoveryClient业务类测试 三、sentinel流控规则基本介绍流控模式直接&#xff08;默认&#xff09;关…

Web前端-移动web开发_流式布局

文章目录 移动web开发流式布局1.0 移动端基础1.1浏览器现状1.2 手机屏幕的现状1.3常见移动端屏幕尺寸1.4移动端调试方法 2.0 视口2.1 布局视口 layout viewport2.2视觉视口 visual viewport2.3理想视口 ideal viewport&#xff08;苹果&#xff09;2.4meta标签 3.0 物理像素(手…

十三、QPalette的简单使用(Qt5 GUI系列)

目录 一、设计需求 二、实现代码 三、代码解析 四、总结 一、设计需求 在实际应用中&#xff0c;经常需要改变某个控件的颜色外观&#xff0c;如背景、文字颜色等。Qt提供的调色板类 QPalette 专门用于管理对话框的外观显示。QPalette 类相当于对话框或是控件的调色板&…

记录:排查create_ap偶发无法开启自发AP的问题

背景说明&#xff1a; 系统&#xff1a;Xubuntu16.04&#xff1b;内核&#xff1a;4.14&#xff1b;无线网卡&#xff1a;EDIMAX EW-7822UAC 关于无线网卡的驱动安装和create_ap配置参考博文&#xff1a;Xubuntu16.04系统中使用EDIMAX EW-7822UAC无线网卡开启5G自发AP 目录 问题…

分布式系统的三字真经CAP

文章目录 前言C&#xff08;Consistency 数据一致性&#xff09;A&#xff08;Availability 服务可用性&#xff09;P&#xff08;Partition Tolerance 分区容错性&#xff09;CAP理论最后 前言 你好&#xff0c;我是醉墨居士&#xff0c;我一起探索一下分布式系统的三字真经C…

大数据调度框架Oozie,这个学习网站让你事半功倍!

Oozie是一个基于工作流引擎的开源框架&#xff0c;由Cloudera公司贡献给Apache。它主要用于管理和调度Apache Hadoop作业&#xff0c;支持的任务类型包括Hadoop MapReduce、Pig Jobs等。 Oozie的核心概念包括workflow jobs和coordinator jobs。Workflow jobs是由多个动作&#…

Jmeter 性能-监控服务器

Jmeter监控Linux需要三个文件 JMeterPlugins-Extras.jar (包&#xff1a;JMeterPlugins-Extras-1.4.0.zip) JMeterPlugins-Standard.jar (包&#xff1a;JMeterPlugins-Standard-1.4.0.zip) ServerAgent-2.2.3.zip 1、Jemter 安装插件 在插件管理中心的搜索Servers Perform…

AIGC视频生成:Pika1.0快速入门详解

Pika1.0快速入门详解 一、简介二、登录三、参数设置1、改变画面大小&#xff08;Aspect ratio&#xff09;2、改变帧数大小&#xff08;Frames per second&#xff09;3、镜头平移&#xff08;Camera control&#xff09;4、画面运动控制&#xff08;Strength of motion&#x…

永不停止,永远在路上!MIAOYUN 2023年度回顾

2023 MIAOYUN年度关键词&#xff1a;坚持/沉淀/成长 2023年&#xff0c;我们身处虚浮遥荡的世界&#xff1a;支原体肺炎、流感接二连三、经济下行成热词、人人思危&#xff1b;更有暴雨成灾&#xff0c;核污水扩散&#xff0c;战火与地震不断。 坏事发生时&#xff0c;你有三种…

【NLP】多标签分类【上】

简介 《【NLP】多标签分类》主要介绍利用三种机器学习方法和一种序列生成方法来解决多标签分类问题&#xff08;包含实验与对应代码&#xff09;。共分为上下两篇&#xff0c;上篇聚焦三种机器学习方法&#xff0c;分别是&#xff1a;Binary Relevance (BR)、Classifier Chain…

绝地求生:【PC】第27赛季第2轮更新公告

各位玩家大家好&#xff01;欢迎收看本期闲游盒更新公告。 正式服维护时间 ※ 下列时间可能会根据维护情况而发生变化。 1月10日上午8:00 – 下午4:30 地图轮换 ※ 地图轮换将于每周三上午10点进行。 ※ 在随机选择地图的地区中&#xff0c;第1周可选择荣都地图&#xff0c…

Java SE入门及基础(11)

程序调试 1. 什么是程序调试 当程序出现问题时&#xff0c;我们希望程序能够暂停下来&#xff0c;然后通过我们操作使代码逐行执行&#xff0c;观察整个过程中变量的变化是否按照我们设计程序的思维变化&#xff0c;从而找问题并解决问题&#xff0c;这个过程称之为程序调试…

从零开发短视频电商 PaddleOCR Java推理 (一)飞桨引擎推理

文章目录 简介方式一&#xff1a;DJL 飞浆引擎 飞桨模型方式二&#xff1a;ONNXRuntime 飞桨转换后的ONNX模型&#xff08;Paddle2ONNX&#xff09; 添加依赖文字识别OCR过程分析文字区域检测文字角度检测文字识别&#xff08;裁减旋转后的文字区域&#xff09; 高级替换模型…