自主Shell命令行解释器

目录

一、自主Shell命令行解释器

1、目标

2、实现原理

3、Makefile

[1. 编译规则](#1. 编译规则)

[2. 清理规则](#2. 清理规则)

使用方式

4、myshell.cc

二、代码解析

1、头文件部分

2、全局变量定义

3、用户信息获取函数

[1. GetUserName() - 获取当前用户名](#1. GetUserName() - 获取当前用户名)

[2. GetHostName() - 获取主机名](#2. GetHostName() - 获取主机名)

[3. GetPwd() - 获取当前工作目录](#3. GetPwd() - 获取当前工作目录)

[4.snprintf 函数](#4.snprintf 函数)

函数原型

[5. GetHome() - 获取家目录](#5. GetHome() - 获取家目录)

4、环境变量初始化

[1. 变量声明与初始化](#1. 变量声明与初始化)

[2. 复制系统环境变量](#2. 复制系统环境变量)

[3. 添加测试环境变量](#3. 添加测试环境变量)

[4. 更新全局环境变量](#4. 更新全局环境变量)

5、内建命令实现

cd命令

[1. 函数功能](#1. 函数功能)

[2. 参数处理逻辑](#2. 参数处理逻辑)

[情况1:无参数(cd 或 cd ~)](#情况1:无参数(cd 或 cd ~))

情况2:有参数

[3. 关键函数与变量](#3. 关键函数与变量)

echo命令

[1. 函数功能](#1. 函数功能)

[2. 参数处理逻辑](#2. 参数处理逻辑)

[情况1:参数为 ?](#情况1:参数为 ?)

[情况2:参数为环境变量(以 开头)](#情况2:参数为环境变量(以 开头))

情况3:普通字符串

[3. 关键函数与变量](#3. 关键函数与变量)

6、路径处理函数

[1. 函数功能](#1. 函数功能)

[2. 代码逻辑解析](#2. 代码逻辑解析)

步骤1:处理根目录

[步骤2:查找最后一个 /](#步骤2:查找最后一个 /)

步骤3:提取最后一部分

7、命令行提示符生成

[1. 函数功能概述](#1. 函数功能概述)

[2. 关键组件解析](#2. 关键组件解析)

[(1) MakeCommandLine 函数](#(1) MakeCommandLine 函数)

[(2) PrintCommandPrompt 函数](#(2) PrintCommandPrompt 函数)

[3. 关键函数与变量](#3. 关键函数与变量)

8、命令输入处理

[1. 函数功能](#1. 函数功能)

[2. 代码逻辑解析](#2. 代码逻辑解析)

步骤1:读取输入

fgets函数:

[1. 函数原型](#1. 函数原型)

[2. 核心行为](#2. 核心行为)

[(1) 读取规则](#(1) 读取规则)

[(2) 安全特性](#(2) 安全特性)

步骤2:去除换行符

步骤3:检查空命令

[9. 命令解析](#9. 命令解析)

[1. 函数功能](#1. 函数功能)

[2. 代码逻辑解析](#2. 代码逻辑解析)

步骤1:初始化

步骤2:首次分割

strtok函数

[1. 函数原型](#1. 函数原型)

[2. 核心特性](#2. 核心特性)

[(1) 分割机制](#(1) 分割机制)

[(2) 修改原字符串](#(2) 修改原字符串)

步骤3:循环分割剩余参数

步骤4:修正计数器

步骤5:返回值

[3. 关键函数与变量](#3. 关键函数与变量)

[10. 内建命令检查](#10. 内建命令检查)

[1. 函数功能](#1. 函数功能)

[2. 代码逻辑解析](#2. 代码逻辑解析)

[(1) 获取命令名](#(1) 获取命令名)

[(2) 内置命令匹配与执行](#(2) 内置命令匹配与执行)

[(3) 默认返回](#(3) 默认返回)

[3. 关键变量与依赖](#3. 关键变量与依赖)

[11. 外部命令执行](#11. 外部命令执行)

[1. 函数功能](#1. 函数功能)

[2. 代码逻辑解析](#2. 代码逻辑解析)

[(1) 创建子进程](#(1) 创建子进程)

[(2) 父进程等待子进程](#(2) 父进程等待子进程)

[(3) 全局状态记录](#(3) 全局状态记录)

[12. 主函数](#12. 主函数)

[1. 主函数流程](#1. 主函数流程)

[(1) 初始化环境](#(1) 初始化环境)

[(2) 主循环](#(2) 主循环)

[2. 循环内步骤解析](#2. 循环内步骤解析)

[(1) 打印提示符](#(1) 打印提示符)

[(2) 读取命令](#(2) 读取命令)

[(3) 解析命令](#(3) 解析命令)

[(4) 检查内置命令](#(4) 检查内置命令)

[(5) 执行外部命令](#(5) 执行外部命令)

三、总结


一、自主Shell命令行解释器

shell也就是命令行解释器,其运行原理就是:当有命令需要执行时,shell创建子进程,让子进程执行命令,而shell只需等待子进程退出即可。

1、目标

  • 支持处理普通命令
  • 支持处理内建命令
  • 帮助理解内建命令、本地变量和环境变量的概念
  • 帮助理解Shell的运行原理

2、实现原理

考虑以下典型的Shell交互示例:

下图展示了事件的时间轴,从左到右表示时间顺序。Shell进程(用"sh"方块表示)随时间从左向右移动。Shell读取用户输入的字符串"ls",创建一个新进程,在该进程中运行ls程序,并等待该进程结束。

Shell会读取新的输入行,创建新进程来运行程序并等待其结束。因此,编写Shell需要循环执行以下步骤:

  1. 获取命令行输入
  2. 解析命令行
  3. 创建子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)

其中,创建子进程使用fork函数,替换子进程使用exec系列函数,等待子进程使用wait或者waitpid函数。

结合这些思路和已掌握的技术,就可以自行实现一个Shell程序了,代码如下:

3、Makefile

bash 复制代码
myshell:myshell.cc
	g++ -o $@ $^ -std=c++11 #-std=c99
.PHONY:clean
clean:
	rm -f myshell

1. 编译规则

  • 目标 : myshell (要生成的可执行文件)

  • 依赖 : myshell.cc (源代码文件)

  • 编译命令:

    • g++: 调用 GNU C++ 编译器

    • -o $@: 指定输出文件名,$@ 会被自动替换为目标名 myshell

    • $^: 表示所有依赖文件,这里就是 myshell.cc

    • -std=c++11: 指定使用 C++11 标准进行编译

    • #-std=c99: 这是一个被注释掉的选项(不会生效),原本是指定 C99 标准

2. 清理规则

  • .PHONY:clean: 声明 clean 是一个伪目标(不是实际要生成的文件)

  • 命令: rm -f myshell: 强制删除 myshell 可执行文件(如果存在)

使用方式

  1. 编译程序 : 直接在终端运行 make,它会自动执行第一个规则(编译 myshell

  2. 清理生成的文件 : 运行 make clean,会删除 myshell 可执行文件

4、myshell.cc

cpp 复制代码
#include <iostream>  // 标准输入输出流库
#include <cstdio>    // C标准输入输出库
#include <cstring>   // C字符串处理库
#include <cstdlib>   // C标准库
#include <unistd.h>  // POSIX操作系统API
#include <sys/types.h>  // 系统类型定义
#include <sys/wait.h>   // 进程等待相关函数
#include <cstring>      // C字符串处理库(重复包含)
#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[1024];  // 环境变量PWD缓冲区

// last exit code
int lastcode = 0;  // 记录上一条命令的退出状态码

// 获取当前用户名函数
const char *GetUserName()
{
    const char *name = getenv("USER");  // 从环境变量获取用户名
    return name == NULL ? "None" : name;  // 如果不存在返回"None"
}

// 获取主机名函数
const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");  // 从环境变量获取主机名
    return hostname == NULL ? "None" : hostname;  // 如果不存在返回"None"
}

// 获取当前工作目录函数
const char *GetPwd()
{
    //const char *pwd = getenv("PWD");
    const char *pwd = getcwd(cwd, sizeof(cwd));  // 获取当前工作目录
    if(pwd != NULL)
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);  // 格式化PWD环境变量
        putenv(cwdenv);  // 设置环境变量
    }
    return pwd == NULL ? "None" : pwd;  // 如果获取失败返回"None"
}

// 获取家目录函数
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;  // 环境变量数组以NULL结尾

    //2. 导成环境变量
    for(int i = 0; g_env[i]; i++)  // 遍历环境变量数组
    {
        putenv(g_env[i]);  // 设置环境变量
    }
    environ = g_env;  // 更新全局环境变量指针
}

// cd命令处理函数
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 == "-")  // 处理cd -命令
        {
            // Todu  // 待实现
        }
        else if(where == "~")  // 处理cd ~命令
        {
            // Todu  // 待实现
        }
        else  // 普通目录
        {
            chdir(where.c_str());  // 切换到指定目录
        }
    }
    return true;  // 返回成功
}

// echo命令处理函数
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 << 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;  // 读取失败返回false
    out[strlen(out)-1] = 0; // 清理\n  // 去掉换行符
    if(strlen(out) == 0) return false;  // 空命令返回false
    return true;  // 成功返回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;  // 如果有参数返回true
}

// 打印参数数组函数(测试用)
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命令
    {
        Cd();  // 执行cd命令
        return true;  // 返回已处理
    }
    else if(cmd == "echo")  // 如果是echo命令
    {
        Echo();  // 执行echo命令
        return true;  // 返回已处理
    }
    else if(cmd == "export")  // 如果是export命令
    {
    }
    else if(cmd == "alias")  // 如果是alias命令
    {
       // std::string nickname = g_argv[1];
       // alias_list.insert(k, v);
    }

    return false;  // 不是内置命令返回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();  // 执行外部命令
    }
    //cleanup();
    return 0;  // 程序结束
}

二、代码解析

1、头文件部分

cpp 复制代码
#include <iostream>  // 标准输入输出流库
#include <cstdio>    // C标准输入输出库
#include <cstring>   // C字符串处理库
#include <cstdlib>   // C标准库
#include <unistd.h>  // POSIX操作系统API
#include <sys/types.h>  // 系统类型定义
#include <sys/wait.h>   // 进程等待相关函数
#include <cstring>      // C字符串处理库(重复包含)
#include <unordered_map>  // 无序映射(哈希表)容器

知识点

  • <iostream>: C++标准输入输出库

  • <cstdio>: C标准输入输出函数

  • <cstring>: C字符串处理函数

  • <cstdlib>: C标准库函数

  • <unistd.h>: POSIX操作系统API

  • <sys/types.h>: 系统数据类型定义

  • <sys/wait.h>: 进程等待相关函数

  • <unordered_map>: C++哈希表容器

2、全局变量定义

cpp 复制代码
#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[1024];  // 环境变量PWD缓冲区

// last exit code
int lastcode = 0;  // 记录上一条命令的退出状态码

知识点

  • 宏定义:用于定义常量

  • 全局变量:存储shell运行时的状态信息

  • unordered_map: C++11引入的哈希表容器,用于存储别名映射

3、用户信息获取函数

cpp 复制代码
// 获取当前用户名函数
const char *GetUserName()
{
    const char *name = getenv("USER");  // 从环境变量获取用户名
    return name == NULL ? "None" : name;  // 如果不存在返回"None"
}

// 获取主机名函数
const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");  // 从环境变量获取主机名
    return hostname == NULL ? "None" : hostname;  // 如果不存在返回"None"
}

// 获取当前工作目录函数
const char *GetPwd()
{
    //const char *pwd = getenv("PWD");
    const char *pwd = getcwd(cwd, sizeof(cwd));  // 获取当前工作目录
    if(pwd != NULL)
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);  // 格式化PWD环境变量
        putenv(cwdenv);  // 设置环境变量
    }
    return pwd == NULL ? "None" : pwd;  // 如果获取失败返回"None"
}

// 获取家目录函数
const char *GetHome()
{
    const char *home = getenv("HOME");  // 从环境变量获取家目录
    return home == NULL ? "" : home;  // 如果不存在返回空字符串
}

1. GetUserName() - 获取当前用户名

cpp 复制代码
// 获取当前用户名函数
const char *GetUserName()
{
    const char *name = getenv("USER");  // 从环境变量获取用户名
    return name == NULL ? "None" : name;  // 如果不存在返回"None"
}
  • 功能:返回当前登录用户的用户名。

  • 实现

    • 使用 getenv("USER") 从环境变量中读取用户名(Linux/Unix 系统通常设置 USERUSERNAME 环境变量)。

    • 如果 getenv 返回 NULL(环境变量未设置),则返回默认字符串 "None"

2. GetHostName() - 获取主机名

cpp 复制代码
// 获取主机名函数
const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");  // 从环境变量获取主机名
    return hostname == NULL ? "None" : hostname;  // 如果不存在返回"None"
}
  • 功能:返回当前主机的主机名。

  • 实现

    • 通过 getenv("HOSTNAME") 从环境变量读取主机名。

    • 如果未设置,返回 "None"

3. GetPwd() - 获取当前工作目录

cpp 复制代码
// 获取当前工作目录函数
const char *GetPwd()
{
    //const char *pwd = getenv("PWD");
    const char *pwd = getcwd(cwd, sizeof(cwd));  // 获取当前工作目录
    if(pwd != NULL)
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);  // 格式化PWD环境变量
        putenv(cwdenv);  // 设置环境变量
    }
    return pwd == NULL ? "None" : pwd;  // 如果获取失败返回"None"
}
  • 功能 :返回当前工作目录的路径,并更新 PWD 环境变量。

  • 实现

    1. 调用 getcwd() 获取当前工作目录(需全局变量 char cwd[1024])。

    2. 如果成功,将路径格式化为 PWD=/path/to/cwd 并调用 putenv() 更新环境变量。

    3. 返回目录路径或 "None"(失败时)。

  • 注意

    • cwdcwdenv 应为全局缓冲区。

    • putenv() 设置的字符串必须是长期有效的(通常用全局变量)。

4.snprintf 函数

snprintf是 C 标准库中的一个格式化输出函数,用于将格式化的数据安全地写入字符数组(字符串缓冲区)。它的主要作用是防止缓冲区溢出 (Buffer Overflow),比传统的 sprintf 更安全。

函数原型

cpp 复制代码
int snprintf(char *str, size_t size, const char *format, ...);
  • 参数

    • str:目标字符数组(缓冲区),用于存储格式化后的字符串。

    • size:缓冲区的大小(最多写入 size-1 个字符,预留 1 位给终止符 \0)。

    • format:格式化字符串(类似 printf 的格式)。

    • ...:可变参数,根据 format 填入具体值。

  • 返回值

    • 成功时返回欲写入的字符串长度 (不包括终止符 \0),即使截断也会返回完整长度。

    • 错误时返回负值。

5. GetHome() - 获取家目录

cpp 复制代码
// 获取家目录函数
const char *GetHome()
{
    const char *home = getenv("HOME");  // 从环境变量获取家目录
    return home == NULL ? "" : home;  // 如果不存在返回空字符串
}
  • 功能 :返回当前用户的家目录路径(如 /home/username)。

  • 实现

    • 通过 getenv("HOME") 读取家目录路径。

    • 如果未设置,返回空字符串 ""

4、环境变量初始化

cpp 复制代码
// 初始化环境变量函数
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;  // 环境变量数组以NULL结尾

    //2. 导成环境变量
    for(int i = 0; g_env[i]; i++)  // 遍历环境变量数组
    {
        putenv(g_env[i]);  // 设置环境变量
    }
    environ = g_env;  // 更新全局环境变量指针
}

这段代码实现了一个环境变量初始化的函数 InitEnv(),主要功能是复制系统环境变量到自定义数组 g_env 中,并添加一个测试变量,最后更新全局环境变量。以下是对代码的详细解析:

1. 变量声明与初始化

cpp 复制代码
extern char **environ;  // 声明系统环境变量(外部全局变量)
memset(g_env, 0, sizeof(g_env));  // 清空自定义环境变量数组
g_envs = 0;  // 重置环境变量计数器
  • environ

    是 Unix/Linux 系统的全局变量(定义在 <unistd.h>),指向当前进程的环境变量表(格式为 "KEY=VALUE" 的字符串数组,以 NULL 结尾)。

  • g_env

    是自定义的全局环境变量数组。

  • g_envs

    记录当前环境变量的数量。

2. 复制系统环境变量

cpp 复制代码
for(int i = 0; environ[i]; i++) {
    g_env[i] = (char*)malloc(strlen(environ[i])+1);  // 分配内存
    strcpy(g_env[i], environ[i]);  // 复制字符串
    g_envs++;  // 计数器递增
}
  • 逻辑

    遍历系统的 environ 数组,为每个环境变量字符串分配内存,并复制到 g_env 中。

  • 关键点

    • 内存分配strlen(environ[i])+1 确保分配的空间足够存储字符串及其终止符 \0

    • 深拷贝strcpy 复制内容而非指针,避免直接引用系统的 environ(防止后续修改冲突)。

3. 添加测试环境变量

cpp 复制代码
g_env[g_envs++] = (char*)"HAHA=for_test";  // 添加测试变量
g_env[g_envs] = NULL;  // 数组以NULL结尾
  • 作用

    在自定义环境变量数组末尾添加一个测试项 "HAHA=for_test",并更新计数器 g_envs

  • 注意 :必须手动以 NULL 结尾,符合环境变量数组的规范。

4. 更新全局环境变量

  • 问题背景
    putenv(g_env[i]) 仅将 g_env[i] 中的单个键值对添加到系统的环境变量表中,但不会自动同步 environ 指针

    • 系统的 environ 可能仍然指向旧的环境变量表。

    • 后续直接通过 environ 访问时,可能无法看到新增的变量(如 "HAHA=for_test")。

  • 解决方案

    通过 environ = g_env 强制让全局指针指向自定义的 g_env 数组,确保所有代码(包括库函数)都能访问到完整的环境变量。

cpp 复制代码
for(int i = 0; g_env[i]; i++) {
    putenv(g_env[i]);  // 设置环境变量
}
environ = g_env;  // 覆盖系统环境变量指针
  • putenv(g_env[i])

    将每个自定义环境变量导入当前进程的环境变量表。

    • 注意putenv 的参数格式必须是 "KEY=VALUE",且会直接引用 g_env[i] 的指针(后续不能释放或修改 g_env[i])。
  • environ = g_env

    直接替换系统的全局环境变量指针,使后续操作(如 getenv)使用自定义的 g_env

5、内建命令实现

cd命令

cpp 复制代码
// cd命令处理函数
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 == "-")  // 处理cd -命令
        {
            // Todu  // 待实现
        }
        else if(where == "~")  // 处理cd ~命令
        {
            // Todu  // 待实现
        }
        else  // 普通目录
        {
            chdir(where.c_str());  // 切换到指定目录
        }
    }
    return true;  // 返回成功
}

1. 函数功能

  • 作用 :模拟 Shell 中的 cd 命令,根据参数切换当前工作目录。

  • 返回值bool 类型,总是返回 true(可能用于后续扩展错误处理)。

2. 参数处理逻辑

情况1:无参数(cdcd ~
cpp 复制代码
if(g_argc == 1) {
    std::string home = GetHome();  // 获取家目录路径
    if(home.empty()) return true;  // 家目录为空则直接返回
    chdir(home.c_str());          // 切换到用户家目录
}
  • 行为

    • 调用 GetHome() 获取用户家目录(如 /home/username)。

    • 如果家目录有效(非空),则通过 chdir 切换到该目录。

  • 关键点

    • g_argc 是全局变量,表示命令参数个数(cd 本身算一个,无参数时 g_argc == 1)。

    • GetHome() 的实现依赖环境变量 HOME

情况2:有参数
cpp 复制代码
else {
    std::string where = g_argv[1];  // 获取目标目录参数
    if(where == "-") {
        // 待实现:切换到上一个工作目录(需记录历史)
    }
    else if(where == "~") {
        // 待实现:切换到用户家目录(与无参数逻辑重复)
    }
    else {
        chdir(where.c_str());  // 切换到普通目录
    }
}
  • 分支逻辑

    1. cd -

      通常用于返回上一个工作目录(需额外实现历史记录功能,如全局变量 std::string prev_dir)。

    2. cd ~

      与无参数 cd 功能重复,可直接调用 GetHome() 并切换。

    3. 普通路径

      直接调用 chdir 切换目录(如 cd /tmp)。

3. 关键函数与变量

  • chdir(const char *path)
    系统调用,用于改变当前工作目录。成功返回 0,失败返回 -1(错误码在 errno 中)。

  • 全局变量

    • g_argc:命令参数数量(如 cd /tmpg_argc == 2)。

    • g_argv[]:参数数组(如 g_argv[0] = "cd", g_argv[1] = "/tmp")。

  • GetHome()

    自定义函数,返回用户家目录路径(通过 getenv("HOME") 实现)。

echo命令

cpp 复制代码
// echo命令处理函数
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 << opt << std::endl;  // 直接输出字符串
        }
    }
}

1. 函数功能

  • 作用 :模拟 Shell 中的 echo 命令,根据参数类型输出不同的内容。

  • 支持的参数类型

    1. $? :输出上一条命令的退出状态码(lastcode)。

    2. $VAR :输出环境变量 VAR 的值(如 $PATH)。

    3. 普通字符串 :直接输出字符串内容(如 "hello world")。

2. 参数处理逻辑

情况1:参数为 $?
cpp 复制代码
if(opt == "$?") {
    std::cout << lastcode << std::endl;  // 输出退出状态码
    lastcode = 0;  // 重置状态码
}
  • 行为

    • 输出全局变量 lastcode(存储上一条命令的退出状态码,通常 0 表示成功,非零表示失败)。

    • 重置 lastcode0(可能为了后续命令的状态码记录)。

  • 关键点lastcode 是全局变量,需在其他命令执行时更新(如通过 exitreturn 值)。

情况2:参数为环境变量(以 $ 开头)
cpp 复制代码
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;  // 输出环境变量值
}
  • 行为

    1. 提取 $ 后的变量名(如 $PATH 提取为 PATH)。

    2. 调用 getenv 获取环境变量的值。

    3. 如果变量存在,输出其值;否则无输出。

情况3:普通字符串
cpp 复制代码
else {
    std::cout << opt << std::endl;  // 直接输出字符串
}
  • 行为 :直接输出参数字符串(如 echo "hello" 输出 hello)。

3. 关键函数与变量

  • getenv(const char *name)

    系统函数,用于获取环境变量的值。存在时返回字符串指针,否则返回 NULL

  • 全局变量

    • g_argc:命令参数个数(echo $PATHg_argc == 2)。

    • g_argv[]:参数数组(g_argv[0] = "echo", g_argv[1] = "$PATH")。

    • lastcode:上一条命令的退出状态码(需在其他命令中维护)。

6、路径处理函数

cpp 复制代码
// 获取目录名的函数
// / /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);  // 返回最后一个斜杠后的部分
}

这段代码实现了一个 DirName 函数,用于从给定的路径字符串中提取最后一个目录名或文件名。

1. 函数功能

  • 作用 :从路径字符串中提取最后一个 / 之后的部分(通常是文件名或最后一级目录名)。

  • 输入const char *pwd(路径字符串,如 "/a/b/c")。

  • 输出std::string(提取的部分,如 "c")。

2. 代码逻辑解析

步骤1:处理根目录
cpp 复制代码
if(dir == SLASH) return SLASH;  // 根目录直接返回 "/"
  • 行为 :如果路径是根目录 "/",直接返回 "/"

  • 意义:根目录没有父目录,无需进一步处理。

步骤2:查找最后一个 /
cpp 复制代码
auto pos = dir.rfind(SLASH);  // 从后向前查找
if(pos == std::string::npos) return "BUG?";  // 无斜杠则返回错误
  • rfind :从字符串末尾反向查找 /,返回其位置(从 0 开始)。

  • 错误处理 :如果路径中无 /(如 "abc"),返回 "BUG?"(可能是调试标记)。

步骤3:提取最后一部分
cpp 复制代码
return dir.substr(pos+1);  // 截取最后一个斜杠后的内容
  • substr(pos+1) :从 / 的下一个字符开始截取到字符串末尾。

    • 例如:"/a/b/c"pos 是 5(第二个 /),返回 "c"

7、命令行提示符生成

cpp 复制代码
// 生成命令行提示符函数
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);  // 刷新输出缓冲区
}

这段代码实现了 Shell 命令行提示符的生成和打印功能。

1. 函数功能概述

  • MakeCommandLine:生成格式化的命令行提示符字符串。

  • PrintCommandPrompt:打印生成的提示符到标准输出。

2. 关键组件解析

(1) MakeCommandLine 函数
cpp 复制代码
void MakeCommandLine(char cmd_prompt[], int size) {
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
}
  • 参数

    • cmd_prompt[]:用于存储生成的提示符的字符数组。

    • size:缓冲区大小,防止溢出。

  • 实现逻辑

    1. 调用辅助函数获取信息:

      • GetUserName():当前用户名。

      • GetHostName():主机名。

      • DirName(GetPwd()):当前工作目录的最后一级名称 (通过 GetPwd() 获取完整路径,DirName 提取最后一部分)。

    2. 使用 snprintf 安全格式化:

      • 将用户名、主机名、目录名按 FORMAT 格式组合到 cmd_prompt 中。

      • size 限制写入长度,避免缓冲区溢出。

  • 关键点

    • FORMAT 为全局定义的格式字符串( "[%s@%s %s]# ")。

    • DirName(GetPwd()).c_str() 获取目录名的 C 风格字符串。

(2) PrintCommandPrompt 函数
cpp 复制代码
void PrintCommandPrompt() {
    char prompt[COMMAND_SIZE];  // 缓冲区
    MakeCommandLine(prompt, sizeof(prompt));  // 生成提示符
    printf("%s", prompt);       // 打印
    fflush(stdout);             // 立即刷新输出
}
  • 流程

    1. 分配固定大小(COMMAND_SIZE)的缓冲区 prompt

    2. 调用 MakeCommandLine 生成提示符。

    3. printf 打印提示符。

    4. fflush(stdout) 确保提示符立即显示(避免行缓冲延迟)。

3. 关键函数与变量

  • 辅助函数

    • GetUserName():返回当前用户名的字符串(如 "user")。

    • GetHostName():返回主机名的字符串(如 "localhost")。

    • GetPwd():返回当前工作目录的完整路径(如 "/home/user/dir")。

    • DirName():从路径中提取最后一级名称(如 "/a/b/c""c")。

  • 全局常量

    • FORMAT:提示符的格式化字符串( "[%s@%s %s]# ")。

    • COMMAND_SIZE:提示符缓冲区的最大长度。

8、命令输入处理

cpp 复制代码
// 获取命令行输入函数
bool GetCommandLine(char *out, int size)
{
    // ls -a -l => "ls -a -l\n" 字符串
    char *c = fgets(out, size, stdin);  // 从标准输入读取命令
    if(c == NULL) return false;  // 读取失败返回false
    out[strlen(out)-1] = 0; // 清理\n  // 去掉换行符
    if(strlen(out) == 0) return false;  // 空命令返回false
    return true;  // 成功返回true
}

这段代码实现了一个 GetCommandLine 函数,用于从标准输入(通常是键盘)获取用户输入的命令行,并进行基本的预处理。

1. 函数功能

  • 作用 :从标准输入(stdin)读取一行用户输入的命令,并进行规范化处理。

  • 输入

    • out:用于存储输入命令的字符缓冲区。

    • size:缓冲区的大小,防止溢出。

  • 输出 :返回 bool 类型:true 表示成功读取有效命令,false 表示失败或空输入。

2. 代码逻辑解析

步骤1:读取输入
cpp 复制代码
char *c = fgets(out, size, stdin);  // 从stdin读取一行
if(c == NULL) return false;         // 读取失败(如EOF)
  • fgets 行为

    • 读取一行(包括换行符 \n),并在末尾添加 \0

    • 最多读取 size-1 个字符,保证缓冲区安全。

  • 错误处理

    • fgets 返回 NULL,表示遇到文件结束(EOF)或错误(如终端断开),直接返回 false
fgets函数:

fgets 是 C 标准库中的一个关键输入函数,用于安全地从文件流中读取一行字符串

1. 函数原型
cpp 复制代码
char *fgets(char *str, int size, FILE *stream);
  • 参数

    • str:目标字符数组(缓冲区),用于存储读取的数据。

    • size:缓冲区大小(最多读取 size-1 个字符,预留 1 位给终止符 \0)。

    • stream:输入流(如 stdin 表示标准输入,或文件指针)。

  • 返回值

    • 成功时返回 str 指针。

    • 失败或到达文件末尾时返回 NULL

2. 核心行为
(1) 读取规则
  • 读取一行:遇到以下情况停止读取:

    • 读取到 \n(换行符)。

    • 已读取 size-1 个字符。

    • 到达文件末尾(EOF)。

  • 自动添加终止符 :在读取的字符后追加 \0,构成合法 C 字符串。

(2) 安全特性
  • 缓冲区溢出保护 :严格限制最多写入 size-1 个字符,避免内存越界。

  • 保留换行符 :若读取到 \n,会将其包含在结果中(与 gets 不同)。

步骤2:去除换行符
cpp 复制代码
out[strlen(out)-1] = 0;  // 替换末尾的\n为\0
  • 逻辑strlen(out)-1 定位到最后一个字符(\n),将其替换为终止符 \0
步骤3:检查空命令
cpp 复制代码
if(strlen(out) == 0) return false;  // 空命令无效
  • 目的:忽略仅含换行符的输入(如用户直接按回车)。

9. 命令解析

cpp 复制代码
// 命令行解析函数
// 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;  // 如果有参数返回true
}

这段代码实现了一个命令行解析函数 CommandParse,用于将输入的命令行字符串拆分为多个参数(类似 Shell 的参数解析)。

1. 函数功能

  • 作用 :将形如 "ls -a -l" 的命令行字符串拆分为参数数组 g_argv,并统计参数个数 g_argc

  • 输入commandline(包含完整命令的字符串,如 "ls -a -l")。

  • 输出

    • 更新全局变量 g_argc(参数数量)和 g_argv[](参数指针数组)。

    • 返回 booltrue 表示解析成功(至少有一个参数),false 表示无参数。

2. 代码逻辑解析

步骤1:初始化
cpp 复制代码
g_argc = 0;  // 重置参数计数器
  • 确保每次解析前计数器归零。
步骤2:首次分割
cpp 复制代码
g_argv[g_argc++] = strtok(commandline, SEP);  // 分割第一个参数
  • strtok 首次调用

    • commandline 中查找第一个分隔符(空格 SEP),将其替换为 \0,并返回起始地址。

    • 结果存入 g_argv[0](如 "ls"),同时 g_argc 递增。

strtok函数

strtok 是 C 标准库中的一个字符串分割函数,用于将字符串按指定的分隔符拆分成多个子字符串(令牌)

1. 函数原型

cpp 复制代码
char *strtok(char *str, const char *delim);
  • 参数

    • str:待分割的字符串(首次调用时传入,后续调用需传 NULL)。

    • delim:分隔符集合(如 " " 表示空格,",;" 表示逗号或分号)。

  • 返回值

    • 成功时返回下一个子字符串的指针。

    • 无更多子字符串时返回 NULL

2. 核心特性

(1) 分割机制
  • 首次调用 :传入待分割字符串,strtok 找到第一个不包含在 delim 中的字符 作为起始,然后在后续字符中查找第一个包含在 delim 中的字符 ,将其替换为 \0,并返回当前子串指针。

  • 后续调用 :传入 NULL,函数会从上一次结束位置继续查找下一个子串。

(2) 修改原字符串
  • strtok 会直接修改原字符串,将分隔符替换为 \0(因此原字符串必须是可写的,如字符数组而非字符串常量)。
步骤3:循环分割剩余参数
cpp 复制代码
while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
  • strtok 后续调用

    • 传入 NULL 表示继续从上一次的位置分割。

    • 循环直到返回 NULL(无更多参数)。

    • 每次结果存入 g_argv[g_argc] 并递增计数器(如 "-a""-l")。

步骤4:修正计数器
cpp 复制代码
g_argc--;  // 因循环最后一次多加了1
  • 原因 :循环结束时 g_argc 会多计数一次(因 while 条件中的递增)。
步骤5:返回值
cpp 复制代码
return g_argc > 0 ? true : false;
  • 至少有一个参数时返回 true,否则 false

3. 关键函数与变量

  • strtok

    • 功能 :按分隔符切割字符串,首次调用需传入原字符串,后续传入 NULL

    • 注意 :会修改原字符串(将分隔符替换为 \0)。

  • 全局变量

    • g_argc:参数个数(如 "ls -a -l" 对应 3)。

    • g_argv[]:参数指针数组(如 g_argv[0] = "ls", g_argv[1] = "-a"),以 NULL 结尾。

10. 内建命令检查

cpp 复制代码
// 检查并执行内置命令函数
bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];  // 获取命令名
    if(cmd == "cd")  // 如果是cd命令
    {
        Cd();  // 执行cd命令
        return true;  // 返回已处理
    }
    else if(cmd == "echo")  // 如果是echo命令
    {
        Echo();  // 执行echo命令
        return true;  // 返回已处理
    }
    else if(cmd == "export")  // 如果是export命令
    {
    }
    else if(cmd == "alias")  // 如果是alias命令
    {
       // std::string nickname = g_argv[1];
       // alias_list.insert(k, v);
    }

    return false;  // 不是内置命令返回false
}

这段代码实现了一个内置命令检查与执行函数 CheckAndExecBuiltin(),用于判断当前输入的命令是否为 Shell 的内置命令(如 cdecho 等),若是则执行对应操作并返回处理状态。

1. 函数功能

  • 作用:检查当前命令是否为内置命令,若是则执行相应操作。

  • 返回值

    • true:命令是内置命令且已处理。

    • false:命令不是内置命令,需交由外部程序处理。

2. 代码逻辑解析

(1) 获取命令名
cpp 复制代码
std::string cmd = g_argv[0];  // 从全局参数数组获取命令名
  • g_argv[0] 存储命令名称(如 "cd""echo")。
(2) 内置命令匹配与执行
cpp 复制代码
if (cmd == "cd") {
    Cd();          // 调用cd命令处理函数
    return true;   // 标记为已处理
} 
else if (cmd == "echo") {
    Echo();        // 调用echo命令处理函数
    return true;
} 
else if (cmd == "export") {
    // 待实现(设置环境变量)
} 
else if (cmd == "alias") {
    // 待实现(命令别名功能)
}
  • 支持的内置命令

    • cd:切换目录(通过 Cd() 函数实现)。

    • echo:输出参数(通过 Echo() 函数实现)。

    • export:预留接口(用于设置环境变量)。

    • alias:预留接口(用于管理命令别名)。

(3) 默认返回
cpp 复制代码
return false;  // 非内置命令
  • 若命令未匹配任何内置项,返回 false,提示调用方需执行外部程序。

3. 关键变量与依赖

  • 全局变量

    • g_argv[]:命令参数数组(如 g_argv[0] = "cd", g_argv[1] = "/tmp")。
  • 依赖函数

    • Cd():处理目录切换。

    • Echo():处理字符串输出。

11. 外部命令执行

cpp 复制代码
// 执行外部命令函数
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;  // 返回成功
}

这段代码实现了一个执行外部命令的函数 Execute(),通过创建子进程并调用 execvp 来运行用户输入的命令

1. 函数功能

  • 作用 :在子进程中执行外部命令(如 /bin/ls),父进程等待子进程结束并记录其退出状态。

  • 返回值 :始终返回 0(可能用于后续扩展错误处理)。

  • 关键操作

    • fork():创建子进程。

    • execvp():替换子进程为外部命令。

    • waitpid():父进程等待子进程结束。

2. 代码逻辑解析

(1) 创建子进程
cpp 复制代码
pid_t id = fork();  // 分裂进程
if (id == 0) {      // 子进程分支
    execvp(g_argv[0], g_argv);  // 执行命令
    exit(1);        // execvp失败时退出
}
  • fork()

    • 成功时返回两次:父进程得到子进程PID(id > 0),子进程得到 0

    • 失败时返回 -1(此处未处理)。

  • execvp()

    • 参数1:命令名(如 "ls"),自动在 PATH 环境变量中查找可执行文件。

    • 参数2:参数数组(如 ["ls", "-l", NULL]),必须以 NULL 结尾。

    • 若成功:子进程被替换为命令,不会返回。

    • 若失败 :继续执行 exit(1),退出状态为 1

(2) 父进程等待子进程
cpp 复制代码
int status = 0;
pid_t rid = waitpid(id, &status, 0);  // 阻塞等待
if (rid > 0) {
    lastcode = WEXITSTATUS(status);   // 记录退出状态
}
  • waitpid()

    • 参数1:目标子进程PID。

    • 参数2:存储子进程状态信息。

    • 参数3:选项(0 表示阻塞等待)。

    • 返回值:成功时返回子进程PID,失败返回 -1

  • WEXITSTATUS :从 status 中提取子进程的退出状态(通常 0 表示成功,非零为错误码)。

(3) 全局状态记录
cpp 复制代码
lastcode = WEXITSTATUS(status);  // 保存到全局变量
  • lastcode 用于后续命令(如 echo $? 显示上一条命令的状态)。

12. 主函数

cpp 复制代码
// 主函数
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();  // 执行外部命令
    }
    //cleanup();
    return 0;  // 程序结束
}

这段代码实现了一个简单的 Shell 主函数,负责初始化环境、读取用户输入、解析命令并执行。

1. 主函数流程

(1) 初始化环境
cpp 复制代码
InitEnv();  // 从父Shell继承环境变量
  • 作用 :加载父 Shell 的环境变量(如 PATHHOME),供后续命令使用。

  • 关键点 :需确保 InitEnv 正确处理环境变量拷贝(如使用 getenvsetenv)。

(2) 主循环
cpp 复制代码
while (true) { ... }  // 无限循环直到用户退出
  • 交互模式:持续等待用户输入命令并执行。

2. 循环内步骤解析

(1) 打印提示符
cpp 复制代码
PrintCommandPrompt();  // 显示如 [user@host dir]$
  • 内容 :通常包含用户名、主机名、当前目录(通过 GetUserName()GetHostName()GetPwd() 实现)。
(2) 读取命令
cpp 复制代码
char commandline[COMMAND_SIZE];
if (!GetCommandLine(commandline, sizeof(commandline))) continue;
  • GetCommandLine

    • 使用 fgetsstdin 读取输入。

    • 移除末尾的换行符(\n)。

    • 返回 false 表示空输入或错误(如 EOF)。

(3) 解析命令
cpp 复制代码
if (!CommandParse(commandline)) continue;
  • CommandParse

    • 将输入字符串拆分为 g_argv[] 数组(如 "ls -l"["ls", "-l", NULL])。

    • 更新 g_argc 记录参数个数。

    • 返回 false 表示解析失败(如空命令)。

(4) 检查内置命令
cpp 复制代码
if (CheckAndExecBuiltin()) continue;
  • CheckAndExecBuiltin

    • 匹配 g_argv[0] 是否为内置命令(如 cdecho)。

    • 若是,执行对应函数并返回 true,跳过外部命令执行。

(5) 执行外部命令
cpp 复制代码
Execute();  // 如 ls、gcc 等
  • Execute

    • fork 创建子进程,子进程调用 execvp 执行命令。

    • 父进程通过 waitpid 等待子进程结束,并记录退出状态到 lastcode


三、总结

这段代码实现了一个简易的shell,主要包含以下功能:

  1. 命令行提示符显示

  2. 命令输入和解析

  3. 内建命令处理(cd、echo等)

  4. 外部命令执行

  5. 环境变量管理

关键技术点:

  • 使用fork()execvp()执行外部命令

  • 使用getenv()putenv()管理环境变量

  • 使用strtok()进行命令行解析

  • 使用unordered_map实现别名功能

  • 进程控制和状态管理

这个实现展示了shell的基本工作原理,包括命令解析、内建命令处理和外部命令执行等核心功能。但是详细的其他功能还是要更加深入地学习后再实现,毕竟也不可能写出现实中Linux中的Shell是吧,太复杂啦!!!

相关推荐
十步杀一人_千里不留行5 分钟前
RustDesk 自建中继服务器教程(Mac mini)
运维·服务器·macos
♛暮辞6 分钟前
hadoop(服务器伪分布式搭建)
服务器·hadoop·分布式
水瓶_bxt36 分钟前
虚拟机centos服务器安装
linux·服务器·centos
_可乐无糖41 分钟前
使用 sudo iftop -i 分析服务器带宽使用情况
运维·服务器·网络
赵思空42 分钟前
CentOS7 内网服务器yum修改
linux·运维·服务器
Web极客码1 小时前
如何在服务器上获取Linux目录大小
linux·服务器·javascript
有想法的py工程师1 小时前
Rocky9安装Ansible
linux·运维·ansible
黑屋里的马1 小时前
ssl相关命令生成证书
服务器·网络·ssl·openssl·gmssl
腾讯蓝鲸智云1 小时前
DevOps落地的终极实践:8大关键路径揭秘!
运维·服务器·自动化·云计算·devops
ShuaiLan_hh2 小时前
RHCE第二次作业
运维·服务器