草庐IT

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

柠檬叶子C 2023-04-14 原文

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

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

📜 本章目录:

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

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

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

0x03 创建进程 & 程序替换

0x04 给命令带颜色

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

0x06 再次理解环境变量

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


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

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

  1. ruby-on-rails - Ruby on Rails with Haml - 如何从 erb 切换 - 2

    我正在从erb文件切换到HAML。我将hamlgem添加到我的系统中。我创建了app/views/layouts/application.html.haml文件。我应该只删除application.html.erb文件吗?此外,仍然有/public/index.html文件被呈现为默认页面。我想创建自己的默认index.html.haml页面。我应该把它放在哪里以及如何使系统呈现该文件而不是默认索引文件?谢谢! 最佳答案 是的,您可以删除任何已转换为HAML的View的ERB版本。至于你的另一个问题,删除public/index/h

  2. ruby-on-rails - 如何在 ruby​​ 交互式 shell 中有多行? - 2

    这可能是个愚蠢的问题。但是,我是一个新手......你怎么能在交互式ruby​​shell中有多行代码?好像你只能有一条长线。按回车键运行代码。无论如何我可以在不运行代码的情况下跳到下一行吗?再次抱歉,如果这是一个愚蠢的问题。谢谢。 最佳答案 这是一个例子:2.1.2:053>a=1=>12.1.2:054>b=2=>22.1.2:055>a+b=>32.1.2:056>ifa>b#Thecode‘if..."startsthedefinitionoftheconditionalstatement.2.1.2:057?>puts"f

  3. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  4. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  5. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  6. ruby-on-rails - rbenv:从 RVM 移动到 rbenv 后,在 Jenkins 执行 shell 中找不到命令 - 2

    我从Ubuntu服务器上的RVM转移到rbenv。当我使用RVM时,使用bundle没有问题。转移到rbenv后,我在Jenkins的执行shell中收到“找不到命令”错误。我内爆并删除了RVM,并从~/.bashrc'中删除了所有与RVM相关的行。使用后我仍然收到此错误:rvmimploderm~/.rvm-rfrm~/.rvmrcgeminstallbundlerecho'exportPATH="$HOME/.rbenv/bin:$PATH"'>>~/.bashrcecho'eval"$(rbenvinit-)"'>>~/.bashrc.~/.bashrcrbenvversions

  7. ruby-on-rails - Rails - 使用/自定义 URL : '/dashboard' 指定根路径 - 2

    如何使此根路径转到:“/dashboard”而不仅仅是http://example.com?root:to=>'dashboard#index',:constraints=>lambda{|req|!req.session[:user_id].blank?} 最佳答案 您可以通过以下方式实现:root:to=>redirect('/dashboard')match'/dashboard',:to=>"dashboard#index",:constraints=>lambda{|req|!req.session[:user_id].b

  8. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  9. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  10. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

随机推荐