目录
📕前言
本篇文章主要讲解一个简单版的shell的实现
,看完这篇文章,你可以对shell的运行原理,Linux进程相关知识
等有一个更深入的认识和理解,适合刚入门Linux的初学者学习。
自定义shell根据下面所述的原理一共分为四个部分实现。(分别为就命令行的输入
,解析
,执行
,内建指令的执行
)还包括了对一些细节
的处理…
🍑shell
Shell
是Unix/Linux系统中的一个特殊程序,是用户与操作系统内核交互的接口。用户通过Shell
向操作系统
输入命令,Shell
负责将这些命令解析
并传递给内核执行,然后将执行结果返回给用户。Shell既是一种命令解释器,也是一种功能强大的编程语言。
📚Shell的工作原理
可以概括为以下几个步骤:
命令行输入
:用户在命令行界面输入命令。命令解析
:Shell接收用户的输入,并对命令进行解析。这个过程包括解析命令名、参数、选项等,将其转换成计算机可以理解的形式。命令执行
:解析完成后,Shell会执行相应的命令。这通常涉及到调用系统调用或者启动新的进程来执行命令。结果输出
:命令执行完成后,Shell将结果输出到命令行界面,供用户查看。
在整个过程中,Shell还会维护一些上下文信息,例如当前的工作目录、环境变量等,这些信息会影响命令的执行结果。
🔒Shell的高级功能
命令补全
:用户输入命令时,Shell会自动补全命令名或参数,提高输入效率。历史命令查看
:用户可以通过特定的命令或快捷键查看之前输入的历史命令,方便重复执行或编辑。管道和重定向
:支持管道(|)
和重定向(>、<)
功能,可以将一个命令的输出作为另一个命令的输入,或将命令的输出保存到文件中。别名和函数
:用户可以定义命令的别名或函数,以简化命令的输入或实现复杂的操作。- …
🚀shell的代码实现
🎈实现一:打印命令行提示符,获取用户输入的命令字符串
如上图所示,我们可以发现:命令提示符
是一个长字符串,其中包括了用户名,主机名
,以及当前的工作目录
。
根据前面的文章的学习,我们可以利用环境变量
来获取这三个信息。
自己可以利用env指令
查看,这里就不赘述了。
用户名:USER
主机名:HOSTNAME
当前工作目录:PWD
打印命令行提示符的代码实现:
const char* HostName() { char *hostname = getenv("HOSTNAME"); if(hostname) return hostname; else return "None"; } const char* UserName() { char *hostname = getenv("USER"); if(hostname) return hostname; else return "None"; } const char *CurrentWorkDir() { char *hostname = getenv("PWD"); if(hostname) return hostname; else return "None"; } printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
获取用户输入指令
这里需要获取的是用户输入的指令,指令一般都是一个字符串,但是值得注意
的是:这里不能使用scanf函数
,原因是scanf遇到空格
就会停止获取,但是我们输入的指令常常含有空格(例如ls -a -l
)。
可以使用其他的函数,比如c++中的getline
,C中的fgets
函数等等。这里选取的是fgets函数,简单介绍一下:参数解释
:参数一是获取的内容存放的地方,参数二是大小,参数三是在哪里获取(这里我们是在键盘中获取,所以填写stdin即可);注意
:当我们输入的时候,回车也会被fgets获取
,所以要考虑是否需要处理;
代码实现
:
char commandline[SIZE]; //存放命令 fgets(commandline, SIZE, stdin); commandline[strlen(commandline)-1] = '\0'; // commandline是空串的情况?
该部分整体封装后的代码:
int Interactive(char out[], int size) //返回值是为了处理后面的情况 { // 输出提示符并获取用户输入的命令字符串"ls -a -l" printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir()); fgets(out, size, stdin); out[strlen(out)-1] = 0; // commandline是空串的情况? return strlen(out); //返回命令字符串的长度 }
🛸实现二:对命令行字符串进行切割
根据上一篇文章对进程替换
相关的接口进行的讲解,这里我们切割后放在一个char* 的数组中,方便只用接口;
这个介绍strtok
函数,可以将一个字符串以指定的分隔符进行切割开,下面进行简单介绍。参数介绍
:参数一是需要切割的字符串,参数二是什么作为分隔符。
注意:
- 只需要第一次调用的时候将第一个参数设置为该字符串,后面只需要设置为NULL即可。
- 当剩余字符串不能够再分隔的时候,会返回一个NULL;
字符串分割的代码实现
:
//将指令字符串切割后放到argv数组中 void Split(char in[]) { int i = 0; argv[i++] = strtok(in, SEP); // "ls -a -l" while(argv[i++] = strtok(NULL, SEP)); //细节一:当字符串不能再切割的时候,返回NULL给argv后,再回来判断,不满足,直接退出循环 //并且根据上面一篇文章可知,argv数组必须以NULL结尾 if(strcmp(argv[0], "ls") ==0) //如果为指令ls,则特殊处理,目的是给显示添加高光 { argv[i-1] = (char*)"--color"; argv[i] = NULL; } }
🌙实现三:执行指令
执行这些指令,不能使用主进程去执行,因为如果只用主进程去执行,如果遇到错误就崩溃了。但是为我们可以发现shell是一直运行的,出错并没有崩溃,这是因为bash进程是主进程,用户的指令大多都是利用子进程去执行的。
这里使用的是execvp
接口,不会的看可以去上一篇文章看看。
void Execute() { pid_t id = fork(); if(id == 0) { // 让子进程执行命名 execvp(argv[0], argv); exit(1); } int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid == id) lastcode = WEXITSTATUS(status); }
🏀实现四:处理内建命令
上面的执行指令,我们可以发现对于一些内建指令子程序执行不了。所以在字符串切割结束后,需要先判断是否为cd,echo,export…内建指令
。
举例实现上面三个内建指令。
cd指令
常见cd指令 :
- cd (切换到家目录)
- cd 路径(切换到对应路径,
.
与..
本质上也是一个路径)
可以使用getenv
函数获取家目录。对应的环境变量为HOME
。
对应的工作目录改变了应该需要更新环境变量,这样当再次打印命令提示符的时候能够显示正确的当前工作目录。
这里使用到一个很好用的函数,snprintf
,简单介绍:
可以用来拼接
字符串,个人认为非常好用。
大概的用法就是,将传入的可变参数的内容,按照指定的格式,放到指定大小的str中,用法如下例子。
char *Home() { return getenv("HOME"); } if(strcmp("cd", argv[0]) == 0) { // 2. 执行 char *target = argv[1]; //cd XXX or cd if(!target) //如果为空,则指令为cd ,切换到家目录 target = Home(); chdir(target); char temp[1024]; getcwd(temp, 1024); snprintf(pwd, SIZE, "PWD=%s", temp);//将PWD=加上temp按照%s的格式,拼接后放到pwd中 putenv(pwd); //导入环境变量 }
export指令
这个很简单,一看就会,不赘述了。
else if(strcmp("export", argv[0]) == 0) { ret = 1; if(argv[1]) { strcpy(env, argv[1]); putenv(env); } }
echo指令
这个则指令常见的四种用法:
- echo xxxxx (向显示屏打印xxxx)
- echo $?才看最近的退出码
- echo $yyy(查找环境变量yyy对应的内容)
- echo (换行)
代码实现:
else if(strcmp("echo", argv[0]) == 0) { ret = 1; if(argv[1] == NULL) { printf("\n"); } else{ if(argv[1][0] == '$') { if(argv[1][1] == '?') { printf("%d\n", lastcode); //这里都lastcode是一个全局的变量 lastcode = 0; //打印后赋值为0 } else { char *e = getenv(argv[1]+1); if(e) printf("%s\n", e); } } else { printf("%s\n", argv[1]); } } }
最后,将各部分封装好的代码进行合并,注意:这里shell应该时一直是一直运行的,并不是运行了执行了一个指令后就退出,所以整体上,应该是一个循环的效果。
main函数的代码实现
:
int main() { while(1) { char commandline[SIZE]; // 1. 打印命令行提示符,获取用户输入的命令字符串 int n = Interactive(commandline, SIZE); if(n == 0) continue; //如果指令为空,则不用执行 // 2. 对命令行字符串进行切割 Split(commandline); // 3. 处理内建命令 n = BuildinCmd(); if(n) continue; //是内建指令,则子程序不用执行 // 4. 执行这个命令 Execute(); //不是内建指令,子程序执行 } return 0; }
⭐总代码实现
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #define SIZE 1024 #define MAX_ARGC 64 #define SEP " " char *argv[MAX_ARGC]; char pwd[SIZE]; char env[SIZE]; // for test int lastcode = 0; const char* HostName() { char *hostname = getenv("HOSTNAME"); if(hostname) return hostname; else return "None"; } const char* UserName() { char *hostname = getenv("USER"); if(hostname) return hostname; else return "None"; } const char *CurrentWorkDir() { char *hostname = getenv("PWD"); if(hostname) return hostname; else return "None"; } char *Home() { return getenv("HOME"); } int Interactive(char out[], int size) { // 输出提示符并获取用户输入的命令字符串"ls -a -l" printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir()); fgets(out, size, stdin); out[strlen(out)-1] = 0; //'\0', commandline是空串的情况? return strlen(out); } void Split(char in[]) { int i = 0; argv[i++] = strtok(in, SEP); // "ls -a -l" while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 = if(strcmp(argv[0], "ls") ==0) { argv[i-1] = (char*)"--color"; argv[i] = NULL; } } void Execute() { pid_t id = fork(); if(id == 0) { // 让子进程执行命名 execvp(argv[0], argv); exit(1); } int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid == id) lastcode = WEXITSTATUS(status); //printf("run done, rid: %d\n", rid); } int BuildinCmd() { int ret = 0; // 1. 检测是否是内建命令, 是 1, 否 0 if(strcmp("cd", argv[0]) == 0) { // 2. 执行 ret = 1; char *target = argv[1]; //cd XXX or cd if(!target) target = Home(); chdir(target); char temp[1024]; getcwd(temp, 1024); snprintf(pwd, SIZE, "PWD=%s", temp); putenv(pwd); } else if(strcmp("export", argv[0]) == 0) { ret = 1; if(argv[1]) { strcpy(env, argv[1]); putenv(env); } } else if(strcmp("echo", argv[0]) == 0) { ret = 1; if(argv[1] == NULL) { printf("\n"); } else{ if(argv[1][0] == '$') { if(argv[1][1] == '?') { printf("%d\n", lastcode); lastcode = 0; } else{ char *e = getenv(argv[1]+1); if(e) printf("%s\n", e); } } else{ printf("%s\n", argv[1]); } } } return ret; } int main() { while(1) { char commandline[SIZE]; // 1. 打印命令行提示符,获取用户输入的命令字符串 int n = Interactive(commandline, SIZE); if(n == 0) continue; // 2. 对命令行字符串进行切割 Split(commandline); // 3. 处理内建命令 n = BuildinCmd(); if(n) continue; // 4. 执行这个命令 Execute(); } // for(int i=0; argv[i]; i++) // { // printf("argv[%d]: %s\n", i, argv[i]); // } return 0; }
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #define SIZE 1024 #define MAX_ARGC 64 #define SEP " " char *argv[MAX_ARGC]; char pwd[SIZE]; char env[SIZE]; // for test int lastcode = 0; const char* HostName(){ char *hostname = getenv("HOSTNAME"); if(hostname) return hostname; else return "None"; } const char* UserName(){ char *hostname = getenv("USER"); if(hostname) return hostname; else return "None"; } const char *CurrentWorkDir(){ char *hostname = getenv("PWD"); if(hostname) return hostname; else return "None"; } char *Home(){ return getenv("HOME"); } int Interactive(char out[], int size){ // 输出提示符并获取用户输入的命令字符串"ls -a -l" printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir()); fgets(out, size, stdin); out[strlen(out)-1] = 0; //'\0', commandline是空串的情况? return strlen(out); } void Split(char in[]){ int i = 0; argv[i++] = strtok(in, SEP); // "ls -a -l" while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 = if(strcmp(argv[0], "ls") ==0){ argv[i-1] = (char*)"--color"; argv[i] = NULL; } } void Execute(){ pid_t id = fork(); if(id == 0){ // 让子进程执行命名 execvp(argv[0], argv); exit(1); } int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid == id) lastcode = WEXITSTATUS(status); //printf("run done, rid: %d\n", rid); } int BuildinCmd(){ int ret = 0; // 1. 检测是否是内建命令, 是 1, 否 0 if(strcmp("cd", argv[0]) == 0){ // 2. 执行 ret = 1; char *target = argv[1]; //cd XXX or cd if(!target) target = Home(); chdir(target); char temp[1024]; getcwd(temp, 1024); snprintf(pwd, SIZE, "PWD=%s", temp); putenv(pwd); } else if(strcmp("export", argv[0]) == 0){ ret = 1; if(argv[1]){ strcpy(env, argv[1]); putenv(env); } } else if(strcmp("echo", argv[0]) == 0) { ret = 1; if(argv[1] == NULL) { printf("\n"); } else{ if(argv[1][0] == '$'){ if(argv[1][1] == '?'){ printf("%d\n", lastcode); lastcode = 0; } else{ char *e = getenv(argv[1]+1); if(e) printf("%s\n", e); } } else{ printf("%s\n", argv[1]); } } } return ret; } int main(){ while(1){ char commandline[SIZE]; // 1. 打印命令行提示符,获取用户输入的命令字符串 int n = Interactive(commandline, SIZE); if(n == 0) continue; // 2. 对命令行字符串进行切割 Split(commandline); // 3. 处理内建命令 n = BuildinCmd(); if(n) continue; // 4. 执行这个命令 Execute(); } return 0; }