目录
[1. 编译规则](#1. 编译规则)
[2. 清理规则](#2. 清理规则)
[1. GetUserName() - 获取当前用户名](#1. GetUserName() - 获取当前用户名)
[2. GetHostName() - 获取主机名](#2. GetHostName() - 获取主机名)
[3. GetPwd() - 获取当前工作目录](#3. GetPwd() - 获取当前工作目录)
[4.snprintf 函数](#4.snprintf 函数)
[5. GetHome() - 获取家目录](#5. GetHome() - 获取家目录)
[1. 变量声明与初始化](#1. 变量声明与初始化)
[2. 复制系统环境变量](#2. 复制系统环境变量)
[3. 添加测试环境变量](#3. 添加测试环境变量)
[4. 更新全局环境变量](#4. 更新全局环境变量)
[1. 函数功能](#1. 函数功能)
[2. 参数处理逻辑](#2. 参数处理逻辑)
[情况1:无参数(cd 或 cd ~)](#情况1:无参数(cd 或 cd ~))
[3. 关键函数与变量](#3. 关键函数与变量)
[1. 函数功能](#1. 函数功能)
[2. 参数处理逻辑](#2. 参数处理逻辑)
[情况1:参数为 ?](#情况1:参数为 ?)
[情况2:参数为环境变量(以 开头)](#情况2:参数为环境变量(以 开头))
[3. 关键函数与变量](#3. 关键函数与变量)
[1. 函数功能](#1. 函数功能)
[2. 代码逻辑解析](#2. 代码逻辑解析)
[步骤2:查找最后一个 /](#步骤2:查找最后一个 /)
[1. 函数功能概述](#1. 函数功能概述)
[2. 关键组件解析](#2. 关键组件解析)
[(1) MakeCommandLine 函数](#(1) MakeCommandLine 函数)
[(2) PrintCommandPrompt 函数](#(2) PrintCommandPrompt 函数)
[3. 关键函数与变量](#3. 关键函数与变量)
[1. 函数功能](#1. 函数功能)
[2. 代码逻辑解析](#2. 代码逻辑解析)
[1. 函数原型](#1. 函数原型)
[2. 核心行为](#2. 核心行为)
[(1) 读取规则](#(1) 读取规则)
[(2) 安全特性](#(2) 安全特性)
[9. 命令解析](#9. 命令解析)
[1. 函数功能](#1. 函数功能)
[2. 代码逻辑解析](#2. 代码逻辑解析)
[1. 函数原型](#1. 函数原型)
[2. 核心特性](#2. 核心特性)
[(1) 分割机制](#(1) 分割机制)
[(2) 修改原字符串](#(2) 修改原字符串)
[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需要循环执行以下步骤:
- 获取命令行输入
- 解析命令行
- 创建子进程(fork)
- 替换子进程(execvp)
- 父进程等待子进程退出(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
可执行文件(如果存在)
使用方式
-
编译程序 : 直接在终端运行
make
,它会自动执行第一个规则(编译myshell
) -
清理生成的文件 : 运行
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 系统通常设置USER
或USERNAME
环境变量)。 -
如果
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
环境变量。 -
实现:
-
调用
getcwd()
获取当前工作目录(需全局变量char cwd[1024]
)。 -
如果成功,将路径格式化为
PWD=/path/to/cwd
并调用putenv()
更新环境变量。 -
返回目录路径或
"None"
(失败时)。
-
-
注意:
-
cwd
和cwdenv
应为全局缓冲区。 -
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:无参数(cd
或 cd ~
)
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()); // 切换到普通目录
}
}
-
分支逻辑:
-
cd -
:通常用于返回上一个工作目录(需额外实现历史记录功能,如全局变量
std::string prev_dir
)。 -
cd ~
:与无参数
cd
功能重复,可直接调用GetHome()
并切换。 -
普通路径 :
直接调用
chdir
切换目录(如cd /tmp
)。
-
3. 关键函数与变量
-
chdir(const char *path)
:
系统调用,用于改变当前工作目录。成功返回0
,失败返回-1
(错误码在errno
中)。 -
全局变量:
-
g_argc
:命令参数数量(如cd /tmp
时g_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
命令,根据参数类型输出不同的内容。 -
支持的参数类型:
-
$?
:输出上一条命令的退出状态码(lastcode
)。 -
$VAR
:输出环境变量VAR
的值(如$PATH
)。 -
普通字符串 :直接输出字符串内容(如
"hello world"
)。
-
2. 参数处理逻辑
情况1:参数为 $?
cpp
if(opt == "$?") {
std::cout << lastcode << std::endl; // 输出退出状态码
lastcode = 0; // 重置状态码
}
-
行为:
-
输出全局变量
lastcode
(存储上一条命令的退出状态码,通常0
表示成功,非零表示失败)。 -
重置
lastcode
为0
(可能为了后续命令的状态码记录)。
-
-
关键点 :
lastcode
是全局变量,需在其他命令执行时更新(如通过exit
或return
值)。
情况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; // 输出环境变量值
}
-
行为:
-
提取
$
后的变量名(如$PATH
提取为PATH
)。 -
调用
getenv
获取环境变量的值。 -
如果变量存在,输出其值;否则无输出。
-
情况3:普通字符串
cpp
else {
std::cout << opt << std::endl; // 直接输出字符串
}
- 行为 :直接输出参数字符串(如
echo "hello"
输出hello
)。
3. 关键函数与变量
-
getenv(const char *name)
:系统函数,用于获取环境变量的值。存在时返回字符串指针,否则返回
NULL
。 -
全局变量:
-
g_argc
:命令参数个数(echo $PATH
时g_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
:缓冲区大小,防止溢出。
-
-
实现逻辑:
-
调用辅助函数获取信息:
-
GetUserName()
:当前用户名。 -
GetHostName()
:主机名。 -
DirName(GetPwd())
:当前工作目录的最后一级名称 (通过GetPwd()
获取完整路径,DirName
提取最后一部分)。
-
-
使用
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); // 立即刷新输出
}
-
流程:
-
分配固定大小(
COMMAND_SIZE
)的缓冲区prompt
。 -
调用
MakeCommandLine
生成提示符。 -
用
printf
打印提示符。 -
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[]
(参数指针数组)。 -
返回
bool
:true
表示解析成功(至少有一个参数),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 的内置命令(如 cd
、echo
等),若是则执行对应操作并返回处理状态。
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 的环境变量(如
PATH
、HOME
),供后续命令使用。 -
关键点 :需确保
InitEnv
正确处理环境变量拷贝(如使用getenv
和setenv
)。
(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
:-
使用
fgets
从stdin
读取输入。 -
移除末尾的换行符(
\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]
是否为内置命令(如cd
、echo
)。 -
若是,执行对应函数并返回
true
,跳过外部命令执行。
-
(5) 执行外部命令
cpp
Execute(); // 如 ls、gcc 等
-
Execute
:-
fork
创建子进程,子进程调用execvp
执行命令。 -
父进程通过
waitpid
等待子进程结束,并记录退出状态到lastcode
。
-
三、总结

这段代码实现了一个简易的shell,主要包含以下功能:
-
命令行提示符显示
-
命令输入和解析
-
内建命令处理(cd、echo等)
-
外部命令执行
-
环境变量管理
关键技术点:
-
使用
fork()
和execvp()
执行外部命令 -
使用
getenv()
和putenv()
管理环境变量 -
使用
strtok()
进行命令行解析 -
使用
unordered_map
实现别名功能 -
进程控制和状态管理
这个实现展示了shell的基本工作原理,包括命令解析、内建命令处理和外部命令执行等核心功能。但是详细的其他功能还是要更加深入地学习后再实现,毕竟也不可能写出现实中Linux中的Shell是吧,太复杂啦!!!