
目录
前言
Part1. Shell 核心原理前置知识
Part2. 整体代码架构总览
Part3. 模块逐段代码解析
Part3.1. 提示符模块
Part3.2. 读取命令行 GetCommandline
Part3.3. 命令字符串分割 CommandlineParse
Part3.4. 重定向解析模块
Part3.5. Execute (),fork + execvp + dup2 重定向
Part3.6. 内建命令实现
Part3.6.1. cd
Part3.6.2. echo
Part3.6.3. export、alias
Part3.7. 主循环 main
Part4. 结语
前言
前面学了这么多知识,我们可以写一个简易的自定义myshell来复习一波,接下来来跟随小编的视角来看看吧。
let's go!!!!!!!!
Part1. Shell 核心原理前置知识
Shell 本质就是运行在用户态的命令行解释程序,它的核心工作流程固定:
重点区分:内建命令 vs 外部命令 外部命令(ls、cat)是磁盘上的可执行文件,必须 fork+exec; cd 为什么不能 fork 执行?如果 fork 后子进程 chdir,只会修改子进程的工作目录,父 shell 当前目录完全不变,所以 cd 必须在本进程执行。
Part2. 整体代码架构总览
全局变量说明
#define COMMAND_SIZE 1024
char* argv[MAX_ARGV_SIZE]; // 存放解析后的命令参数数组,execvp要求char*[]格式
int argc=0; // 参数个数
int lastcode=0; // 保存上一条命令退出码,对应shell里$?
std::unordered_map<std::string,std::string> alias_table; // 别名表
// 重定向标记
#define NO_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
int redir=0;
std::string file_name; // 重定向目标文件名
Part3. 模块逐段代码解析
Part3.1. 提示符模块
const char* GetPwd()
{
const char* pwd=getcwd(cwd_buf,sizeof(cwd_buf));
if(pwd!=NULL)
{
snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",cwd_buf);
putenv(cwdenv);
}
return pwd==NULL?"None":pwd;
}
- getcwd获取当前进程工作目录;
- 同步更新环境变量PWD,其他程序读取 PWD 时拿到最新路径;
- snprintf拼接提示符字符串;
- fflush(stdout):关键,printf 默认行缓冲,如果输出不带换行,缓冲区不会自动刷出,提示符会卡住不显示,必须手动 fflush。
Part3.2. 读取命令行 GetCommandline
bool GetCommandline(char* out,int size)
{
char* comline=fgets(out,size,stdin);
if(comline==NULL) return false;
out[strlen(out)-1]=0; // 把fgets读到的末尾'\\n'替换成'\\0'
if(strlen(out)==0) return false;
return true;
}
- fgets读取一行输入,会保留回车换行\\n;
- 手动覆盖掉\\n,方便后续字符串解析;
- 返回 false 代表空输入,直接跳过本轮循环。
缺陷:fgets 遇到 Ctrl+D(EOF)返回 NULL,代码仅 return false,没有退出 shell,真实 bash 收到 EOF 直接退出。
Part3.3. 命令字符串分割 CommandlineParse
bool CommandlineParse(char* out)
{
#define SEP " "
argc=0;
argv[argc++]=strtok(out,SEP);
while((bool)(argv[argc++]=strtok(NULL,SEP)));
argc–;
return argc>0?true:false;
}
strtok字符串分割,按空格切分,填充 argv 数组。
注意:strtok 会原地修改原字符串,把分隔符替换成\\0,适合这里解析场景。 致命短板:不支持引号。原生 bash 支持echo "hello world"作为单个参数,strtok 直接按空格切,引号失效;同时连续多个空格可以分割,但 tab 制表符无法识别。
Part3.4. 重定向解析模块
void CheckRedir(char* out)
{
redir=NO_REDIR;
std::string s=out;
if(s.find(">>")!=s.npos)
{
redir=OUTPUT_REDIR;
size_t pos=s.find(">>");
out[pos]=0; // 在>>位置截断原命令字符串,后面留给重定向文件
file_name=s.substr(fpos);
}
else if(s.find('>')!=s.npos)
{
redir=APPEND_REDIR;
// …逻辑类似
}
}
核心思路:
代码里这里有一个 BUG:>>识别后标记为 OUTPUT_REDIR,>识别标记 APPEND_REDIR,宏定义写反了,后续执行逻辑会错乱! 宏定义:OUTPUT_REDIR=2,APPEND_REDIR=3,代码逻辑写反: >>追加 应该绑定 APPEND_REDIR;>覆盖绑定 OUTPUT_REDIR,原代码写反。
Part3.5. Execute (),fork + execvp + dup2 重定向
int Execute()
{
pid_t id=fork();
if(id==0)
{
int fd=-1;
if(redir==INPUT_REDIR)
{
fd=open(file_name.c_str(),O_RDONLY);
dup2(fd,0); // fd复制到标准输入0
close(fd);
}
// > >> 的输出重定向同理dup2(fd,1)标准输出
execvp(argv[0],argv);
exit(1);
}
int status=0;
pid_t rid=waitpid(id,&status,0);
if(rid>0)
{
lastcode=WEXITSTATUS(status);
}
return 0;
}
系统调用拆解:
注意:重定向逻辑只写在外部命令分支,原代码 echo 内建命令没有处理重定向,echo hello > a.txt不会生效。
Part3.6. 内建命令实现
Part3.6.1. cd
bool Cd()
{
if(argc==1) // cd不带参数,切home
{
PREVPATH=getcwd(prevpath_buf,sizeof(prevpath_buf));
chdir(home.c_str());
}
else
{
std::string where=argv[1];
if(where=="-") // cd – 切回上一次目录
{
chdir(PREVPATH);
}
}
}
核心:chdir修改当前进程工作目录,同时保存上一次路径 PREVPATH,实现cd -。
Part3.6.2. echo
支持 echo $?打印上一条退出码,echo $PATH读取环境变量。
限制:仅支持单个参数,echo a b c只会打印第一个参数。
Part3.6.3. export、alias
- export:维护自定义环境变量数组 envs,调用 putenv 注册环境变量;
- alias:unordered_map 存储别名映射;CheckAlias在解析完命令后,查找别名,如果命中,重新解析别名对应的字符串执行。
Part3.7. 主循环 main
while(true)
{
PrintCommandPrompt();
char commandline[COMMANDLINE_SIZE];
if(!GetCommandline(commandline,sizeof(commandline)))
continue;
CheckRedir(commandline);
if(!CommandlineParse(commandline))
continue;
if(CheckAndExecBuiltin())
continue;
if(CheckAlias(commandline))
continue;
Execute();
}
标准事件循环:打印提示符→读命令→解析重定向→分割参数→优先执行内建命令→别名替换→fork 执行外部命令。
Part4. 结语
这篇文章我们知道了自定义myshell的实现,接下来,小编还会带来更多Linux相关的知识,敬请期待~
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!
最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~





