【Linux】简易Shell的实现

news/2024/7/21 12:46:39 标签: linux, vim, 运维

  🤣 爆笑教程 👉 《看表情包学Linux》👈 猛戳订阅  🔥

💭 写在前面:本章是个 "插叙",前几章我们学了程序替换,现在我们可以尝试动手做一个 "会创建,会终止,会等待,会程序替换" 的简易 shell 了。通过本章的内容,可以进一步巩固进程替换,学习内建命令的概念以实现路径切换,并再次理解环境变量。

💭 本章目录:

0x00 补充:Vim 小技巧之文本替换

0x01 显示提示符和获取用户输入

0x02 将接收到的字符串拆开

0x03 创建进程 & 程序替换

0x04 给命令带颜色

0x05 内建命令:实现路径切换

0x06 再次理解环境变量


0x00 补充:Vim 小技巧之文本替换

 在开始之前,我们先补充一个 \textrm{<a class=vim}" class="mathcode" src="https://latex.codecogs.com/gif.latex?%5Ctextrm%7Bvim%7D" /> 使用小技巧: :%s///g

0x01 显示提示符和获取用户输入

 shell 本质就是个死循环,我们不关心获取这些属性的接口,如果要实现 shell:

  • Step1:显示提示符 →  #
  • Step2:获取用户输入 → fgets
  • Step3:将接收到的字符串拆开  →  把 "ls -a -l" 转换成  "ls"  "-a"  "-l" 
  • ……

 我们先从简单的入手,先来实现前两步,显示提示符 获取用户输入

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

#define NUM 1024

char command_line[NUM];   // 用来接收命令行内容

int main(void)
{
    while (1) {
        /* Step1:显示提示符 */
        printf("[柠檬叶子@我的主机名 当前目录] # ");
        fflush(stdout);

        /* Step2:获取用户输入 */
        memset (
            command_line, 
            '\0', 
            sizeof(command_line) * sizeof(char)
        );
        fgets(command_line, NUM, stdin);  /* 从键盘获取,标准输入,stdin 
            获取到 C 风格的字符串,默认添加 '\0' */
        printf("%s\n", command_line);
    }
}

💡 说明:我们利用 fgets 函数从键盘上获取,标准输入 stdin,获取到 C 风格的字符串,

注意默认会添加 \0 ,我们先把获取到的结果 command_line 打印出来看看:

 因为 command_line 里有一个 \n,我们把它替换成 \0 即可:

 command_line[strlen(command_line) - 1] = '\0';  // 消除 '\0'

🚩 运行结果如下:

至此,我们已经完成了提示用户输入,并且也获取到用户的输入了。

0x02 将接收到的字符串拆开

下面我们需要 将接收到的字符串拆开,比如:把 "ls -a -l" 拆成  "ls"  "-a"  "-l" 

 因为 exec 函数簇无论是列表传参还是数组传参,一定是要逐个传递的!

"所以我们不得不拆,我的四十米长刀早已饥渴难耐!"

 我们可以使用 strtok 函数,将一个字符串按照特定的分隔符打散,将子串依次返回:

char* strtok(char* str, const char* delim);

💬 代码演示:

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

#define NUM 1024
#define SEP " "

char command_line[NUM];     // 存储命令行内容
char* command_args[SIZE];   // 命令参数

int main(void)
{
    while (1) {
        /* Step1:显示提示符 */
        printf("[柠檬叶子@我的主机名 当前目录] # ");
        fflush(stdout);

        /* Step2:获取用户输入 */
        memset (
            command_line, 
            '\0', 
            sizeof(command_line) * sizeof(char)
        );
        fgets(command_line, NUM, stdin);  /* 从键盘获取,标准输入,stdin 
            获取到 C 风格的字符串,默认添加 '\0' */
        command_line[strlen(command_line) - 1] = '\0';  // 消除 '\0'

        /* Step3: 将接收到的字符串拆开 - 字符串切分 */
        command_args[0] = strtok(command_line, SEP);    // 按空格切分
        int idx = 1;
        /* 这里的 = 是故意这么写的,因为 strtok 截取成功返回字符串起始地址
            截取失败,返回 NULL */
        while (command_args[idx++] = strtok(NULL, SEP));

        // 我们来测试一下看看 
        for (int i = 0; i < idx; i++) {
            printf("%d : %s\n", command_args[i]);
        }

        printf("%s\n", command_line);
    }
}

 🚩 运行结果如下:

字符串切分搞定了!

0x03 创建进程 & 程序替换

下面我们实现 创建进程,执行它。

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

#define NUM 1024
#define SEP " "
#define SIZE 128

char command_line[NUM];
char* command_args[SIZE];

int main(void)
{
    while (1) {
        /* Step1:显示提示符 */
        printf("[柠檬叶子@我的主机名 当前目录] # ");
        fflush(stdout);

        /* Step2:获取用户输入 */
        memset (
            command_line, 
            '\0', 
            sizeof(command_line) * sizeof(char)
        );
        fgets(command_line, NUM, stdin);  /* 从键盘获取,标准输入,stdin 
            获取到 C 风格的字符串,默认添加 '\0' */
        command_line[strlen(command_line) - 1] = '\0';  // 消除 '\0'

        /* Step3: 将接收到的字符串拆开 - 字符串切分 */
        command_args[0] = strtok(command_line, SEP);
        int idx = 1;
        
        /* 这里的 = 是故意这么写的,因为 strtok 截取成功返回字符串起始地址
            截取失败,返回 NULL */
        while (command_args[idx++] = strtok(NULL, SEP));

        //我们来测试一下看看 
        // for (int i = 0; i < idx; i++) {
        //     printf("%d : %s\n", i, command_args[i]);
        // }

        // printf("%s\n", command_line);

        /* Step4. TODO */
        /* Step5. 创建进程,执行 */
        pid_t id = fork();
        if (id == 0) {
            /* child */
            /* Step6: 程序替换 */
            execvp (
                command_args[0],  // 保存的是我们要执行的程序名字
                command_args
            );

            exit(1);   // 只要执行到这里,子进程一定是替换失败了,直接退出。
        }

        /* Father */
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if (ret > 0) {   // 等待成功
            printf("等待成功!sig: %d, code: %d\n", status&0x7F, (status>>8)&0xFF);
        }
    } // end while

}

🚩 运行结果如下:

0x04 给命令带颜色

还有很多地方不完美,比如:如何让我们的命令带颜色呢?

💬 代码演示:给 ls 命令添加颜色

        /* Step3: 将接收到的字符串拆开 - 字符串切分 */
        command_args[0] = strtok(command_line, SEP);
        int idx = 1;

        // 颜色的添加 -> 提出程序名,如果名师输入 ls,在 command 里添加 --color
        if (strcmp(command_args[0] /* 程序名 */, "ls") == 0) {
            command_args[idx++] = (char*)"--color=auto";
        }

🚩 运行结果如下:

0x05 内建命令:实现路径切换

目前还有一个问题,我们 cd.. 回退到上级目录时,我们的路径是不发生变化的:

真相:虽然系统中存在 cd 命令,但我们写的 shell 脚本中用的根本就不是这个 cd 命令。

当你在执行 cd 命令时,调用 execvp 执行的实际上是系统特定路径下的 cd:

        if (id == 0) {
            /* child */
            /* Step6: 程序替换 */
            execvp (
                command_args[0],  // 保存的是我们要执行的程序名字
                command_args
            );

            exit(1);   // 只要执行到这里,子进程一定是替换失败了,直接退出。
        }

它只影响了子进程,如果我们直接 exec* 执行 cd,那么最多只是让子进程进行路径切换。

但是请不要忘了:子进程是一运行就完毕的进程!运行完了你切换它的路径,毫无意义。

所以,我们在 shell 中,更希望谁的路径发生变化呢?父进程!(shell 本身)

父进程对应的路径发生变化,这一块稍微有一点绕:

只要让我执行 cd,按照之前的代码就是进程替换,和父进程有什么关系,子进程一跑就完了,曾经的复出没有任何意义了实际上是想让父进程的路径发生变化。那么在我们现有的代码中能做到让父进程的路径发生变化吗?不可能因为我们现有的代码在进行操作的时候最终的结果都会落实到 fork,然后 exec。这也就意味着,不管是什么命令,最后你都是创建子进程,cd 命令也不除外。

所以,对我们来说我们此时就有一个需求了:如果有些行为是必须让父进程 shell 执行的,不想让子进程执行,这样的场景下,绝对不能创建子进程!进位一旦创建了子进程最后执行任务的是子进程,和你就没有任何干系了,只能是父进程自实现对应的代码。

这部分由 shell 自己执行的命令,我们称之为 内建指令 (build-in) 。

 下面我们就来解决路径切换的问题:

/* Shell 内置函数: 路径跳转 */
int ChangeDir(const char* new_path) {
    chdir(new_path);

    return 0;  // 调用成功
}

int main(void) 
{
    ...
        /* Step4. TODO 编写后面的逻辑,内建命令 */
        if (strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL) {
            ChangeDir(command_args[1]);  // 让调用方进行路径切换
            continue;
        }
    ...
}

🚩 运行结果如下:

 💡 说明:在上层你看到的是个命令,但是在 shell 内部本质上是由父 shell 自己实现、调用的一个函数(并没有创建子进程),这种就是对应上上层的 内建命令。

内建命令表现是用用户层面的一条命令,本质就是 Shell 内部的一个函数,由父 Shell 自己执行,而不创建子进程。

0x06 再次理解环境变量

我们上一章学过的 exec 的函数,是可以直接执行这指定的命令、环境变量的。

 获取环境变量,直接遍历环境变量列表就行:

// 方便测试,我们创建一个 hello.c 文件

#include <stdio.h>

int main(void)
{
    /* 获取环境变量列表 */
    extern char** environ;
    for (int i = 0; environ[i] != NULL; i++) {
        printf("[%d]: %s\n", i, environ[i]);
    }

    return 0;
}

环境变量具有全局属性,我们可以在程序中添加环境变量的声明:

extern char** environ;   // 环境变量指针声明

            /* Step6: 程序替换 */
            execvp (
                command_args[0],  // 保存的是我们要执行的程序名字
                command_args,
                environ   // 添加环境变量
            );

程序替换中,对于 exec 函数簇,如果如果函数名没 e,所有的环境变量是会被继承的。

不带 e,环境变量依旧是可以被继承的,如果我们自己定一个环境变量的指针数组,

它会覆盖我们的环境变量列表,我现在不想覆盖,我想新增:

/* 放置环境变量 */
void PutEnvMyShell(const char* new_env) {
    putenv(new_env);
}

        if (strcmp(command_args[0], "export") == 0 && command_args[1] != NULL) {
            PutEnvMyShell((char*)command_args[1]);   // export myval=100
            continue;
        } 

这是为什么呢?因为当前环境变量信息存储在了 command_line 中,会被清空。

那么环境变量也会随之清空而丢失,所以我么需要一个专门存储环境变量的:

char env_buffer[NUM];  // 保存环境变量  just for test

        if (strcmp(command_args[0], "export") == 0 && command_args[1] != NULL) {
            // 目前,环境变量信息在 command_line,会被清空,环境变量也随之清空
            // 此处我们需要自己保存一下环境变量的内容
            strcpy(env_buffer, command_args[1]);
            PutEnvMyShell(env_buffer);   // export myval=100
            continue;
        }

🚩 运行结果如下: 

📚 环境变量的数据在进程的上下文中:

① 环境变量会被子进程继承下去,所以他会有全局属性。
② 当我们进行程序替换时, 当前进程的环境变量非但不会替换,而且是继承父进程的!

环境你不传,默认子进程全部都会自动继承。

如果你 exel 函数簇带 e,就相当于你选择了自己传,就会覆盖式地把原本的环境变量弄没,然后你自己交给子进程。如果不带 e,那么环境变量就会自己被子进程继承。

如果既不想覆盖系统,也不想新增,所以我们采用 putEnv 的方式向父 Shell 导入新增一个它自己的环境变量,这样的话原始的环境变量还在,我们能在 shell 上下文上给它新增环境变量。

所以,如何理解环境变量具有全局属性?

因为所有的环境变量会被当前进程之下的所有子进程默认继承下去。

如何在 Shell 内部自己导入新增自己的环境变量?

putEnv,要注意的是,需要一个独立的空间,放置环境变量的数据被改写。

📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   2022.3.20
❌ [ 勘误 ]   /* 暂无 */
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料 

C++reference[EB/OL]. []. http://www.cplusplus.com/reference/.

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. Linux[EB/OL]. 2021[2021.8.31 xi


http://www.niftyadmin.cn/n/175285.html

相关文章

关于加解密、加签验签的那些事 | 得物技术

1前言 面对MD5、SHA、DES、AES、RSA等等这些名词你是否有很多问号&#xff1f;这些名词都是什么&#xff1f;还有什么公钥加密、私钥解密、私钥加签、公钥验签。这些都什么鬼&#xff1f;或许在你日常工作没有听说过这些名词&#xff0c;但是一旦你要设计一个对外访问的接口&a…

Linux Systemctl命令

1 Systemctl简介 Systemctl是一个系统管理守护进程、工具和库的集合&#xff0c;用于取代System V、service和chkconfig命令&#xff0c;初始进程主要负责控制systemd系统和服务管理器。通过Systemctl –help可以看到该命令主要分为&#xff1a;查询或发送控制命令给systemd服…

Spring入门篇3 --- 依赖注入(DI)方式、集合注入

目录1.依赖注入方式依赖注入(DI&#xff1a;dependence injection)&#xff1a;在容器中建立bean与bean之间的依赖关系的整个过程。向一个bean中传递数据有三种方法&#xff1a;setter注入、构造器注入、自动装配/src/main/java/com/itheima/dao/BookDao.javapackage com.ithei…

HBuilderX使用uniapp中的video标签开发视频应用APP,出现视频覆盖<view>图层无法遮住等问题如何解决?

1、普通情况&#xff1a;图片要遮住<video></video> 请使用cover-image或cover-view cover-image | uni-app官网https://uniapp.dcloud.net.cn/component/cover-image.html#cover-image cover-view | uni-app官网https://uniapp.dcloud.net.cn/component/cover-v…

基于Springboot框架实现垃圾分类网站【源码+论文】

基于Springboot框架实现垃圾分类网站【源码论文】开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Mav…

【技术分享】华为防火墙配置FTP服务器的负载均衡

组网需求 如图1所示&#xff0c;企业有三台FTP服务器Server1、Server2和Server3&#xff0c;且这三台服务器的硬件性能顺次降低&#xff0c;Server1性能是Server2的两倍、Server2性能是Server3的两倍。通过配置负载均衡&#xff0c;让这三台服务器联合对外提供FTP服务&#xff…

vue3 组件篇 Message

文章目录组件介绍组件使用组件代码参数说明关于vue3-dxui组件库组件介绍 Message组件用以消息提示&#xff0c;用户在前端完成某些交互时&#xff0c;在页面弹出的某种反馈。该组件一共有四种类型&#xff0c;info、success、warning、error。与一般的组件不同&#xff0c;该组…

记录--我在前端干工地(three.js)

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 前段时间接触了Three.js后&#xff0c;试着用他加载了一些模型three.js初体验简陋的了解了一下three.js的相关使用&#xff0c;并且写下了第一篇文章。但是过后还是对很多一知半解&#xff0c;作为不会…