🎬慕斯主页:修仙—别有洞天
♈️今日夜电波:HEART BEAT—YOASOBI
2:20━━━━━━️💟──────── 5:35
🔄 ◀️ ⏸ ▶️ ☰
💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍
目录
什么是shell?
怎么实现shell?
shell命令提示符的实现
创建子进程执行命令
内建命令的引入
内建命令的实现
shell的拼接
shell的总体代码
什么是shell?
Shell是一种应用程序,它连接了用户和Linux内核,让用户能够更加高效、安全、低成本地使用Linux内核。 Shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。 它接收用户输入的命令并把它送入内核去执行。Shell并不是内核的一部分,而是一个建立在内核基础上的应用程序,与QQ、迅雷、Firefox等其它软件类似。
说大白话:他就是一个进程!作为一个进程,他当然是可以实现的啦,那么我们就简简单单手撕一个简易的进程吧!
怎么实现shell?
shell命令提示符的实现
Shell提示符是Linux系统中的一种表示形式,它出现在用户登录并启动终端模拟包或从Linux控制台登录后。它是用户与Shell进行交互的重要途径,提示符就象征着通往Shell的大门,用户可以在提示符处输入Shell命令。对于普通用户而言,Base shell的默认提示符就是一个美元符号"$",表示等待用户输入命令。如下:
[amazon@iZ7xvfrafhk3mf5qwrf2gxZ myshell]$
要实现这个命令提示符,我们需要获取当前的用户、当前主机以及当前路径,因此根据之前所学的知识,写出以下的函数获取对应的数据:
对于getenv()的回忆—主要用于搜索和返回环境变量的值。这个函数的参数是环境变量的名称,如果对应的环境变量存在,那么getenv函数就会返回一个指向该环境变量值的指针。
//获取用户名
const char* getUsername()
{
const char* name = getenv("USER");
if (name) return name;
else return "none";
}
//获取主机名
const char* getHostname()
{
const char* hostname = getenv("HOSTNAME");
if (hostname) return hostname;
else return "none";
}
//获取当前路径
const char* getCwd()
{
const char* cwd = getenv("PWD");
if (cwd) return cwd;
else return "none";
}
在得到这些数据后我们就可以组装出一个简易的命令行提示符了!再接收命令行输入的命令就可以完成基本的命令输入。需要注意的是:这里使用fgets是为了将空格也接收进来,为什么return strlen(command)呢?这是判断是否有命令输入,如果没有输入,即只是传了一个回车,那么会返回一个0,后续主函数中会用于接收,通过continue跳过后续的函数(总体是一个死循环)。
//获取用户输入命令
int getUserCommand(char* command, int num)
{
printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
char* r = fgets(command, num, stdin); // 最终你还是会输入\n
if (r == NULL) return -1;
// "abcd\n" "\n"
command[strlen(command) - 1] = '\0'; // 为了将末尾的\n去掉,注意这样并不会越界 ,因为只要是输入都至少会有个\n
return strlen(command);
}
创建子进程执行命令
从上面的程序我们可知,存储的命令是整段的,对此我们需要进行命令的分割,用strtok按空格进行分割。从前面的知识中我们也知道:shell是创建子进程来让他执行任务的!对此,我们也创建一子进程,通过execvp的程序替换来执行对应的命令。而父进程需要执行的任务是waitpid等待子进程完成任务后储存对应的返回值,用于子进程任务完成怎么样的判断,对此shell的基本功能实现就完成了。但是还是需要改进的!
//按照空格分割命令
void commandSplit(char* in, char* out[])
{
int argc = 0;
out[argc++] = strtok(in, SEP);
while (out[argc++] = strtok(NULL, SEP));
}
//总体运行
int execute(char* argv[])
{
pid_t id = fork();
if (id < 0) return -1;
else if (id == 0) //child
{
//进程替换
execvp(argv[0], argv); // cd ..
exit(1);
}
else // father
{
int status = 0;//存储返回值,echo $?
pid_t rid = waitpid(id, &status, 0);
if (rid > 0) {
lastcode = WEXITSTATUS(status);//存储返回值,echo $?
}
}
return 0;
}
内建命令的引入
由上图我们可知,当我们使用ls、pwd等等命令时自定义的shell是能够正常运行的,但是对于cd命令还有export是不能运行的。回过头想想,我们是怎么实现shell的呢?我们运用了进程的替换,我们替换了子进程中的进程而上面的shell中并没有实现!对此我们可以肯定的是:这些命令并不是外部的进程!这也引出了—内建命令。内建命令,如其名称所示,是由Shell自身提供的命令。这些命令已经和shell编译为一体,不需要借助外部程序文件来运行。这意味着它们执行速度更快,效率更高。通俗的讲,内建命令实际上就是shell中的一个函数!
内建命令的实现
由于内建命令已经和shell编译为一体,不需要借助外部程序文件来运行,因此我们需要一一的实现对应的内建命令,这里就先实现三个,如果要接着实现可以接着else if 实现下去。详细解释见代码:
//获取家目录
char* homepath()
{
char* home = getenv("HOME");
if (home) return home;
else return (char*)".";
}
void cd(const char* path)//用于cd命令
{
chdir(path);//用于改变当前工作目录
char tmp[1024];//临时储存,但不能直接使用,需要全局变量,否则栈帧销毁也会销毁
getcwd(tmp, sizeof(tmp));//获取当前工作目录的绝对路径
sprintf(cwd, "PWD=%s", tmp); //输出到全局变量中,保证不会失效
putenv(cwd);//改变或增加环境变量的内容
}
// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char* argv[])
{
if (strcmp(argv[0], "cd") == 0)//通过改变当前环境变量的内容从而改变->命令行提示符路径
{
char* path = NULL;
if (argv[1] == NULL) path = homepath();//cd 回到家目录
else path = argv[1];//根据命令到指定路径
cd(path);
return 1;
}
else if (strcmp(argv[0], "export") == 0)
{
if (argv[1] == NULL) return 1;
strcpy(enval, argv[1]);//同理需要全局变量防止失效
putenv(enval); // 改变环境变量
return 1;
}
else if (strcmp(argv[0], "echo") == 0)
{
if (argv[1] == NULL) {//echo 输出换行
printf("\n");
return 1;
}
if (*(argv[1]) == '$' && strlen(argv[1]) > 1) {//根据指令输出
char* val = argv[1] + 1; // $PATH $?
if (strcmp(val, "?") == 0)
{
printf("%d\n", lastcode);
lastcode = 0;
}
else {
const char* enval = getenv(val);
if (enval) printf("%s\n", enval);
else printf("\n");
}
return 1;
}
else {//只是输出到屏幕
printf("%s\n", argv[1]);
return 1;
}
}
else if (0) {}//接下来的内建命令
return 0;
}
shell的拼接
shell在Linux中是一直运行的,因此为一个死循环!,我们定义一个usercommand字符数组用于接收储存从命令行收到的初步命令,定义一个字符串数组来接收通过commandSplit分割后的字符串,接下来区分是否为内建命令,按照argv里面的命令进行执行程序替换或者执行内建命令。
int main()
{
while (1) {
char usercommand[NUM];
char* argv[SIZE];
// 1. 打印提示符&&获取用户命令字符串获取成功
int n = getUserCommand(usercommand, sizeof(usercommand));
if (n <= 0) continue;
// 2. 分割字符串
// "ls -a -l" -> "ls" "-a" "-l"
commandSplit(usercommand, argv);
// 3. check build-in command
n = doBuildin(argv);
if (n) continue;
// 4. 执行对应的命令
execute(argv);
}
}
shell的总体代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define NUM 1024
#define SIZE 64
#define SEP " "
char cwd[1024];
char enval[1024]; // for test
int lastcode = 0;
//获取家目录
char* homepath()
{
char* home = getenv("HOME");
if (home) return home;
else return (char*)".";
}
//获取用户名
const char* getUsername()
{
const char* name = getenv("USER");
if (name) return name;
else return "none";
}
//获取主机名
const char* getHostname()
{
const char* hostname = getenv("HOSTNAME");
if (hostname) return hostname;
else return "none";
}
//获取当前路径
const char* getCwd()
{
const char* cwd = getenv("PWD");
if (cwd) return cwd;
else return "none";
}
//获取用户输入命令
int getUserCommand(char* command, int num)
{
printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
char* r = fgets(command, num, stdin); // 最终你还是会输入\n
if (r == NULL) return -1;
// "abcd\n" "\n"
command[strlen(command) - 1] = '\0'; // 为了将末尾的\n去掉,注意这样并不会越界 ,因为只要是输入都至少会有个\n
return strlen(command);
}
//按照空格分割命令
void commandSplit(char* in, char* out[])
{
int argc = 0;
out[argc++] = strtok(in, SEP);
while (out[argc++] = strtok(NULL, SEP));
}
//总体运行
int execute(char* argv[])
{
pid_t id = fork();
if (id < 0) return -1;
else if (id == 0) //child
{
//进程替换
execvp(argv[0], argv); // cd ..
exit(1);
}
else // father
{
int status = 0;//存储返回值,echo $?
pid_t rid = waitpid(id, &status, 0);
if (rid > 0) {
lastcode = WEXITSTATUS(status);//存储返回值,echo $?
}
}
return 0;
}
void cd(const char* path)
{
chdir(path);//用于改变当前工作目录
char tmp[1024];//临时储存,但不能直接使用,需要全局变量,否则栈帧销毁也会销毁
getcwd(tmp, sizeof(tmp));//获取当前工作目录的绝对路径
sprintf(cwd, "PWD=%s", tmp); //输出到全局变量中,保证不会失效
putenv(cwd);//改变或增加环境变量的内容
}
// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char* argv[])
{
if (strcmp(argv[0], "cd") == 0)
{
char* path = NULL;
if (argv[1] == NULL) path = homepath();
else path = argv[1];
cd(path);
return 1;
}
else if (strcmp(argv[0], "export") == 0)
{
if (argv[1] == NULL) return 1;
strcpy(enval, argv[1]);//同理需要全局变量防止失效
putenv(enval); // ???
return 1;
}
else if (strcmp(argv[0], "echo") == 0)
{
if (argv[1] == NULL) {
printf("\n");
return 1;
}
if (*(argv[1]) == '$' && strlen(argv[1]) > 1) {
char* val = argv[1] + 1; // $PATH $?
if (strcmp(val, "?") == 0)
{
printf("%d\n", lastcode);
lastcode = 0;
}
else {
const char* enval = getenv(val);
if (enval) printf("%s\n", enval);
else printf("\n");
}
return 1;
}
else {
printf("%s\n", argv[1]);
return 1;
}
}
else if (0) {}
return 0;
}
int main()
{
while (1) {
char usercommand[NUM];
char* argv[SIZE];
// 1. 打印提示符&&获取用户命令字符串获取成功
int n = getUserCommand(usercommand, sizeof(usercommand));
if (n <= 0) continue;
// 2. 分割字符串
// "ls -a -l" -> "ls" "-a" "-l"
commandSplit(usercommand, argv);
// 3. check build-in command
n = doBuildin(argv);
if (n) continue;
// 4. 执行对应的命令
execute(argv);
}
}
感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o!
给个三连再走嘛~