【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_#include


【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_环境变量_02

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

📜 本章目录:

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

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

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

0x03 创建进程 & 程序替换

0x04 给命令带颜色

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

0x06 再次理解环境变量


【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_linux_03

未上榜 


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

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_运维_04

 在开始之前,我们先补充一个 

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_#include_05

 使用小技巧: :%s///g

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_#include_06

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

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_运维_07

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

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

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_环境变量_08

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

#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

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_linux_09

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_运维_10

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

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

🚩 运行结果如下:

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_linux_11

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

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

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

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_vim_12

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

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

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_vim_13

 我们可以使用 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);
    }
}

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_vim_14

 🚩 运行结果如下:

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_vim_15

字符串切分搞定了!

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

}

🚩 运行结果如下:

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_运维_16

0x04 给命令带颜色

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_运维_17

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

💬 代码演示:给 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";
        }

🚩 运行结果如下:

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_linux_18

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

目前还有一个问题,我们 cd..

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_linux_19

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

当你在执行 cd 命令时,调用 execvp

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) 。

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_vim_20

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

/* 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;
        }
    ...
}

🚩 运行结果如下:

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_vim_21

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

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

0x06 再次理解环境变量

我们上一章学过的 exec

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_运维_22

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

// 方便测试,我们创建一个 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;
        }

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_#include_23

这是为什么呢?因为当前环境变量信息存储在了 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;
        }

🚩 运行结果如下: 

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_运维_24

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

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

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

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

如果既不想覆盖系统,也不想新增,所以我们采用 putEnv

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

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

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

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

【看表情包学Linux】插叙:实现简易的 Shell | 通过内建命令实现路径切换 | 再次理解环境变量_vim_25

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

📜 参考资料 

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