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. 如果不是内建命令,则执行外部命令

总结

感觉如何呢!

相关推荐
自学AI的鲨鱼儿11 分钟前
Ubuntu / WSL 安装pipx
linux·运维·ubuntu
企鹅侠客16 分钟前
centos停服 迁移centos7.3系统到新搭建的openEuler
linux·运维·centos·openeuler·迁移
Fanche4042 小时前
MySQL 8 自动安装脚本(CentOS-7 系统)
linux·运维·数据库·mysql·centos
会讲英语的码农3 小时前
php基础
开发语言·后端·php
W_kiven3 小时前
Centos安装Dockers+Postgresql13+Postgis3.1
linux·运维·docker·postgresql·centos
liulilittle4 小时前
FTTR 全屋光纤架构分享
linux·服务器·网络·ip·通信·光纤·fttr
niuTaylor5 小时前
从入门到精通:CMakeLists.txt 完全指南
linux·服务器·cmake
SoFlu软件机器人6 小时前
飞算 JavaAI 与 Spring Boot:如何实现微服务开发效率翻倍?
spring boot·后端·微服务
ptu小鹏7 小时前
类和对象(中)
开发语言·c++
jack_xu7 小时前
经典大厂面试题——缓存穿透、缓存击穿、缓存雪崩
java·redis·后端