【Linux】:实现一个简易的shell

news2024/11/16 0:36:53

目录

1.命令行提示符

2.命令行参数 

2.1 获取命令行参数

2.2 解析命令行参数

3.判断指令类型

3.1 模拟cd命令

3.2 模拟export和echo

bash的环境变量来源

4.外部指令的执行

1.命令行提示符

在我们输入指令前,终端界面一般有一个命令行提示符, 我们先实现这个功能,

我们这里把当前工作路径的全路径打印出来了,如果只要打印当前工作目录名,可以分割路径提取。

2.命令行参数 

2.1 获取命令行参数

如果我们用scanf读取输入的命令行参数,只会读到一个不含空白符的字符串,因为scanf会以空白符为结尾。我们可以看看,

所以我们不能用scanf读取命令行参数,可以用fgets, fgets - cppreference.com

注意:

puts遇到空字符停止输出,在输出字符串时会自动在字符串末尾(\0)加一个换行符。
gets()丢弃输入中的换行符,puts()在输出中添加换行符。

fgets()保留输入中的换行符,fputs()不在输出中添加换行符。

 我们编写函数Interact,用来实现用户和命令行交互的功能,使程序获取用户输入的命令行参数,

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3 #include <assert.h>
    4 #include <string.h>
    5 #define LEFT "["
    6 #define RIGHT "]"
    7 #define LABEL "#"//root用户是#,普通用户是@
    8 #define LINE_SIZE 1024
    9 
   10 char commandline[LINE_SIZE];
   11  
   12 //获取用户名
   13 const char* getUsername()          
   14 {
   15     return getenv("USER");                                                        
   16 }                                     
   17                                                                   
   18 //获取主机名                                            
   19 const char* getHostname()                            
   20 {                                             
   21     return getenv("HOSTNAME");    
   22 }   
   23                     
   24 //获取当前工作路径                                           
   25 const char* getPwd()                                
   26 {
   27     return getenv("PWD");
   28 }
   29                                 
   30 void Interact(char* cline,int size)
   31 {               
E> 32     printf(LEFT"%s@%s %s"RIGHT""LABEL" ",getUsername(),getHostname(),getPwd());
   33     char *s = fgets(cline,size,stdin);             
   34     assert(s);//assert只在debug模式下作用,release版本下会被优化掉
   35     //所以变量s在release版本下,可能只定义而没有被使用。
   36     //编译器对于定义了但没有使用的变量可能会报warning
   37     //所以为了让编译器编过,我们加下面一句代码
   38     (void)s;//抵消编译器的一些报警
   39     
   40     //"ls -a -l\n\0"
   41     //fgets会将最后用作结束的换行符保存,我们要删除这个换行符
   42     cline[strlen(cline) - 1] = '\0';//"ls -a -l\0\0"
   43 }
   44 int main()
   45 {
   46     char commandline[LINE_SIZE];
   47     int quit = 0;
   48     while(!quit)
   49     {
   50          Interact(commandline,sizeof(commandline));
   51          printf("echo:%s\n",commandline);
   52     }
   53     return 0;
   54 }

 运行

我们可以一直向命令行中输入参数。

2.2 解析命令行参数

 用户输入参数(指令)后,shell会对该行参数进行解析,一般会将字符串进行分割,我们这里用strtok函数分割字符串。

我们用splitstring接口实现字符串解析的功能, 同时用一个for循环检测解析后的结果。

   10 #define ARGC_SIZE 32
   11 #define DELIMIT " \t"
   //其余部分代码同上
   47 int splitstring(char cline[],char *argv[])
   48 {
   49     //分割字符串cline
   50     int i = 0;
   51     argv[i++] = strtok(cline,DELIMIT);
W> 52     while(argv[i++] = strtok(NULL,DELIMIT));
   53     return i - 1;//返回分割后字符串个数
   54 }
   55 int main()
   56 {
   57     char commandline[LINE_SIZE];//
   58     char *argv[ARGC_SIZE];
   59     int quit = 0;
   60     while(!quit)
   61     {
   62         //1.打印命令行标识符并等待参数输入
   63         Interact(commandline,sizeof(commandline));
   64         printf("echo:%s\n",commandline);
   65 
   66         //commandline -> "ls -a -l\0" -> "ls" "-a" "-l"
   67         //2.解析参数,对子串进行分割                                              
   68         int argc = splitstring(commandline,argv);
   69         if(argc == 0) continue;
   70         //检测分割后的结果
   71         for(int i = 0;argv[i];i++) printf("[%d]:%s\n",i,argv[i]);
   72     }
   73     return 0;
   74 }

运行, 命令行提示符、用户与命令行交互、解析参数的功能实现了,现在我们需要根据用户输入的参数(指令),执行程序。

但在执行程序之前,我们必须对指令进行判断。

3.判断指令类型

我们之前学习过指令,Linux系统的指令一般可以分为两类 ,

一类是内部指令(builtin shell command),内部指令是指内建在shell中的指令,但我们执行该类指令时,不需要额外创建进程,所以内部指令执行的效率高。

另一类是外部指令(external shell command)。外部指令是指非内建于shell的指令,我们执行该类指令时,会额外创建一个进程。

我们可以通过指令type判断一个指令是否是内建指令。

type命令来自英文单词“类型”,其功能是用于查看命令类型,如需区分某个命令是Shell内部指令还是外部命令,则可以使用type命令进行查看。
原文链接:type命令 – 查看命令类型 – Linux命令大全(手册)

显示出文件路径的一般是外部命令,显示“is a shell builtin”是内部命令。

我们判断是否是内建命令,是一条一条判断的,

3.1 模拟cd命令

拿“cd”举例,cd + 目标路径,我们可以通过chdir系统调用,将当前的工作路径换为“目标路径”,

 注意,chdir只会更改进程的当前工作目录,只会影响当前进程及其子进程的工作目录,不会影响环境变量中的PWD,PWD 是由 shell 自动维护,而不是由内核直接管理。

对于典型的 shell,PWD 在你使用命令(如 cd)改变目录时会被更新,但是,直接用系统调用 chdir 不会自动更新 PWD。linux环境 调用chdir函数虽然更改了工作目录,但是加载动态库还是之前的目录下 - CSDN文库

所以当我们调用chdir更改当前工作目录后,还要对环境变量PWD进行修改。先通过调用getcwd()使全局变量pwd获取当前工作目录的绝对路径,然后将全局变量pwd拷贝到环境变量PWD中,可以通过getenv()的返回值获取对应环境变量的地址,再将字符串pwd写入getenv返回的指针指向的内存区域。

返回值为1,表示内部指令的执行。

同样的,我们还可以实现几个内建命令,

3.2 模拟export和echo

我们检验一下内建命令export,没有问题,我们先把代码中的检测注释掉,再检验一下echo,

看上去没有问题,我们用export增加一个环境变量,然后用echo打印该环境变量, 

很奇怪,没有打印新增的环境变量val_env,我们输入env指令检查一下,发现也没有新增的环境变量,但我们之前export新增命令后输入env,可以查看到新增环境变量,这是为什么呢?为什么中间输入echo指令查看环境变量,env就不显示新增环境变量了?

这是因为,当进程启动时,会专门开辟一块空间存储命令行参数和环境变量,同时用一个字符串指针数组管理这些环境变量,这个管理环境变量的字符串指针数组就叫做环境变量表(char* envrion[]),

当我们用putenv新增一个环境变量_argv[1],这时环境变量表会分配一个元素,也就是字符串指针指向这个新增的环境变量,这个新增的环境变量并没有添加到专门存储环境变量的内存空间中,而是在栈区(因为_argv[1]是一个全局变量),

当我们重新在命令行输入指令时,会刷新命令行参数表_argv(char* argv[]),如果argv会分割成两个字符串,第二个字符串就会覆盖原来的_argv[1],“echo $val_env”中的“$val_env”,就会覆盖原来的“export val_env=1111111”中的“val_env=1111111”,这样环境变量表environ就找不到原来新增的环境变量了val_env=1111111,因为被覆盖了。​​​​​​​

 为了防止再次输入命令会覆盖环境变量的问题,我们要自己维护一个存放环境变量的空间myenv,将新增的环境变量存放在myenv中,

这样就不会覆盖了。 (但再添加一个新的环境变量会覆盖旧的环境变量,大家也可以把自己维护的环境变量设置成二维数组的形式,在堆上申请空间)

上面的myenv属于自定义的环境变量表,具有全局属性,我们还可以自定义一个本地变量表,【Linux】:环境变量-CSDN博客 当我们用echo检查最近一个进程的退出码时,可增加一个if语句, 

bash的环境变量来源

当我们成功登录linux系统时,会启动一个shell进程(bash进程),我们在命令行运行的程序,实例化的进程的环境变量都是继承bash进程。bash进程的环境变量保存在用户目录的".bash.profile"文件中,该文件中保存了导入环境变量的方式。

4.外部指令的执行

 我们用externalExecute接口实现外部指令的执行,

5.完整代码

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define LEFT "["
#define RIGHT "]"
#define LABEL "#"//root用户是#,普通用户是@
#define LINE_SIZE 1024//命令行标识符的大小
#define ARGC_SIZE 32//字符串指针数组的字符串个数
#define DELIMIT " \t"//strtok()的分割符
#define EXIT_CODE 303//子进程程序替换失败后的退出码

extern char **environ;//环境变量
char commandline[LINE_SIZE];//命令行标识符
int lastcode = 0;//退出码
int quit = 0;//充当bash进程结束条件的判断
char *argv[ARGC_SIZE];//存储分割后的参数序列
char pwd[LINE_SIZE];//获取当前目录的工作路径
char myenv[LINE_SIZE];//环境变量存放空间

//获取用户名
const char* getUsername()
{
    return getenv("USER");
}

//获取主机名
const char* getHostname()
{
    return getenv("HOSTNAME");
}

//获取当前工作目录路径
void getpwd()
{
    getcwd(pwd,1024);
}

void Interact(char* cline,int size)
{
    getpwd();
    printf(LEFT"%s@%s %s"RIGHT""LABEL" ",getUsername(),getHostname(),pwd);
    char *s = fgets(cline,size,stdin);
    assert(s);//assert只在debug模式下作用,release版本下会被优化掉
    //所以变量s在release版本下,可能只定义而没有被使用。
    //编译器对于定义了但没有使用的变量可能会报warning
    //所以为了让编译器编过,我们加下面一句代码
    (void)s;//抵消编译器的一些报警
    
    //"ls -a -l\n\0"
    //fgets会将最后用作结束的换行符保存,我们要删除这个换行符
    cline[strlen(cline) - 1] = '\0';//"ls -a -l\0\0"
}

int splitstring(char cline[],char *_argv[])
{
    //分割字符串cline
    int i = 0;
    _argv[i++] = strtok(cline,DELIMIT);
    while(_argv[i++] = strtok(NULL,DELIMIT));
    return i - 1;//返回分割后字符串个数
}

void externalExecute(char* _argv[])
{

    pid_t id = fork();
    if(id < 0)
    {
        //子进程创建失败
        perror("fork");
        return;
    }
    else if(id == 0)
    {
        //子进程创建成功,开始执行子进程
        //采用程序替换执行命令
        //execvpe(_argv[0],_argv,environ);
        execvp(_argv[0],_argv);
        //程序替换成功,子进程的旧代码(也就是下一句代码)不会执行,
        //如果执行了,说明程序替换失败
        exit(EXIT_CODE);
    }
    else
    {
        //执行父进程,父进程等待回收子进程
        int status = 0;
        pid_t rid = waitpid(id,&status,0);
        if(rid == id) 
        {
            lastcode = WEXITSTATUS(status);
        }
    }
}

int buildCmd(char* _argv[],int _argc)
{

    if(_argc == 2 && strcmp(_argv[0],"cd") == 0)
    {
        printf("检验环境变量PWD是否改变:%s\n",getenv("PWD"));
        chdir(_argv[1]);
        getpwd();
        sprintf(getenv("PWD"),"%s",pwd);//修改环境变量PWD
        printf("检验环境变量PWD是否改变:%s\n",getenv("PWD"));
        return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0],"export") == 0)
    {
        strcpy(myenv,_argv[1]);
        putenv(myenv);
        return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0],"echo") == 0)
    {
        if(strcmp(_argv[1],"$?") == 0)
        {
            printf("%d\n",lastcode);
            lastcode = 0;
        }
        else if(*_argv[1] == '$')
        {
            //如果是字符串打印环境变量
            char *val = getenv(_argv[1] + 1);
            if(val) printf("%s\n",val);//要判断一下环境变量是否存在
        }
        else
        {
            printf("%s\n",_argv[1]);
        }
        return 1;
    }

    // 特殊处理一下ls
    if(strcmp(_argv[0], "ls") == 0)
    {
        _argv[_argc++] = "--color";
        _argv[_argc] = NULL;
    }
    return 0;
}

int main()
{
    while(!quit)
    {
        //1.
        //2.获取命令行参数(指令)
        Interact(commandline,sizeof(commandline));
        //printf("echo:%s\n",commandline);

        //commandline -> "ls -a -l\0" -> "ls" "-a" "-l"
        //3.解析参数,对子串进行分割
        int argc = splitstring(commandline,argv);
        if(argc == 0) continue;
        //检测分割后的结果
        //for(int i = 0;argv[i];i++) printf("[%d]:%s\n",i,argv[i]);

        //4.判断指令
        int n = buildCmd(argv,argc);

        //5.外部指令的执行
        if(!n) externalExecute(argv);
    }
    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

性能优化理论篇 | swap area是个什么东西

我们知道每台计算机的内存&#xff08;RAM&#xff09;都是有限的&#xff0c;而我们的应用程序需要加载到内存才能被运行&#xff0c;如果一台机器运行多个应用程序时&#xff0c;内存可能会耗尽。Linux 系统中的“交换空间&#xff08;也称为交换分区&#xff09;”可以帮助缓…

VM相关配置及docker

NAT——VMnet8网卡 桥接——WLAN/网线 仅主机——VMnet1网卡 docker与虚拟机的区别 启动docker服务 systemctl start docker 重启 systemctl start docker关闭docker服务 systemctl stop docker.servicedocker的两大概念 镜像&#xff1a;images&#xff0c;应用程序的静态文…

nssctf-[SWPUCTF 2022 新生赛]1z_unserialize-简单的序列化题目

1. 打开题目是一段很简单的php代码 对代码进行分析&#xff0c;题目中有一个__destruct析构函数&#xff0c;这个函数是在对象被销毁的时候触发&#xff0c;那那么在这里new一下就相当于销毁一个对象&#xff0c;$a$this->lt;和$a($this->lly);相当于是$this->lt(thi…

CSS3-新特性

1.新增选择器 1.属性选择器 2.结构伪类选择器 3.伪元素选择器&#xff08;重点&#xff09; 4.CSS3 盒子模型 2.CSS3滤镜filter 3.CSS3 calc 函数 4.CSS3 过渡&#xff08;重点&#xff09;

8月18日微语报,星期日,农历七月十五

8月18日微语报&#xff0c;星期日&#xff0c;农历七月十五&#xff0c;周末愉快&#xff01; 一份微语报&#xff0c;众览天下事&#xff01; 1、南昌从业者谈蔬菜涨价&#xff1a;天热易变质增加损耗&#xff0c;农户收入未明显提升。 2、委员建议“行政教学分离”&#x…

后端开发刷题 | 寻找峰值【二分法】

描述 给定一个长度为n的数组nums&#xff0c;请你找到峰值并返回其索引。数组可能包含多个峰值&#xff0c;在这种情况下&#xff0c;返回任何一个所在位置即可。 1.峰值元素是指其值严格大于左右相邻值的元素。严格大于即不能有等于 2.假设 nums[-1] nums[n] −∞ 3.对于…

【精选】基于Java摄影约拍系统设计与实现(全网独一无二,最新定制)

目录 目录&#xff1a; 系统简介&#xff1a; 核心技术介绍 mysql技术介绍 IDEA编译器介绍 Springboot框架简介 springmvc框架简介 Mybatis技术简介 Node.js技术简介 Vue.js技术简介 系统数据库详细设计 系统功能设计 系统测试运行 模块测试 系统整体测试 测试过程 测试…

启明欣欣STM32开发板运行ThreadX

ThreadX是非常优秀的RTOS&#xff0c;微软收购了ThreadX后就开源了&#xff0c;后来又交给Eclipse基金会&#xff0c; 本文讲述如何在STM32上运行ThreadX&#xff0c;使用CubeMX来实现。本人环境如下&#xff0c; CM4芯片&#xff1a;STM32F407ZGT6&#xff0c;内存192KB&am…

分布式事务方案——基于两阶段提交的 XA事务

分布式事务方案——基于两阶段提交的 XA事务 在这篇文章中深入理解分布式事务中的两阶段提交&#xff08;2PC&#xff09;&#xff0c;什么是2PC&#xff0c;2PC原理是怎样&#xff1f;2PC有没有什么问题&#xff1f;解决方案&#xff1f;无法解决的情况&#xff1f; 我们详细…

24/8/18算法笔记 目标导向强化学习

目标导向强化学习&#xff08;Goal-Oriented Reinforcement Learning&#xff0c;简称GORL&#xff09;是强化学习的一个分支&#xff0c;它关注于智能体如何通过与环境的交互来实现特定的目标或任务。与传统的强化学习不同&#xff0c;目标导向强化学习更加关注目标的设定和达…

图像数据处理13

三、空域滤波 3.1滤波器的基本概念 什么是滤波&#xff1f; 简单来说就是从干扰信号中提取出有用的信号 3.1.1空域滤波&#xff08;Spatial Domain Filtering&#xff09; 空域滤波适用于简单的滤波任务&#xff0c;直接对图像的像素空间进行操作。它通过对图像中的每个像…

如何选择流量与商业潜力兼备的SEO关键词?

如何选择流量与商业潜力兼备的SEO关键词&#xff1f; 你选择的关键词可以成就或破坏你的SEO活动。 如果你明智地选择关键词&#xff0c;那么你制作的内容将有可能月复一月地吸引有价值的自然搜索流量。如果你选择了错误的关键词&#xff0c;你的内容将只能吸引低价值的流量&a…

Java流程控制07:增强for循环

本节内容视频链接&#xff1a;Java流程控制10&#xff1a;增强for循环_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV12J41137hu?p42&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5 Java中的增强for循环&#xff08;‌也称为“for-each”循环&#xff09;‌是…

实用好软-----电脑端好用的免费音乐下载小工具 简单 快速

目前很多很多音乐软件都是收费的。要么是试听。前段时间分享了一款嗅探工具感觉很不错。今天分享的这款小工具超小。下载只有3M大小。解压后运行窗口简单。直接输入歌曲名字即可搜索出来。然后选择下载即可 界面 测试了下还是比较好用的。而且下载很快 &#xff0c;不过软件显…

Ajax-02.Axios

Axios入门 1.引入Axios的js文件 <script src"js/axios-0.18.0.js"></script> Axios 请求方式别名: axios.get(url[,config]) axios.delete(url[,config]) axios.post(url[,data[,config]]) axios.put(url[,data[,config]]) 发送GET/POST请求 axios.get…

PROLOG实现亲属关系小型演绎数据库

问题 试编写一个描述亲属关系的PROLOG程序&#xff0c;然后给出一些事实数据&#xff0c;建立一个小型演绎数据库。 代码实现 % 基本事实 father(john, mike). father(john, lisa). father(boluo, ana). father(boluo, peter).mother(mary, mike). mother(mary, lisa). mother…

大厂进阶五:React源码解析之深度剖析Diff算法

本文主要针对React源码进行解析&#xff0c;内容有&#xff1a; 1、Diff算法原理、两次遍历 2、Diff瓶颈及限制 3、Diff更新之单节点和多节点原理 一、Diff源码解析 以下是关于 React Diff 算法的详细解析及实例&#xff1a; 1、React Diff 算法的基本概念和重要性 1.1 概念…

【串口助手开发】--温度曲线图实时显示功能,全流程小白教程,zedgraph控件的使用方法,Visual Studio 软件C#语言

1、ZedGraph.dll 控件下载 链接&#xff1a;下载ZedGraph&#xff08;ZedGraph.dll 和ZedGraph.Web.dll文件&#xff09; 打开链接后&#xff0c;图中红框显示的是仅下载zedgraph控件&#xff0c;本文所需的文件是ZedGraph.dll。 2、ZedGraph.dll 控件添加进Visual Studio 软…

【Arduino】ATmega328PB 连接 LSM6DS3 姿态传感器,并读数据(不确定 ESP 系列是否可行,但大概率是可行的)

总览 1.初始化 ATmega328PB&#xff0c;默认大家已经完成了 328 的配置准备工作&#xff0c;已经直接能够向里面写入程序 2.接线&#xff0c;然后验证 mega328 的 I2C 设备接口能否扫描到 LSM6DS3 3.编写代码&#xff0c;上传&#xff0c;查看串口数据。完成。 一、初始化 AT…