当前位置: 首页 > news >正文

Linux:自定义Shell

        本文旨在通过自己完成一个简单的Shell来帮助理解命令行Shell这个程序。

目录

一、输出“提示”

二、获取输入

三、切割字符串

 四、执行指令

1.子进程替换

2.内建指令


一、输出“提示”

        这个项目基于虚拟机Ubuntu22.04.5实现。

        

        打开终端界面如图所示。

        其中。

@之前:utocoo是用户名
@之后:utocoo-virtul-machine是主机名
":"之后是当前路径,"~"表示用户家目录
"$"是普通用户的提示符,如果是root用户,则为"#"
光标闪烁位置在等待输入

         当前的用户名主机名当前工作目录这些信息都有对应的环境变量,故可以利用getenv拿到对应的值。

#include <stdio.h>
#include <stdlib.h>
//获取用户名
const char* UserName()
{const char* username = getenv("USER");if(username)return username;elsereturn "None";
}
//获取主机名
const char* HostName()
{const char* hostname = getenv("HOSTNAME");if(hostname)return hostname;elsereturn "None";
}
//获取目录
const char* CurrentWorkDir()
{const char* cwd = getenv("PWD");if(cwd)return cwd;else return "None";
}
int main()
{printf("%s@%s:%s$",UserName(),HostName(),CurrentWorkDir());return 0;
}

二、获取输入

        用户的输入是作为一个字符串被输入,故需要定义一个数组作为缓冲区。

        使用scanf输入时,遇到空格则会刷新缓冲区,故推荐使用fgets函数作为输入函数。关于C语言的各组输入函数,这篇文章做了很好的说明。https://blog.csdn.net/qq_53139964/article/details/142820767 

        补全其他板块的代码完成“获取输入”这一步骤。

#include <stdio.h>
#include <stdlib.h>#define SIZE 1024 //定义缓冲区数组大小const char* HostName()
{const char* hostname = getenv("HOSTNAME");if(hostname)return hostname;elsereturn "None";
}
const char* UserName()
{const char* username = getenv("USER");if(username)return username;elsereturn "None";
}
const char* CurrentWorkDir()
{const char* cwd = getenv("PWD");if(cwd)return cwd;else return "None";
}
int main()
{char commandline[SIZE];printf("%s@%s:%s$ ",UserName(),HostName(),CurrentWorkDir());//获取用户输入fgets(commandline,SIZE,stdin);printf("test:%s\n",commandline);return 0;
}

        测试结果如下。

        不难看出,打印结果中有两次“换行操作”。原因是fgets在stdin流中读取一定数量的信息时,会将我们自己输入的'\n'也读取进来,因此需要在commandline数组中去掉这个字符。

commandline[strlen(commandline)-1] = 0;//将'\n'修改为'0'

        封装处理后。

//命令行交互
void Interactive(char* out,int size)
{	printf("%s@%s:%s$ ",UserName(),HostName(),CurrentWorkDir());//获取用户输入fgets(out,size,stdin);out[strlen(out)-1] = 0;//将'\n'修改为'0'
}

三、切割字符串

        我们知道,命令行也是正在运行的程序,而在命令行执行输入的指令,其实是命令行这个进程创建子程序后再做程序替换,注意程序替换时,传参方式要么是可变参数,要么是指针数组,因此,无论如何,都要先将当前的字符串按照空格切割成一个个的子串,如果是指针数组的形式,要以NULL结尾。

        切割字符串可以利用C语言的字符串处理函数strtok

#define MAX_ARGC 64
#define SPC " "
char* argv[MAX_ARGC];//切割字符串
void Split(char* in)
{int i = 0;argv[i++] = strtok(in,SPC);while(argv[i++] = strtok(NULL,SPC));
}

        在这里,切割字符串的语句是这样一句while循环,循环体为空。仅仅这样一行代码就可以实现我们对字符串切割的要求,因为argv数组要求要以NULL结尾,而这句赋值语句将最后一个字符'\0'赋值给数组元素后,数组尾的数据就是NULL,同时表达式的值也为假,跳出循环。

 四、执行指令

1.子进程替换

        我们当前做的所有工作都是模拟shell这个程序,而模拟的命令行要执行我们输入的指令,必然要通过程序替换来完成,但是不能用shell这个进程做替换,应该创建子进程,让子进程做程序替换,父进程等待子进程。

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>//3.执行命令
pid_t id = fork();
if(id == 0)
{//程序替换,子进程执行命令exit(1);
}pid_t rid = waitpid(id,Null,0);
printf("run done!:%d",rid);
return 0;

        程序替换时,选择适当的替换函数也是很重要的,我们在模拟的时候是创建了argv数组,故选择带v的exec函数,其次,带p的exec函数可以不用指定系统指令的全部路径,故选择execvp这个函数。

//执行指令
void Execute()
{pid_t id = fork();if(id == 0){//程序替换,子进程执行命令execvp(argv[0],argv);exit(1);}pid_t rid = waitpid(id,NULL,0);printf("run done!:%d\n",rid);
}

        

        由于我的Ubuntu系统当前的环境变量没有主机名这个变量,因此主机名结果显示了None。

        但是,当前的shell只能执行一次程序替换,所以需要加上死循环,让shell一直运行。

int main()
{while(1){char commandline[SIZE];//1.打印命令行信息Interactive(commandline,SIZE);//2.切割字符串Split(commandline);//3.执行命令Execute();}return 0;
}

2.内建指令

        但是有些指令的执行结果是不符合预期的,比如cd指令这部分指令称为内建指令,具体请看这篇文章。https://blog.csdn.net/chen1415886044/article/details/103015950

        基于这一点,我们模拟的shell程序在执行cd指令的时候,其实是子程序替换为cd 指令,子程序执行了cd指令,路径发生改变的仅仅是子程序的路径,而我们平时在命令行所打印出来的路径,其实都是shell程序的路径,运行结果当然不符合预期。

        因此,在子程序替换执行指令之前,先判断要执行的指令是否要内建指令,如果是内建指令,则不需要创建子进程做替换,而是shell这个进程直接执行。

int main()
{while(1){char commandline[SIZE];//1.打印命令行信息Interactive(commandline,SIZE);//2.切割字符串Split(commandline);//3.执行内建指令int i = BuildinCmd();if(i) continue;//4.执行命令Execute();}return 0;
}
/执行内建指令
int BuildinCmd()
{//判断是否为内建指令,如果是,则返回1,否则返回0//并且执行内建指令//此处只列举cd这一条内建指令int ret = 0;if(strcmp("cd",argv[0])== 0){// cd *** :cd到具体路径 // cd 空 :cd到家目录char* Target = argv[1];if(!Target) Target = getenv("HOME");chdir(Target);ret = 1;}return ret;
}

         执行结果。

         在执行结果中,依旧有两个错误。

        1.输入为空,结果是段错误。

        2.cd指令执行后,命令行的输出提示中路径并未发生改变。


        要解决输入为空后发生的段错误,可以在交互的函数中返回输入字符串的长度,然后做if条件判断,特殊处理。

   


        至于在执行cd指令后,显示结果的路径并未发生改变,原因就是显示结果是由getenv得到,而此时的环境变量PWD并没有发生改变,因为当前myshell进程所在路径没有发生改变

        同时,不难总结出来,cd指令的执行和环境变量PWD的value息息相关

        可以利用snprintf这个函数,将格式化信息输出到指定大小的pwd字符串中,再利用putenv导入环境变量,则myshell程序就模拟出修改环境变量PWD的效果了

        putenv,导出环境变量,新建或者修改一个环境变量,如果putenv的参数是新的环境变量,则新建,如果是已经存在的环境变量,则修改。

        snprintf,和printf是一类函数,printf默认把格式化信息输出到屏幕,而多加了s (string)和n(表示字符串的大小)的snprintf表示把格式化信息输出到n长度的字符串中。

//关联环境变量,定义一个字符串,或者字符数组
char pwd[SIZE];
//执行内建指令
int BuildinCmd()
{//判断是否为内建指令,如果是,则返回1,否则返回0//并且执行内建指令//此处只列举cd这一条内建指令int ret = 0;if(strcmp("cd",argv[0])== 0){// cd *** | cd char* Target = argv[1];if(!Target) Target = getenv("HOME");chdir(Target);//关联环境变量,格式化信息会输出到pwd字符数组中snprintf(pwd,SIZE,"PWD=%s",Target);putenv(pwd);ret = 1;}return ret;
}

        但是在用cd指令执行下面这样的情况后,路径名并不达预期。

        原因是我们是使用Target来更新了环境变量,Target是我们输入的内容。

        可以利用getcwd函数获取当前进程的绝对路径再用getcwd的返回结果来更新环境变量

//执行内建指令
int BuildinCmd()
{//判断是否为内建指令,如果是,则返回1,否则返回0//并且执行内建指令//此处只列举cd这一条内建指令int ret = 0;if(strcmp("cd",argv[0])== 0){// cd *** | cd char* Target = argv[1];if(!Target) Target = getenv("HOME");chdir(Target);//关联环境变量,格式化信息会输出到pwd字符数组中char tmp[999];getcwd(tmp,999);snprintf(pwd,SIZE,"PWD=%s",tmp);putenv(pwd);ret = 1;}return ret;
}


      export指令也是内建指令,在export的指令被切割为argv数组后,argv数组的第二个元素就是要导入环境变量的字符串,可以直接putenv导入。

if(strcmp("export",argv[0])==0)
{ret = 1;if((argv[1]))putenv(argv[1]);
}

         随便导入一个环境变量,执行env命令后就能看到这个环境变量。但是在你执行一系列指令后,再执行env指令查看这个环境变量,可能会出现找不到的情况。

        原因就是,上面这段代码是通过argv数组导入的,在执行env指令,显示的时候指向了argv数组的值,而argv数组中的值在一次次执行指令的过程中会不断变换,因此已经导入的环境变量可能又会消失不见。

        正确做法是用数组保存要导入的环境变量。

//存储新的环境变量
char env[SIZE];if(strcmp("export",argv[0])==0)
{ret = 1;if((argv[1])){strcpy(env,argv[1]);putenv(env);}
}

         echo指令也是内建指令,执行echo指令一般有如下几种情况。

echo abcdef //输出一些信息
echo $HOME  //输出环境变量
echo $?     //输出上一次执行结果的进程退出码
echo        //输入echo后回车,结果会显示回车

        需要注意,在执行echo $?这个命令后,会显示上一条命令执行的退出码,如果再执行一次echo $?则显示结果应该为0。 

int lastExitCode = 0;
if(strcmp("echo",argv[0])==0){ret =1;if(argv[1]){if(argv[1][0] == '$'){if(argv[1][1] == '?'){printf("%d\n",lastExitCode);lastExitCode=0;//重置为0}else{char* tmp2 = getenv(argv[1]+1);if(tmp2) printf("%s\n",tmp2);else{printf("\n");}}}else{printf("%s\n",argv[1]);}}else{printf("\n");}}

        小细节,为ls指令添加颜色效果。

//切割字符串
void Split(char* in)
{int i = 0;argv[i++] = strtok(in,SPC);while(argv[i++] = strtok(NULL,SPC));if(strcmp("ls",argv[0]) == 0){argv[i-1] = (char*)"--color";argv[i] = NULL;}
}

相关文章:

Linux:自定义Shell

本文旨在通过自己完成一个简单的Shell来帮助理解命令行Shell这个程序。 目录 一、输出“提示” 二、获取输入 三、切割字符串 四、执行指令 1.子进程替换 2.内建指令 一、输出“提示” 这个项目基于虚拟机Ubuntu22.04.5实现。 打开终端界面如图所示。 其中。 之前&#x…...

vue项目中中怎么获取环境变量

在 Vue 项目中&#xff0c;有几种获取环境变量的方法。最常用的是通过 import.meta.env 来访问。 1.首先在项目根目录创建环境变量文件&#xff1a; .env # 所有环境都会加载 .env.development # 开发环境 .env.production # 生产环境2.在环境变量文件…...

C#里怎么样使用正则表达式?

C#里怎么样使用正则表达式? 正则表达式是由普通字符(如英文字母)以及特殊字符(也称为元字符)组成的一种文字模式 这种文字模式可用于检查字符串的值是否满足一定的规则,例如: 验证输入的邮箱是否合法 输入的身份证号码是否合法 输入的用户名是否满足条件等 也可以…...

《生成式 AI》课程 第5講:訓練不了人工智慧?你可以訓練你自己 (下)

资料来自李宏毅老师《生成式 AI》课程&#xff0c;如有侵权请通知下线 Introduction to Generative AI 2024 Springhttps://speech.ee.ntu.edu.tw/~hylee/genai/2024-spring.php 摘要 这一系列的作业是为 2024 年春季的《生成式 AI》课程设计的&#xff0c;共包含十个作业。…...

Vue 动态给 data 添加新属性深度解析:问题、原理与解决方案

在 Vue 中,动态地向 data 中添加新的属性是一个常见的需求,但它也可能引发一些问题,尤其是关于 响应式更新 和 数据绑定 的问题。Vue 的响应式系统通过 getter 和 setter 来追踪和更新数据,但 动态添加新属性 时,Vue 并不会自动为这些新属性创建响应式链接。 1. 直接向 V…...

【Pytest+Yaml+Allure】实现接口自动化测试框架

一、框架思想 requestsyamlpytestallure实现接口自动化框架。结合数据驱动和分层思想&#xff0c;将代码与数据分离&#xff0c;易维护&#xff0c;易上手。使用yaml编写编写测试用例&#xff0c;利用requests库发送请求&#xff0c;使用pytest管理用例&#xff0c;allure生成…...

el-input绑定点击回车事件意外触发页面刷新

小伙伴们在项目中应该还是比较常用键盘指定按键事件的&#xff0c;尤其是一些筛选条件的通过点击键盘回车按键去触发搜索 例如&#xff1a; <el-form><el-form-item label条件title><el-input v-modelformData.searchKey keydown.entersearch></el-input…...

Golang的语言特性与鸭子类型

Golang的语言特性与鸭子类型 前言 什么是鸭子类型&#xff1f; Suppose you see a bird walking around in a farm yard. This bird has no label that says ‘duck’. But the bird certainly looks like a duck. Also, he goes to the pond and you notice that he swims l…...

如何在Linux系统中排查GPU上运行的程序

如何在Linux系统中排查GPU上运行的程序 在Linux系统中&#xff0c;随着深度学习和高性能计算的普及&#xff0c;GPU资源的管理和监控变得越来越重要。当您遇到GPU资源不足或性能下降的问题时&#xff0c;需要能够快速定位并解决这些问题。本文将介绍几种常用的方法来帮助您排查…...

VSCode 新建 Python 包/模块 Pylance 无法解析

问题描述&#xff1a; 利用 VSCode 写代码&#xff0c;在项目里新建一个 Python 包或者模块&#xff0c;然后在其他文件里正常导入这个包或者模块时出现&#xff1a; Import “xxxx” could not be resolved Pylance (reportMissingImports) 也就是说 Pylance 此时无法解析我们…...

Unet++改进44:添加MogaBlock(2024最新改进模块)|在纯基于卷积神经网络的模型中进行判别视觉表示学习,具有良好的复杂性和性能权衡。

本文内容:添加MogaBlock 目录 论文简介 1.步骤一 2.步骤二 3.步骤三 4.步骤四 论文简介 通过将内核尽可能全局化,现代卷积神经网络在计算机视觉任务中显示出巨大的潜力。然而,最近在深度神经网络(dnn)内的多阶博弈论相互作用方面的进展揭示了现代卷积神经网络的表示瓶…...

计算机网络(14)ip地址超详解

先看图&#xff1a; 注意看第三列蓝色标注的点不会改变&#xff0c;A类地址第一个比特只会是0&#xff0c;B类是10&#xff0c;C类是110&#xff0c;D类是1110&#xff0c;E类是1111. IPv4地址根据其用途和网络规模的不同&#xff0c;分为五个主要类别&#xff08;A、B、C、D、…...

【C语言】野指针问题详解及防范方法

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C语言 文章目录 &#x1f4af;前言&#x1f4af;什么是野指针&#xff1f;&#x1f4af;未初始化的指针代码示例问题分析解决方法 &#x1f4af;指针越界访问代码示例问题分析解决方法 &#x1f4af;指向已释放内存的…...

【SVN和GIT】版本控制系统详细下载使用教程

文章目录 ** 参考文章一、什么是SVN和GIT二、软件使用介绍1 SVN安装1.1 服务端SVN下载地址1.2 客户端SVN下载地址2 SVN使用2.1 服务端SVN基础使用2.1.1 创建存储库和用户成员2.1.2 为存储库添加访问人员2.2 客户端SVN基础使用2.2.1 在本地下载库中的内容2.2.2 版本文件操作--更…...

【Vue】Vue3.0(二十六)Vue3.0中的作用域插槽

上篇文章 【Vue】Vue3.0&#xff08;二十五&#xff09;Vue3.0中的具名插槽 的概念和使用场景 &#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;Vue专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年11月20日17点30分 文章目录 概念使用场景示…...

神经网络(系统性学习二):单层神经网络(感知机)

此前篇章&#xff1a; 神经网络中常用的激活函数 神经网络&#xff08;系统性学习一&#xff09;&#xff1a;入门篇 单层神经网络&#xff08;又叫感知机&#xff09; 单层网络是最简单的全连接神经网络&#xff0c;它仅有输入层和输出层&#xff0c;没有隐藏层。即&#x…...

CTF之密码学(BF与Ook)

BrainFuck&#xff08;通常也被称为Brainfuck或BF&#xff09;和Ook是两种非常特殊且有趣的编程语言。以下是对这两种语言的详细介绍&#xff1a; 一、BrainFuck 简介&#xff1a; BrainFuck是一种极小化的计算机语言&#xff0c;由Urban Mller在1993年创建。由于“fuck”在英…...

【TEST】Apache JMeter + Influxdb + Grafana

介绍 使用Jmeter发起测试&#xff0c;测试结果存入Influxdb&#xff0c;Grafana展示你的测试结果。 环境 windows 10docker desktopJDK17 安装 Apache JMeter 访问官网&#xff08;Apache JMeter - Apache JMeter™&#xff09;下载JMeter&#xff08;目前最新版本5.6.3&a…...

SpringBoot集成多个rabbitmq

1、pom文件 <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-amqp --> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId><versio…...

从零开始学习数据库 day0(基础)

在当今的信息时代&#xff0c;数据已经成为了企业和组织最重要的资产之一。无论是电子商务平台&#xff0c;社交媒体&#xff0c;还是科研机构&#xff0c;几乎每个地方都离不开数据库。今天&#xff0c;我们将一起走进数据库的世界&#xff0c;学习它的基础知识&#xff0c;帮…...

MongoDB相关问题

视频教程 【GeekHour】20分钟掌握MongoDB Complete MongoDB Tutorial by Net Ninja MongoDB开机后调用缓慢的原因及解决方法 问题分析&#xff1a; MongoDB开机后调用缓慢&#xff0c;通常是由于以下原因导致&#xff1a; 索引重建&#xff1a; MongoDB在启动时会重建索引…...

linux基本命令(1)

1. 文件和目录操作 ls — 列出目录内容 ls # 显示当前目录的文件和目录 ls -l # 显示详细的文件信息&#xff08;权限、大小、修改时间等&#xff09; ls -a # 显示所有文件&#xff08;包括隐藏文件&#xff09; ls -lh # 显示详细信息并以易读的方式显示文件大小 cd — 改…...

【机器学习】超简明Python基础教程

Python是一种简单易学、功能强大的编程语言&#xff0c;适用于数据分析、人工智能、Web开发、自动化脚本等多个领域。本教程面向零基础学习者&#xff0c;逐步讲解Python的基本概念、语法和操作。 1. 安装与运行 安装Python 从官网 Welcome to Python.org 下载适合自己系统的…...

基于信创环境的信息化系统运行监控及运维需求及策略

随着信息技术的快速发展和国家对信息安全的日益重视&#xff0c;信创环境&#xff08;信息技术应用创新环境&#xff09;的建设已成为行业发展的重要趋势。本指南旨在为运维团队在基于信创环境的系统建设及运维过程中提供参考&#xff0c;确保项目顺利实施并满足各项技术指标和…...

【Mysql】视图--介绍和作用 视图的创建

1、介绍 &#xff08;1&#xff09;视图&#xff08;view&#xff09;是一个虚拟表&#xff0c;非真实存在&#xff0c;其本质是根据SQL语句获取动态的数据集&#xff0c;并为其命名&#xff0c;用户使用时只需使用视图名称既可获取结果集&#xff0c;并可以将其当作表来使用。…...

【JavaEE初阶 — 多线程】定时器的应用及模拟实现

目录 1. 标准库中的定时器 1.1 Timer 的定义 1.2 Timer 的原理 1.3 Timer 的使用 1.4 Timer 的弊端 1.5 ScheduledExecutorService 2. 模拟实现定时器 2.1 实现定时器的步骤 2.1.1 定义类描述任务 定义类描述任务 第一种定义方法 …...

Win10系统开启了文件夹管控(文件夹限制访问)导致软件向系统公共文档目录写入失败的问题排查分享

目录 1、问题说明 2、查看系统是否开启了文件夹管控 3、在未安装杀毒软件的Win10电脑上可能会自动打开文件夹管控 4、到微软官网上查看Windows 安全中心的病毒和威胁防护与文件夹管控的详细说明 5、解决办法探讨 6、最后 C++软件异常排查从入门到精通系列教程(专栏文章列…...

大数据的数据整合

数据整合是对导入的各类源数据进行整合&#xff0c;新进入的源数据匹配到平台上的标准数据&#xff0c;或者成为系统中新的标准数据。数据整合工具对数据关联关系进行设置。经过整合的源数据实现了基本信息的唯一性&#xff0c;同时又保留了与原始数据的关联性。具体功能包括关…...

回溯法经典难题解析

本文将通过几个经典的回溯问题&#xff0c;展示回溯算法的应用及其在解决问题时的核心思想和技巧。这些问题包括全排列、全排列II、N皇后以及数独问题&#xff0c;本文将分别介绍每个问题的思路与实现。 46. 全排列 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有…...

LLM的原理理解6-10:6、前馈步骤7、使用向量运算进行前馈网络的推理8、注意力层和前馈层有不同的功能9、语言模型的训练方式10、GPT-3的惊人性能

目录 LLM的原理理解6-10: 6、前馈步骤 7、使用向量运算进行前馈网络的推理 8、注意力层和前馈层有不同的功能 注意力:特征提取 前馈层:数据库 9、语言模型的训练方式 10、GPT-3的惊人性能 一个原因是规模 大模型GPT-1。它使用了768维的词向量,共有12层,总共有1.…...