【Linux】自制shell

2023-09-12 19:06:02

本期我们利用之前学过的知识,写一个shell命令行程序


目录

一、初始代码

二、使用户输入的ls指令带有颜色分类

三、解决cd指令后用户所在路径不变化问题

3.1 chdir函数

四、关于环境变量的问题


 

一、初始代码

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

#define MAX 1024    
#define ARGc 512    

void shift_commend(char** s, char* c, int n)
{
    assert(s);
    assert(c);
    int num = 0;
    *s = c;
    ++s;
    while (num < n)
    {
        if (*(c + num) == ' ')//将每个空格字符替换为'\0',再将选项的首地址存入s
        {
            *(c + num) = '\0';
            if (*(c + num + 1) != ' ')//防止用户输入多个空字符
            {
                *s = c + 1 + num;
                ++s;
            }
        }
        ++num;
    }
}
int main()
{
    while (1)
    {
        printf("[%s@MyShell]#", getenv("USER"));
        fflush(stdout);
        char comment[MAX] = { 0 };
        char* p = fgets(comment, sizeof(comment), stdin);
        assert(p);//assert函数在release版本下会被编译器删去    
        (void)p;//防止在release版本下assert函数被删除导致p指针从未被使用而报错    
        comment[strlen(comment) - 1] = '\0';//去除fgets函数最后获取的\n字符    
        char* s[ARGc] = { NULL };
        shift_commend(s, comment, strlen(comment));//分割输入的命令行字符串,将每一个选项的地址存入指针数组s    
        pid_t id = fork();
        if (id == 0)
        {
            execvp(*s, s);
            exit(1);
        }
        int status = 0;
        waitpid(id, &status, 0);
    }
}

上述代码我们实现了一个基本的shell命令行程序,其实现基本思路为:

使用fgets函数获取用户输入的命令行,获取后切除字符串最后的换行符(用户输入指令后必须使用换行符来输入缓冲区中,而fgets并不会自动切除其换行符),然后将输入的指令进行切割(将指令与选项一个个切割开,方便传入execvp函数(例如fgets函数获取到用户输入的“ls -a -l”,经过切割会变成“ls/0-a/0-l/0”))。最后再创建子进程,用子进程调用execvp函数,传入用户指令最后实现shell命令行程序。

运行效果:af43d80d76c04fb09bede967d98cddc4.png

我们拿xshell来对比一下我们自己实现的shell命令行:

704fb0baae0a4f509308a4d20373baae.png

咦?xshell的ls指令有颜色变化,为什么我们自己实现的shell就没有呢?

fca1f4bc7b28448da2fd549a427b38c7.png

这是因为xshell下的ls指令多了一行颜色配置:--color=auto

如果我们要想自己的shell命令行的ls指令有颜色分类的话,我们只需要在切割用户输入的指令后判断其是否为ls指令,如果是,在该指针数组的最后一项加上"--color=auto"字符串即可:

 

二、使用户输入的ls指令带有颜色分类

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

#define MAX 1024    
#define ARGc 512    

void shift_commend(char** s, char* c, int n)
{
    assert(s);
    assert(c);
    int num = 0;
    *s = c;
    ++s;
    while (num < n)
    {
        if (*(c + num) == ' ')//将每个空格字符替换为'\0',再将选项的首地址存入s
        {
            *(c + num) = '\0';
            if (*(c + num + 1) != ' ')//防止用户输入多个空字符
            {
                *s = c + 1 + num;
                ++s;
            }
        }
        ++num;
    }
}
int main()
{
    while (1)
    {
        printf("[%s@MyShell]#", getenv("USER"));
        fflush(stdout);
        char comment[MAX] = { 0 };
        char* p = fgets(comment, sizeof(comment), stdin);
        assert(p);//assert函数在release版本下会被编译器删去    
        (void)p;//防止在release版本下assert函数被删除导致p指针从未被使用而报错    
        comment[strlen(comment) - 1] = '\0';//去除fgets函数最后获取的\n字符    
        char* s[ARGc] = { NULL };
        shift_commend(s, comment, strlen(comment));//分割输入的命令行字符串,将每一个选项的地址存入指针数组s
        if (strcmp(s[0], "ls") == 0)//使用户输入的ls指令带有颜色分类
        {
            int n = 0;
            while (*(s + n))
            {
                ++n;
            }
            *(s + n) = (char*)"--color=auto";
        }
        pid_t id = fork();
        if (id == 0)
        {
            execvp(*s, s);
            exit(1);
        }
        int status = 0;
        waitpid(id, &status, 0);
    }
}

运行效果: 

9f8f50e77750433a8fa66ae3d1d1b9bc.png

 

三、解决cd指令后用户所在路径不变化问题

上面代码还有一个问题,在我们使用cd指令后,再使用pwd来查看所在路径时,会发现没有任何变化:

31dca27a1aeb40a89f3c0402071815ea.png

这是因为当用户输入cd指令后,通过子进程调用的execvp函数来执行cd指令,改变的是子进程的目录路径和父进程压根没有关系!

所以当类似cd这样要改变父进程环境的指令(内建指令),就要父进程自己来执行,我们需要特殊判断做特殊处理:

 

3.1 chdir函数

chdir函数(包含在头文件unistd.h中)可以改变进程所在路径:

69b55cd89ec74fb7bbdfc19ca87cc4a4.png

我们可以向path传入想要进入的路径,这样子chdir函数就会自动帮我们改变当前进程的所处路径:

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

#define MAX 1024    
#define ARGc 512    

void shift_commend(char** s, char* c, int n)
{
    assert(s);
    assert(c);
    int num = 0;
    *s = c;
    ++s;
    while (num < n)
    {
        if (*(c + num) == ' ')//将每个空格字符替换为'\0',再将选项的首地址存入s
        {
            *(c + num) = '\0';
            if (*(c + num + 1) != ' ')//防止用户输入多个空字符
            {
                *s = c + 1 + num;
                ++s;
            }
        }
        ++num;
    }
}
int main()
{
    while (1)
    {
        printf("[%s@MyShell]#", getenv("USER"));
        fflush(stdout);
        char comment[MAX] = { 0 };
        char* p = fgets(comment, sizeof(comment), stdin);
        assert(p);//assert函数在release版本下会被编译器删去    
        (void)p;//防止在release版本下assert函数被删除导致p指针从未被使用而报错    
        comment[strlen(comment) - 1] = '\0';//去除fgets函数最后获取的\n字符    
        char* s[ARGc] = { NULL };
        shift_commend(s, comment, strlen(comment));//分割输入的命令行字符串,将每一个选项的地址存入指针数组s
        if (strcmp(s[0], "cd") == 0)//cd指令直接使用chdir函数改变父进程所在路径
        {
            if(s[1]!=NULL)
            chdir(s[1]);
            continue;
        }
        if (strcmp(s[0], "ls") == 0)//使用户输入的ls指令带有颜色分类
        {
            int n = 0;
            while (*(s + n))
            {
                ++n;
            }
            *(s + n) = (char*)"--color=auto";
        }
        pid_t id = fork();
        if (id == 0)
        {
            execvp(*s, s);
            exit(1);
        }
        int status = 0;
        waitpid(id, &status, 0);
    }
}

2d1a274b401d4bfdab9ba28cd5a2895e.png

 

四、关于环境变量的问题

在xshell中我们可以使用export导入自定义的环境变量,但是在我们自实现的代码中还没有这个功能,下面我们来实现一下:

使用我们在往期博客中介绍过导入环境变量的函数:putenv

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

#define MAX 1024
#define ARGc 512

void shift_commend(char** s, char* c, int n)
{
    assert(s);
    assert(c);
    int num = 0;
    *s = c;
    ++s;
    while (num < n)
    {
        if (*(c + num) == ' ')//将每个空格字符替换为'\0',再将选项的首地址存入s
        {
            *(c + num) = '\0';
            if (*(c + num + 1) != ' ')//防止用户输入多个空字符
            {
                *s = c + 1 + num;
                ++s;
            }
        }
        ++num;
    }
}
int main()
{
    while (1)
    {
        printf("[%s@MyShell]#", getenv("USER"));
        fflush(stdout);
        char comment[MAX] = { 0 };
        char* p = fgets(comment, sizeof(comment), stdin);
        assert(p);//assert函数在release版本下会被编译器删去    
        (void)p;//防止在release版本下assert函数被删除导致p指针从未被使用而报错    
        comment[strlen(comment) - 1] = '\0';//去除fgets函数最后获取的\n字符    
        char* s[ARGc] = { NULL };
        shift_commend(s, comment, strlen(comment));//分割输入的命令行字符串,将每一个选项的地址存入指针数组s
        if (strcmp(s[0], "cd") == 0)//cd指令直接使用chdir函数改变父进程所在路径
        {
            if(s[1]!=NULL)
                chdir(s[1]);
            continue;
        }
        if (strcmp(s[0], "export") == 0)//导入用户自定义的环境变量
        {
            if (s[1] != NULL)
                putenv(s[1]);
            continue;
        }
        if (strcmp(s[0], "ls") == 0)//使用户输入的ls指令带有颜色分类
        {
            int n = 0;
            while (*(s + n))
            {
                ++n;
            }
            *(s + n) = (char*)"--color=auto";
        }
        pid_t id = fork();
        if (id == 0)
        {
            execvp(*s, s);
            exit(1);
        }
        int status = 0;
        waitpid(id, &status, 0);
    }
}

运行结果: 

4a32bf8f5e924a41bfc4696486e3b732.png

这张图不太好看,博主仔细找过,在子进程调用env指令查找环境变量时,并没有看到导入的环境变量“Myenv=100”

这是为什么呢?子进程的环境变量不应该继承父进程的嘛?

这点没错,子进程确实基础了父进程的环境变量,我们再来仔细看看可以看到,在我们导入环境变量后,再一次调用env指令,屏幕上在最后多打印了一行空行。为什么会这样?

因为使用putenv函数导入环境变量时,该函数只是把形参所获取到的地址添加到进程中的环境变量表中了,我们仔细看看代码中存储环境变量的是一个变量s[1],当我们下一次在调用其他指令时该地址的数据就发生了变化!也就是说自定义环境变量是由我们自己维护的,我们应该将其放在一个不会被覆盖的空间里

下面我们改写一下代码:

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

#define MAX 1024
#define ARGc 512

void shift_commend(char** s, char* c, int n)
{
    assert(s);
    assert(c);
    int num = 0;
    *s = c;
    ++s;
    while (num < n)
    {
        if (*(c + num) == ' ')//将每个空格字符替换为'\0',再将选项的首地址存入s
        {
            *(c + num) = '\0';
            if (*(c + num + 1) != ' ')//防止用户输入多个空字符
            {
                *s = c + 1 + num;
                ++s;
            }
        }
        ++num;
    }
}
int main()
{
    char user_env[32][256];//存储自定义环境变量
    int env_num = 0;
    while (1)
    {
        printf("[%s@MyShell]#", getenv("USER"));
        fflush(stdout);
        char comment[MAX] = { 0 };
        char* p = fgets(comment, sizeof(comment), stdin);
        assert(p);//assert函数在release版本下会被编译器删去    
        (void)p;//防止在release版本下assert函数被删除导致p指针从未被使用而报错    
        comment[strlen(comment) - 1] = '\0';//去除fgets函数最后获取的\n字符    
        char* s[ARGc] = { NULL };
        shift_commend(s, comment, strlen(comment));//分割输入的命令行字符串,将每一个选项的地址存入指针数组s
        if (strcmp(s[0], "cd") == 0)//cd指令直接使用chdir函数改变父进程所在路径
        {
            if(s[1]!=NULL)
                chdir(s[1]);
            continue;
        }
        if (strcmp(s[0], "export") == 0)//导入用户自定义的环境变量
        {
            if (s[1] != NULL)
            {
                strcpy(*user_env, s[1]);//将用户输入的环境变量复制到自定义存储空间里
                putenv(user_env[env_num++]);
            }
            continue;
        }
        if (strcmp(s[0], "ls") == 0)//使用户输入的ls指令带有颜色分类
        {
            int n = 0;
            while (*(s + n))
            {
                ++n;
            }
            *(s + n) = (char*)"--color=auto";
        }
        pid_t id = fork();
        if (id == 0)
        {
            execvp(*s, s);
            exit(1);
        }
        int status = 0;
        waitpid(id, &status, 0);
    }
}

运行效果: 

a9bec864d04d4af0b1a6afc7cb181368.png

但是这还不够,我们最终是用子进程调用env来打印环境变量的,但是不排除子进程因为要进行某些操作会修改环境变量,所以最保险的方法还是自己实现一个打印父进程的环境变量的函数:

void show_env()
{
    extern char** environ;//使用environ前先声明  
    int i = 0;
    for (i; environ[i]; ++i)
    {
        printf("environ[%d]:%s\n", i, environ[i]);
    }
}

检测到env指令时调用一下该函数即可:

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

#define MAX 1024
#define ARGc 512

void shift_commend(char** s, char* c, int n)
{
    assert(s);
    assert(c);
    int num = 0;
    *s = c;
    ++s;
    while (num < n)
    {
        if (*(c + num) == ' ')//将每个空格字符替换为'\0',再将选项的首地址存入s
        {
            *(c + num) = '\0';
            if (*(c + num + 1) != ' ')//防止用户输入多个空字符
            {
                *s = c + 1 + num;
                ++s;
            }
        }
        ++num;
    }
}
void show_env()
{
    extern char** environ;//使用environ前先声明  
    int i = 0;
    for (i; environ[i]; ++i)
    {
        printf("environ[%d]:%s\n", i, environ[i]);
    }
}
int main()
{
    char user_env[32][256];//存储自定义环境变量
    int env_num = 0;
    while (1)
    {
        printf("[%s@MyShell]#", getenv("USER"));
        fflush(stdout);
        char comment[MAX] = { 0 };
        char* p = fgets(comment, sizeof(comment), stdin);
        assert(p);//assert函数在release版本下会被编译器删去    
        (void)p;//防止在release版本下assert函数被删除导致p指针从未被使用而报错    
        comment[strlen(comment) - 1] = '\0';//去除fgets函数最后获取的\n字符    
        char* s[ARGc] = { NULL };
        shift_commend(s, comment, strlen(comment));//分割输入的命令行字符串,将每一个选项的地址存入指针数组s
        if (strcmp(s[0], "cd") == 0)//cd指令直接使用chdir函数改变父进程所在路径
        {
            if(s[1]!=NULL)
                chdir(s[1]);
            continue;
        }
        if (strcmp(s[0], "export") == 0)//导入用户自定义的环境变量
        {
            if (s[1] != NULL)
            {
                strcpy(*user_env, s[1]);//将用户输入的环境变量复制到自定义存储空间里
                putenv(user_env[env_num++]);
            }
            continue;
        }
        if (strcmp(s[0], "env") == 0)//直接打印父进程环境变量
        {
            show_env();
            continue;
        }
        if (strcmp(s[0], "ls") == 0)//使用户输入的ls指令带有颜色分类
        {
            int n = 0;
            while (*(s + n))
            {
                ++n;
            }
            *(s + n) = (char*)"--color=auto";
        }
        pid_t id = fork();
        if (id == 0)
        {
            execvp(*s, s);
            exit(1);
        }
        int status = 0;
        waitpid(id, &status, 0);
    }
}

运行效果: 

820121e12ebc4808b9782aa4f688f58f.png

从上面的实现过程我们可以得出一个结论:其实我们之前学习到的几乎所有的环境变量命令,都是内建命令!


这就是本期博客的全部内容,如有纰漏还请各位大佬指点~

 

更多推荐

requests模块高级用法练习

文章目录模拟浏览器指纹发送get请求发送post请求文件上传服务器超时模拟浏览器指纹打开http://10.9.75.164/php/functions/setcookie.php网页,找到请求头的UA字段,这段信息是浏览器的指纹(包括当前系统、浏览器名称和版本):在Python脚本中新建一个headers字段,将该U

【ES6】

ES61ES6简介1.1什么是ES61.2为什么使用ES62ES6的新增语法2.1let2.2const2.3let、const、var的区别2.4解构赋值2.4.1数组解构2.4.2对象解构2.5箭头函数2.6剩余参数3ES6的内置对象扩展3.1Array的扩展方法3.1.1扩展运算符(展开语法)3.1.2构造函数方

华为云云耀云服务器L实例评测|基于华为云云耀云服务器L实例搭建EMQX大规模分布式 MQTT 消息服务器场景体验

文章目录前言一、😄华为云云耀服务器二、😄产品实例创建相关1、🧨开通华为云云耀服务器2、🧨创建华为云云耀服务器实例3、🧨终端登录4、🧨华为云云耀云服务器密码重置三、😄安装开源产品EMQX四、😄开放安全组五、😄访问EMQXDashboard六、😄消息传输建立测试1、🧨客户端连接消息服务器2、🧨客户端

Python数据容器——列表、元组、字符串、集合、字典

作者:Insist--个人主页:insist--个人主页本文专栏:Python专栏专栏介绍:本专栏为免费专栏,并且会持续更新python基础知识,欢迎各位订阅关注。目录一、了解数据容器1.为什么需要数据容器?2.数据容器是什么?二、数据容器—列表(list)1.列表的定义2.列表的特点3.例题三、数据容器—元组(tup

【计算机网络】网络层和数据链路层

文章目录IP协议网段划分分类划分法CIDR方案路由NAT网络地址转换技术IP报文的另外三个参数mac帧ARP协议交换机ICMP代理服务器IP协议TCP有将数据可靠、高效发给对方的策略,而IP具有发送的能力,即将数据从A主机送到B主机的能力。用户要的是100%的概率送到!IP地址=目标网络+目标主机IP协议报头4位首部长

Python灰帽编程——网页信息爬取

文章目录网页信息爬取1.相关模块1.1requests模块1.1.1模块中的请求方法1.1.2请求方法中的参数1.1.3响应对象中属性1.2RE模块1.2.1匹配单个字符1.2.2匹配一组字符1.2.3其他元字符1.2.4核心函数2.网页信息爬取2.1获取网页HTML源代码2.2提取图片地址2.3下载图片2.4完整脚本

【卖出备兑看涨期权策略(Covered_call)】

卖出备兑看涨期权策略(Covered_call)卖出备兑看涨期权策略是一种最基本的收入策略,该策略主要操作就是在持有标的资产的同时卖出对应的看涨期权合约,以此来作为从持有的标的资产中获取租金的一种方法。如果标的资产的价格上涨到行权价之上,那么卖出的看涨期权将被行权,那么投资者可以卖出手中持有的标的资产,投资者获得最初的

数据挖掘18大算法实现以及其他相关经典DM算法:决策分类,聚类,链接挖掘,关联挖掘,模式挖掘、图算法,搜索算法等

【机器学习入门与实践】入门必看系列,含数据挖掘项目实战:模型融合、特征优化、特征降维、探索性分析等,实战带你掌握机器学习数据挖掘专栏详细介绍:【机器学习入门与实践】合集入门必看系列,含数据挖掘项目实战:数据融合、特征优化、特征降维、探索性分析等,实战带你掌握机器学习数据挖掘。本专栏主要方便入门同学快速掌握相关知识。声明

React 全栈体系(十一)

第五章React路由五、向路由组件传递参数数据1.效果2.代码-传递params参数2.1Message/*src/pages/Home/Message/index.jsx*/importReact,{Component}from"react";import{Link,Route}from'react-router-d

向量数据库,能让AI再次起飞吗?

9月7-8日,深圳国际会展中心18号馆来了,来了,腾讯面向产业互联网领域规格最高、规模最大、覆盖最广的年度科技盛会-——-腾讯全球数字生态大会。9月7日,我们将聚焦产业未来发展新趋势,针对云计算、大数据、人工智能、安全、SaaS等核心数字化工具做关键进展发布,并联合生态伙伴推出最新行业场景解决方案。同时,携手全球权威商

mysql:列类型之float、double

mysql:列类型之float、double_mysqldouble_普通网友的博客-CSDN博客环境:window10vs2022.net6mysql8.0.25DBeaver参考:《MSDN:浮点数值类型(C#引用)》《mysql:11.1.4Floating-PointTypes(ApproximateValue

热文推荐