Linux系统之----模拟实现shell
在前面一个阶段的学习中,我们已经学习了环境变量、进程控制等等一系列知识,也许有人会问,学这个东西有啥用?那么,今天我就和大家一起综合运用一下这些知识,模拟实现下shell!
首先我们来看一看我们的shell都有些什么,打开一个shell:
有一个命令行提示符, 由用户名,主机名,当前目录,提示符$构成,那我们一点一点破解!
1.获取
用户名,主机名
首先,用户名和主机名都可以通过getenv来查看,那么我们是不是也可以通过getenv来获取呢?于是,我们便可以像如下一样写出代码:
static std::string GetUserName()
{std::string username = getenv("USER");return username.empty() ? "None" : username;
}
static std::string GetHostName()
{std::string hostname = getenv("HOSTNAME");return hostname.empty() ? "None" : hostname;
}
那为什么要加上那个static呢?就是为了增强鲁棒性和健壮性,保证代码只能在这个文件里面使用!
具体代码内容比较简单,就不解释了!
2.路径的设置
之后是这个路径,要求我们要随着我们操作的变化而变化:
路径也可以通过env来查看,所以我们写出如下代码:
char pwd[1024]; // 全局变量空间,保存当前shell进程的工作路径
int lastcode = 0;
static std::string GetPwd()
{// 环境变量的变化,可能会依赖于进程,pwd需要shell自己更新环境变量的值//std::string pwd = getenv("PWD");//return pwd.empty() ? "None" : pwd;char temp[1024];getcwd(temp, sizeof(temp));// 顺便更新一下shell自己的环境变量pwdsnprintf(pwd, sizeof(pwd), "PWD=%s", temp);putenv(pwd);// /// /home/whb/code/codestd::string pwd_lable = temp;const std::string pathsep = "/";auto pos = pwd_lable.rfind(pathsep);if(pos == std::string::npos){return "None";}pwd_lable = pwd_lable.substr(pos+pathsep.size());return pwd_lable.empty() ? "/" : pwd_lable;
代码逻辑:使用getcwd
函数获取当前工作目录的绝对路径,并将其存储在字符数组temp
中,再使用snprintf
函数将temp
中的路径格式化为PWD=路径
的形式,并存储在pwd
中。然后使用putenv
函数将这个新的环境变量PWD
设置到当前进程的环境变量中, 说人话就是:更新环境变量PWD,之后便是找“/”
,如果没有找到/
(即pos
为std::string::npos
),则返回字符串"None"
。寻找的具体操作就是先定义一个变量,并将temp赋值过去,之后找最后一个/,最后是使用substr
函数提取pwd_label
中最后一个/
之后的部分。如果提取后的部分为空(即路径以/
结尾),则返回"/"
;否则返回提取后的路径部分。
具体显示效果请参考centos的,我这里是Ubuntu22.04的,可能显示界面不是很一样,代码以centos为准!
补充:这里我们用到了snprintf,其本质和printf是差不多的,Printf是根据给定的格式进行写入,而这个snprintf是像字符串中写入
3.获取家目录
之后我们还要获取家目录,即当有人用我的shell的时候输入env的时候我们的家目录也要有显示:
这里直接给出代码:
static std::string GetHomePath()
{std::string home = getenv("HOME");return home.empty() ? "/" : home;
}
4.输出提示符
但是,我们将这些都获取了就完事了吗?我们得输出啊!因此,我们还要写一个命令行输出的函数!用于输出提示符
void PrintCommandPrompt()
{std::string user = GetUserName();std::string hostname = GetHostName();std::string pwd = GetPwd();printf("[%s@%s %s]# ", user.c_str(), hostname.c_str(), pwd.c_str());
}
上述代码没什么解释的内容,应该大家都会的~
5.输入指令
之后输出了命令行提示符就完事了吗?我们还要输入指令啊!就像下图一样
代码如下图所示:
//获取用户的键盘输入
bool GetCommandString(char cmd_str_buff[], int len)
{if(cmd_str_buff == NULL || len <= 0)return false;char *res = fgets(cmd_str_buff, len, stdin);if(res == NULL)return false;// ls -a -l\n -> ls -a -l\0cmd_str_buff[strlen(cmd_str_buff) - 1] = 0;return strlen(cmd_str_buff) == 0 ? false : true;
}
这里我解释一下,用户输入的指令假设为ls -l -a -n,我们要是普通的scanf或者cin的话那肯定不行,我们要以回车为结尾,而不是以空格为结尾,为此我们使用fgets函数, 之后还要检查一下是否成功正确写入!但是,这样真的没问题吗?(假设没有注释下面的那句代码),试一下就知道了,不行的,会多打出来一个空行!原因就是我们表面上输入的是ls -a -l,但是我们还按了回车!!!实际上获取的确是ls -a -l\n!!!改起来也容易,我们直接把\n换为\0就Ok了~
好了,现在我们的指令是获取完了,但是佢现在还是一坨啊,是一大串,我们的shell还是识别不了我们想干什么,所以下一步我们肯定就是将其进行分割!
6.分割指令
这里由于一条命令语句里面会有诸多条,如命令 ls -a -l 我们要将其分开,这时我们可以借助命令行参数表(实际上就是一个数组),那我们怎么分割呢?我们在之前学过strtok,可以用起来!
这里补充介绍一下strtok,它按照指定的分隔符(通常是空白字符或逗号等)将一个字符串分割成多个子字符串,用法如下:
char *strtok(char *str, const char *delim);
值得注意的是,strtok是有记忆性的,会自动记住本次切割到哪里了!所以之后的调用中,第一个参数传NULL即可 ,而且strtok
会修改原始字符串,因为它在每个标记后插入空字符 \0
来分割字符串。这意味着原始字符串在 strtok
处理后将不再保持原样。
// 命令行参数表,我故意定义成为全局
char *gargv[ARGS] = {NULL};
int gargc = 0;
bool ParseCommandString(char cmd[])
{if(cmd == NULL)return false;
#define SEP " "//3. "ls -a -l" -> "ls" "-a" "-l"gargv[gargc++] = strtok(cmd, SEP);// 整个数字,最后以NULL结尾while((bool)(gargv[gargc++] = strtok(NULL, SEP)));// 回退一次,命令行参数的格式gargc--;//#define DEBUG
#ifdef DEBUGprintf("gargc: %d\n", gargc);printf("----------------------\n");for(int i = 0; i < gargc; i++){printf("gargv[%d]: %s\n",i, gargv[i]);}printf("----------------------\n");for(int i = 0; gargv[i]; i++){printf("gargv[%d]: %s\n",i, gargv[i]);}
#endifreturn true;
}
7.初始化
在基础变量都准备完成之后,我们可以尝试运行我们的shell了,但是在运行之前,我们要先进行初始化!
void InitGlobal()
{gargc = 0;memset(gargv, 0, sizeof(gargv));
}
8. 父子进程的创建
通过前面学习,我们知道进程在创建的时候会被fork()分为父子进程,所以我们也要模仿实现以下~
void ForkAndExec()
{pid_t id = fork();if(id < 0){//for : XXXXXperror("fork"); // errno -> errstringreturn;}else if(id == 0){//子进程execvp(gargv[0], gargv);exit(0);}else{//父进程int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);}}
}
这里仅解释一下中间部分:如果 fork()
返回 0,表示当前是子进程。使用 execvp()
函数执行一个新的程序,该程序由全局变量 gargv
指定(gargv[0]
是程序名,gargv
是参数列表)。如果 execvp()
执行失败,将调用 exit(0)
终止子进程。
9.内建命令的构建
在原版shell中,我们输入cd或者echo,我们的shell会对其进行响应,这个就是内建命令,那我们实现下:
bool BuiltInCommandExec()
{//内建命令: 是shell自己执行的命令,如同shell执行一个自己的函数//gargv[0]std::string cmd = gargv[0];bool ret = false;if(cmd == "cd"){// buildif(gargc == 2){std::string target = gargv[1];if(target == "~"){ret = true;chdir(GetHomePath().c_str());}else{ret = true;chdir(gargv[1]);}}else if(gargc == 1){ret = true;chdir(GetHomePath().c_str());}else{//BUG}}else if(cmd == "echo"){if(gargc == 2){std::string args = gargv[1];if(args[0] == '$'){if(args[1] == '?'){printf("lastcode: %d\n", lastcode);lastcode = 0;ret = true;}else{const char *name = &args[1];printf("%s\n", getenv(name));lastcode = 0;ret = true;}}else{printf("%s\n", args.c_str());ret = true;}}}return ret;
}
如此,代码主体设计完成,下面我们写一下main.cc文件以及myshell.h文件:
由于这两个文件就是函数头文件以及各个接口函数调用,这里直接以汇总的形式给出!
这里说明一下,我们的几乎所有软件都是一个死循环!!!我们之前写的程序其实就是一个代码片段!
这是main.cc文件
#include "myshell.h"#define SIZE 1024int main()
{char commandstr[SIZE];while(true){// 0. 初始化操作InitGlobal();// 1. 输出命令行提示符PrintCommandPrompt();// 2. 获取用户输入的命令if(!GetCommandString(commandstr, SIZE))continue;// 3. "ls -a -l" -> "ls" "-a" "-l"// 对命令字符串,进行解析 -> 命令行参数表ParseCommandString(commandstr);// 4. 检测命令,内键命令,要让shell自己执行!if(BuiltInCommandExec()){continue;}// 5.执行命令, 让子进程来进行执行ForkAndExec();}return 0;
}
这是myshell.h文件
#ifndef __MYSHELL_H__
#define __MYSHELL_H__#include <stdio.h>#define ARGS 64void Debug();
void InitGlobal();
void PrintCommandPrompt();
bool GetCommandString(char cmd_str_buff[], int len);
bool ParseCommandString(char cmd[]);
void ForkAndExec();
bool BuiltInCommandExec();
#endif
这是myshell.cc文件
#include "myshell.h"
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string.h>
#include <stdlib.h>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>// 命令行参数表,我故意定义成为全局
char *gargv[ARGS] = {NULL};
int gargc = 0;
char pwd[1024]; // 全局变量空间,保存当前shell进程的工作路径
int lastcode = 0;void Debug()
{printf("hello shell!\n");
}//void GetUserName(char name[], int len)
//{
//
//}static std::string GetUserName()
{std::string username = getenv("USER");return username.empty() ? "None" : username;
}
static std::string GetHostName()
{std::string hostname = getenv("HOSTNAME");return hostname.empty() ? "None" : hostname;
}
static std::string GetPwd()
{// 环境变量的变化,可能会依赖于进程,pwd需要shell自己更新环境变量的值//std::string pwd = getenv("PWD");//return pwd.empty() ? "None" : pwd;char temp[1024];getcwd(temp, sizeof(temp));// 顺便更新一下shell自己的环境变量pwdsnprintf(pwd, sizeof(pwd), "PWD=%s", temp);putenv(pwd);// /// /home/whb/code/codestd::string pwd_lable = temp;const std::string pathsep = "/";auto pos = pwd_lable.rfind(pathsep);if(pos == std::string::npos){return "None";}pwd_lable = pwd_lable.substr(pos+pathsep.size());return pwd_lable.empty() ? "/" : pwd_lable;
}static std::string GetHomePath()
{std::string home = getenv("HOME");return home.empty() ? "/" : home;
}// 输出提示符
void PrintCommandPrompt()
{std::string user = GetUserName();std::string hostname = GetHostName();std::string pwd = GetPwd();printf("[%s@%s %s]# ", user.c_str(), hostname.c_str(), pwd.c_str());
}//获取用户的键盘输入
bool GetCommandString(char cmd_str_buff[], int len)
{if(cmd_str_buff == NULL || len <= 0)return false;char *res = fgets(cmd_str_buff, len, stdin);if(res == NULL)return false;// ls -a -l\n -> ls -a -l\0cmd_str_buff[strlen(cmd_str_buff) - 1] = 0;return strlen(cmd_str_buff) == 0 ? false : true;
}bool ParseCommandString(char cmd[])
{if(cmd == NULL)return false;
#define SEP " "//3. "ls -a -l" -> "ls" "-a" "-l"gargv[gargc++] = strtok(cmd, SEP);// 整个数字,最后以NULL结尾while((bool)(gargv[gargc++] = strtok(NULL, SEP)));// 回退一次,命令行参数的格式gargc--;//#define DEBUG
#ifdef DEBUGprintf("gargc: %d\n", gargc);printf("----------------------\n");for(int i = 0; i < gargc; i++){printf("gargv[%d]: %s\n",i, gargv[i]);}printf("----------------------\n");for(int i = 0; gargv[i]; i++){printf("gargv[%d]: %s\n",i, gargv[i]);}
#endifreturn true;
}void InitGlobal()
{gargc = 0;memset(gargv, 0, sizeof(gargv));
}void ForkAndExec()
{pid_t id = fork();if(id < 0){//for : XXXXXperror("fork"); // errno -> errstringreturn;}else if(id == 0){//子进程execvp(gargv[0], gargv);exit(0);}else{//父进程int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);}}
}bool BuiltInCommandExec()
{//内建命令: 是shell自己执行的命令,如同shell执行一个自己的函数//gargv[0]std::string cmd = gargv[0];bool ret = false;if(cmd == "cd"){// buildif(gargc == 2){std::string target = gargv[1];if(target == "~"){ret = true;chdir(GetHomePath().c_str());}else{ret = true;chdir(gargv[1]);}}else if(gargc == 1){ret = true;chdir(GetHomePath().c_str());}else{//BUG}}else if(cmd == "echo"){if(gargc == 2){std::string args = gargv[1];if(args[0] == '$'){if(args[1] == '?'){printf("lastcode: %d\n", lastcode);lastcode = 0;ret = true;}else{const char *name = &args[1];printf("%s\n", getenv(name));lastcode = 0;ret = true;}}else{printf("%s\n", args.c_str());ret = true;}}}return ret;
}
好了,本篇文章到此结束~