从零手搓实现 Linux 简易 Shell:内建命令 + 环境变量 + 程序替换全解析

从零手搓实现 Linux 简易 Shell:内建命令 + 环境变量 + 程序替换全解析
在这里插入图片描述

🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:

在这里插入图片描述

文章目录


前言:

Shell 是 Linux 系统的核心交互工具,本质是一个命令行解释器 —— 接收用户输入、解析命令、创建子进程执行程序(或直接执行内建命令),最后返回执行结果。看似复杂的交互逻辑,核心依赖 进程创建(fork)、程序替换(exec)、进程等待(wait) 三大技术,再配合内建命令处理和环境变量管理,就能实现一个基础可用的 Shell。本文带你从零理解 Shell 的运行原理。

一. Shell核心工作流程

一个简易 Shell 的核心逻辑是 “循环 + 五大步骤”,无论是复杂程度如何,底层流程是始终差不多的:

  • 打印命令提示符:显示 [用户名@主机名 当前目录]# 格式的提示符;
  • 读取用户命令:通过 fgets 获取用户输入的命令行(如 ls -lcd /home);
  • 解析命令行:将输入字符串分割为命令名和参数(如 ls -l 分割为 argv[0]="ls"、argv[1]="-l");
  • 执行内建命令:对 cdexport 等需 Shell 自身执行的命令,直接调用内置函数;
  • 执行外部命令:对 lsps 等外部命令,创建子进程,通过程序替换(exec)加载执行,父进程等待子进程退出。
在这里插入图片描述
💡关键区别:内建命令(如 cd)必须由 Shell 进程自身执行(修改 Shell 的工作目录),无法通过子进程执行;外部命令(如 ls)需创建子进程执行,避免影响 Shell 主进程。

二. 完整实现源代码

2.1 Makefile文件

mybash:myshell.c main.c gcc -o $@ $^ .PHONY:clean clean: rm -rf mybash 

2.2 头文件(myshell.h)和 主函数(main.c)

#pragmaonce#include<stdio.h>// Shell主循环函数voidbash();
#include"myshell.h"intmain(){bash();// 启动Shellreturn0;}

2.3 核心实现(myshell.c 优化版)

#include"myshell.h"#include<stdlib.h>#include<unistd.h>#include<string.h>#include<sys/stat.h>#include<sys/wait.h>#include<ctype.h>// 提示符相关:用户名、主机名、当前工作目录staticchar username[32];staticchar hostname[64];staticchar cwd[256];staticchar commandLine[256];// 命令行解析相关:argv存储命令参数,argc为参数个数,sep为分割符staticchar* argv[64];staticint argc =0;staticconstchar* sep =" ";// 退出码:记录上一条命令的执行结果(0成功,非0失败)staticint lastCode =0;// 环境变量相关:存储Shell的环境变量(从系统bash拷贝+用户export添加)char** _environ;//方便实现通过声明(_environ)就能直接用环境变量staticint envc =0;// 环境变量计数// -------------------------- 环境变量相关函数 --------------------------// 初始化环境变量:从系统bash拷贝环境变量(如PATH、HOME等)staticvoidInitEnv(){externchar** environ;// 系统环境变量数组(以NULL结尾)for(envc =0; environ[envc]; envc++){ _environ[envc]= environ[envc];}}// 查找环境变量:返回环境变量值(如查找PATH返回"/bin:/usr/bin")staticchar*GetEnv(constchar* key){if(key ==NULL)returnNULL;int key_len =strlen(key);for(int i =0; env[i]!=NULL; i++){// 环境变量格式:"KEY=VALUE",找到"KEY="开头的条目if(strncmp(env[i], key, key_len)==0&& env[i][key_len]=='='){return env[i]+ key_len +1;// 返回"VALUE"部分}}returnNULL;}// 添加环境变量:用于export命令(如export my_value=100)staticvoidAddEnv(constchar* val)// argv[1];{ _environ[envc]=(char*)malloc(strlen(val)+1);strcpy(_environ[envc], val); _environ[++envc]=NULL;}// 打印所有环境变量:用于env命令staticvoidPrintAllEnv(){int i =0;for(; _environ[i]; i++){printf("%s\n", _environ[i]);}}// -------------------------- 提示符相关函数 --------------------------// 获取用户名(从环境变量USER读取)staticvoidGetUserName(){char* _username =getenv("USER");strcpy(username,(_username ? _username :"None"));}// 获取主机名(从环境变量HOSTNAME读取)staticvoidGetHostName(){char* _hostname =getenv("HOSTNAME");strcpy(hostname,(_hostname ? _hostname :"None"));}// 获取当前工作目录(简化显示:只显示最后一级目录)staticvoidGetCmd(){// char* _cwd = getenv("PWD");// strcpy(cwd, (_cwd ? _cwd : "None"));char _cwd[256];getcwd(_cwd,sizeof(_cwd));if(strcmp(_cwd,"/")==0){strcpy(cwd, _cwd);}else{int end =strlen(_cwd)-1;//while(end >= 0)//{// if(_cwd[end] == '/')// {// strcpy(cwd, &_cwd[end + 1]);// break;//}//end--;//}// 从后往前找 '/',截取最后一级目录while(end >=0&& _cwd[end]!='/') end--;strcpy(cwd,&_cwd[end +1]);}}// 打印命令提示符:[用户名@主机名 目录]#staticvoidPrintPromt(){GetUserName();GetHostName();GetCmd();printf("[%s@%s %s]# ", username, hostname, cwd);fflush(stdout);// 刷新缓冲区,确保提示符及时显示}// -------------------------- 命令读取与解析 --------------------------// 读取用户输入的命令行staticvoidGetCommandLine(){memset(commandLine,0,sizeof(commandLine));if(fgets(commandLine,sizeof(commandLine),stdin)!=NULL){// 去除换行符(fgets会读取回车符'\n') commandLine[strlen(commandLine)-1]='\0';}}// 解析命令行:将输入字符串分割为argv数组staticvoidParseCommandLine(){ argc =0;memset(argv,0,sizeof(argv));// 清空argvif(strlen(commandLine)==0)return;// 判空// 分割命令和参数(以空格为分隔符) argv[argc]=strtok(commandLine, sep);while((argv[++argc]=strtok(NULL, sep)));// argv数组必须以NULL结尾(exec函数要求)}// -------------------------- 内建命令处理 --------------------------// 检查并执行内建命令:返回1表示内建命令,0表示外部命令staticintCheckBuiltinAndExcute(){int ret =0;// 1. cd命令:切换工作目录(必须内建,子进程切换不影响Shell)if(strcmp(argv[0],"cd")==0){ ret =1;if(argc ==2){chdir(argv[1]);}}// 2. echo命令:打印字符串或环境变量(如echo $PATH、echo $?)elseif(strcmp(argv[0],"echo")==0){ ret =1;if(argc ==2){if(argv[1][0]=='$'){if(strcmp(argv[1],"$?")==0){printf("%d\n", lastCode); lastCode =0;}else{// envchar* val =GetEnv(argv[1]+1);// 跳过'$',获取变量值if(val !=NULL){printf("%s\n", val);}else{printf("\n");// 变量不存在,输出空行}}}// 打印普通字符串else{printf("%s\n", argv[1]);}}}// 3. export命令:添加环境变量(如export MY_VAR=123)elseif(strcmp(argv[0],"export")==0){ ret =1;if(argc ==2){AddEnv(argv[1]);}}// 4. env命令:打印所有环境变量elseif(strcmp(argv[0],"env")==0){ ret =1;PrintAllEnv();}// 5. exit命令:退出Shellelseif(strcmp(argv[0],"exit")==0){exit(0);}// 非内建命令,返回0return ret;}// -------------------------- 外部命令执行 --------------------------// 执行外部命令:创建子进程+程序替换staticvoidExcute(){ pid_t id =fork();// 创建子进程if(id <0){perror("fork error");return;}elseif(id ==0){// 子进程:程序替换(execvp自动搜索PATH环境变量)execvp(argv[0], argv);exit(1);// 子进程退出,退出码1}else{// 父进程:等待子进程退出,获取退出码int status =0; pid_t rid =waitpid(id,&status,0);(void)rid; lastCode =WEXITSTATUS(status);}}// -------------------------- Shell主循环 --------------------------voidbash(){// 环境变量相关,方便实现通过声明(_environ)就能直接用环境变量staticchar* env[64]; _environ = env;// 除此以外我们还可以通过一个数组存储本地变量// 以及可以通过一个来存储别名…// 初始化环境变量(从系统拷贝)InitEnv();while(1){// 第一步: 输出提示命令行PrintPromt();// 第二步: 等待用户输入, 获取用户输入GetCommandLine();// 第三步: 解析字符串,"ls -a -l" -> "ls" "-a" "-l"ParseCommandLine();if(argc ==0)continue;// 第四步: 有些命令, cd echo env等等不应该让子进程执行// 而是让父进程自己执行,这些是内建命令. bash内部的函数if(CheckBuiltinAndExcute())continue;// 第五步: 执行命令Excute();}}
  • 原版代码lesson22/myshell.c
  • 注意:大家可以自己下去测试一下使用这个简易版的Shell去进行一些指令操作。

补充Excute里面进行执行命令的权限设置相关操作,大家可以自己去试一下。

在这里插入图片描述

三. 核心功能解析

3.1 环境变量管理(补充重点)

环境变量是 Shell 的重要特性,本文实现了完整的环境变量生命周期管理:

  • 初始化InitEnv() 从系统 environ 数组拷贝环境变量(如 PATH、HOME、USER),确保 lsps 等命令能通过 PATH 找到执行文件;
  • 添加 / 覆盖AddEnv() 用于 export 命令,支持新增环境变量或覆盖已有变量(如 export PATH=$PATH:/my/bin);
  • 查找GetEnv() 用于 echo $KEY 场景,根据键名查找环境变量值;
  • 打印PrintAllEnv() 用于 env 命令,打印所有环境变量。

3.2 内建命令实现

  • cd:调用 chdir() 系统调用切换工作目录,必须内建(子进程切换目录不影响 Shell 主进程);
  • echo:支持打印普通字符串、退出码($?)和环境变量($PATH);
  • export:调用 AddEnv() 添加环境变量;
  • env:调用 PrintAllEnv() 打印所有环境变量;
  • exit:调用 exit(0) 退出 Shell。

3.3 外部命令执行

  • 进程创建fork() 创建子进程,避免外部命令执行影响 Shell 主进程;
  • 程序替换execvp() 自动搜索 PATH 环境变量,无需写命令全路径(如 ls 无需 /bin/ls);
  • 进程等待waitpid() 等待子进程退出,获取退出码并存储到 lastCode,供 echo $? 使用。

3.4 关键技术点总结

  • 程序替换本质execvp 会替换子进程的代码和数据段,加载新程序执行,进程 PID 不变;
  • 内建命令必要性:修改 Shell 自身状态的命令(如 cd、export)必须内建,子进程执行无法影响父进程;
  • 环境变量格式:环境变量数组必须以 NULL 结尾,exec 系列函数依赖此格式解析参数;
  • 命令解析细节:需处理连续空格、开头空格等异常输入,确保解析后的 argv 符合规范。

四. 思考:函数和进程之间的相似性

exec/exit 就像 call/return

  • 一个 C 程序有很多函数组成。一个函数可以调用另一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过 call/return 系统进行通信。
  • 这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间。如下图:
在这里插入图片描述
  • 一个 C 程序可以 fork/exec 另一个程序,并传给它一些参数。这个调用的程序执行一定的操作,然后通过 exit(n) 来返回值。调用它的进程可以通过 wait(&ret) 来获取exit的返回值。

结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 

结语:本文实现的简易 Shell 虽不及 bash 功能强大,但覆盖了核心工作原理 —— 通过 fork 创建子进程、exec 替换程序、wait 等待结果,再配合内建命令和环境变量管理,就能完成与用户的交互。在此基础上,还可扩展更多功能:支持管道(ls | grep txt)、重定向(ls > log.txt)、后台运行(sleep 10 &)等。如果需要进一步优化,可重点关注命令解析的健壮性(如支持引号包含空格的参数)和信号处理(如 Ctrl+C 终止前台进程)。

✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど

Read more

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

摘要:本文聚焦OpenClaw从测试环境走向生产环境的核心痛点,围绕“性能优化、安全加固、监控运维”三大维度展开实操讲解。先明确生产环境硬件/系统选型标准,再通过硬件层资源管控、模型调度策略、缓存优化等手段提升响应速度(实测响应效率提升50%+);接着从网络、权限、数据三层构建安全防护体系,集成火山引擎安全方案拦截高危操作;最后落地TenacitOS可视化监控与Prometheus告警体系,配套完整故障排查清单和虚拟实战案例。全文所有配置、代码均经实测验证,兼顾新手入门实操性和进阶读者的生产级部署需求,帮助开发者真正实现OpenClaw从“能用”到“放心用”的跨越。 优质专栏欢迎订阅! 【DeepSeek深度应用】【Python高阶开发:AI自动化与数据工程实战】【YOLOv11工业级实战】 【机器视觉:C# + HALCON】【大模型微调实战:平民级微调技术全解】 【人工智能之深度学习】【AI 赋能:Python 人工智能应用实战】【数字孪生与仿真技术实战指南】 【AI工程化落地与YOLOv8/v9实战】【C#工业上位机高级应用:高并发通信+性能优化】 【Java生产级避坑指南:

By Ne0inhk
ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》 ❄专栏传送门: 《freertos专栏》《STM32 HAL库专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言  一、实验基础说明 1.1、互斥体简介 1.2 本次实验设计思路 二、硬件原理分析(看过之前博客的可以忽略) 三、实验程序编写 3.1 互斥体 LED 驱动代码(mutex.c) 3.2.1、设备结构体定义(28-39

By Ne0inhk
Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 后端工程师扔给你一个 Swagger (OpenAPI) 文档地址,你会怎么做? 1. 对着文档,手写 Dart Model 类(容易写错字段类型)。 2. 手写 Retrofit/Dio 的 API 接口定义(容易拼错 URL)。 3. 当后端修改了字段名,你对着报错修半天。 这是重复劳动的地狱。 swagger_dart_code_generator 可以将 Swagger (JSON/YAML) 文件直接转换为高质量的 Dart 代码,包括: * Model 类:支持 json_serializable,带 fromJson/

By Ne0inhk
Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

文章目录 * 前言 * make/makefile * 文件的三个时间 * Linux第一个小程序-进度条 * 回车和换行 * 缓冲区 * 程序的代码展示 * git指令 * 关于gitee * Linux调试器-gdb使用 * 作业部分 前言 做 Linux 开发时,你是不是也遇到过这些 “卡脖子” 时刻?写 makefile 时,明明语法没错却报错,最后发现是依赖方法行没加 Tab;想提交代码到 gitee,记不清 git add/commit/push 的 “三板斧”,还得反复搜教程;用 gdb 调试程序,输了命令没反应,才想起编译时没加-g生成 debug 版本;甚至连写个进度条,都搞不懂\r和\n的区别,导致进度条乱跳…… 其实这些问题,

By Ne0inhk