【Linux】自定义shell的编写

发布于:2025-05-09 ⋅ 阅读:(14) ⋅ 点赞:(0)

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

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


一,要实现的基本功能

  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]; // 这个要设置成全局的,不然不能返回cosnt
 46     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,当没有分隔符的时候返回整个字符串,当最后一个字符串也遍历了,就返回NULL
 79     while(bool(argv[argc++] = strtok(nullptr," "))); // 实际 argc 会比参数个数大1
 80     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 // 及时更新环境变量PWD
 29 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 100
 20 
 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


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


网站公告

今日签到

点亮在社区的每一天
去签到