当前位置: 首页 > news >正文

【Linux】自定义shell的编写

📝前言:
这篇文章我们来讲讲==【Linux】简单自定义shell的编写==,通过这个简单的模拟实现,进一步感受shell的工作原理。

🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记,C语言入门基础,python入门基础,C++刷题专栏


目录

  • 一,要实现的基本功能
  • 二,打印命令行提示符
    • (1)获取环境变量
      • 对应接口 / 函数用法
      • 实现
    • (2)格式化打印提示符
      • 对应接口 / 函数用法
      • 实现
  • 三,读取用户命令
    • 对应接口 / 函数用法
    • 实现
  • 四,分析命令
    • 用到的全局变量
    • 对应接口 / 函数用法
    • 实现
  • 五,处理命令
    • 对应接口 / 函数用法
    • 实现
  • 六,内建命令
    • (1)cd
      • 全局变量
      • 对应接口 / 函数用法
      • 实现
    • (2)echo
      • 实现
    • (3)判断内建并调用
  • 七,模拟导入环境变量
    • 全局变量
    • 对应接口 / 函数用法
    • 实现
  • 八,完整实现代码

一,要实现的基本功能

  1. 打印命令行提示符
  2. 读取用户输入命令
  3. 分析命令,得到命令行参数表
  4. 处理内建命令
  5. 处理命令

以及补充的:

  • 加载环境变量和 更新环境变量(在cd后更新环境变量)

下面我们分别对这些功能的实现进行讲解

二,打印命令行提示符

先看系统的shell的命令行提示符
在这里插入图片描述
格式:USERNAME@主机名:工作路径$
为了区分,我们计划实现的格式为:USERNAME@主机名:工作路径#

(1)获取环境变量

我们可以通过getenv来获取环境变量一下是对应关系

  • USERNAME:环境变量USER
  • 工作路径:环境变量PWD
  • 主机名:环境变量HOSTNAME(但是我的电脑上getenv(HOSTNAME)拿不到,所以我用库函数:gethostname

对应接口 / 函数用法

  • getenv
    • 头文件:<stdlib.h>
    • 用法:getenv(变量名)
    • 返回值:
      • 成功:对应的字符串指针
      • 失败:NULL
  • gethostname
    • 头文件:<unistd.h>
    • 用法:gethostname(char *name, size_t len)
      • name:字符指针,指向用来存储获取到的hostname的缓冲区(这缓冲区就是代指一篇空间)
      • len:表示name的大小
    • 返回
      • 成功:0
      • 失败:1

实现

 31 const char* GetName()32 {33     char* user = getenv("USER");34     return user == NULL? "None" : user;35 }36 37 const char* GetPwd()38 {39     // 因为在我们 chdir 改变工作路径以后,shell先获取变化,然后才会更新环境变量40     char* pwd = getenv("PWD");41     return pwd == NULL? "None" : pwd;42 }                                                                                                                                                                                                            43 44 const char* GetHost() {45     static char hostname[128]; // 这个要设置成全局的,不然不能返回cosnt46     if (gethostname(hostname, sizeof(hostname)) == 0) {47         return hostname;48     }49     return "None";50 }51 52 const char* GetHome()53 {54     char* home = getenv("HOME");55     return home == NULL? "None" : home;56 }

(2)格式化打印提示符

这里我们用到snprintf,将格式化的数据写入字符串缓冲区。

对应接口 / 函数用法

memset(用来给指定内存设置值):

  • 头文件:<stdio.h>
  • 用法:void *memset(void *s, int c, size_t n)
    • s:要操作的内存块的指针
    • c:要设置的值
    • n:要设置的字节数

snprintf

  • 头文件:<stdio.h>
  • 用法:int snprintf(char *str, size_t size, const char *format, ...);
    • str:指向字符串缓冲区
    • size:缓冲区的大小
    • format:格式化字符串,就像printf里面的格式化一样

实现

 58 void PrintCommandPrompt()59 {60     char Prompt[128];61     memset(Prompt, 0, sizeof(Prompt));62     snprintf(Prompt, sizeof(Prompt), "%s@%s:%s# ", GetName(), GetHost(), GetPwd());63     printf("%s", Prompt);64 }

三,读取用户命令

先看系统的:
在这里插入图片描述
我们输入ls -a -l的本质是,把它当做了一个长字符串"ls -a -l"

对应接口 / 函数用法

fgets

  • 头文件:<stdio.h>
  • 用法:char *fgets(char *str, int size, FILE *stream);
    • stream流里面读一行数据到str,遇到空格和不会停止,直到读取完size - 1个字符或者读完\n才停止。
    • 读完以后末尾会加\0
    • str:用来存储的缓冲区
    • size:缓冲区大小

实现

 66 bool GetCommand(char* buffer, int size) // 获取用户输入的命令67 {68     char* s = fgets(buffer, size, stdin); // 当输入内容小于size - 1,有多少读多少,当输入内容大于size - 1,读size - 1个【末尾都加 \0】69     if(s == NULL) return false;70     s[strlen(buffer) - 1] = 0; // 清理\n (strlen也会计算 \n 的长度)71     if(strlen(buffer) == 0) return false;72     return true;73 }

四,分析命令

分析命令,就是形成命令行参数表,为后续的调用程序准备

用到的全局变量

 11 // 命令行参数表12 #define MAX_ARGC 128 // 命令行参数个数最大值13 char* argv[MAX_ARGC]; // 命令行参数表14 int argc = 0; // 命令行参数实际个数

对应接口 / 函数用法

strtok

  • 用法:char *strtok(char *str, const char *delim);
    • 功能说明:把字符串strdelim进行切割
  • 返回值
    • 本次有遇到分隔符:把分隔符替换成\0然后返回切下来那一段字符
    • 最后一段字符串后面没有分割符:返回后面的整个字符串
    • 当最后一段字符串也切割完了:返回NULL
  • str传参讲究:
    • 第一次:str传要分割的字符串
    • 第一次后:strnullptr

实现

 75 bool CommandParse(char* command) // 得到argv命令行参数表76 {77     argc = 0;78     argv[argc++] = strtok(command, " "); // strtok按字符串将字符切割,把分割字符替换成 \0,当没有分隔符的时候返回整个字符串,当最后一个字符串也遍历了,就返回NULL79     while(bool(argv[argc++] = strtok(nullptr," "))); // 实际 argc 会比参数个数大180     argc--;81     return argc > 0? true: false;82 }

我们就会得到类似这样的一张参数表:
在这里插入图片描述

五,处理命令

处理命令的本质
命令处理的本质是:调用了ls程序,怎么调用的?
bash进程创建了一个子进程,然后子进程通过程序切换完成调用的

对应接口 / 函数用法

forkexecvp,这两用法就不说了,不懂的可以看:进程控制

实现

 94 int Execute()95 {96     pid_t ret = fork();97     if(ret == 0)98     {99         execvp(argv[0], argv);
100         exit(1); // 如果没调成功,返回码为 1 代表返回结果不正确
101     }
102     // father
103     // 这里实现 echo $? 功能,修改lastcode
104     int status = 0;
105     pid_t id = waitpid(ret, &status, 0);
106     if(id > 0)
107     {
108         lastcode = WEXITSTATUS(status);
109     }
110     return 0;
111 }

六,内建命令

内建命令需要shell亲自执行,这里我们主要实现cdecho内建命令的部分功能

(1)cd

主要实现:cdcd -cd ~cd pathpath为自己写的路径,并且要确保正确)

思路:

  • 得到对应的要改变的路径,然后通过父进程chdir来改变。在-
  • 这里不能fork() + cd,因为如果fork了改变的只是子进程的工作路径,而一个父进程有多个子进程,我们的shell的工作路径还是没有改变,所以这里直接在父进程上chdir

全局变量

 25 #define MAX_P 100 // 最长路径长26 char CWD[MAX_P];27 28 // 及时更新环境变量PWD29 char PWD[MAX_P];30 char OLDPWD[MAX_P];

对应接口 / 函数用法

putenv

  • 头文件:<stdlib.h>
  • 已存在:覆盖,不存在:创建
  • 仅修改当前进程及其子进程的环境变量,不会影响父进程或系统全局环境

这里用它是为了,通过直接覆盖的方式,更新环境变量PWDOLDPWD,因为cd会用到这两个变量

实现

139 bool Cd()
140 {
141     memset(PWD, 0, sizeof(PWD));
142     if(argc == 1)
143     {
144         string home = GetHome();
145         if(home.empty()) return true;
146         snprintf(OLDPWD, sizeof(OLDPWD), "OLDPWD=%s", getcwd(CWD, sizeof(CWD))); // 在更新路径之前记录OLDPWD
147         putenv(OLDPWD);
148         chdir(home.c_str());
149         snprintf(PWD, sizeof(PWD), "PWD=%s", home.c_str());
150         putenv(PWD);
151     }
152     else
153     {
154         string where = argv[1];
155         if(where == "-")
156         {
157             char* oldpwd = getenv("OLDPWD");
158             if(oldpwd == NULL) return true;
159             snprintf(OLDPWD, sizeof(OLDPWD), "OLDPWD=%s", getcwd(CWD, sizeof(CWD))); // 在更新路径之前记录OLDPWD
160             putenv(OLDPWD);
161             chdir(getenv("OLDPWD"));
162             snprintf(PWD, sizeof(PWD), "PWD=%s", oldpwd);
163             putenv(PWD);
164         }
165         else if(where == "~")
166         {
167             string home = GetHome();
168             if(home.empty()) return true;
169             snprintf(OLDPWD, sizeof(OLDPWD), "OLDPWD=%s", getcwd(CWD, sizeof(CWD))); // 在更新路径之前记录OLDPWD
170             putenv(OLDPWD);
171             chdir(home.c_str());
172             snprintf(PWD, sizeof(PWD), "PWD=%s", home.c_str());
173             putenv(PWD);
174         }
175         else
176         {                                                                                                                                                                                                    
177             snprintf(OLDPWD, sizeof(OLDPWD), "OLDPWD=%s", getcwd(CWD, sizeof(CWD))); // 在更新路径之前记录OLDPWD
178             putenv(OLDPWD);
179             chdir(where.c_str());
180             snprintf(PWD, sizeof(PWD), "PWD=%s", where.c_str());
181             putenv(PWD);
182         }
183     }
184 
185     return true;
186 }

(2)echo

主要实现:echo $?echo $环境变量名echo 长字符串

实现

189 bool Echo()
190 {
191     if(argc == 1) return false;
192     string s = argv[1];
193     if(s == "$?")
194         printf("%d\n", lastcode);
195     else if(s[0] == '$')
196     {
197         string ss = "";
198         for(int i = 1; i < s.size(); i++)
199             ss += s[i];
200         if(getenv(ss.c_str()))
201             printf("%s\n",getenv(ss.c_str()));
202         else return false;
203     }
204     else
205     {
206         string ss = "";
207         int i = 1;
208         while(argv[i])
209         {
210             string tmp = argv[i];
211             ss += tmp + " ";
212             i++;
213         }
214         printf("%s\n", ss.c_str());
215     }
216     return true;
217 }

(3)判断内建并调用

220 bool CheckAndExecBuiltin()
221 {
222     string cmd = argv[0];
223     // 识别内建命令
224     if(cmd == "cd")
225         Cd();
226     else if(cmd == "echo")
227         Echo();
228     else
229     {
230         return false;
231     }
232     return true;
233 }

七,模拟导入环境变量

最后我们再加一个模拟导入环境变量,实际上应该是从配置文件导入的,这里我们从系统bash获取了,然后模拟导入

全局变量

 19 #define MAX_ENVS 10020 21 // 全局环境变量表22 char* env[MAX_ENVS];23 int env_nums = 0;

对应接口 / 函数用法

strcpy

  • char *strcpy(char *dest, const char *src);
  • 将源字符串 src 复制到目标字符串 dest 中,其中包含字符串结束符 '\0'(是简单的浅拷贝)

实现

113 void InitEnv()
114 {
115     memset(env, 0, sizeof(env));
116     env_nums = 0;
117     extern char** environ;
118 
119     // 获取环境变量
120     for(int i = 0; environ[i]; i++)
121     {
122         env[i] = (char*)malloc(strlen(environ[i]) + 1); // 多开一个bit放\0
123         strcpy(env[i], environ[i]);
124         env_nums++;
125     }
126     env[env_nums++] = (char*)"myvalue=12345";
127     env[env_nums] = NULL;
128 
129     // 加载环境变量
130     for(int i = 0; env[i]; i++)                                                                                                                                                                              
131     {
132         putenv(env[i]); // 这里对于已经存在的环境变量,putenv会更新。
133     }
134     environ = env;
135     // 上面模拟从配置文件加载的过程
136 }

八,完整实现代码

当然,这份实现,还存在者各种各样的问题,主要是用来巩固学习到的知识。

如果想要获取完整代码,可以访问我的Github


🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!

相关文章:

  • 【IDEA_Maven】(进阶版)永久性的更改IDEA中每个项目所依赖的Maven默认配置文件及其仓库路径
  • 【Python 元组】
  • 网上商城系统
  • Kafka集群加入新Broker节点会发生什么
  • SQLite 转换为 MySQL 数据库
  • Go语言中 源文件开头的 // +build 注释的用法
  • LeetCode难题解析:数字字符串的平衡排列数目
  • 力扣:轮转数组
  • Python字典:数据操作的核心容器
  • .Net HttpClient 概述
  • C++线程库
  • 记录一下学习kafka的使用以及思路
  • 黄金、碳排放期货市场API接口文档
  • AI日报 · 2025年5月09日|OpenAI Deep Research 上线 GitHub Connector Beta
  • 【相机标定】OpenCV 相机标定中的重投影误差与角点三维坐标计算详解
  • 【论文阅读】——Articulate AnyMesh: Open-Vocabulary 3D Articulated Objects Modeling
  • Python 基础语法与数据类型(六) - 条件语句、循环、循环控制
  • 全球实物文件粉碎服务市场洞察:合规驱动下的安全经济与绿色转型
  • Flink之Table API
  • U9C对接飞书审批流完整过程
  • 名帅大挪移提前开启,意属皇马的阿隆索会是齐达内第二吗
  • 人民日报刊文:守护“技术进步须服务于人性温暖”的文明底线
  • 东方红资管官宣:41岁原国信资管董事长成飞出任新总经理
  • 红场阅兵即将开始!中国人民解放军仪仗队亮相
  • 悬疑推理联合书单|虫神山事件
  • 长三角地区中华老字号品牌景气指数发布,哪些牌子是你熟悉的?