分布自定义shell脚本(详写)附带全代码

涉及知识全排列

常见指令

小知识点

操作系统

什么是进程

进程控制

步骤 1:项目准备

在开始编写代码之前,你需要创建一个新的项目文件夹,并在其中创建一个 .cpp 文件,例如 my_shell.cpp。同时,确保你已经安装了 C++ 编译器(如 g++),可以在终端中使用以下命令检查:

复制代码
g++ --version

步骤 2:包含必要的头文件和定义宏

打开 my_shell.cpp 文件,在文件开头包含所需的头文件,并定义一些宏,这些宏将用于设置命令行大小和提示符格式。

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

#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "

头文件解释

  • iostream:用于标准输入输出流,方便进行信息的显示和获取用户输入。

  • cstdio :提供标准输入输出函数,如 printffgets

  • cstring :包含字符串处理函数,像 strcpystrlen

  • cstdlib :提供通用工具函数,如 mallocexit

  • unistd.h :包含许多 Unix 系统调用,如 forkexecvpchdir

  • sys/types.h:定义了系统数据类型,在系统调用中经常用到。

  • sys/wait.h:用于处理进程等待,如 waitpid 函数。

  • unordered_map:C++ 标准库中的哈希表容器,用于存储命令别名映射。

宏定义解释

  • COMMAND_SIZE:设置用户输入命令的最大长度为 1024 字节。

  • FORMAT:定义命令行提示符的格式,会显示用户名、主机名和当前工作目录。

步骤 3:定义全局变量

在头文件和宏定义之后,定义一些全局变量,用于存储命令行参数、环境变量和命令别名等信息。

复制代码
// 命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0;

// 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;

// 别名映射表
std::unordered_map<std::string, std::string> alias_list;

// 当前工作目录相关
char cwd[1024];
char cwdenv[2048];

// 上一次退出码
int lastcode = 0;
  • 命令行参数

    • MAXARGC:最大命令行参数数量为 128。

    • g_argv:存储命令行参数的指针数组。

    • g_argc:记录命令行参数的实际数量。

  • 环境变量

    • MAX_ENVS:最大环境变量数量为 100。

    • g_env:存储环境变量的指针数组。

    • g_envs:记录环境变量的实际数量。

  • 别名映射表alias_list 是一个哈希表,用于存储命令别名和其对应的实际命令。

  • 当前工作目录

    • cwd:存储当前工作目录的字符数组。

    • cwdenv :存储 PWD 环境变量的字符数组,大小为 2048 字节。

  • 上一次退出码lastcode 保存上一个执行命令的退出状态码。

步骤 4:编写获取系统信息的函数

接下来,编写几个函数用于获取系统信息,如用户名、主机名、当前工作目录和用户主目录。

复制代码
// 获取用户名
const char *GetUserName() {
    const char *name = getenv("USER");
    return name == nullptr ? "None" : name;
}

// 获取主机名
const char *GetHostName() {
    const char *hostname = getenv("HOSTNAME");
    return hostname == nullptr ? "None" : hostname;
}

// 获取当前工作目录
const char *GetPwd() {
    const char *pwd = getcwd(cwd, sizeof(cwd));
    if (pwd != nullptr) {
        size_t cwd_len = strlen(cwd);
        size_t prefix_len = strlen("PWD=");
        if (cwd_len + prefix_len + 1 <= sizeof(cwdenv)) {
            snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
            putenv(cwdenv);
        }
    }
    return pwd == nullptr ? "None" : pwd;
}

// 获取用户主目录
const char *GetHome() {
    const char *home = getenv("HOME");
    return home == nullptr ? "" : home;
}
  • GetUserName 函数 :使用 getenv 函数获取 USER 环境变量的值,如果获取不到则返回 "None"

  • GetHostName 函数 :使用 getenv 函数获取 HOSTNAME 环境变量的值,如果获取不到则返回 "None"

  • GetPwd 函数

    • 使用 getcwd 函数获取当前工作目录并存储到 cwd 中。

    • 检查 cwdenv 缓冲区是否足够大,如果足够则将 PWD 环境变量的值设置为当前工作目录。

    • 如果获取不到当前工作目录,则返回 "None"

  • GetHome 函数 :使用 getenv 函数获取 HOME 环境变量的值,如果获取不到则返回空字符串。

步骤 5:编写初始化环境变量的函数

编写一个函数用于初始化环境变量,将系统环境变量复制到自定义的环境变量数组中,并添加一个测试用的环境变量。

复制代码
// 初始化环境变量
void InitEnv() {
    extern char **environ;
    memset(g_env, 0, sizeof(g_env));
    g_envs = 0;

    // 从系统环境变量复制
    for (int i = 0; environ[i]; ++i) {
        g_env[i] = static_cast<char *>(malloc(strlen(environ[i]) + 1));
        strcpy(g_env[i], environ[i]);
        ++g_envs;
    }
    g_env[g_envs++] = strdup("HAHA=for_test");
    g_env[g_envs] = nullptr;

    // 设置环境变量
    for (int i = 0; g_env[i]; ++i) {
        putenv(g_env[i]);
    }
    environ = g_env;
}
  • InitEnv 函数

    • g_env 数组初始化为 0。

    • 从系统环境变量 environ 复制环境变量到 g_env 中。

    • 添加一个测试用的环境变量 "HAHA=for_test"

    • 使用 putenv 函数将 g_env 中的环境变量设置到当前进程。

    • environ 指针指向 g_env

步骤 6:编写处理内置命令的函数

编写几个函数用于处理内置命令,如 cdechoexportalias

复制代码
// 处理 cd 命令
bool Cd() {
    if (g_argc == 1) {
        const char *home = GetHome();
        if (home[0] != '\0') {
            if (chdir(home) == 0) {
                setenv("OLDPWD", getenv("PWD"), 1);
                GetPwd();
            }
        }
    } else {
        std::string target = g_argv[1];
        if (target == "-") {
            const char *old_pwd = getenv("OLDPWD");
            if (old_pwd != nullptr) {
                if (chdir(old_pwd) == 0) {
                    setenv("OLDPWD", getenv("PWD"), 1);
                    GetPwd();
                }
            }
        } else if (target == "~") {
            const char *home = GetHome();
            if (home[0] != '\0') {
                if (chdir(home) == 0) {
                    setenv("OLDPWD", getenv("PWD"), 1);
                    GetPwd();
                }
            }
        } else {
            if (chdir(target.c_str()) == 0) {
                setenv("OLDPWD", getenv("PWD"), 1);
                GetPwd();
            }
        }
    }
    return true;
}

// 处理 echo 命令
void Echo() {
    if (g_argc == 2) {
        std::string arg = g_argv[1];
        if (arg == "$?") {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        } else if (arg[0] == '$') {
            std::string env_name = arg.substr(1);
            const char *env_value = getenv(env_name.c_str());
            if (env_value != nullptr) {
                std::cout << env_value << std::endl;
            } else {
                std::cout << "Environment variable not found." << std::endl;
            }
        } else {
            std::cout << arg << std::endl;
        }
    }
}

// 检查并执行内置命令
bool CheckAndExecBuiltin() {
    std::string cmd = g_argv[0];
    if (cmd == "cd") {
        return Cd();
    } else if (cmd == "echo") {
        Echo();
        return true;
    } else if (cmd == "export") {
        if (g_argc == 2) {
            char *equal_sign = strchr(g_argv[1], '=');
            if (equal_sign != nullptr) {
                *equal_sign = '\0';
                setenv(g_argv[1], equal_sign + 1, 1);
            }
        }
        return true;
    } else if (cmd == "alias") {
        if (g_argc == 3) {
            alias_list[g_argv[1]] = g_argv[2];
        }
        return true;
    }
    return false;
}
  • Cd 函数

    • 如果没有参数,则切换到用户主目录。

    • 如果参数是 -,则切换到上一个工作目录。

    • 如果参数是 ~,则切换到用户主目录。

    • 其他情况,切换到指定目录。

    • 每次切换目录后,更新 OLDPWD 环境变量并重新获取当前工作目录。

  • Echo 函数

    • 如果参数是 $?,则输出上一次命令的退出状态码并将其重置为 0。

    • 如果参数以 $ 开头,则输出对应的环境变量值,如果变量不存在则给出提示。

    • 其他情况,直接输出参数内容。

  • CheckAndExecBuiltin 函数

    • 检查命令是否为内置命令(cdechoexportalias)。

    • 如果是 export 命令,则设置新的环境变量。

    • 如果是 alias 命令,则将别名和实际命令存储到 alias_list 中。

步骤 7:编写辅助函数

编写一些辅助函数,用于获取目录名、生成命令行提示符、获取用户输入、解析命令行参数和打印命令行参数。

复制代码
// 获取目录名
std::string DirName(const char *pwd) {
    std::string path = pwd;
    if (path == "/") return "/";
    size_t pos = path.rfind('/');
    if (pos == std::string::npos) return "BUG?";
    return path.substr(pos + 1);
}

// 生成命令行提示符
void MakeCommandLine(char cmd_prompt[], int size) {
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
}

// 打印命令行提示符
void PrintCommandPrompt() {
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt, sizeof(prompt));
    std::cout << prompt;
    std::cout.flush();
}

// 获取用户输入的命令行
bool GetCommandLine(char *out, int size) {
    char *input = fgets(out, size, stdin);
    if (input == nullptr) return false;
    size_t len = strlen(out);
    if (len > 0 && out[len - 1] == '\n') {
        out[len - 1] = '\0';
    }
    return len > 0;
}

// 解析命令行参数
bool CommandParse(char *commandline) {
    g_argc = 0;
    g_argv[g_argc++] = strtok(commandline, " ");
    while ((g_argv[g_argc++] = strtok(nullptr, " ")) != nullptr);
    --g_argc;
    return g_argc > 0;
}

// 打印命令行参数
void PrintArgv() {
    for (int i = 0; g_argv[i]; ++i) {
        std::cout << "argv[" << i << "]->" << g_argv[i] << std::endl;
    }
    std::cout << "argc: " << g_argc << std::endl;
}
  • DirName 函数:从路径中提取目录名。

  • MakeCommandLine 函数 :按照 FORMAT 格式生成命令行提示符。

  • PrintCommandPrompt 函数:打印生成的命令行提示符并刷新输出缓冲区。

  • GetCommandLine 函数:从标准输入读取用户输入的命令行,去除换行符。

  • CommandParse 函数 :使用 strtok 函数将命令行分割成参数,存储到 g_argv 中。

  • PrintArgv 函数:打印命令行参数和参数数量。

步骤 8:编写执行外部命令的函数

编写一个函数用于执行外部命令,使用 fork 创建子进程,在子进程中使用 execvp 执行命令。

复制代码
// 执行外部命令
int Execute() {
    pid_t pid = fork();
    if (pid == 0) {
        if (execvp(g_argv[0], g_argv) == -1) {
            perror("execvp");
            exit(EXIT_FAILURE);
        }
    } else if (pid > 0) {
        int status;
        waitpid(pid, &status, 0);
        lastcode = WEXITSTATUS(status);
    } else {
        perror("fork");
    }
    return 0;
}
  • Execute 函数

    • 使用 fork 函数创建子进程。

    • 子进程使用 execvp 函数执行外部命令,如果执行失败则输出错误信息并退出。

    • 父进程使用 waitpid 函数等待子进程结束,获取退出状态码并保存到 lastcode 中。

步骤 9:编写主函数

最后,编写主函数,初始化环境变量,进入无限循环,不断获取用户输入的命令并执行。

复制代码
// 主函数
int main() {
    InitEnv();

    while (true) {
        PrintCommandPrompt();

        char commandline[COMMAND_SIZE];
        if (!GetCommandLine(commandline, sizeof(commandline))) {
            continue;
        }

        if (!CommandParse(commandline)) {
            continue;
        }

        if (CheckAndExecBuiltin()) {
            continue;
        }

        Execute();
    }

    // 释放环境变量内存
    for (int i = 0; i < g_envs; ++i) {
        free(g_env[i]);
    }

    return 0;
}

main 函数

  • 调用 InitEnv 函数初始化环境变量。

  • 进入无限循环,不断打印命令行提示符。

  • 获取用户输入的命令行并进行解析。

  • 检查是否为内置命令,如果是则执行并继续循环。

  • 如果不是内置命令,则调用 Execute 函数执行外部命令。

  • 程序结束前,释放 g_env 数组中分配的内存。

步骤 10:编译和运行程序

终端中,使用 g++ 编译器编译 my_shell.cpp 文件:

复制代码
g++ my_shell.cpp -o my_shell

编译成功后,会生成一个名为 my_shell 的可执行文件。运行该文件:

复制代码
./my_shell

全代码

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

#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "

// 下面是shell定义的全局数据

// 1. 命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0; 

// 2. 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;

// 3. 别名映射表
std::unordered_map<std::string, std::string> alias_list;

// for test
char cwd[1024];
char cwdenv[2048]; // 增大缓冲区,避免snprintf警告

// last exit code
int lastcode = 0;

const char *GetUserName()
{
    const char *name = getenv("USER");
    return name == NULL? "None" : name;
}

const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");
    return hostname == NULL? "None" : hostname;
}

const char *GetPwd()
{
    const char *pwd = getcwd(cwd, sizeof(cwd));
    if(pwd != NULL)
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
        putenv(cwdenv);
    }
    return pwd == NULL? "None" : pwd;
}

const char *GetHome()
{
    const char *home = getenv("HOME");
    return home == NULL? "" : home;
}

void InitEnv()
{
    extern char **environ;
    memset(g_env, 0, sizeof(g_env));
    g_envs = 0;

    // 本来要从配置文件来
    // 1. 获取环境变量
    for(int i = 0; environ[i]; i++)
    {
        // 1.1 申请空间
        g_env[i] = (char*)malloc(strlen(environ[i]) + 1);
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs++] = (char*)"HAHA=for_test"; // for_test
    g_env[g_envs] = NULL;

    // 2. 导成环境变量
    for(int i = 0; g_env[i]; i++)
    {
        putenv(g_env[i]);
    }
    environ = g_env;
}

// command
bool Cd()
{
    // cd argc = 1
    if(g_argc == 1)
    {
        std::string home = GetHome();
        if(home.empty()) return true;
        chdir(home.c_str());
    }
    else
    {
        std::string where = g_argv[1];
        // cd - / cd ~
        if(where == "-")
        {
            const char *old_pwd = getenv("OLDPWD");
            if (old_pwd)
            {
                chdir(old_pwd);
                setenv("OLDPWD", getcwd(cwd, sizeof(cwd)), 1);
            }
        }
        else if(where == "~")
        {
            std::string home = GetHome();
            if (!home.empty())
            {
                chdir(home.c_str());
            }
        }
        else
        {
            chdir(where.c_str());
        }
    }
    return true;
}

void Echo()
{
    if(g_argc == 2)
    {
        // echo "hello world"
        // echo $?
        // echo $PATH
        std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char *env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
            else
            {
                std::cout << "Environment variable not found." << std::endl;
            }
        }
        else
        {
            std::cout << opt << std::endl;
        }
    }
}

// / /a/b/c
std::string DirName(const char *pwd)
{
#define SLASH "/"
    std::string dir = pwd;
    if(dir == SLASH) return SLASH;
    auto pos = dir.rfind(SLASH);
    if(pos == std::string::npos) return "BUG?";
    return dir.substr(pos + 1);
}

void MakeCommandLine(char cmd_prompt[], int size)
{
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
    //snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}

void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt, sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}

bool GetCommandLine(char *out, int size)
{
    // ls -a -l => "ls -a -l\n" 字符串
    char *c = fgets(out, size, stdin);
    if(c == NULL) return false;
    out[strlen(out) - 1] = 0; // 清理\n
    if(strlen(out) == 0) return false;
    return true;
}

// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " "
    g_argc = 0;
    // 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
    g_argv[g_argc++] = strtok(commandline, SEP);
    while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
    g_argc--;
    return g_argc > 0? true:false;
}

void PrintArgv()
{
    for(int i = 0; g_argv[i]; i++)
    {
        printf("argv[%d]->%s\n", i, g_argv[i]);
    }
    printf("argc: %d\n", g_argc);
}

bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    else if(cmd == "export")
    {
        if (g_argc == 2)
        {
            std::string var = g_argv[1];
            char *equal_pos = strchr(var.c_str(), '=');
            if (equal_pos)
            {
                *equal_pos = '\0';
                setenv(var.c_str(), equal_pos + 1, 1);
            }
        }
        return true;
    }
    else if(cmd == "alias")
    {
        if (g_argc == 3)
        {
            std::string nickname = g_argv[1];
            std::string real_cmd = g_argv[2];
            alias_list[nickname] = real_cmd;
        }
        return true;
    }

    return false;
}

int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        execvp(g_argv[0], g_argv);
        exit(1);
    }
    int status = 0;
    // father
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);
    }
    return 0;
}

int main()
{
    // shell 启动的时候,从系统中获取环境变量
    // 我们的环境变量信息应该从父shell统一来
    InitEnv();

    while(true)
    {
        // 1. 输出命令行提示符
        PrintCommandPrompt();

        // 2. 获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline)))
            continue;

        // 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
        if(!CommandParse(commandline))
            continue;
        //PrintArgv();

        // 检测别名
        // 4. 检测并处理内键命令
        if(CheckAndExecBuiltin())
            continue;

        // 5. 执行命令
        Execute();
    }
    // 释放内存
    for (int i = 0; i < g_envs; ++i)
    {
        free(g_env[i]);
    }
    return 0;
}
相关推荐
深海蜗牛8 分钟前
Jenkins linux安装
linux·jenkins
愚戏师20 分钟前
Linux复习笔记(三) 网络服务配置(web)
linux·运维·笔记
JANYI201842 分钟前
嵌入式MCU和Linux开发哪个好?
linux·单片机·嵌入式硬件
熊大如如1 小时前
Java NIO 文件处理接口
java·linux·nio
晚秋大魔王1 小时前
OpenHarmony 开源鸿蒙南向开发——linux下使用make交叉编译第三方库——nettle库
linux·开源·harmonyos
农民小飞侠1 小时前
ubuntu 24.04 error: cannot uninstall blinker 1.7.0, record file not found. hint
linux·运维·ubuntu
某不知名網友1 小时前
Linux 软硬连接详解
linux·运维·服务器
hnlucky1 小时前
通俗易懂版知识点:Keepalived + LVS + Web + NFS 高可用集群到底是干什么的?
linux·前端·学习·github·web·可用性测试·lvs
Jogging-Snail1 小时前
Linux工作台文件操作命令全流程解析(高级篇之vim和nano精讲)
linux·运维·vim·文件操作·文本编辑·nano
爱学习的章鱼哥2 小时前
计算机网络|| 常用网络命令的作用及工作原理
linux·服务器·计算机网络