《Linux从练气到飞升》No.21 Linux简单实现一个shell

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 《Linux从练气到飞升》No.21 Linux简单实现一个shell

前言

前面我们讲述了进程的相关知识,包括进程创建、进程等待、进程替换等,这些我们都在Linux上进行了测试,并且通常使用的shell来执行命令,那么我们能不能自己来实现一个简单的shell呢?

我们知道在shell上执行命令时,其原理不过也只是调用和执行文件罢了,也就是创建进程来执行程序,而shell一般是不退出的,那么我们现在开始玩一下

01. 框架搭建

命令行解释器一定是一个常驻内存的进程,不退出,所以我们使用while包起来

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串
char *g_argv[SIZE];
// 写一个环境变量的buffer,用来测试
char g_myval[64];
// shell 运行原理 : 通过让子进程执行命令,父进程等待&&解析命令
int main()
{
    extern char**environ;
    //0. 命令行解释器,一定是一个常驻内存的进程,不退出
    while(1)
    {
    }
}

02. 打印提示信息

参考shell的提示信息:

它们都有各自的含义和获取方式,但是这里为了简化,不考虑这些细枝末节,由大家自己改进!

我这里就写死了哦~

我们可以直接使用printf函数打印,但是会有一个问题,如果设置了\n,它会换行,但是我们使用shell时并不会换行,那么我们就需要用到fflush函数来冲刷缓存区。

//1. 打印出提示信息 [venus@localhost myshell]# 
        printf("[root@localhost myshell]# ");
        fflush(stdout);

03. 获取用户键盘输入

如何获取用户在命令行的输入呢?

我们用一个数组来存储命令,使用fgets函数来获取输入

步骤:

  • 先初始化数组
  • 获取存储命令
  • 将最后的回车符号设置为'\0'
//2. 获取用户的键盘输入[输入的是各种指令和选项: "ls -a -l -i"]
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
            continue;
        }
        cmd_line[strlen(cmd_line)-1] = '\0';

04. 命令行字符串解析

到这一步,我们已经将命令行的字符串存储到数组中了,接下来就是解析它

步骤:

  • 这里要使用strtok函数来裁剪字符串
  • 将存储的命令和系统内部命令做比对,如果有就执行
g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串
int index = 1;
if(strcmp(g_argv[0], "ls") == 0)
{
    g_argv[index++] = "--color=auto";//加入配色
}
while(g_argv[index++] = strtok(NULL, SEP)); //第二次,如果还要解析原始字符串,传入NULL

05. 创建子进程执行命令

怎么知道要调用的程序在哪里呢?

直接使用进程替换,使用execvp函数,它可以直接使用环境变量不用自己写了,也就是直接掉用系统中的指令的程序来使用即可。

为什么要替换?

一切和应用场景有关,我们有时候必须要让子进程执行新的程序

环境变量相关的数据,会被替换吗??

没有!它不会被替换,它会把父进程的环境变量拷贝继承过来,它具有全局属性

pid_t id = fork();
if(id == 0) //child
{
    printf("下面功能让子进程进行的\n");
    execvp(g_argv[0], g_argv); // ls -a -l -i
    exit(1);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));

此时程序基本功能就已经实现了

但是,我们发现一个问题,使用cd命令时,他的路径不会改变,这是个bug

原因是:

  • 在cd的时候,自己写的shell都会执行execvp,它只会影响子进程的路径
  • 但是我们需要改变父进程的路径,所以像cd这种命令,我们不想让子进程去执行它而让父进程去执行它
  • 这种让父进程自己执行的命令叫做内置命令、内建命令,它的本质是shell中的一个函数调用

我们来修改下功能

06. 内置命令 —— cd

这里可以使用chdir函数来实现

chdir函数可以改变文件路径

我们可以使用下面代码,使得cd命令的使用合理,但是可能其他类似的命令也会出现相似的bug,需要一一比对实现,这里仅针对cd命令

if(strcmp(g_argv[0], "cd") == 0) //not child execute, father execute
{
    if(g_argv[1] != NULL) chdir(g_argv[1]); //cd path, cd ..
    continue;
}

07. 内置命令 —— export

上面我们讲了cd的bug,而export也和cd一样,也需要进行处理,export的作用是导入环境变量,我们既不想覆盖父进程的环境变量,又想导入自己的环境变量,该怎么做呢?

代码如下:

//导入环境变量
//比较第一个是不是export
if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
{
    strcpy(g_myval, g_argv[1]);//是的就取出后面的值
    int ret = putenv(g_myval);//将它导入环境变量中
    if(ret == 0) printf("%s export success\n", g_argv[1]);//如果导入成功就打印出来
    continue;
}

shell 执行的命令通常有两种

  1. 第三方提供的对应的在磁盘中具有二进制文件的可执行程序(由子进程执行)
  2. shell内部自己实现的方法,由自己(父进程)来执行,有些命令就是要影响shell本身,如改变路径的(cd、export),shell代表的是用户。

shell的环境变量从哪里来的?(了解)

环境变量是写在配置文件中的,shell启动的时候,通过读取配置文件获得的起始环境变量

08. 类似ll这种别名命令无法识别

ll是ls -l的别名

想要支持就要当识别到ll时执行ls命令即可

if(strcmp(g_argv[0], "ll") == 0)
{
    g_argv[0] = "ls";
    g_argv[index++] = "-l";
    g_argv[index++] = "--color=auto";
}

系统中肯定不是这样实现的,但是大致原理相同

后记

最后我们就实现了一个简易的shell解释器

全部代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串
char *g_argv[SIZE];
// 写一个环境变量的buffer,用来测试
char g_myval[64];
// shell 运行原理 : 通过让子进程执行命令,父进程等待&&解析命令
int main()
{
    extern char**environ;
    //0. 命令行解释器,一定是一个常驻内存的进程,不退出
    while(1)
    {
        //1. 打印出提示信息 [whb@localhost myshell]# 
        printf("[root@localhost myshell]# ");
        fflush(stdout);
        memset(cmd_line, '\0', sizeof cmd_line);
        //2. 获取用户的键盘输入[输入的是各种指令和选项: "ls -a -l -i"]
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
            continue;
        }
        cmd_line[strlen(cmd_line)-1] = '\0';
        //3. 命令行字符串解析:"ls -a -l -i" -> "ls" "-a" "-i"
        // export myval=105
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串
        int index = 1;
        if(strcmp(g_argv[0], "ls") == 0)
        {
            g_argv[index++] = "--color=auto";
        }
        if(strcmp(g_argv[0], "ll") == 0)
        {
            g_argv[0] = "ls";
            g_argv[index++] = "-l";
            g_argv[index++] = "--color=auto";
        }
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次,如果还要解析原始字符串,传入NULL
        if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
        {
            strcpy(g_myval, g_argv[1]);
            int ret = putenv(g_myval);
            if(ret == 0) printf("%s export success\n", g_argv[1]);
            continue;
        }
        //4.内置命令, 让父进程(shell)自己执行的命令,我们叫做内置命令,内建命令
        //内建命令本质其实就是shell中的一个函数调用
        if(strcmp(g_argv[0], "cd") == 0) //not child execute, father execute
        {
            if(g_argv[1] != NULL) chdir(g_argv[1]); //cd path, cd ..
            continue;
        }
        //5. fork()
        pid_t id = fork();
        if(id == 0) //child
        {
            printf("下面功能让子进程进行的\n");
            printf("child, MYVAL: %s\n", getenv("MYVAL"));//测试环境变量
            printf("child, PATH: %s\n", getenv("PATH"));//测试环境变量
            //环境变量相关的数据,会被替换吗??没有!
            execvp(g_argv[0], g_argv); // ls -a -l -i
            exit(1);
        }
        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));
    }
}


相关文章
|
2月前
|
Shell Linux
Linux shell编程学习笔记30:打造彩色的选项菜单
Linux shell编程学习笔记30:打造彩色的选项菜单
|
1月前
|
Web App开发 网络协议 Linux
linux命令总结(centos):shell常用命令汇总,平时用不到,用到就懵逼忘了,于是专门写了这篇论文,【便持续更新】
这篇文章是关于Linux命令的总结,涵盖了从基础操作到网络配置等多个方面的命令及其使用方法。
62 1
linux命令总结(centos):shell常用命令汇总,平时用不到,用到就懵逼忘了,于是专门写了这篇论文,【便持续更新】
|
19天前
|
运维 监控 Shell
深入理解Linux系统下的Shell脚本编程
【10月更文挑战第24天】本文将深入浅出地介绍Linux系统中Shell脚本的基础知识和实用技巧,帮助读者从零开始学习编写Shell脚本。通过本文的学习,你将能够掌握Shell脚本的基本语法、变量使用、流程控制以及函数定义等核心概念,并学会如何将这些知识应用于实际问题解决中。文章还将展示几个实用的Shell脚本例子,以加深对知识点的理解和应用。无论你是运维人员还是软件开发者,这篇文章都将为你提供强大的Linux自动化工具。
|
2月前
|
Shell Linux
Linux shell编程学习笔记82:w命令——一览无余
Linux shell编程学习笔记82:w命令——一览无余
|
2月前
|
人工智能 监控 Shell
常用的 55 个 Linux Shell 脚本(包括基础案例、文件操作、实用工具、图形化、sed、gawk)
这篇文章提供了55个常用的Linux Shell脚本实例,涵盖基础案例、文件操作、实用工具、图形化界面及sed、gawk的使用。
450 2
|
1月前
|
存储 Shell Linux
【Linux】shell基础,shell脚本
Shell脚本是Linux系统管理和自动化任务的重要工具,掌握其基础及进阶用法能显著提升工作效率。从简单的命令序列到复杂的逻辑控制和功能封装,Shell脚本展现了强大的灵活性和实用性。不断实践和探索,将使您更加熟练地运用Shell脚本解决各种实际问题
25 0
|
2月前
|
Shell Linux 开发工具
linux shell 脚本调试技巧
【9月更文挑战第3天】在Linux中调试shell脚本可采用多种技巧:使用`-x`选项显示每行命令及变量扩展情况;通过`read`或`trap`设置断点;利用`echo`检查变量值,`set`显示所有变量;检查退出状态码 `$?` 进行错误处理;使用`bashdb`等调试工具实现更复杂调试功能。
|
3月前
|
JavaScript 关系型数据库 Shell
Linux shell编写技巧之随机取字符串(一)
本文介绍了Linux Shell脚本的编写技巧,包括环境配置、变量命名规则和缩进语法,并提供了一个实例练习,展示如何使用`$RANDOM`变量和`md5sum`命令来生成随机的8位字符串。
54 4
|
3月前
|
Ubuntu Linux Shell
在Linux中,如何使用shell脚本判断某个服务是否正在运行?
在Linux中,如何使用shell脚本判断某个服务是否正在运行?
|
3月前
|
Java Shell Linux
【Linux入门技巧】新员工必看:用Shell脚本轻松解析应用服务日志
关于如何使用Shell脚本来解析Linux系统中的应用服务日志,提供了脚本实现的详细步骤和技巧,以及一些Shell编程的技能扩展。
55 0
【Linux入门技巧】新员工必看:用Shell脚本轻松解析应用服务日志