【Linux】自定义shell的编写
📝前言:
这篇文章我们来讲讲==【Linux】简单自定义shell的编写==,通过这个简单的模拟实现,进一步感受shell的工作原理。
🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记,C语言入门基础,python入门基础,C++刷题专栏
目录
- 一,要实现的基本功能
- 二,打印命令行提示符
- (1)获取环境变量
- 对应接口 / 函数用法
- 实现
- (2)格式化打印提示符
- 对应接口 / 函数用法
- 实现
- 三,读取用户命令
- 对应接口 / 函数用法
- 实现
- 四,分析命令
- 用到的全局变量
- 对应接口 / 函数用法
- 实现
- 五,处理命令
- 对应接口 / 函数用法
- 实现
- 六,内建命令
- (1)cd
- 全局变量
- 对应接口 / 函数用法
- 实现
- (2)echo
- 实现
- (3)判断内建并调用
- 七,模拟导入环境变量
- 全局变量
- 对应接口 / 函数用法
- 实现
- 八,完整实现代码
一,要实现的基本功能
- 打印命令行提示符
- 读取用户输入命令
- 分析命令,得到命令行参数表
- 处理内建命令
- 处理命令
以及补充的:
- 加载环境变量和 更新环境变量(在
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);
- 功能说明:把字符串
str
按delim
进行切割
- 功能说明:把字符串
- 返回值
- 本次有遇到分隔符:把分隔符替换成
\0
然后返回切下来那一段字符 - 最后一段字符串后面没有分割符:返回后面的整个字符串
- 当最后一段字符串也切割完了:返回
NULL
- 本次有遇到分隔符:把分隔符替换成
str
传参讲究:- 第一次:
str
传要分割的字符串 - 第一次后:
str
传nullptr
- 第一次:
实现
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
进程创建了一个子进程,然后子进程通过程序切换完成调用的
对应接口 / 函数用法
fork和execvp,这两用法就不说了,不懂的可以看:进程控制
实现
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亲自执行,这里我们主要实现cd
和echo
内建命令的部分功能
(1)cd
主要实现:cd
、cd -
、cd ~
、cd path
(path
为自己写的路径,并且要确保正确)
思路:
- 得到对应的要改变的路径,然后通过父进程
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>
- 已存在:覆盖,不存在:创建
- 仅修改当前进程及其子进程的环境变量,不会影响父进程或系统全局环境
这里用它是为了,通过直接覆盖的方式,更新环境变量PWD
和OLDPWD
,因为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
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!