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


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

目录


📕前言

本篇文章主要讲解一个简单版的shell的实现,看完这篇文章,你可以对shell的运行原理,Linux进程相关知识等有一个更深入的认识和理解,适合刚入门Linux的初学者学习。

自定义shell根据下面所述的原理一共分为四个部分实现。(分别为就命令行的输入解析执行内建指令的执行)还包括了对一些细节的处理...

🍑shell

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

📚Shell的工作原理

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

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

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

🔒Shell的高级功能

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

🚀shell的代码实现

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

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

根据前面的文章的学习,我们可以利用环境变量来获取这三个信息。

自己可以利用env指令查看,这里就不赘述了。

用户名:USER

主机名:HOSTNAME

当前工作目录:PWD

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

cpp 复制代码
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获取,所以要考虑是否需要处理;

代码实现

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

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

cpp 复制代码
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;

字符串分割的代码实现

cpp 复制代码
//将指令字符串切割后放到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接口,不会的看可以去上一篇文章看看。

cpp 复制代码
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中,用法如下例子。

cpp 复制代码
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指令

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

cpp 复制代码
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 (换行)

代码实现:

cpp 复制代码
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函数的代码实现

cpp 复制代码
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;
}

⭐总代码实现

cpp 复制代码
#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;
}
cpp 复制代码
#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;
}
相关推荐
阿伟来咯~31 分钟前
记录学习react的一些内容
javascript·学习·react.js
Suckerbin1 小时前
Hms?: 1渗透测试
学习·安全·网络安全
水豚AI课代表1 小时前
分析报告、调研报告、工作方案等的提示词
大数据·人工智能·学习·chatgpt·aigc
聪明的墨菲特i1 小时前
Python爬虫学习
爬虫·python·学习
hjjdebug1 小时前
linux 下 signal() 函数的用法,信号类型在哪里定义的?
linux·signal
其乐无涯1 小时前
服务器技术(一)--Linux基础入门
linux·运维·服务器
Diamond技术流1 小时前
从0开始学习Linux——网络配置
linux·运维·网络·学习·安全·centos
写bug的小屁孩1 小时前
前后端交互接口(三)
运维·服务器·数据库·windows·用户界面·qt6.3
斑布斑布1 小时前
【linux学习2】linux基本命令行操作总结
linux·运维·服务器·学习
紅色彼岸花1 小时前
第六章:DNS域名解析服务器
运维·服务器