🎬 胖咕噜的稞达鸭 :个人主页
🔥 个人专栏 : 《数据结构》《C++初阶高阶》
《Linux系统学习》
《算法入门》
⛺️技术的杠杆,撬动整个世界!

shell的原理:
以环境变量继承为基础,通过「提示符显示→命令读取→解析→内建命令执行 / 外部命令 fork+exec 启动子进程」的无限闭环,模拟命令交互与执行能力。
头文件和宏定义:
cpp
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <unordered_map>//哈希表,用来存储别名映射:alias 名称-原始命令
#define COMMAND_SIZE 1024//命令行/提示符的最大长度
#define FORMAT "[%s@%s %s]输入命令 "
//命令提示符格式([用户名@主机名 当前目录]输入命令)
全局变量定义
shell命令行解释器有两张表:
- 一张是命令行参数表,存储解析之后的命令和参数;(我们宏定义命令行参数,一行命令最多有128个参数,而且每一个参数都要封装在一个参数数组中,初始化每一个参数的值都是0);
- 一张是环境变量表,用于shell自身维护的环境变量的集合(我们宏定义环境变量个数是100个,都被封装在一个环境变量数组中,初始化环境变量的个数为0)
- 映射表的存在意义是:
alias_list是一个C++哈希表,存储shell的命令别名(映射关系),就是给常用命令起一个小名。ls -l 简化为ll
cpp
// 下面是shell定义的全局数据
// 1. 命令行参数表
#define MAXARGC 128//最大参数个数(如ls -a -l 是3个参数)
char *g_argv[MAXARGC];//参数数组
int g_argc = 0; //实际参数个数(如ls -a -l 对应g_argc=3)
// 2. 环境变量表(shell自身维护的环境变量集合)
#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];
// last exit code
int lastcode = 0;
获取用户名,主机名,家目录,当前工作目录等
cpp
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 = getenv("PWD");
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;
}
cpp
// getcwd:系统调用,获取真实当前目录
getcwd:系统调用,获取真实当前目录
格式:PWD=%s,环境变量的标准格式是键=值,举例子:如果cwd是/home/user,格式化cwdenv之后是:PWD = home/user;
接着更新系统环境变量PWD,putenv(cwdenv)的作用是把格式化之后的"PWD=/home/user"注册到系统中,覆盖原来的PWD环境变量。
snprintf中的四个参数意义:
bash
snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",cwd);
第一个参数cwdenv目标缓冲区,
第二个参数缓冲区的最大容量,
第三个参数格式化输入的字符串,
第四个参数替换占位符的实际内容
为什么getcwd有两个参数?
getcwd,第一个参数是存放目录路径的缓冲区,第二个参数可以判断这个命令输入的内容是否超过了缓冲区大小,导致内存溢出。
为什么要判断pwd是否为空?
如果直接格式化PWD,如果当前目录的路径长度超过size()第二个参数,getcwd写不下完整的路径,就会返回NULL;或者是权限问题,普通用户状态下无法执行超级用户下的某些指令,getcwd无法获取路径,就会返回NULL;
这样getcwd一旦执行失败,cwd中的内容是随机的,会导致PWD环境变量被设置为无效值,甚至程序奔溃。
环境变量初始化
为什么要环境变量初始化?
环境变量初始化的本质是为自定义shell建立独立而且可控的环境变量体系,避免直接依赖父shell传递的environ(系统全局环境变量数组):
- 父 Shell 的
environ是只读或共享的,直接修改可能导致权限错误; - 无法灵活添加、删除、修改环境变量。
cpp
void InitEnv()
{
extern char **environ;//1.声明系统全局环境变量数组
memset(g_env, 0, sizeof(g_env));//2.清空自定义环境变量数组
g_envs = 0;//3.重置环境变量计数
//本来要从配置文件来
//核心步骤:从父 Shell 传递的 environ 复制环境变量到自定义 g_env(深拷贝)
//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;
}
内建命令实现
Cd命令实现:
cpp
//command
bool Cd()
{
// cd argc = 1
if(g_argc == 1)//已经解析出有效的命令名(g_argv[0] == "cd"),g_argc == 0是不可能出现的场景//情况1:输入cd,没有参数
{
std::string home = GetHome();//获取家目录
if(home.empty()) return true;//家目录获取失败直接返回
chdir(home.c_str());//系统调用,切换工作目录(参数为c字符串)
}
else//输入:cd + 参数
{
std::string where = g_argv[1];//获取目标目录参数
// cd - / cd ~
if(where == "-")//切换到上一级目录
{
// Todu
}
else if(where == "~")//切换到家目录
{
// Todu
}
else//直接切换到目录路径
{
chdir(where.c_str());
}
}
return true;
}
内建echo命令的实现函数,无返回值(内建命令执行结果通过输出体现)
g_argc== 2:仅仅处理一个参数的场景(命令名echo+1个参数,总个数为2)
适用于三种场景:
echo $?上一个命令的退出码;echo $PATH输出环境变量xxx的值echo+普通字符串
cpp
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;// 输出后重置退出码(避免重复使用,符合原生 Shell 行为)
}
else if(opt[0] == '$')
{
std::string env_name = opt.substr(1);//截取$后面的环境变量名($PATH->path)
const char *env_value = getenv(env_name.c_str());//读取环境变量值
if(env_value)//读取出来环境变量存在就输出
std::cout << env_value << std::endl;
}
else
{
std::cout << opt << std::endl;
}
}
}
辅助函数:构造命令提示符
- 提取目录名
cpp
// / /a/b/c
std::string DirName(const char *pwd)
{
#define SLASH "/"
// 路径分隔符(Linux 下为 /)
std::string dir = pwd;
if(dir == SLASH) return SLASH;//特殊情况:路径是根目录(/),直接返回 /
auto pos = dir.rfind(SLASH);
// 从字符串末尾查找第一个 / 的位置(如 /home/user → 5)
if(pos == std::string::npos) return "BUG?";
return dir.substr(pos+1);
}
- 构造提示符字符串
char cmd_prompt[]:让函数能把生成的提示符 "交出来" 的结果容器
int size:防止容器装不下导致内存溢出的安全保障
cpp
void MakeCommandLine(char cmd_prompt[], int size)
{
snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
}
- 打印命令行提示符
cpp
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
// 存储提示符的缓冲区
MakeCommandLine(prompt, sizeof(prompt));
printf("%s", prompt);
fflush(stdout);
}
命令读取与解析
- 读取用户输入
cpp
//读取用户输入的命令
bool GetCommandLine(char* out,int size)
{
char* c = fgets(out,size,stdin);//将fgets:从键盘stdin读取用户输入,存到out中
if(c ==NULL)return false;
out[strlen(out)-1]=0;//去掉fgets读取的换行符
if(strlen(out) ==0)return false;//用户只按回车,返回false
return true;
}
- 命令解析:
cpp
// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " " // 命令参数分隔符(空格)
g_argc = 0; // 重置参数计数器(每次解析前清空)
// 第一次调用 strtok:以 SEP(空格)分割 commandline,获取第一个参数(命令名)
g_argv[g_argc++] = strtok(commandline, SEP);
// 循环调用 strtok:后续传入 NULL,继续分割上一次的字符串,获取后续参数
while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
// 上面的循环会多计数一次(最后一次 strtok 返回 NULL,g_argc 仍+1),这里修正
g_argc--;
return g_argc > 0 ? true : false; // 若解析到有效参数(至少有命令名),返回 true
}
辅助函数:打印参数
cpp
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);
}
内建命令检测
cpp
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")
{
}
else if(cmd == "alias")
{
// std::string nickname = g_argv[1];
// alias_list.insert(k, v);
}
return false;
}
外部命令执行
cpp
int Execute()
{
pid_t id = fork();
if(id == 0)//判断是不是子进程,fork()子进程会返回0
{
//child
execvp(g_argv[0], g_argv);//替换当前进程的代码段,数据段,堆栈,执行指定的外部程序
exit(1);
}
int status = 0;//用于存储waitpid获取的子进程退出状态
//父进程在这里阻塞,直到子进程执行完毕,无论是execvp成功执行后退出还是失败后exit(1),才会执行后续代码。
// father
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
}
//只有子进程 "正常终止"(比如exit(n)、return n)时,WEXITSTATUS才有意义;如果子进程被信号终止(比如kill -9),WEXITSTATUS返回 0。
return 0;
}
调用fork()之后,操作系统就会复制当前进程(父进程)的内存空间,上下文等,形成一个子进程。
返回值规则:
父进程中返回子进程的PID;
子进程中返回0;
id变量在父子进程中存储不同的值,是区分父子进程的关键。
g_argv[0]:要执行的程序路径 / 名称(比如"ls"、"/bin/ps");
g_argv:字符串数组,是传递给新程序的命令行参数(格式要求:最后一个元素必须是NULL,比如{"ls", "-l", NULL})。
g_argv:全局变量(从变量名前缀g_可判断),存储命令行参数列表。
核心逻辑 :
子进程调用execvp后,自身的代码会被g_argv[0]指定的程序替换;
如果execvp执行成功,子进程后续的代码(比如exit(1))永远不会执行 ;
如果execvp执行失败(比如程序不存在、权限不足),才会执行后续的exit(1)。
主函数
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,需要循环以下过程:
- 获取命令行
- 解析命令行
- 建立一个子进程
(fork) - 替换子进程
(execvp) - 父进程等待子进程退出
(wait)
根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了。
完整源码:
cpp
#include <iostream>
#include <cstdio>
#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[1024];
// 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 = getenv("PWD");
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 == "-")
{
// Todu
}
else if(where == "~")
{
// Todu
}
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 << 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")
{
}
else if(cmd == "alias")
{
// std::string nickname = g_argv[1];
// alias_list.insert(k, v);
}
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();
}
//cleanup();
return 0;
}
