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

🎬 博主简介:

文章目录
前言:
Shell 是 Linux 系统的核心交互工具,本质是一个命令行解释器 —— 接收用户输入、解析命令、创建子进程执行程序(或直接执行内建命令),最后返回执行结果。看似复杂的交互逻辑,核心依赖
进程创建(fork)、程序替换(exec)、进程等待(wait)三大技术,再配合内建命令处理和环境变量管理,就能实现一个基础可用的 Shell。本文带你从零理解 Shell 的运行原理。
一. Shell核心工作流程
一个简易 Shell 的核心逻辑是 “循环 + 五大步骤”,无论是复杂程度如何,底层流程是始终差不多的:
- 打印命令提示符:显示
[用户名@主机名 当前目录]#格式的提示符; - 读取用户命令:通过
fgets获取用户输入的命令行(如ls -l、cd /home); - 解析命令行:将输入字符串分割为命令名和参数(如
ls -l分割为argv[0]="ls"、argv[1]="-l"); - 执行内建命令:对
cd、export等需 Shell 自身执行的命令,直接调用内置函数; - 执行外部命令:对
ls、ps等外部命令,创建子进程,通过程序替换(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)
#pragma once
#include <stdio.h>
// Shell主循环函数
void bash();
#include "myshell.h"
int main()
{
bash(); // 启动Shell
return 0;
}
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>
// 提示符相关:用户名、主机名、当前工作目录
static char username[32];
static char hostname[64];
static char cwd[256];
static char commandLine[256];
// 命令行解析相关:argv存储命令参数,argc为参数个数,sep为分割符
static char* argv[64];
static int argc = 0;
static const char* sep = " ";
// 退出码:记录上一条命令的执行结果(0成功,非0失败)
static int lastCode = 0;
// 环境变量相关:存储Shell的环境变量(从系统bash拷贝+用户export添加)
char** _environ; //方便实现通过声明(_environ)就能直接用环境变量
static int envc = 0; // 环境变量计数
// -------------------------- 环境变量相关函数 --------------------------
// 初始化环境变量:从系统bash拷贝环境变量(如PATH、HOME等)
static void InitEnv()
{
extern char** environ; // 系统环境变量数组(以NULL结尾)
for(envc = 0; environ[envc]; envc++)
{
_environ[envc] = environ[envc];
}
}
// 查找环境变量:返回环境变量值(如查找PATH返回"/bin:/usr/bin")
static char* GetEnv(const char* key)
{
if (key == NULL) return NULL;
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"部分
}
}
return NULL;
}
// 添加环境变量:用于export命令(如export my_value=100)
static void AddEnv(const char* val) // argv[1];
{
_environ[envc] = (char*)malloc(strlen(val) + 1);
strcpy(_environ[envc], val);
_environ[++envc] = NULL;
}
// 打印所有环境变量:用于env命令
static void PrintAllEnv()
{
int i = 0;
for( ; _environ[i]; i++ )
{
printf("%s\n", _environ[i]);
}
}
// -------------------------- 提示符相关函数 --------------------------
// 获取用户名(从环境变量USER读取)
static void GetUserName()
{
char* _username = getenv("USER");
strcpy(username, (_username ? _username : "None"));
}
// 获取主机名(从环境变量HOSTNAME读取)
static void GetHostName()
{
char* _hostname = getenv("HOSTNAME");
strcpy(hostname, (_hostname ? _hostname : "None"));
}
// 获取当前工作目录(简化显示:只显示最后一级目录)
static void GetCmd()
{
// 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]);
}
}
// 打印命令提示符:[用户名@主机名 目录]#
static void PrintPromt()
{
GetUserName();
GetHostName();
GetCmd();
printf("[%s@%s %s]# ", username, hostname, cwd);
fflush(stdout); // 刷新缓冲区,确保提示符及时显示
}
// -------------------------- 命令读取与解析 --------------------------
// 读取用户输入的命令行
static void GetCommandLine()
{
memset(commandLine, 0, sizeof(commandLine));
if (fgets(commandLine, sizeof(commandLine), stdin) != NULL) {
// 去除换行符(fgets会读取回车符'\n')
commandLine[strlen(commandLine) - 1] = '\0';
}
}
// 解析命令行:将输入字符串分割为argv数组
static void ParseCommandLine()
{
argc = 0;
memset(argv, 0, sizeof(argv)); // 清空argv
if (strlen(commandLine) == 0) return; // 判空
// 分割命令和参数(以空格为分隔符)
argv[argc] = strtok(commandLine, sep);
while((argv[++argc] = strtok(NULL, sep))); // argv数组必须以NULL结尾(exec函数要求)
}
// -------------------------- 内建命令处理 --------------------------
// 检查并执行内建命令:返回1表示内建命令,0表示外部命令
static int CheckBuiltinAndExcute()
{
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 $?)
else if(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{
// env
char* 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)
else if (strcmp(argv[0], "export") == 0) {
ret = 1;
if (argc == 2)
{
AddEnv(argv[1]);
}
}
// 4. env命令:打印所有环境变量
else if (strcmp(argv[0], "env") == 0)
{
ret = 1;
PrintAllEnv();
}
// 5. exit命令:退出Shell
else if (strcmp(argv[0], "exit") == 0) {
exit(0);
}
// 非内建命令,返回0
return ret;
}
// -------------------------- 外部命令执行 --------------------------
// 执行外部命令:创建子进程+程序替换
static void Excute()
{
pid_t id = fork(); // 创建子进程
if (id < 0)
{
perror("fork error");
return;
}
else if (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主循环 --------------------------
void bash()
{
// 环境变量相关,方便实现通过声明(_environ)就能直接用环境变量
static char* 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),确保ls、ps等命令能通过 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 终止前台进程)。
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど
更多推荐


所有评论(0)