【寻找Linux的奥秘】第九章:自定义SHELL

【寻找Linux的奥秘】第九章:自定义SHELL
QQ20250530-182747


请君浏览

前言

本专题将基于Linux操作系统来带领大家学习操作系统方面的知识以及学习使用Linux操作系统。前面我们认识并熟悉了进程的基本概念以及操作,那么本章让我们对前面所学进行融会贯通,来自定义编写一下我们使用的命令行解释器,也就是shell。本章我们要学习的是——自定义shell的编写。

1.目标

Shell 是一种用于与操作系统交互的命令行界面程序。它充当用户和操作系统内核之间的中介,通过用户输入的命令来执行操作,提供与操作系统的互动。

具体来说,Shell 可以做以下几件事:

  • 命令解释和执行:Shell 接受用户输入的命令,解析并传递给操作系统的内核执行。例如,用户可以通过 Shell 执行文件操作(如复制、移动、删除文件)、程序启动、系统管理等操作。
  • 脚本编程:Shell 还支持脚本编程,允许用户将一系列命令写入一个文件中,形成一个脚本(如 Bash 脚本)。这些脚本可以自动化任务,执行复杂的操作。
  • 交互式环境:Shell 提供一个交互式环境,用户可以在其中执行命令、查看命令输出、管理系统进程等。

我们在Linux中最常用的shell就是Bash(Bourne Again Shell)。今天我们将在bash上实现一个自定义的shell,不过由于所学知识有限,我们的自定义shell主要可以进行命令解释和执行,来贯通一下我们之前所学的内容,并且对这些知识有更深刻地认识。

下面来介绍一下我们实现自定义shell的目标:

  • 能处理普通命令
  • 能处理内建命令
  • 帮助我们理解内建命令、本地变量、环境变量这些概念
  • 帮助我们理解shell的运行原理

2. 运行原理

既然要实现一个自定义shell,那么我们就需要先知道shell的运行原理,从而搭出一个大致的框架。

下面以这个与shell典型的互动为例:

QQ20250601-113423

下图的用时间轴来表⽰事件的发⽣次序。其中时间从左向右。shell由标识为bash的⽅块代表,它随着时间的流逝从左向右移动。shell从⽤⼾读⼊字符串"ls"后建⽴⼀个新的子进程,然后在子进程中运⾏ls并等待子进程结束,结束后读入"ps"同理。 如下图所示:

QQ20250601-114826

因此,shell读取一行输入,建立一个新的子进程,在子进程中运行指定程序并等待子进程结束,然后循环往复。

所以我们要写一个自定义shell,就需要循环下列过程:

  • 获取命令⾏
  • 解析命令⾏
  • 建⽴⼀个⼦进程(fork)
  • 替换⼦进程(exec)
  • ⽗进程等待⼦进程退出(wait)

根据这个过程,我们的框架就有了,更多的细节让我们在实现的过程中再一一发现并解决。

3. 实现

像我们使用的shell底层是用C语言编写的,为了简化一些较为繁琐的操作,接下来我们的编写采用C、C++混编,主体以C语言为主,对于特定的地方也会使用C++的一些语法。

我们实现的shell较为简单,这里就不分文件进行编写,都在一个.cc文件(Linux传统C++文件后缀)中进行编写。

接下来让我们创建好Makefile文件,方便我们进行编译:

QQ20250601-143607

最后,创建我们的myshell.cc文件,开始编写我们的程序:

QQ20250601-143631

3.1 打印命令行提示符

首先观察我们的使用的shell,可以发现shell总是会先打印出命令行提示符,然后处于阻塞状态等待用户输入。所以我们的第一步就是打印命令行提示符:

intmain(){while(true){//1.输出命令行提示符 PrintCommandPrompt();}return0;}

分析一下命令行提示符,我们发现它是由[用户名@主机名 当前工作目录]$组成的,并且用户名、主机名、当前工作目录我们都可以从环境变量中获取,那么打印命令行提示符就变得非常简单了,我们需要用到之前提过的getenv函数:

QQ20250601-145134
//包一下头文件,以C++的风格#include<cstdlib>//获得当前工作路径constchar*GetPwd(){constchar*pwd =getenv("PWD");return pwd ==NULL?"None": pwd;}//获得当前主机名constchar*GetHostName(){constchar*hostname =getenv("HOSTNAME");return hostname ==NULL?"None": hostname;}//获得当前用户名constchar*GetUser(){constchar*user =getenv("USER");return user ==NULL?"None": user;}
我们的自定义shell是在bash上创建的,所以当执行时我们的自定义shell就是bash的一个子进程,所以我们自定义shell的环境变量表是继承于bash的。

此时我们的需要把获得的当前工作路径进行拆分,获得当前工作目录,我们借助C++中stringrfind函数和substr函数可以很轻松的做到。其中rfind用于倒着从字符串查找,substr用来裁剪字符串,我们只需要使用rfind找到第一个’/'的位置,然后用substr把相对应的位置裁剪出来即可:

//对于查找的字符我们可以定义一个宏,便于修改#defineSLASH"/" std::string GetPwdDir(constchar*pwd){ 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);}

准备工作都做完了,接下来就可以打印我们的命令行提示符了:

//我们把最后一个字符改为#,方便一会与bash进行区分#defineFORMAT"[%s@%s %s]# "#defineCMDLINE_MAX1024//C++的模块化设计voidMakeCommandPrompt(char*out,int size){//将命令行提示符制作完后放入字符串中snprintf(out, size, FORMAT,GetUser(),GetHostName(),GetPwdDir(GetPwd()).c_str());}voidPrintCommandPrompt(){char prompt[CMDLINE_MAX];MakeCommandPrompt(prompt, CMDLINE_MAX);printf("%s", prompt);fflush(stdout);}

这样,我们的命令行提示符就以及可以打印完成了。

3.2 获取命令行参数

接下来我们就要获取命令行参数了:

intmain(){InitEnv();while(true){//1.输出命令行提示符 PrintCommandPrompt();//2.获取命令行参数char commandline[CMDLINE_MAX];if(!GetCommandLine(commandline, CMDLINE_MAX))continue;return0;}

我们将获取命令行参数的返回值设置为bool类型可以在我们直接输入回车时重新进入循环,再次打印命令行提示符并重新输入,与在bash中的行为一致。接下来我们就来实现一下这个函数:

#include<cstdio.h>boolGetCommandLine(char*out,int size){//stdin:从键盘中输入(本质是从键盘文件中读取)char*c =fgets(out, size,stdin);if(c ==NULL)returnfalse;//去掉换行符'\n' out[strlen(out)-1]=0;//只有换行符的话返回false,循环重新开始if(strlen(out)==0)returnfalse;returntrue;}

我们通过fgets函数来获得用户的输入:

QQ20250601-152503

fgets 是 C语言 中安全、可靠的行读取函数,它的安全性更高,只是需要我们手动处理换行符。

我们将处理好的字符串放入commandline中,方便下一步对我们输入的命令行参数进行解释。

3.3 命令行解析

上面我们已经获得了命令行参数,那么接下来就该对命令行参数进行分析:

intmain(){while(true){//1.输出命令行提示符PrintCommandPrompt();//2.获取命令行参数char commandline[CMDLINE_MAX];if(!GetCommandLine(commandline, CMDLINE_MAX))continue;//3.分析命令行参数 if(!CommandParse(commandline))continue;return0;}

那么如何分析呢?也非常简单,我们只需要将命令行参数,也就是commandline以空格为分隔符,将其填入到我们的命令行参数表中即可:

#defineDELIM" "//命令行参数表#defineARGV_MAX1024char*g_argv[ARGV_MAX];int g_argc =0;boolCommandParse(char*commandline){ g_argc =0; g_argv[g_argc++]=strtok(commandline, DELIM);if(g_argv[0]==NULL)returnfalse;while((bool)(g_argv[g_argc++]=strtok(NULL, DELIM))); g_argc--;returntrue;}

以指定字符分割字符串,这个时候就需要用到我们的strtok函数了:

QQ20250601-180308

使用strtok函数时我们需要注意第一次调用我们需要传入待分割的字符串,后续的调用传入NULL即可,因此我们只需要在一个whlie循环中不断调用即可,当字符串无法再分割时返回NULL,刚好结束while循环。

命令行解析所需要进行的最主要操作就是要将我们的命令行参数分割之后填入到我们的命令行参数表中。

3.4 执行命令

我们有了命令行参数表后就可以开始执行对应的命令了:

intmain(){while(true){//1.输出命令行提示符PrintCommandPrompt();//2.获取命令行参数char commandline[CMDLINE_MAX];if(!GetCommandLine(commandline, CMDLINE_MAX))continue;//3.分析命令行参数if(!CommandParse(commandline))continue;//4.执行命令Execute();}return0;}

我们执行命令时通过创建一个子进程,让子进程进行程序替换来执行对应的命令。那么对于那么多的程序替换函数,我们在这里该选择哪一个呢?这里我们有了命令行参数表,那么最适合我们的就是execvp函数了:

voidExecute(){ pid_t id =fork();if(id <0){perror("fork false:");exit(1);}elseif(id ==0){execvp(g_argv[0], g_argv);exit(1);} pid_t rid =waitpid(id,NULL,0);}

这样一来,我们的自定义shell的基本逻辑就已经实现了,我们可以在自定义shell上执行像ls、ps这些命令。不过许多地方的细节还没有完善,接下来让我们继续。

3.5 内建命令

在 Linux 系统中,内建命令(Built-in Commands) 是 Shell直接提供的命令,无需调用外部程序,因此执行效率更高。也就是说在执行内建命令时,父进程不会创建子进程去调用外部程序,而是父进程自己去执行。我们目前常见的内建指令有cd、pwd、echo等等这些命令,所以在执行命令前,我们需要先判断命令是否为内建命令:

intmain(){while(true){//1.输出命令行提示符 PrintCommandPrompt();//2.获取命令行参数 char commandline[CMDLINE_MAX];if(!GetCommandLine(commandline, CMDLINE_MAX))continue;//3.分析命令行参数if(!CommandParse(commandline))continue;//PrintArgv(); //4.检查是否为内建命令if(IsBuiltInCommand())continue;//5.执行命令Execute();}return0;}

判断的方法也很简单,我们直接用命令行参数表的第一个元素,也就是命令名去一一进行比较即可,这里我们就简单的对cd、echo这两个简单的命令进行编写:

boolIsBuiltInCommand(){if(!strcmp(g_argv[0],"cd")){CommandCd();returntrue;}elseif(!strcmp(g_argv[0],"echo")){CommandEcho();returntrue;}//else...returnfalse;}
3.5.1 cd

在执行cd命令时,我们还有cd -返回上次所在目录和cd ~返回家目录以及cd返回根目录这些特殊的的参数,这些都可以通过环境变量来实现:

constchar*GetOldpwd(){constchar*oldpwd =getenv("OLDPWD");return oldpwd ==NULL?"None": oldpwd;}constchar*GetHome(){constchar*home =getenv("HOME");return home ==NULL?"None": home;}voidCommandCd(){//cdint ret; std::string old_dir =get_current_dir_name();if(g_argc ==1){ std::string home =GetHome();if(home.empty())exit(1); ret =chdir(home.c_str());}//cd whereelse{ std::string where = g_argv[1];//cd -/cd ~if(where =="-"){ ret =chdir(GetOldpwd());}elseif(where =="~"){ ret =chdir(GetHome());}else{ ret =chdir(where.c_str());}}if(ret ==-1){perror("cd"); lastcode =1;}else{ lastcode =0;setenv("PWD",get_current_dir_name(),1);setenv("OLDPWD", old_dir.c_str(),1);}}

对于返回上次所在目录,环境变量中有一个名为OLDPWD的变量专门用来记录,因此我们只需要在执行cd命令前先保存一下当前所处的路径,然后再执行完后用其去更新环境变量OLDPWD即可,同时,在我们cd命令执行完后,我们也需要更新环境变量PWD,使其的值是我们当前所在的目录。

3.5.2 echo

对于echo命令,我们除了可以在显示器上打印对应的字符,还可以查看环境变量对应的值以及通过echo $?查看上一个程序的退出码:

int lastcode =0;voidCommandEcho(){if(g_argc ==1){printf("\n");}else{ std::string opt = g_argv[1];if(opt =="$?"){ std::cout << lastcode << std::endl;}elseif(opt[0]=='$'){ std::string env_name = opt.substr(1);constchar*env_value =getenv(env_name.c_str());if(env_value) std::cout << env_value << std::endl;}else{ std::cout << opt << std::endl;}} lastcode =0;}

其他程序的退出码我们可以通过waitpid来获得,只需要更改一下Execute函数即可:

voidExecute(){//...int statue; pid_t rid =waitpid(id,&statue,0);if(rid >0) lastcode =WEXITSTATUS(statue);}

4. 小结

这样一来,我们的自定义shell大致上就已经是完成了,当然对比真的shell还差的远的远。不过我们编写这个自定义shell的目的还是为了巩固之前所学的知识,对他们有更深刻的认识。

其实我们的进程与我们的函数有很大的相似性:exec/exit就像call/return⼀个C程序有很多函数组成。⼀个函数可以调⽤另外⼀个函数,同时传递给它⼀些参数。被调⽤的函数执⾏⼀定的操作,然后返回⼀个值。⼀个C程序可以fork/exec另⼀个程序,并传给它⼀些参数。这个被调⽤的程序执⾏⼀定的操作,然后通过exit来返回值。调⽤它的进程可以通过wait来获取exit的返回值。

这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux⿎励将这种应⽤于程序之内的模式扩展到程序之间。 如下图所示

4. 源码

#include<iostream>#include<cstring>#include<string>#include<cstdlib>#include<sys/types.h>#include<sys/wait.h>#include<unistd.h>#include<errno.h>#defineCMDLINE_MAX1024#defineFORMAT"[%s@%s %s]# "#defineDELIM" "#defineSLASH"/"//命令行参数表#defineARGV_MAX1024char*g_argv[ARGV_MAX];int g_argc =0;//环境变量表#defineENV_MAX1024char*g_env[ENV_MAX];int g_envs;int lastcode =0;voidInitEnv(){externchar**environ;memset(g_env,0,sizeof(g_env)); g_envs =0;//1.模仿从配置文件中设置环境变量for(int i =0; environ[i]; i++){ g_env[i]=(char*)malloc(strlen(environ[i])+1);strcpy(g_env[i], environ[i]); g_envs++;} g_env[g_envs]=NULL;//2.导成环境变量for(int i =0; g_env[i]; i++){putenv(g_env[i]);} environ = g_env;}constchar*GetPwd(){constchar*pwd =getenv("PWD");return pwd ==NULL?"None": pwd;}constchar*GetOldpwd(){constchar*oldpwd =getenv("OLDPWD");return oldpwd ==NULL?"None": oldpwd;}constchar*GetHome(){constchar*home =getenv("HOME");return home ==NULL?"None": home;}constchar*GetHostName(){constchar*hostname =getenv("HOSTNAME");return hostname ==NULL?"None": hostname;}constchar*GetUser(){constchar*user =getenv("USER");return user ==NULL?"None": user;} std::string GetPwdDir(constchar*pwd){ 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);}voidMakeCommandPrompt(char*out,int size){snprintf(out, size, FORMAT,GetUser(),GetHostName(),GetPwdDir(GetPwd()).c_str());}voidPrintCommandPrompt(){char prompt[CMDLINE_MAX];MakeCommandPrompt(prompt, CMDLINE_MAX);printf("%s", prompt);fflush(stdout);}boolGetCommandLine(char*out,int size){char*c =fgets(out, size,stdin);if(c ==NULL)returnfalse; out[strlen(out)-1]=0;if(strlen(out)==0)returnfalse;returntrue;}boolCommandParse(char*commandline){ g_argc =0; g_argv[g_argc++]=strtok(commandline, DELIM);if(g_argv[0]==NULL)returnfalse;while((bool)(g_argv[g_argc++]=strtok(nullptr, DELIM))); g_argc--;returntrue;}//testvoidPrintArgv(){for(int i =0; i <= g_argc; i++){printf("argv[%d]->%s\n", i, g_argv[i]);}printf("argc->%d\n", g_argc);}voidExecute(){ pid_t id =fork();if(id <0){perror("fork false:");exit(1);}elseif(id ==0){execvp(g_argv[0], g_argv);exit(1);}int statue; pid_t rid =waitpid(id,&statue,0);if(rid >0) lastcode =WEXITSTATUS(statue);}voidCommandCd(){//cdint ret; std::string old_dir =get_current_dir_name();if(g_argc ==1){ std::string home =GetHome();if(home.empty())exit(1); ret =chdir(home.c_str());}//cd whereelse{ std::string where = g_argv[1];//cd -/cd ~if(where =="-"){ ret =chdir(GetOldpwd());}elseif(where =="~"){ ret =chdir(GetHome());}else{ ret =chdir(where.c_str());}}if(ret ==-1){perror("cd"); lastcode =1;}else{ lastcode =0;setenv("PWD",get_current_dir_name(),1);setenv("OLDPWD", old_dir.c_str(),1);}}voidCommandEcho(){if(g_argc ==1){printf("\n");}else{ std::string opt = g_argv[1];if(opt =="$?"){ std::cout << lastcode << std::endl;}elseif(opt[0]=='$'){ std::string env_name = opt.substr(1);constchar*env_value =getenv(env_name.c_str());if(env_value) std::cout << env_value << std::endl;}else{ std::cout << opt << std::endl;}} lastcode =0;}boolIsBuiltInCommand(){if(!strcmp(g_argv[0],"cd")){CommandCd();returntrue;}elseif(!strcmp(g_argv[0],"echo")){CommandEcho();returntrue;}//else...returnfalse;}intmain(){InitEnv();while(true){//1.输出命令行提示符PrintCommandPrompt();//2.获取命令行参数char commandline[CMDLINE_MAX];if(!GetCommandLine(commandline, CMDLINE_MAX))continue;//3.分析命令行参数if(!CommandParse(commandline))continue;//PrintArgv();//4.检查是否为内建命令if(IsBuiltInCommand())continue;//5.执行命令Execute();}return0;}

尾声

本章讲解就到此结束了,若有纰漏或不足之处欢迎大家在评论区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!

Read more

AI agent:介绍 PicoClaw 安装,使用说明

PicoClaw 是一个超轻量级的个人 AI 助手,可以用在从嵌入式开发板到普通电脑的各类设备上。它最吸引人的特点就是极低的资源占用和飞快的启动速度。下面我来为你详细介绍它的安装和使用方法。 📖 PicoClaw 简介 PicoClaw 由矽速科技(Sipeed)开发,使用 Go 语言编写。它的核心优势在于,通过将计算密集的大模型推理任务交给云端 API,本地只负责轻量的协调工作,从而实现了惊人的轻量化。 特性OpenClawNanoBotPicoClaw编程语言TypeScriptPythonGo内存占用>1GB>100MB< 10MB启动时间 (0.8GHz核心)>500秒>30秒<1秒硬件成本参考Mac Mini (约$599)多数Linux开发板 (~$50)任意Linux板 (最低$10) 📦 安装指南 你可以根据自己的需求和环境,选择以下任意一种方式安装。 * 💾 方式一:预编译二进制(最简单)

By Ne0inhk

Agent实习模拟面试之Dify + Skill本地部署大模型智能体:从零构建企业级可落地的AI Agent系统

Agent实习模拟面试之Dify + Skill本地部署大模型智能体:从零构建企业级可落地的AI Agent系统 摘要:本文以一场高度仿真的Agent实习生岗位模拟面试为载体,聚焦当前热门的低代码Agent开发平台 Dify 与 自定义Skill(技能)机制,深入探讨如何在完全本地化环境中部署一个安全、可控、可扩展的大模型智能体(Agent)。通过“面试官提问—候选人回答—连环追问”的对话形式,系统性地拆解了Dify的核心架构、Skill插件开发、本地大模型集成(如Llama-3、Qwen)、RAG优化、权限控制、监控告警等关键环节,并结合企业实际场景(如内部知识问答、自动化办公)给出完整落地路径。全文超过9500字,适合对AI Agent开发、私有化部署、企业智能化转型感兴趣的工程师、架构师与在校学生阅读。 引言:为什么企业需要“本地部署的Dify + 自定义Skill”? 在2024–2026年的大模型应用浪潮中,一个显著趋势是:企业不再满足于调用公有云API,而是强烈要求数据不出域、模型可审计、能力可定制的私有化Agent解决方案。

By Ne0inhk
Openclaw高星开源框架:三省六部·用古代官制设计的 AI Agent 协作架构

Openclaw高星开源框架:三省六部·用古代官制设计的 AI Agent 协作架构

作者:cft0808 项目地址:https://github.com/cft0808/edict |许可:MIT 概述 三省六部·Edict 是一个基于中国古代官制设计的 AI 多 Agent 协作架构。它把唐朝以来运行了一千多年的三省六部制搬到了 AI 世界,创建了一套具有分权制衡、专职审核、完全可观测特性的 Agent 协作系统。 项目目前 6.9k+ Stars,581 Fork,Star 增长很快。 核心设计思想 问题:为什么大多数 Multi-Agent 框架不好用? 当前主流的多 Agent 框架(CrewAI、AutoGen、LangGraph)通常采用「自由对话」模式: Agent A

By Ne0inhk
你以为你在部署 AI 助手,其实也可能在打开一扇“数据侧门”:OpenClaw 安全风险全解析

你以为你在部署 AI 助手,其实也可能在打开一扇“数据侧门”:OpenClaw 安全风险全解析

🔥 个人主页:杨利杰YJlio❄️ 个人专栏:《Sysinternals实战教程》《Windows PowerShell 实战》《WINDOWS教程》《IOS教程》《微信助手》《锤子助手》《Python》《Kali Linux》《那些年未解决的Windows疑难杂症》🌟 让复杂的事情更简单,让重复的工作自动化 你以为你在部署 AI 助手,其实也可能在打开一扇“数据侧门”:OpenClaw 安全风险全解析 * * 1、你以为你在装 AI 助手,其实你可能在给系统加一个“高权限自动化入口” * 2、OpenClaw 和普通 AI 最大的区别,到底在哪里? * 3、我为什么说:OpenClaw 更像“拿到部分权限的数字操作员”? * 4、为什么说 AI 助手不是“更聪明的搜索框”? * 5、OpenClaw 的 5

By Ne0inhk