跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Cjava

用 C 实现一个简易 Linux Shell

用 C 实现简易 Shell 时,核心是三步:用 fgets 读取一行命令,再用 strtok 切分参数,最后通过 fork 和 execvp 执行外部命令。为了保证交互正常,父进程需要用 waitpid 阻塞等待子进程结束。基础版本之外,cd、export 这类内置命令必须在父进程处理,因为它们修改的是 Shell 自身状态;echo $PATH 这类变量展开则可以先用 getenv 取值再输出。

编程诗人发布于 2026/6/300 浏览
用 C 实现一个简易 Linux Shell

简易版:Shell 基础构建

一个 Shell 启动后,先显示提示符,再等用户输入。要把这个动作做出来,最先需要拿到当前环境变量,比如用户名和家目录。这里直接用 getenv() 就够了,头文件是 <stdlib.h>。

命令行输入与解析

Shell 的核心其实就是处理一行字符串。这里用 fgets() 从标准输入读取命令,比如 ls、pwd、touch。读完以后要把末尾的换行去掉,不然后面做比较或者拼接都会别扭。

#define MAX 32
char str[MAX];
fgets(str, sizeof(str) - 1, stdin);
str[strnlen(str, MAX) - 1] = '\0'; // 安全地去除换行符

接下来要把这一整行按空格切成参数数组。strtok() 正好适合干这个活,不过它有个老毛病:会直接改原字符串,把分隔符写成 \0,而且调用方式也有点绕,第一次传原串,后面继续切就传 NULL。

#include <string.h>
char* argv[MAX] = { NULL };
const char* delim = " \t\n"; // 常见的空白符作为分隔符
int i = 0;
argv[i++] = strtok(str, delim);
while (argv[i - 1]) {
    argv[i++] = strtok(NULL, delim);
}

进程创建与执行

拿到参数数组后,就可以把命令真正跑起来了。这里的主线很固定:先 fork() 出子进程,再在子进程里用 execvp() 覆盖当前进程映像。

pid_t d = fork();
if (d == 0) {
    // 子进程逻辑
    if (argv[0] == NULL) return 0;
    int count = execvp(argv[0], argv);
    if (count < 0) perror("execvp failed");
    exit(EXIT_FAILURE);
} else {
    // 父进程逻辑
    waitpid(-1, NULL, 0); // 阻塞等待子进程结束
}

这里用阻塞等待,交互会更稳:子进程没结束,父进程就不急着进入下一轮循环。做一个简单 Shell,这样最省事,也最不容易把控制流搞乱。真要做成更完整的 Shell,再考虑非阻塞和作业控制。

代码封装

把上面的逻辑拆成几个函数,结构会清楚很多。变量作用域要收好,尤其是全局的 delim 和 argv,别让输入解析和执行过程互相踩。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define LEFT "["
#define RIGHT "]"
#define MAX 32

const char* delim = " \t\n";
char str[MAX];
char* argv[MAX] = { NULL };

// 打印提示符
void command_printf() {
    printf(LEFT "%s"@"%s" "#" RIGHT " ", getenv("USER"), getenv("HOME"));
}

// 获取输入
char* command_get(char str[MAX], int size) {
    char* pc = fgets(str, size, stdin);
    if (pc) {
        size_t len = strlen(str);
        if (len > 0 && str[len - 1] == '\n') str[len - 1] = '\0';
    }
    return pc;
}

// 解析参数
void command_extraction(char str[MAX], const char* delim, char* argv[MAX]) {
    int i = 0;
    argv[i++] = strtok(str, delim);
    while (argv[i - 1]) {
        argv[i++] = strtok(NULL, delim);
    }
}

// 执行命令
void command_use(char* argv[MAX]) {
    pid_t d = fork();
    if (d == 0) {
        if (argv[0] == NULL) return;
        int count = execvp(argv[0], argv);
        if (count < 0) perror("execvp failed");
        exit(EXIT_FAILURE);
    } else {
        waitpid(-1, NULL, 0);
    }
}

int main() {
    while (1) {
        command_printf();
        char* pc = command_get(str, sizeof(str));
        if (!pc) break;
        command_extraction(str, delim, argv);
        command_use(argv);
    }
    return 0;
}

运行起来后,Shell 能回显并执行外部命令:

文章配图


挑战版:内置命令处理

基础版做完以后,很快会碰到 cd 和 echo $PATH 这种命令不太对劲。原因也不复杂:Shell 里的命令分两类。

  1. 外部命令:像 ls、cat,本质上是磁盘里的可执行文件,走 fork + exec 就行。
  2. 内置命令:像 cd、exit、export,它们要改的是 Shell 自己的状态,不能扔到子进程里做。

解决 cd 指令

cd 改的是当前工作目录。如果放到子进程里执行,子进程改完就退出了,父进程还是停在原来的目录。这个坑很常见,所以 cd 必须在父进程里直接调用 chdir()。

if (strcmp(argv[0], "cd") == 0 && argc == 2) {
    if (strcmp(argv[1], "./") == 0) return; // 不做任何操作
    const char* path = argv[1];
    if (chdir(path) == -1) {
        printf("路径执行错误\n");
        return;
    }
}

解决 echo 命令

echo $PATH 这类输入,Shell 先要做变量展开,把 $PATH 换成真实值,比如 /usr/local/sbin:...,再交给程序或者直接打印。这里的处理方式比较直接:如果参数以 $ 开头,就拿变量名去 getenv() 查。

if (strcmp(argv[0], "echo") == 0 && argc == 2) {
    char* pc = argv[1];
    if (pc[0] == '$' && strlen(pc) > 1) {
        char* var_name = pc + 1;
        char* value = getenv(var_name);
        if (value) argv[1] = value;
    }
}

这样以后,echo 就能正常输出环境变量内容了。像 export 这类命令也可以照着这个思路继续补,只是边界会多一点,重点还是要分清:哪些东西只是给子进程用,哪些东西真的要改 Shell 自己的环境。

目录

  1. 简易版:Shell 基础构建
  2. 命令行输入与解析
  3. 进程创建与执行
  4. 代码封装
  5. 挑战版:内置命令处理
  6. 解决 cd 指令
  7. 解决 echo 命令
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 用 Python 读取和查看 NPZ 文件
  • 昇腾平台上的 Llama-2-7b 部署与测评记录
  • 6 篇新论文:记忆、长上下文、医疗评测与机器人策略
  • libpqxx 安装与配置实战
  • LLaMA-Factory 微调入门与实战配置
  • Jenkins 和 GitLab CI 该怎么选
  • 在 AutoDL 上用 LLaMA-Factory 微调 GPT-OSS-20B
  • Java 内部类:实例、静态、匿名和局部写法
  • 修复 Copilot 和 Codex 写入中文乱码
  • 海康机器人 3D 激光轮廓仪调试记录
  • ES6 的三个常用新特性:进制、Symbol 和 Class
  • 浏览器端播放 H.265:WebAssembly、FFmpeg 与 WebCodecs 的组合方案
  • 用 Uptime Kuma 和 cpolar 做远程服务器监控
  • Mac Mini M4 本地部署 AI 模型:Ollama 与 Stable Diffusion 实操
  • 在 Windows 上运行 Python 脚本
  • 链表三题:环检测、数组交集与随机链表复制
  • KoboldAI 安装、配置与使用要点
  • 10 篇 VLA 论文,看机器人视觉语言动作模型怎么演进
  • Boost.Filesystem 清洗 HTML 文档并生成索引数据
  • 用 Python 生成 Node.js 项目结构的桌面工具

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online