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

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

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

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


   本篇博客全站热榜排名:未上榜 


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

在开始之前,我们先补充一个 使用小技巧: :%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,要注意的是,需要一个独立的空间,放置环境变量的数据被改写。

📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   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

相关文章
|
6天前
|
机器学习/深度学习 缓存 监控
linux查看CPU、内存、网络、磁盘IO命令
`Linux`系统中,使用`top`命令查看CPU状态,要查看CPU详细信息,可利用`cat /proc/cpuinfo`相关命令。`free`命令用于查看内存使用情况。网络相关命令包括`ifconfig`(查看网卡状态)、`ifdown/ifup`(禁用/启用网卡)、`netstat`(列出网络连接,如`-tuln`组合)以及`nslookup`、`ping`、`telnet`、`traceroute`等。磁盘IO方面,`iostat`(如`-k -p ALL`)显示磁盘IO统计,`iotop`(如`-o -d 1`)则用于查看磁盘IO瓶颈。
|
3天前
|
监控 Linux Windows
50个必知的Linux命令技巧,你都掌握了吗?(下)
50个必知的Linux命令技巧,你都掌握了吗?(下)
|
3天前
|
Linux Shell Windows
Linux 常用基本命令
Linux 常用基本命令
|
4天前
|
Ubuntu Linux Shell
linux免交互登陆远程主机并执行命令(密钥对和Expect)
linux免交互登陆远程主机并执行命令(密钥对和Expect)
|
4天前
|
Linux
【Linux】常用命令
【Linux】常用命令
24 0
|
4天前
|
安全 Ubuntu Linux
Linux 网络操作命令Telnet
Linux 网络操作命令Telnet
18 0
Linux 网络操作命令Telnet
|
5天前
|
Linux 数据安全/隐私保护
Linux常用命令实例带注释
Linux常用命令实例带注释
30 0
|
5天前
|
Linux 开发工具 数据安全/隐私保护
Linux(19)常用解压命令记录
Linux(19)常用解压命令记录
9 0
|
6天前
|
Linux Perl
Linux系统替换字符串常用命令
请注意,`sed`命令可以非常强大,可以根据不同的需求使用不同的选项和正则表达式来进行更复杂的字符串替换操作。
18 0
|
7天前
|
存储 Shell 数据安全/隐私保护
ZooKeeper【基础知识 04】控制权限ACL(原生的 Shell 命令)
【4月更文挑战第11天】ZooKeeper【基础知识 04】控制权限ACL(原生的 Shell 命令)
25 7