分布自定义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;
}
相关推荐
保证四个小时充足睡眠13 分钟前
【并行分布计算】Hadoop伪分布搭建
linux
ZaaaaacK24 分钟前
守护进程编程
linux·运维·网络
晓龙的Coding之路1 小时前
python生成项目依赖文件requirements.txt
linux·开发语言·python
gblfy1 小时前
DeepSeek + Dify + Ollama + Docker + Linux 私有化部署,构建你的专属私人 AI 助手
linux·docker·dify·本地部署·ollama·deepseek·私有化
ℳℓ白ℳℓ夜ℳℓ1 小时前
Linux网络UDP与TCP
linux·网络·udp
小oo呆2 小时前
【自然语言处理与大模型】Linux环境下Ollama下载太慢了该怎么处理?
linux·服务器·人工智能
菜鸡上道2 小时前
Linux 文件系统目录结构详解
linux
eli9602 小时前
LIB-ZC, 一个跨平台(Linux)平台通用C/C++扩展库, 网络socket
linux·c语言·c++
破刺不会编程2 小时前
关于进程状态
linux·服务器
632972 小时前
Linux守护进程
linux·运维·服务器