Linux编写一个极简版本的Shell

Linux编写一个极简版本的Shell

📟作者主页:慢热的陕西人

🌴专栏链接:Linux

📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言

本博客主要内容在Linux环境下,简易实现了一个Shell,顺便讲解和实现了一些内建命令

文章目录

  • Linux编写一个极简版本的Shell
    • ①读取命令行
    • ②父子进程框架
    • ③切割命令行
    • ④子进程借用分割的结果来替换程序
    • ⑤优化:
    • ⑥内建命令(重要)

首先我们观察到:

bash的命令行提示符:[用户名@主机名 当前目录]

[mi@lavm-5wklnbmaja demo1]

image-20231109205329152

所以我们无限循环去打印这个命令行提示符

#include<stdio.h>    
#include<unistd.h>                                                                                                                                            
int main()    
{    while(1)    {    printf("[xupt@my_machine currpath]#");    //这里因为我们不能加换行,所以得刷新缓冲区    fflush(stdout);    sleep(1);    }    return 0;    
} 

运行效果:

image-20231109210135984

①读取命令行

接下来我们就要获取命令输入的命令行参数:

我们创建一个字符数组用来专门存放用户输入的命令行

#define MAX 1024  //因为命令行最长支持到1024
char commondstr[MAX] = {0};

我们用fgets来获取命令行

fgets(commondstr, sizeof(commondstr), stdin);  

我们测试一下:

结果正常,但是我们的命令重新被打印的时候多打印了一个换行符,因为fgets读取了换行符,并且存储到了commondstr中了.

[mi@lavm-5wklnbmaja demo1]$ ./myshell 
[xupt@my_machine currpath]#ls -a
ls -a

解决方案:

commondstr[strlen(commondstr) - 1] = '\0';//处理fget获取了换行符的问题 

运行结果:

image-20231109212142534

②父子进程框架

这个时候我们就需要用到子进程了,因为执行命令行的时候需要用到程序替换,那么如果我们用父进程的话,直接就全崩掉了。

每次输入命令,都把命令交给子进程去执行,而父进程去等待子进程就好了:

    pid_t id = fork();    assert(id >= 0);    (void) id; //和上面的处理原因一样    if(id == 0)    {    //child    }    int status = 0;             waitpid(id, &status, 0);  

在子进程执行之前,我们先要将用户输入进来的命令行进行拆分

③切割命令行

切割的原理很简单,我们只需要把命令行中间的空格变成\0即可。

ls -a -l ----> ls\0-a\0-l;

这个时候我们要引入一个C库提供的函数strtok,它是一个专门用来分隔字符串的函数。

我们需要封装一下这个函数来达到为我们分割命令行的目的:

注意strtok函数第二次切割的时候只需要传入NULL即可。

int split(char* commondstr, char* argv[])    
{    assert(commondstr);    assert(argv);    argv[0] = strtok(commondstr, SEP);    int i = 1;    while((argv[i++] = strtok(NULL, SEP)));    
//  {    
//      argv[i] = strtok(NULL, SEP);    
//      if(argv[i] == NULL) break;    
//      i++;    
//  }                                                     //表示切割成功    return 0;    
}  

main函数内部这样去调用分割函数

    int n = split(commondstr, argv);//等于0表示切割成功if(n != 0) continue;//DebugPrint(argv);    

我们再设计一个函数来打印我们切割的结果,查看我们切割的结果是否正确:

void DebugPrint(char* argv[])
{for(int i = 0; argv[i]; ++i){printf("%d : %s\n", i, argv[i]);}
}

运行结果:

image-20231109224418103

④子进程借用分割的结果来替换程序

因为我们用split函数将命令行分装到argv字符串指针数组内部了,所以我们只能用带v的加载函数。

另外因为我们不能固定路径,所以我们也只能用带p的。

所以综上:我们的加载函数就选择到了execvp函数:

在子进程内部调用:

    if(id == 0)    {    //child    execvp(argv[0], argv);    exit(0);    }    

那么这时候我们在运行一下:

image-20231109225424556


⑤优化:

我们看到我们在用bash提供的ls的时候,它产生的结果是带有颜色的。

image-20231110135556660

但是我们自己实现的简易Shell是没有颜色的,那么这到底是为什么?

我们which ls查看一下,原来系统在ls后边面追加了一个参数--color==auto;

image-20231110135703672

那么我们也可以对我们的简易Shell进行一些优化让他支持这样的显示:

我们只需要在代码中特判一下即可:

    if(strcmp(argv[0], "ls") == 0)    {    //先找到末尾    int pos  = 0;    while(argv[pos]) pos++;    //追加color参数    argv[pos] = (char*)"--color=auto";    //安全处理    pos++;    argv[pos] = NULL;                                                                                                                }

运行效果:

image-20231110140325861


⑥内建命令(重要)

(1)内建命令的概念:

—>首先我们先明确一下内建命令/内置命令的概念,就是让我们bash自己执行的命令,我们称之为内建命令/内置命令。

(2)cd命令

当我们在我们的简易Shell中切换目录时:

我们发现不论我们怎么切换目录,结果都是目录没有变化,**原因是我们是在子进程中运行这些命令行的,**进程具有独立性。其实我们切换目录是切换了子进程的目录,但是父进程也就是我们pwd显示的目录却没有任何变化,并且这里其实pwd的也是子进程的当前目录,但是因为子进程在执行完cd命令后,就被exit了。当我们再执行pwd的时候是一个新的子进程在帮我们完成这个命令,因为我们之前cd没有改变父进程的当前目录,那么新创建的子进程的目录也就变成了和父进程一样的,所以看起来我们就是没有改变当前目录一样。

image-20231110140734849

所以这里的cd命令,我们要在父进程中交给一个函数chdir()来让我们的bash来执行:

代码:

    //当我们输入cd命令的时候    if(strcmp(argv[0], "cd") == 0)    {    if(argv[1] != NULL) chdir(argv[1]);                                                                                              continue;    }  

运行结果:

image-20231110142305250

(3)export命令

此外不止我们的cd,包括我们当时去在bash中执行我们的export添加环境变量的时候,实际上是添加到我们的bash内部的,那么如果我们的简易Shell去把这个命令交给我们的子进程去执行了,那么就不太合适了,应该让我们的父进程自行去执行这个命令!

所以我们依旧采用内建命令的方式:

    //当我们输入export命令时    if(strcmp(argv[0], "export") == 0)    {    //我们把这个环境变量存储在我们自己设定的数组内部    if(argv[1] != NULL)    {    strcpy(myenv[env_index], argv[1]);    //再将数组内部的环境变量放到父进程的环境变量中    putenv(myenv[env_index++]);    }    }  

我们尝试测试一下:

image-20231110151609156

最终我们找到了

image-20231110151622993

但是我们的env打印的好像是子进程的环境变量,这似乎不是我们想要的,我们应该想要的是父进程的环境变量,所以我们再做一下处理:

我们自行实现一个函数去打印我们的环境变量:

    void PrintEnv(){extern char **environ;for(int i = 0; environ[i]; ++i){printf("%d:%s\n",i, environ[i]);}}//当我们查看环境变量的时候if(strcmp(argv[0], "env") == 0){PrintEnv();continue; }                                                                                     

运行效果:

image-20231110152530084

image-20231110152557349

所以其实我们之前学习的几乎所有的环境变量,相关的命令都是内建命令

我们在将echo支持成内建命令:

    //当我们echo的时候if(strcmp(argv[0], "echo") == 0){//先确认一下echo后面第一个跟的是$if(argv[1][0] == '$'){char* env_ret = getenv(argv[1] + 1);                                                                                           if(env_ret != NULL){printf("%s=%s\n", argv[1] + 1, env_ret);}}continue;}

运行结果:

image-20231110155212975

既然支持了环境变量的查询,我们再来顺便支持一下进程退出码的支持,也就是我们的echo $?

    //当我们echo的时候    if(strcmp(argv[0], "echo") == 0)    {    //先确认一下echo后面第一个跟的是$                                                                                                if(argv[1][0] == '$')    {    if(argv[1][1] == '?')    {    printf("%d\n", last_exit);      continue;    }    else    {    char* env_ret = getenv(argv[1] + 1);    if(env_ret != NULL)  printf("%s=%s \n", argv[1] + 1, env_ret);    }    }  int status = 0;pid_t ret  = waitpid(id, &status, 0);if(ret > 0){                                                                                                                                  last_exit = WEXITSTATUS(status);//last_exit我们放在main函数里但不要放在循环里,他要长期保留。} 

测试结果:

image-20231110160243796

⑦代码汇总:

#include<stdio.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>//因为命令行最长支持到1024
#define MAX 1024
//限制最多切割为64段
#define ARGC 64#define SEP " "int split(char* commondstr, char* argv[])                                                           
{assert(commondstr);assert(argv);argv[0] = strtok(commondstr, SEP);int i = 1;while((argv[i++] = strtok(NULL, SEP)));
//  {
//      argv[i] = strtok(NULL, SEP);                                                                                                   
//      if(argv[i] == NULL) break;
//      i++;
//  }//表示切割成功return 0;
}void PrintEnv()
{extern char **environ;for(int i = 0; environ[i]; ++i){printf("%d:%s\n",i, environ[i]);}
}void DebugPrint(char* argv[])
{for(int i = 0; argv[i]; ++i){                                                                                    printf("%d : %s\n", i, argv[i]);}
}int main()
{int last_exit = 0; //存储上一个进程的退出码int env_index = 0; //环境变量数组的下标char myenv[32][64];while(1){//每次进来都初始化一下char commondstr[MAX] = {0};char* argv[ARGC] = {NULL};printf("[xupt@my_machine currpath]#");fflush(stdout);//这里因为我们不能加换行,所以得刷新缓冲区char* s = fgets(commondstr, sizeof(commondstr), stdin);                                                                            assert(s);(void)s;//保证在release发布的时候,因为assert去掉,而导致s没有被使用过而产生的告警,什么都没做,充当一次使用commondstr[strlen(commondstr) - 1] = '\0'; //解决了fgets读入换行符的问题int n = split(commondstr, argv);//等于0表示切割成功if(n != 0) continue;//DebugPrint(argv);//当我们输入export命令时if(strcmp(argv[0], "export") == 0){//我们把这个环境变量存储在我们自己设定的数组内部if(argv[1] != NULL)strcpy(myenv[env_index], argv[1]);                                                                                             //再将数组内部的环境变量放到父进程的环境变量中putenv(myenv[env_index++]);}}//当我们查看环境变量的时候if(strcmp(argv[0], "env") == 0){PrintEnv();continue; }//当我们echo的时候if(strcmp(argv[0], "echo") == 0){//先确认一下echo后面第一个跟的是$if(argv[1][0] == '$'){                                                                                                                                if(argv[1][1] == '?'){printf("%d\n", last_exit);  continue;}else{char* env_ret = getenv(argv[1] + 1);if(env_ret != NULL)  printf("%s=%s \n", argv[1] + 1, env_ret);}}continue;}//当我们输入cd命令的时候if(strcmp(argv[0], "cd") == 0){if(argv[1] != NULL) chdir(argv[1]);continue;}                                                                                                                                  //当我们输入ls命令的时候if(strcmp(argv[0], "ls") == 0){//先找到末尾int pos  = 0;while(argv[pos]) pos++;//追加color参数argv[pos] = (char*)"--color=auto";//安全处理pos++;argv[pos] = NULL;}pid_t id = fork();assert(id >= 0);(void) id; //和上面的处理原因一样if(id == 0)                                                                                                                        {//childexecvp(argv[0], argv);exit(0);}int status = 0;pid_t ret  = waitpid(id, &status, 0);if(ret > 0){last_exit = WEXITSTATUS(status);} // printf("%s\n", commondstr);}                         return 0;
}        

到这本篇博客的内容就到此结束了。
如果觉得本篇博客内容对你有所帮助的话,可以点赞,收藏,顺便关注一下!
如果文章内容有错误,欢迎在评论区指正

在这里插入图片描述

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

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

相关文章

Markdown使用教程

这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个…

笔记本电脑的麦克风没有声音

笔记本电脑的麦克风没有声音是一个常见的问题&#xff0c;可能是由于以下几个原因导致的&#xff1a; 第一&#xff0c;麦克风没有启用或者被禁用了。在Windows系统中&#xff0c;右键单击任务栏上的音量图标&#xff0c;选择“录音设备”&#xff0c;在弹出窗口中找到麦克风&a…

Python+Appium自动化测试-编写自动化脚本

一&#xff0c;连接测试手机&#xff0c;获取测试机及被测APP配置 配置信息如下&#xff1a; {"platformName": "Android","platformVersion": "10","deviceName": "PCT_AL10","appPackage": "c…

【技术支持】DevTools中重写覆盖源js文件

sources面板下&#xff0c;左侧overrides标签下添加一个文件夹&#xff0c;并同意。 勾选Enable Local overrides 然后在page标签下&#xff0c;修改文件后ctrls保存 直接就保存在overrides的文件夹下了 或者文件上右键Override content

【leaflet】1. 初见

▒ 目录 ▒ &#x1f6eb; 导读需求开发环境 1️⃣ 概念概念解释特点 2️⃣ 学习路线图3️⃣ html示例&#x1f6ec; 文章小结&#x1f4d6; 参考资料 &#x1f6eb; 导读 需求 要做游戏地图了&#xff0c;看到大量产品都使用的leaflet&#xff0c;所以开始学习这个。 开发环境…

【操作系统内核】线程

【操作系统内核】线程 为什么需要线程 比如我要做一个视频播放器&#xff0c;就需要实现三个功能&#xff1a; ① 从磁盘读取视频数据 ② 对读取到的视频数据进行解码 ③ 对解码的数据进行播放 如果串行执行&#xff08;通过一个进程来执行&#xff09;&#xff1a; 那么…

redis配置文件详解

一、配置文件位置 以配置文件启动 Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf ( Windows名为redis.windows. conf) 例: # 这里要改成你自己的安装目录 cd ./redis-6.0.8 vim redis.conf redis对配置文件对大小写不敏感 二、配置文件 1、获取当前服务的…

CSS特效第一弹:右上角tag标志纯代码前端实现(非图片)

&#x1f60e;效果&#xff1a; &#x1f937;‍♂️思路&#xff1a; 分为2个部分&#xff1a; 1.文字方块右下角折角 文字方块用绝对定位z-index让文字方块悬浮在右上角的位置 2.右下角折角通过before伪元素border属性实现(三角形实现方法&#xff09; &#x1f44d;核心代…

15 # 手写 throttle 节流方法

什么是节流 节流是限制事件触发的频率&#xff0c;当持续触发事件时&#xff0c;在一定时间内只执行一次事件&#xff0c;这个效果跟英雄联盟里的闪现技能释放差不多。 函数防抖关注一定时间连续触发的事件只在最后执行一次&#xff0c;而函数节流侧重于一段时间内只执行一次…

现在个人想上架微信小游戏已经这么难了吗...

点击上方亿元程序员关注和★星标 引言 大家好&#xff0c;最近我突然想起来我还有一款微信小游戏还没有上架&#xff0c;于是捣鼓了一天把游戏完善了一下&#xff0c;然后准备提交审核&#xff0c;却发现异常的艰难… 1.为什么难&#xff1f; 相信大家都大概知道&#xff0c…

畅通工程之局部最小花费问题 (C++)

目录 题目&#xff1a; 思路&#xff1a; 代码&#xff1a; 结果 题目&#xff1a; 思路&#xff1a; 详细思路都在代码注释里 。 代码&#xff1a; #include<iostream>//无向图邻接矩阵 #include<map> #include<algorithm> #define mvnum 1005 using …

yo!这里是STL::unordered系列简单模拟实现

目录 前言 相关概念介绍 哈希概念 哈希冲突与哈希函数 闭散列 框架 核心函数 开散列 框架 核心函数 哈希表&#xff08;开散列&#xff09;的修改 迭代器实现 细节修改 unordered系列封装 后记 前言 我们之前了解过map和set知道&#xff0c;map、set的底层结构是…

基于PHP的设云尘资讯网站设计与实现

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…

K8s----资源管理

目录 一、Secret 1、创建 Secret 1.1 用kubectl create secret命令创建Secret 1.2 内容用 base64 编码&#xff0c;创建Secret 2、使用方式 2.1 将 Secret 挂载到 Volume 中&#xff0c;以 Volume 的形式挂载到 Pod 的某个目录下 2.2 将 Secret 导出到环境变量中 二、Co…

Win11专业版安装Docker Desktop,并支持映射主机的gpu

一、Windows环境下安装 Docker 必须满足: 1. 64位Windows 11 Pro(专业版和企业版都可以) 2. Microsoft Hyper-V,Hyper-V是微软的虚拟机,在win11上是自带的,我们只需要启动就可以了 二、下载Docker Desktop安装包 方式一:进入官网下载 https://docs.docker.com/desktop…

IDEA调试总结

前言 由于 IDEA 每个人使用的版本不同以及快捷键的设置不同&#xff0c;所以忽略了快捷键的使用。如果不知道快捷键请在 IDEA 工具栏里面点开 Run 菜单即可知悉 图标介绍 下面咱们进入看图说话环节&#xff0c;下列图标小伙伴知道是啥功能么&#xff1f;日常开发进行 Debug 使…

逐步学习 Swagger enum:从入门到精通

enum 是 Swagger 规范中用来定义枚举类型的一种方式。它允许开发者在 API 文档中明确列出该接口的参数、返回值或请求体中可接受的枚举值。通过使用 Swagger enum&#xff0c;开发者可以更清晰地描述 API 的输入和输出&#xff0c;提高 API 文档的可读性和可维护性。 enum 使用…

渗透实战靶机2wp

0x00 简介 1、测试环境 目标IP&#xff1a;10.xxxx 测试IP&#xff1a;192.168.139.128 测试环境&#xff1a;win10、kali等 测试时间&#xff1a;2021.7.22-2021.7.22 测试人员&#xff1a;ruanruan 2、测试过程 本次实战主要通过对收集到的端口、目录等信息进行持续整…

基于Quartz实现动态定时任务

生命无罪&#xff0c;健康万岁&#xff0c;我是laity。 我曾七次鄙视自己的灵魂&#xff1a; 第一次&#xff0c;当它本可进取时&#xff0c;却故作谦卑&#xff1b; 第二次&#xff0c;当它在空虚时&#xff0c;用爱欲来填充&#xff1b; 第三次&#xff0c;在困难和容易之…

Google Firebase PHP实现消息推送

获取key的方法&#xff1a; 登录谷歌开发者后台 https://console.firebase.google.com/?hlzh-cn function firebaseNotice($title,$body){$token_arr[token1,token2]; //用户的firebasetoken列表$notify_msg ["notification" > ["title" > $title…