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;}
}

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

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

相关文章

《图像梯度与常见算子全解析:原理、用法及效果展示》

简介:本文深入探讨图像梯度相关知识&#xff0c;详细介绍图像梯度是像素灰度值在不同方向的变化速度&#xff0c;并以 “pig.JPG” 图像为例&#xff0c;通过代码展示如何选取图像部分区域并分析其像素值以论证图像梯度与边缘信息的关联。接着全面阐述了 Sobel 算子&#xff0c…

项目进度计划表:详细的甘特图的制作步骤

甘特图&#xff08;Gantt chart&#xff09;&#xff0c;又称为横道图、条状图&#xff08;Bar chart&#xff09;&#xff0c;是一种用于管理时间和任务活动的工具。 甘特图由亨利劳伦斯甘特&#xff08;Henry Laurence Gantt&#xff09;发明&#xff0c;是一种通过条状图来…

A045-基于spring boot的个人博客系统的设计与实现

&#x1f64a;作者简介&#xff1a;在校研究生&#xff0c;拥有计算机专业的研究生开发团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339; 赠送计算机毕业设计600…

QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现

一、编码问题 在计算机编程中&#xff0c;流&#xff08;Stream&#xff09;是一种抽象的概念&#xff0c;用于表示数据的输入或输出。根据处理数据的不同方式&#xff0c;流可以分为字节流&#xff08;Byte Stream&#xff09;和字符流&#xff08;Character Stream&#xff0…

Python爬虫项目 | 二、每日天气预报

文章目录 1.文章概要1.1 实现方法1.2 实现代码1.3 最终效果1.3.1 编辑器内打印显示效果实际应用效果 2.具体讲解2.1 使用的Python库2.2 代码说明2.2.1 获取天气预报信息2.2.2 获取当天日期信息&#xff0c;格式化输出2.2.3 调用函数&#xff0c;输出结果 2.3 过程展示 3 总结 1…

百度在下一盘大棋

这两天世界互联网大会在乌镇又召开了。 我看到一条新闻&#xff0c;今年世界互联网大会乌镇峰会发布“2024 年度中国互联网企业创新发展十大典型案例”&#xff0c;百度文心智能体平台入选。 这个智能体平台我最近也有所关注&#xff0c;接下来我就来讲讲它。 百度在下一盘大棋…

UG NX二次开发(C++)-UIStyler-指定平面的对象和参数获取

文章目录 1、前言2、在UG NX中创建平面和一个长方体,3、在UI Styler中创建一个UI界面4、在VS中创建一个工程4.1 创建并添加工程文件4.2 在Update_cb方法中添加选择平面的代码4.3 编译完成并测试效果1、前言 在采用NXOpen C++进行二次开发时,采用Menu/UIStyler是一种很常见的…

【软考】数据库

1. 数据模型 1.1 概念数据模型 概念数据模型一般用 E-R 图表示&#xff0c;常用术语如下&#xff1a; 实体&#xff1a;客观存在的事物&#xff0c;如&#xff1a;一个单位、一个职工、一个部门、一个项目。属性&#xff1a;学生实体有学号、姓名、出生日期等属性。码&#…

【强化学习的数学原理】第04课-值迭代与策略迭代-笔记

学习资料&#xff1a;bilibili 西湖大学赵世钰老师的【强化学习的数学原理】课程。链接&#xff1a;强化学习的数学原理 西湖大学 赵世钰 文章目录 一、值迭代算法二、策略迭代算法三、截断策略迭代算法四、本节课内容summary 一、值迭代算法 值迭代算法主要包括两部分。 第一…

jupyter notebook的 markdown相关技巧

目录 1 先选择为markdown类型 2 开关技巧 2.1 运行markdown 2.2 退出markdown显示效果 2.3 注意点&#xff1a;一定要 先选择为markdown类型 3 一些设置技巧 3.1 数学公式 3.2 制表 3.3 目录和列表 3.4 设置各种字体效果&#xff1a;加粗&#xff0c;斜体&#x…

Spring Boot3远程调用工具RestClient

Spring Boot3.2之后web模块提供了一个新的远程调用工具RestClient&#xff0c;它的使用比RestTemplate方便&#xff0c;开箱即用&#xff0c;不需要单独注入到容器之中&#xff0c;友好的rest风格调用。下面简单的介绍一下该工具的使用。 一、写几个rest风格测试接口 RestCont…

vscode可以编译通过c++项目,但头文件有红色波浪线的问题

1、打开 VSCode 的设置&#xff0c;可以通过快捷键 Ctrl Shift P 打开命令面板&#xff0c;然后搜索并选择 “C/C: Edit Configurations (JSON)” 命令&#xff0c;这将在 .vscode 文件夹中创建或修改 c_cpp_properties.json 文件 {"configurations": [{"name…

近源渗透|HID ATTACK从0到1

前言 对于“近源渗透”这一术语&#xff0c;相信大家已经不再感到陌生。它涉及通过伪装、社会工程学等手段&#xff0c;实地侵入企业办公区域&#xff0c;利用内部潜在的攻击面——例如Wi-Fi网络、RFID门禁、暴露的有线网口、USB接口等——获取关键信息&#xff0c;并以隐蔽的…

Python爬取豆瓣电影全部分类数据并存入数据库

在当今数字化的时代&#xff0c;网络上丰富的影视资源信息吸引着众多开发者去挖掘和利用。今天&#xff0c;我就来和大家分享一段有趣的代码&#xff0c;它能够从豆瓣电影平台获取相关数据并存储到数据库中哦。 结果展示&#xff08;文末附完整代码&#xff09;&#xff1a; 目…

java: itext8.05 create pdf

只能调用windows 已安装的字体&#xff0c;这样可以在系统中先预装字体&#xff0c;5.0 可以调用自配文件夹的字体文件。CSharp donetItext8.0 可以调用。 /*** encoding: utf-8* 版权所有 2024 ©涂聚文有限公司 言語成了邀功盡責的功臣&#xff0c;還需要行爲每日來值班…

基于Java Springboot公园管理系统

一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse 数据…

Java中的File和IO流

File对象 File对象本质是一个文件或文件夹&#xff0c;用于写入和读取文件内容 注意&#xff1a;对于相对路径而言&#xff0c;在单元测试方法中的File是相对于Module&#xff0c;在main中的File是相对于Project 构造器 File(String pathname)File file1 new File("D:…

(Keil)MDK-ARM各种优化选项详细说明、实际应用及拓展内容

参考 MDK-ARM各种优化选项详细说明、实际应用及拓展内容 本文围绕MDK-ARM优化选项,以及相关拓展知识(微库、实际应用、调试)进行讲述,希望对你今后开发项目有所帮助。 1 总述 我们所指的优化,主要两方面: 1.代码大小(Size) 2.代码性能(运行时间) 在MDK-ARM中,优…

ssm实战项目──哈米音乐(二)

目录 1、流派搜索与分页 2、流派的添加 3、流派的修改 4、流派的删除 接上篇&#xff1a;ssm实战项目──哈米音乐&#xff08;一&#xff09;&#xff0c;我们完成了项目的整体搭建&#xff0c;接下来进行后台模块的开发。 首先是流派模块&#xff1a; 在该模块中采用分…

STM32的中断(什么是外部中断和其他中断以及中断号是什么)

一、什么是EXTI 和NVIC EXTI&#xff08;External Interrupt/Event Controller&#xff09;EXTI 是外部中断/事件控制器&#xff0c;它负责处理外部信号变化&#xff0c;并将信号传递给中断控制器&#xff08;如 NVIC&#xff09;。主要负责以下功能&#xff1a; 外部事件检测…