当前位置: 首页 > 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;帮…...

挑战杯推荐项目

“人工智能”创意赛 - 智能艺术创作助手&#xff1a;借助大模型技术&#xff0c;开发能根据用户输入的主题、风格等要求&#xff0c;生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用&#xff0c;帮助艺术家和创意爱好者激发创意、提高创作效率。 ​ - 个性化梦境…...

linux之kylin系统nginx的安装

一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源&#xff08;HTML/CSS/图片等&#xff09;&#xff0c;响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址&#xff0c;提高安全性 3.负载均衡服务器 支持多种策略分发流量…...

python打卡day49

知识点回顾&#xff1a; 通道注意力模块复习空间注意力模块CBAM的定义 作业&#xff1a;尝试对今天的模型检查参数数目&#xff0c;并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...

【机器视觉】单目测距——运动结构恢复

ps&#xff1a;图是随便找的&#xff0c;为了凑个封面 前言 在前面对光流法进行进一步改进&#xff0c;希望将2D光流推广至3D场景流时&#xff0c;发现2D转3D过程中存在尺度歧义问题&#xff0c;需要补全摄像头拍摄图像中缺失的深度信息&#xff0c;否则解空间不收敛&#xf…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?

uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件&#xff0c;用于在原生应用中加载 HTML 页面&#xff1a; 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...

【7色560页】职场可视化逻辑图高级数据分析PPT模版

7种色调职场工作汇报PPT&#xff0c;橙蓝、黑红、红蓝、蓝橙灰、浅蓝、浅绿、深蓝七种色调模版 【7色560页】职场可视化逻辑图高级数据分析PPT模版&#xff1a;职场可视化逻辑图分析PPT模版https://pan.quark.cn/s/78aeabbd92d1...

Golang——6、指针和结构体

指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...

MinIO Docker 部署:仅开放一个端口

MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...

为什么要创建 Vue 实例

核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...