Linux自行实现的一个Shell(15)

文章目录


前言

MyShell源代码公开

本篇是对之前知识的一个综合运用,也是检验你是否对前置知识有个较为透彻的理解的好时机


一、头文件和全局变量

头文件

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

全局变量

cpp 复制代码
const int basesize = 1024;
const int argvnum = 64;
const int envnum = 64;

// 全局的命令行参数表
char *gargv[argvnum];
int gargc = 0;

// 全局的变量
int lastcode = 0;

// 我的系统的环境变量
char *genv[envnum];

// 全局的当前shell工作路径 
char pwd[basesize];
char pwdenv[basesize];
  • basesize:缓冲区基本大小
  • argvnum 和 envnum:参数和环境变量的最大数量
  • gargv 和 gargc:存储解析后的命令参数
  • lastcode:存储上一条命令的退出状态码
  • genv:存储环境变量
  • pwd 和 pwdenv:存储当前工作目录

二、辅助函数

获取用户名

cpp 复制代码
string GetUserName()
{
    string name = getenv("USER");
    return name.empty() ? "None" : name;
}
  • 通过 getenv("USER") 获取当前用户名
  • 如果获取失败返回 "None"

获取主机名

cpp 复制代码
string GetHostName()
{
    string hostname = getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}
  • 通过 getenv("HOSTNAME") 获取主机名
  • 如果获取失败返回 "None"

获取当前工作目录

cpp 复制代码
string GetPwd()
{
    if(nullptr == getcwd(pwd, sizeof(pwd))) return "None";
    snprintf(pwdenv, sizeof(pwdenv),"PWD=%s", pwd);
    putenv(pwdenv); // PWD=XXX
    return pwd;
}
  • 使用 getcwd() 获取当前工作目录
  • 如果失败返回 "None"
  • 将当前目录设置到环境变量 PWD 中
  • 返回当前目录路径

获取最后一级目录名

cpp 复制代码
string LastDir()
{
    string curr = GetPwd();
    if(curr == "/" || curr == "None") return curr;
   
    size_t pos = curr.rfind("/");
    if(pos == std::string::npos) return curr;
    return curr.substr(pos+1);
}
  • 获取当前目录
  • 如果是根目录或无效目录直接返回
  • 查找最后一个 '/' 的位置
  • 返回最后一个 '/' 之后的部分

生成命令行提示符

cpp 复制代码
string MakeCommandLine()
{
    char command_line[basesize];
    snprintf(command_line, basesize, "[%s@%s %s]# ",\
            GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());
    return command_line;
}
  • 生成类似 [user@host dirname]# 的提示符

打印命令行提示符

cpp 复制代码
void PrintCommandLine() // 1. 命令行提示符
{
    printf("%s", MakeCommandLine().c_str());
    fflush(stdout);
}
  • 打印提示符
  • fflush(stdout) 确保立即显示

三、命令处理

获取用户输入

cpp 复制代码
bool GetCommandLine(char command_buffer[], int size)
{
    char *result = fgets(command_buffer, size, stdin);
    if(!result)
    {
        return false;
    }
    
    // 因为 command_line 里有一个 \n,我们把它替换成 \0 即可
    command_buffer[strlen(command_buffer)-1] = '\0';
    if(strlen(command_buffer) == 0) return false;
    
    return true;
}
  • 使用 fgets 读取用户输入
  • 移除末尾的换行符
  • 检查是否为空输入

解析命令行

获取用户输入后,我们需要将接收到的字符串拆分为命令及其参数

将接收到的字符串拆开

通过 strtok 函数,我们可以将一个字符串按照特定的分隔符打散,依次返回子串

cpp 复制代码
void ParseCommandLine(char command_buffer[], int len)
{
    (void)len;
    memset(gargv, 0, sizeof(gargv));
    
    gargc = 0;
    const char *sep = " ";
    
    gargv[gargc++] = strtok(command_buffer, sep);
    while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
    gargc--;
}
  • 重置参数数组和计数器
  • 使用 strtok 以空格为分隔符分割命令
  • 将分割后的参数存入 gargv 数组
  • 调整 gargc 为实际参数数量

执行外部命令

cpp 复制代码
bool ExecuteCommand()
{
    pid_t id = fork();
    
    if(id < 0) return false;
    if(id == 0)
    {
        execvpe(gargv[0], gargv, genv);
        exit(1);
    }

    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    
    if(rid > 0)
    {
        if(WIFEXITED(status))
        {
            lastcode = WEXITSTATUS(status);
        }
        else
        {
            lastcode = 100;
        }
        return true;
    }
    return false;
}
  • 创建子进程
  • 子进程使用 execvpe 执行命令
  • 父进程等待子进程结束
  • 保存子进程退出状态到 lastcode

四、内建命令

内建命令是指直接内置在操作系统内核中的一些命令,与普通的外部命令(外部程序文件)不同。这些内建命令是直接由shell解释器(如Bash、Zsh等)所处理,而不需要通过外部文件的方式来执行。这些内建命令通常在操作系统的shell环境中被频繁使用,并且执行速度更快,因为它们不需要创建新的进程来执行

在Unix和类Unix操作系统中,通常会有一些内建命令,比如cd、echo、exit等。这些命令不需要单独的可执行文件,而是直接由shell内核提供支持。当用户在shell中输入这些命令时,shell会直接处理它们,而不需要通过搜索系统路径来找到可执行文件

值得一提的是,某些shell也允许用户通过自定义的方式添加新的内建命令,这样用户可以根据自己的需求来扩展shell的内建功能

添加环境变量

cpp 复制代码
void AddEnv(const char *item)
{
    int index = 0;
    while(genv[index])
    {
        index++;
    }

    genv[index] = (char*)malloc(strlen(item)+1);
    strncpy(genv[index], item, strlen(item)+1);
    genv[++index] = nullptr;
}
  • 找到环境变量数组的末尾
  • 分配内存并复制新环境变量
  • 确保数组以 NULL 结尾

检查和执行内建命令

cpp 复制代码
bool CheckAndExecBuiltCommand()
{
    if(strcmp(gargv[0], "cd") == 0)
    {
        if(gargc == 2)
        {
            chdir(gargv[1]);
            lastcode = 0;
        }
        else
        {
            lastcode = 1;
        }
        return true;
    }
    else if(strcmp(gargv[0], "export") == 0)
    {
        if(gargc == 2)
        {
            AddEnv(gargv[1]);
            lastcode = 0;
        }
        else
        {
            lastcode = 2;
        }
        return true;
    }
    else if(strcmp(gargv[0], "env") == 0)
    {
        for(int i = 0; genv[i]; i++)
        {
            printf("%s\n", genv[i]);
        }
        lastcode = 0;
        return true;
    }
    else if(strcmp(gargv[0], "echo") == 0)
    {
        if(gargc == 2)
        {
            if(gargv[1][0] == '$')
            {
                if(gargv[1][1] == '?')
                {
                    printf("%d\n", lastcode);
                    lastcode = 0;
                }
            }
            else
            {
                printf("%s\n", gargv[1]);
                lastcode = 0;
            }
        }
        else
        {
            lastcode = 3;
        }
        return true;
    }
    return false;
}

支持的内建命令有:

  1. cd:改变工作目录
  2. export:设置环境变量
  3. env:显示所有环境变量
  4. echo:打印内容或上一条命令的退出码

五、初始化

初始化环境变量

cpp 复制代码
void InitEnv()
{
    extern char **environ;
    int index = 0;
    while(environ[index])
    {
        genv[index] = (char*)malloc(strlen(environ[index])+1);
        strncpy(genv[index], environ[index], strlen(environ[index])+1);
        index++;
    }
    genv[index] = nullptr;
}

从父进程复制环境变量

主循环

cpp 复制代码
int main()
{
    InitEnv();
    
    char command_buffer[basesize];
    
    while(true)
    {
        PrintCommandLine();
        if( !GetCommandLine(command_buffer, basesize) )
        {
            continue;
        }

        ParseCommandLine(command_buffer, strlen(command_buffer));

        if ( CheckAndExecBuiltCommand() )
        {
            continue;
        }

        ExecuteCommand();
    }
    return 0;
}

主循环流程:

  1. 打印提示符
  2. 获取用户输入
  3. 解析命令
  4. 尝试执行内建命令
  5. 如果不是内建命令,则执行外部命令

总结

感觉如何呢!

相关推荐
子玖1 小时前
go实现通过ip解析城市
后端·go
Java不加班1 小时前
Java 后端定时任务实现方案与工程化指南
后端
心在飞扬2 小时前
RAG 进阶检索学习笔记
后端
Moment2 小时前
想要长期陪伴你的助理?先从部署一个 OpenClaw 开始 😍😍😍
前端·后端·github
Das1_2 小时前
【Golang 数据结构】Slice 底层机制
后端·go
得物技术2 小时前
深入剖析Spark UI界面:参数与界面详解|得物技术
大数据·后端·spark
古时的风筝2 小时前
花10 分钟时间,把终端改造成“生产力武器”:Ghostty + Yazi + Lazygit 配置全流程
前端·后端·程序员
Cache技术分享2 小时前
340. Java Stream API - 理解并行流的额外开销
前端·后端
初次攀爬者2 小时前
RocketMQ 消息可靠性保障与堆积处理
后端·消息队列·rocketmq