Linux自定义shell编写

Linux自定义shell编写

  • 一.最终版本展示
    • 1.动图展示
    • 2.代码展示
  • 二.具体步骤
    • 1.打印提示符
    • 2.解析命令行
    • 3.分析是否是内建命令
      • 1.shell对于内建名令的处理
      • 2.cd命令
      • 3.cd函数的实现
      • 4.echo命令的实现
      • 5.export命令的实现
      • 6.内建命令函数的实现
    • 4.创建子进程通过程序替换执行命令
    • 5.循环往复即可
  • 三.shell运行原理

经过了创建进程,终止进程,进程等待和进程程序替换之后,
我们就可以借助这些知识实现一个简单的shell命令行解释器了

温馨提示:
建议大家自己写一遍,这些代码分块之后每一个函数都很简单,
不过实现过程中可能会有各种各样非常细枝末节的地方被我们所忽视
因此可能会发生一看就懂,一写就废的情况…

一.最终版本展示

输入命令行时想要删除字符时不能直接按backspace,而是要按ctrl+backspace才能成功删除

1.动图展示

在这里插入图片描述

2.代码展示

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//#define DEBUG 1
#define SEP " "char cwd[1024]={'\0'};
int lastcode=0;//上一次进程退出时的退出码char env[1024][1024]={'\0'};
int my_index=0;const char* getUsername()
{const char* username=getenv("USER");if(username==NULL) return "none";return username;
}const char* getHostname()
{const char* hostname=getenv("HOSTNAME");if(hostname==NULL) return "none";return hostname;
}const char* getPwd()
{const char* pwd=getenv("PWD");if(pwd==NULL) return "none";return pwd;
}//分割字符串填入usercommand数组当中
//例如: "ls -a -l" 分割为"ls" "-a" "-l"
void CommandSplit(char* usercommand[],char* command)
{int i=0;usercommand[i++]=strtok(command,SEP);while(usercommand[i++]=strtok(NULL,SEP));
}//解析命令行
void GetCommand(char* command,char* usercommand[])
{command[strlen(command)-1]='\0';//清理掉最后的'\0'CommandSplit(usercommand,command);
#ifdef DEBUGint i=0;while(usercommand[i]!=NULL){printf("%d : %s\n",i,usercommand[i]);i++;}
#endif
}//创建子进程,完成任务
void Execute(char* usercommand[])
{pid_t id=fork();if(id==0){//子进程执行部分execvp(usercommand[0],usercommand);//如果子进程程序替换失败,已退出码为1的状态返回exit(1);}else{//父进程执行部分int status=0;//阻塞等待pid_t rid=waitpid(id,&status,0);if(rid>0){lastcode=WEXITSTATUS(status);}}
}void cd(char* usercommand[])
{chdir(usercommand[1]);char tmp[1024]={'\0'};getcwd(tmp,sizeof(tmp));sprintf(cwd,"PWD=%s",tmp);putenv(cwd);lastcode=0;
}   int echo(char* usercommand[])
{//1.echo后面什么都没有,相当于'\n'if(usercommand[1]==NULL){printf("\n");lastcode=0;return 1;}//2.echo $?  echo $PWD echo $char* cmd=usercommand[1];int len=strlen(cmd);if(cmd[0]=='$' && len>1){//echo $?if(cmd[1]=='?'){printf("%d\n",lastcode);lastcode=0;}//echo $PWDelse{char* tmp=cmd+1;const char* env=getenv(tmp);//找不到该环境变量,打印'\n',退出码依旧为0if(env==NULL){printf("\n");}else{printf("%s\n",env);}lastcode=0;}}else{printf("%s\n",cmd);}return 1;
}void export(char* usercommand[])
{//exportif(usercommand[1]==NULL){lastcode=0;return;}strcpy(env[my_index],usercommand[1]);putenv(env[my_index]);my_index++;
}int doBuildIn(char* usercommand[])
{//cdif(strcmp(usercommand[0],"cd")==0){if(usercommand[1]==NULL) return -1;cd(usercommand);return 1;}//echoelse if(strcmp(usercommand[0],"echo")==0){return echo(usercommand);}//exportelse if(strcmp(usercommand[0],"export")==0){export(usercommand);}return 0;
}int main()
{while(1){//1.打印提示符信息并获取用户的指令printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());char command[1024]={'\0'};fgets(command,sizeof(command),stdin);char* usercommand[1024]={NULL};//2.解析command字符串,放入usercommand指针数组当中GetCommand(command,usercommand);//3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0int flag=doBuildIn(usercommand);//返回值!=0说明是内建命令,无需执行第4步if(flag!=0) continue;//4.创建子进程,交由子进程完成任务Execute(usercommand);}return 0;
}

二.具体步骤

1.打印提示符

在这里插入图片描述

const char* getUsername()
{const char* username=getenv("USER");if(username==NULL) return "none";return username;
}const char* getHostname()
{const char* hostname=getenv("HOSTNAME");if(hostname==NULL) return "none";return hostname;
}const char* getPwd()
{const char* pwd=getenv("PWD");if(pwd==NULL) return "none";return pwd;
}int main()
{while(1){//1.打印提示符信息并获取用户的指令printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());char command[1024]={'\0'};fgets(command,sizeof(command),stdin);}return 0;
}

注意:
1.因为scanf默认读取到空格或者’\n’时就会停止继续读取,
可是我们命令行中要求读取到空格也不能停止,(否则我们的指令就无法带选项了,因为选项之间是用空格来分割的)
因此我们需要fgets函数

char command[1024]={'\0'};
fgets(command,sizeof(command),stdin);

从命令行当中读取一行字符串
在这里插入图片描述
2.因为我们用户输入的时候在最后的时候一定会输入一个’\n’,因此我们需要把’\n’置为’\0’

command[strlen(command)-1]='\0';//去除我们最后输入的'\n'

把’\n’置为’\0’的操作我们放到了下一个函数当中来完成

2.解析命令行

因为有些命令带有选项:例如 “ls -a -l”
我们在进行程序替换的时候需要分别传入"ls" “-a” "-l"这几个字符串,所以需要把用户输入的字符串分割为若干个字符串存放到一个指针数组当中,可以使用strtok字符串切割函数
在这里插入图片描述

#define DEBUG 1
#define SEP " "//分割字符串填入usercommand数组当中
//例如: "ls -a -l" 分割为"ls" "-a" "-l"
void CommandSplit(char* usercommand[],char* command)
{int i=0;usercommand[i++]=strtok(command,SEP);while(usercommand[i++]=strtok(NULL,SEP));
}//解析命令行
void GetCommand(char* command,char* usercommand[])
{command[strlen(command)-1]='\0';//清理掉最后的'\0'CommandSplit(usercommand,command);
#ifdef DEBUGint i=0;while(usercommand[i]!=NULL){printf("%d : %s\n",i,usercommand[i]);i++;}
#endif
}

我们可以使用条件编译来方便我们自由选择是否需要打印
解析命令行后的usercommand数组中的内容

3.分析是否是内建命令

1.shell对于内建名令的处理

在这里插入图片描述
下面我们就一起来实现一下
cd,echo,export这几个内建命令

2.cd命令

在这里插入图片描述
可是如果shell进行进程程序替换了,那么shell执行完之后不就没了吗?
因此shell执行内建命令时直接封装函数调用系统调用接口即可

内建命令的个数是有限的,所以shell是可以对内建命令进行穷举的

因此我们就能更好地理解内建命令了:

内建命令:不需要创建子进程执行,shell自己执行,本质就是调用系统调用接口

3.cd函数的实现

对于cd而言,我们可以调用chdir这个系统调用接口来改变当前进程的工作目录
在这里插入图片描述
同时也可以设置一个全局的char类型的数组cwd来保存当前路径
每次cd之后用cwd来记录新路径,然后通过putenv来修改环境变量PWD
让我们第一步打印的提示符中的PWD路径可以动态调整

//cwd:存放当前路径,是一个全局变量
char cwd[1024]={'\0'};void cd(char* usercommand[])
{//1.chdir改变当前进程的工作目录chdir(usercommand[1]);//2.获取当前进程所在工作目录到tmp数组当中char tmp[1024]={'\0'};getcwd(tmp,sizeof(tmp));//3.把tmp数组中的内容格式化为"PWD=tmp数组中的内容"放到cwd数组当中sprintf(cwd,"PWD=%s",tmp);//4.导入环境变量putenv(cwd);//5.最后一次进程的退出码置为0(是为了echo $?获取最后一次进程的退出码的实现,跟cd无关)lastcode=0;
}  

注意:cwd数组必须是全局变量,如果cwd是局部变量,那么出了cd这个函数之后cwd数组就会销毁,其中的内容也将会消失
而我们putenv导入环境变量时其实是把cwd的地址放入了环境变量当中,
而不是拷贝一份这个cwd放入环境变量当中,因此cwd数组销毁时,对应导入的环境变量中的内容就不是原来的内容了

4.echo命令的实现

echo命令也是一个内建命令

echo 空串      打印换行
echo $ ?      打印上次进程退出时的退出码
echo $环境变量  打印环境变量
echo 字符串    打印字符串
注意:
echo $       打印$这个字符串
因此即使判断了$,还要继续判断$后面还有没有字符
//全局变量
int lastcode=0;//上一次进程退出时的退出码int echo(char* usercommand[])
{//1.echo后面什么都没有,相当于'\n'if(usercommand[1]==NULL){printf("\n");lastcode=0;return 1;}//2.echo $?  echo $PWD echo $char* cmd=usercommand[1];int len=strlen(cmd);if(cmd[0]=='$' && len>1){//echo $?if(cmd[1]=='?'){printf("%d\n",lastcode);lastcode=0;}//echo $PWDelse{char* tmp=cmd+1;const char* env=getenv(tmp);//找不到该环境变量,打印'\n',退出码依旧为0if(env==NULL){printf("\n");}else{printf("%s\n",env);}lastcode=0;}}else{printf("%s\n",cmd);}return 1;
}

5.export命令的实现

export导入环境变量
注意:
1.刚才介绍cd函数的时候.我们说明了环境变量导入时其实是导入的对应字符串的地址
因此我们环境变量字符串必须要保证全局有效

2.由于我们可以导入很多环境变量,因此env需要是一个二维数组,同时还需要一个index下标来标记该数组当中已经导入过的环境变量

char env[1024][1024]={'\0'};
int my_index=0;void export(char* usercommand[])
{//1.export后面什么都没跟,什么都不执行,直接返回即可if(usercommand[1]==NULL){lastcode=0;return;}//2.要导入的环境变量拷贝到env数组当中strcpy(env[my_index],usercommand[1]);//3.将env数组当中的环境变量导入该进程当中putenv(env[my_index]);my_index++;
}

6.内建命令函数的实现

写好了cd,echo,export这几个函数之后,我们只需要在内建命令函数当中调用这几个函数即可

//返回值=0,说明不是内建命令
//返回值=1,说明是内建命令并且执行成功
int doBuildIn(char* usercommand[])
{//cdif(strcmp(usercommand[0],"cd")==0){if(usercommand[1]==NULL) return -1;cd(usercommand);return 1;}//echoelse if(strcmp(usercommand[0],"echo")==0){return echo(usercommand);}//exportelse if(strcmp(usercommand[0],"export")==0){export(usercommand);}return 0;
}int main()
{while(1){//1.打印提示符信息并获取用户的指令printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());char command[1024]={'\0'};fgets(command,sizeof(command),stdin);char* usercommand[1024]={NULL};//2.解析command字符串,放入usercommand指针数组当中GetCommand(command,usercommand);//3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0int flag=doBuildIn(usercommand);//返回值!=0说明是内建命令,无需执行第4步if(flag!=0) continue;//4.创建子进程,交由子进程完成任务Execute(usercommand);}return 0;
}

4.创建子进程通过程序替换执行命令

使用execvp这个函数来进行程序替换
带上v:因为我们的命令都是放在数组当中的
带上p:因为我们输入的都是系统指令,带上p才可以自动在环境变量中查找我们的命令
否则就要显式传入路径

注意lastcode的设置

//创建子进程,完成任务
void Execute(char* usercommand[])
{pid_t id=fork();if(id==0){//子进程执行部分execvp(usercommand[0],usercommand);//如果子进程程序替换失败,已退出码为1的状态返回exit(1);}else{//父进程执行部分int status=0;//阻塞等待pid_t rid=waitpid(id,&status,0);if(rid>0){lastcode=WEXITSTATUS(status);}}
}

5.循环往复即可

main函数加上while(1)死循环即可

int main()
{while(1){//1.打印提示符信息并获取用户的指令printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());char command[1024]={'\0'};fgets(command,sizeof(command),stdin);char* usercommand[1024]={NULL};//2.解析command字符串,放入usercommand指针数组当中GetCommand(command,usercommand);//3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0int flag=doBuildIn(usercommand);//返回值!=0说明是内建命令,无需执行第4步if(flag!=0) continue;//4.创建子进程,交由子进程完成任务Execute(usercommand);}return 0;
}

三.shell运行原理

shell内部提取用户输入的命令行进行解析
判断是否是内建命令,
1.如果是内建命令的话,shell自己通过调用自己封装的函数来执行该命令
2.如果不是内建命令,shell创建子进程,通过程序替换来然子进程执行该命令
shell进程阻塞等待回收子进程的退出状态
然后循环往复

以上就是Linux自定义shell编写的全部内容,希望能对大家有所帮助!

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

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

相关文章

微软开源,全平台通用:Shell 自动补全工具 | 开源日报 No.132

microsoft/inshellisense Stars: 7.6k License: MIT inshellisense 是一个为 Shell 提供 IDE 风格自动补全的工具。它是一个终端本地运行时自动完成&#xff0c;支持 600 多个命令行工具&#xff0c;并且可以在 Windows、Linux 和 macOS 上使用。主要功能包括安装后可通过运行…

30 UVM Adder Testbench Example

1 Adder Design 加法器设计在时钟的上升沿产生两个变量的加法。复位信号用于clear out信号。注&#xff1a;加法器可以很容易地用组合逻辑开发。引入时钟和重置&#xff0c;使其具有测试台代码中时钟和重置的样子/风格。 module adder(input clk, reset, input [7:0] in1, in…

【操作系统】处理机调度

文章目录 一. 实验目的二. 实验内容三. 实验步骤四. 实验结果五. 实验总结附&#xff1a;系列文章 一. 实验目的 &#xff08;1&#xff09;加深对进程概念的理解&#xff0c;明确进程和程序的区别 &#xff08;2&#xff09;深入理解系统如何组织进程 &#xff08;3&#xff…

Frappe Charts:数据可视化的强大工具

一、产品简介&#xff1a; 一个简单、零依赖、响应式的 开源SVG 图表库。这个图表库无论是数据更新还是屏幕大小变化&#xff0c;都能快速响应并更新图表。数据生成和悬停查看都有舒服的交互动效&#xff0c;体验感很好。不仅支持配置颜色&#xff0c;外观定制也很方便。还支持…

最新ChatGPT网站AI系统源码,附详细搭建教程/支持GPT4.0/AI绘画/GPT语言对话/DALL-E3文生图/自定义知识库

一、前言 SparkAi创作系统是基于ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作Ch…

Google Chrome 现在会在后台扫描泄露的密码

谷歌表示&#xff0c;Chrome 安全检查功能将在后台运行&#xff0c;检查网络浏览器中保存的密码是否已被泄露。 如果桌面用户正在使用标记为危险的扩展程序&#xff08;从 Chrome Web Store 中删除&#xff09;、最新的 Chrome 版本&#xff0c;或者如果启用安全浏览来阻止 Go…

修改jenkins的目录(JENKINS_HOME)

默认JENKINS_HOME是/var/lib/jenkins/ 现要修改为/home/jenkins_data/jenkins 最开始 sudo cp -a /var/lib/jenkins/ /home/jenkins_data/ 然后如下操作&#xff1a; 1、首先 /etc/sysconfig/jenkins&#xff1a;jenkins配置文件&#xff0c;“端口”&#xff0c;“JENKIN…

message: 没有找到可以构建的 NPM 包,请确认需要参与构建的 npm 都在 `miniprogra

第一步&#xff1a;修改 project.config.json 文件 "packNpmRelationList": [{"packageJsonPath": "./package.json","miniprogramNpmDistDir": "./miniprogram/"}], "packNpmManually": true 第二步&#xff1a;…

pytorch03:transforms常见数据增强操作

目录 一、数据增强二、transforms--Crop裁剪2.1 transforms.CenterCrop2.2 transforms.RandomCrop2.3 RandomResizedCrop2.4 FiveCrop和TenCrop 三、transforms—Flip翻转、旋转3.1RandomHorizontalFlip和RandomVerticalFlip3.2 RandomRotation 四、transforms —图像变换4.1 t…

[MySQL] MySQL 高级(进阶) SQL 语句

一、高效查询方式 1.1 指定指字段进行查看 事先准备好两张表 select 字段1&#xff0c;字段2 from 表名; 1.2 对字段进行去重查看 SELECT DISTINCT "字段" FROM "表名"; 1.3 where条件查询 SELECT "字段" FROM 表名" WHERE "条件…

两种方法求解平方根 -- 牛顿法、二分法

Leetcode相关题目&#xff1a; 69. x 的平方根 以求解 a a a 的平方根为例&#xff0c;可转换为求解方程 f ( x ) f(x) f(x)的根。 f ( x ) x 2 − a f(x)x^2-a f(x)x2−a 牛顿法迭代公式如下&#xff1a; x n 1 x n − f ( x n ) f ′ ( x n ) x_{n1} x_n - \frac {f…

华为交换机生成树STP配置案例

企业内部网络怎么防止网络出现环路&#xff1f;学会STP生成树技术就可以解决啦。 STP简介 在二层交换网络中&#xff0c;一旦存在环路就会造成报文在环路内不断循环和增生&#xff0c;产生广播风暴&#xff0c;从而占用所有的有效带宽&#xff0c;使网络变得无法正常通信。 在…

鸿蒙 DevEco Studio 3.1 入门指南

本文主要记录开发者入门&#xff0c;从软件安装到项目运行&#xff0c;以及后续的学习 1&#xff0c;配置开发环境 1.1 下载安装包 官网下载链接 点击立即下载找到对应版版本 下载完成&#xff0c;按照提示默认安装即可 1.2 下载SDK及工具链 运行已安装的DevEco Studio&…

odoo17 | 创建一个新应用程序

前言 本章的目的是为创建一个全新的Odoo模块奠定基础。 我们将从头开始&#xff0c;以使我们的模块被Odoo识别所需的最低限度。 在接下来的章节中&#xff0c;我们将逐步添加功能以构建一个真实的业务案例。 教程 假设我门需要在odoo上开发一个新app模块例如房地产广告模块。…

uniapp Vue3 日历 可签到 跳转

上干货 <template><view class"zong"><view><view class"top"><!-- 上个月 --><view class"sgy" click"sgy">◀</view><view class"nianyue">{{ year }}年{{ month 1 }}…

MD5算法

一、引言 MD5&#xff08;Message-Digest Algorithm 5&#xff09;是一种广泛应用的密码散列算法&#xff0c;由Ronald L. Rivest于1991年提出。MD5算法主要用于对任意长度的消息进行加密&#xff0c;将消息压缩成固定长度的摘要&#xff08;通常为128位&#xff09;。在密码学…

右键菜单“以notepad++打开”,在windows文件管理器中

notepad 添加到文件管理器的右键菜单中 找到安装包&#xff0c;重新安装一般即可。 这里有最新版&#xff1a;地址 密码:f0f1 方法 在安装的时候勾选 “Context Menu Entry” 即可 Notepad的右击打开文件功能 默认已勾选 其作用是添加右键快捷键。即&#xff0c;对于任何…

黑马程序员SSM框架-SpringBoot

视频连接&#xff1a;SpringBoot-01-SpringBoot工程入门案例开发步骤_哔哩哔哩_bilibili SpringBoot简介 入门程序 也可以基于官网创建项目。 SpringBoot项目快速启动 下面的插件将项目运行所需的依赖jar包全部加入到了最终运行的jar包中&#xff0c;并将入口程序指定。 Spri…

2023/12/30 c++ work

定义一个Person类&#xff0c;私有成员int age&#xff0c;string &name&#xff0c;定义一个Stu类&#xff0c;包含私有成员double *score&#xff0c;写出两个类的构造函数、析构函数、拷贝构造和拷贝赋值函数&#xff0c;完成对Person的运算符重载(算术运算符、条件运算…

JavaScript 工具库 | PrefixFree给CSS自动添加浏览器前缀

新版的CSS拥有多个新属性&#xff0c;而标准有没有统一&#xff0c;有的浏览器厂商为了吸引更多的开发者和用户&#xff0c;已经加入了最新的CSS属性支持&#xff0c;这其中包含了很多炫酷的功能&#xff0c;但是我们在使用的时候&#xff0c;不得不在属性前面添加这些浏览器的…