Linux《自主Shell命令行解释器》
在上一篇的进程控制当中我们已经了解了进程退出、进程等待以及进程替换的相关概念,那么在了解了这些的概念之后接下来在本篇当中我们就可以结合之前我们学习的知识来实现一个自主的Shell命令行解释器,通过Shell的实现能让我们进一步的理解操作系统当中的shell外壳是如何将我们输入的指令执行起来的。接下来就开始本篇的学习,一起加油吧!!!
1.实现目标
在本篇当中我们实现的myshell目标是实现一个与Linux当中类似的命令行解释器,也就是能实现先获取用户输入的内容;之后再进行命令行解析;之后再进行相应的指令执行。并且除了能处理普通的指令外还能处理echo、cd等内建命令。
2.实现myhsell
在实现myshell的过程当中只使用C的方式在一些部分的代码实现会较为繁琐,因此在本篇当中实现myshell的过程当中会采取C与C++混编的方式。
首先创建一个名为myshell的目录
在该目录当中创建一个名为myshell.cc的文件来存储实现的源代码,再创建对应的makefile文件
2.1 命令行提示符实现
在实现myshell的第一步就是先来实现和Linux当中的命令行类似的命令行提示符,在用户每次输入对应的的指令之前都需要将对于的命令行提示符先输出出来。
接下来就试着来实现命令行的提示符,通过Linux当中大的命令行提示符就可以看出需要输出对应当前登录的用户名、当前的主机名以及当前所在的路径。
以上的这些数据从环境变量表当中都可以获取,以下就先来实现获取以上三个环境变量的函数。
#include<iostream>
#include<cstdio>
#include<stdlib.h>
#include<cstring>//获取当前登录的用户名
const char* GetUserName()
{const char* name=getenv("USER");return name==NULL?"None":name;
}//获取当前主机名
const char* GetHostName()
{const char* hostname=getenv("HOSTNAME");return hostname==NULL?"None":hostname;}//获取当前路径
const char* GetPwd()
{const char* pwd=getenv("PWD");return pwd==NULL?"None":pwd;
}
得到了以上对应的环境变量之后接下来就要分析如何将得到的环境变量输入到命令行提示符当中,通过此时还需要构建对应的命令行提示符的格式。
观察Linux当中的命令行提示符就可以看到是划分为三个部分的,那么接下来就着实现对应命令行提示符的格式。
注:在此还需要解决一个小细节就是在命令行提示符当中最后的文件需要是当前路径的末尾文件,而以上创建的GetPwd得到的是完整的路径,这就需要对以上得到的路径再进行切割。
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# " //将得到的当前路径当中得到最后的文件名
std::string DirName(const char* pwd)
{//得到当前路径以/之后的文件名
#define SLASH "/"std::string dir=pwd;if(dir==SLASH)return SLASH;auto pos=dir.rfind(SLASH);//当pos的返回值为npos时此时出现bug if(pos==std::string::npos)return "BUG";return dir.substr(pos+1);
}//构建命令行提示符的输出格式
void MakeCommandLine( char cmd_prompt[],int size )
{ snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),DirName(GetPwd()).c_str());
} //输出命令行提示符
void PrintCommandPrompt()
{ char prompt[COMMAND_SIZE]; MakeCommandLine(prompt,sizeof(prompt)); printf("%s",prompt); fflush(stdout);
}
以上函数实现之后接下来就可以main函数当中调用对应的PrintCommandPrompt函数。在此需要在用户输入对应数据之后再输出命令行提示符就需要死循环的调用PrintCommandPrompt函数。
int main()
{while(true){//1.输出命令行提示符PrintCommandPrompt();//进行阻塞,或者会死循环打印命令行提示符char arr[1024]; scanf("%s",arr);//……}return 0;
}
将以上的myshell.cc进行编译链接出名为myshell的可执行程序,运行myshell就可以看到可以输出命令行提示符了
2.1 获取用户输入的命令
以上实现了自定义shell当中的命令行提示符后接下来就从标准输入流当中获取用户输入的数据
实现的函数代码如下所示:
#include<cstring>//从标准输入流当中获取用户输入的内容
bool GetCommandLine(char* out,int size)
{ char* c=fgets(out,size,stdin); if(c==NULL)return false; //使得数组最后以\0结尾 out[strlen(out)-1]=0; if(strlen(out)==0)return false; return true; }
注:以上使用到了strlen就需要在myshell.cc当中包含的头文件添加上cstring
实现了对应的函数之后接下来在main函数当中调用该函数。
int main()
{ //从父进程当中获取环境变量表 //InitEnv(); while(true) { //1.输出命令行提示符 PrintCommandPrompt(); //2.获取用户输入的命令 //创建数组存储用户输出的数据 char commandline[COMMAND_SIZE]; if(!GetCommandLine(commandline,sizeof(commandline))) //当以上的函数返回值为false时就说明用户为输入有效的命令,那么此时就不再执行之后的代码 continue; }return 0;
}
2.3 命令行解析
以上创建了commandline数组来存储用户输入的命令,那么接下来就需要对获得的命令进行解析并存储至命令行参数表当中。
因此接下来首先要做的是创建对应的命令行参数表。
//1.命令行参数表
#define MAXARGC 128 //指向命令行参数表的指针
char* g_argv[MAXARGC];//统计命令行参数的变量g_argc
int g_argc=0;
在自定义shell当中创建了命令行接下来就来实现命令行解析的功能
//进行对用户输入的命令进行解析
bool CommandParse(char *commandline)
{
#define SEP " " g_argc=0; //使用strtok进行参数的分割,之后再将参数存储至命令行参数表当中 g_argv[g_argc++]=strtok(commandline,SEP); while((bool)(g_argv[g_argc++]=strtok(nullptr,SEP))); //最终统计的参数个数会比实际的多一个,此时需要将g_argc减一 g_argc--; //最后通过判断g_argc的个数来判断用户是否输入有效的命令 return g_argc>0? true:false;
}
以上在命令行参数解析功能的实现当中就使用到strtok函数来实现,在分隔不同的参数时以" "为标识。
接下来就在mian函数当中调用以上的函数
int main()
{ //从父进程当中获取环境变量表 //InitEnv(); while(true) { //1.输出命令行提示符 PrintCommandPrompt(); //2.获取用户输入的命令 //创建数组存储用户输出的数据 char commandline[COMMAND_SIZE]; if(!GetCommandLine(commandline,sizeof(commandline))) //当以上的函数返回值为false时就说明用户为输入有效的命令,那么此时就不再执行之后的代码 continue;//3.命令行解析if(!CommandParse(commandline))//当以上的函数返回值为false时就说明用户为输入有效的命令,那么此时就不再执行之后的代码 continue; }return 0;
}
那么对应以上实现的命令行解析是否正确呢?要验证那么这时就需要创建一个Print的函数来打印g_argv获取的。
void Print()
{ for(int i=0;g_argv[i];i++) { printf("argv[%d]->%s\n",i,g_argv[i]); } printf("argv:%d\n",g_argc); }
实现了Print函数之后接下来就在每次完成命令行就将命令行参数表输出进行验证实现的命令行解析工作是否正确。
运行myshell,接下来输入几个指令。
通过以上输出的结果可以看出实现的命令行的解析是符合我们的要求的。
2.4 检查并处理内建命令
以上已经实现了命令行解析的功能,在此之后在命令行参数表当中已经存储了用户输入的命令。但是接下来还是不能执行对应的指令,而是需要处理cd、echo等内建命令。
内建命令就是指当前需要执行的指令不是由父进程创建子进程之后由子进程来执行的,而是直接由父进程执行。
以下就来实现处理内建命令的函数CheckAndExecBuiltin,在该函数当中会处理echo、cd、export三个内建命令,其他的内建命令可自主实现。
//检查用户输入的指令是否为内建命令bool CheckAndExecBuiltin()
{ std::string cmd=g_argv[0];if(cmd=="cd"){cd(); return true;}else if(cmd=="echo"){ Echo();return true; } else if(cmd=="export"){Export();return true;} else{ //……}
return false;
}
注:在以上使用到string,在此就要在myshell.cc的头文件当中添加string
main函数内实现
int main()
{//从父进程当中获取环境变量表InitEnv();while(true){//1.输出命令行提示符 PrintCommandPrompt();//2.获取用户输入的命令 //创建数组存储用户输出的数据 char commandline[COMMAND_SIZE]; if(!GetCommandLine(commandline,sizeof(commandline))) continue; //3.命令行分析 if(!CommandParse(commandline)) continue; //4.检测并处理内建命令 if(CheckAndExecBuiltin()) continue; } return 0;
}
cd指令
通过之前的学习我们知道cd实现的是路径的切换,那么要在myshell当中实现该功能先在myshell当中构建对应的环境变量表。Linux中Shell外壳一开始生成环境变量表的数据其实是从配置文件当中得到的,我们实现的myshell也是可以从配置文件当中获取但是这样的操作很繁琐因此在接下来当中的myshell的环境变量表是直接从父进程当中获取。
首先来创建对应的环境变量表
//2.环境变量表
#define MAX_ENVS 200
char* g_env[MAX_ENVS];
int g_envs=0;
接下来在myshell程序执行的开始就从父进程当中将对应的环境变量表导入当前进程当中。
//导入父进程的环境变量表至当前进程当中
void InitEnv()
{extern char** environ;memset(g_env,0,sizeof(g_env));g_envs=0;//获取环境变量for(int i=0;environ[i];i++){g_env[i]=(char*)malloc(strlen(environ[i])+1);strcpy(g_env[i],environ[i]);g_envs++;}g_env[g_envs]=NULL;//导入环境变量for(int i=0;g_env[i];i++){putenv(g_env[i]);}//修改全局指针envrionenviron=g_env;}
int main()
{//从父进程当中获取环境变量表InitEnv();while(true){//1.输出命令行提示符PrintCommandPrompt();//2.获取用户输入的命令//创建数组存储用户输出的数据char commandline[COMMAND_SIZE];if(!GetCommandLine(commandline,sizeof(commandline)))continue;//3.命令行分析 if(!CommandParse(commandline))continue;Print();//4.检测并处理内建命令if(CheckAndExecBuiltin())continue;}return 0;
}
以上就实现了从程序刚开始执行的时候就从父进程当中获取环境变量表,那么接下来我们就可以来实现cd指令了
在cd指令当中有一个选项是很特殊的,那就 - ,当用户使用cd - 的时候shell需要切换到最近的一次路径,那么要实现该功能就需要在使用一部分的空间来存储上一次的路径。
//使用prev_pwd存储最近一次的路径
std::string prev_pwd;
除此之外当我们身处的路径改变时,环境变量表当中的PWD也是需要改变的,那么此时就在运行完对应的cd指令之后修改环境变量表当中的PWD。
以下就是按照以上要求实现的Cd函数
//执行cd指令
bool cd()
{//判断执行的是否是cd -,是的话就获取当前的当前的路径if(!(g_argc==2 && (strcmp(g_argv[1],"-")==0))){//获取当前路径prev_pwd=GetPwd();}//判断命令行参数的个数if(g_argc==1){//命令行参数的个数为1就说明用户输出的命令是cd,此时就直接返回当前用户的家目录即可std::string home=GetHome();if(home.empty())return true;chdir(home.c_str());}else{//使用变量where得到第二个参数 std::string where=g_argv[1];//当第二个参数为-时执行的命令是返回最近一次的路径if(where=="-"){std::string tmp=GetPwd();std::cout<<prev_pwd<<std::endl;chdir(prev_pwd.c_str());prev_pwd=tmp;}//当第二个参数是~时执行的命令是返回当前用户的家目录else if(where=="~"){std::string home=GetHome();//std::string homestr=home.substr(0);// std::cout<<home<<std::endl;chdir(home.c_str());} //不是以上的情况就将路径切换为用户指定的路径else{chdir(where.c_str());}}//更新环境变量表int pwd_idx = -1;for(int i = 0; g_env[i] != NULL; i++){if (strncmp(g_env[i], "PWD=", 4) == 0){pwd_idx = i;break;}}// 获取当前工作目录char* cwd = getcwd(NULL, 0);if (cwd == NULL){return "None"; // 错误处理}// 构建新的 PWD 环境变量字符串size_t len = strlen(cwd) + 5; // "PWD=" + 字符串长度 + 1char* pwd_str =(char*) malloc(len); if (pwd_str == NULL){free(cwd);return "None"; // 内存分配失败}snprintf(pwd_str, len, "PWD=%s", cwd);free(cwd);// 更新环境变量表if (pwd_idx != -1){free(g_env[pwd_idx]); // 释放旧的 PWD 条目//printf("%s\n",prev_pwd);g_env[pwd_idx] = pwd_str; // 设置新的 PWD 条目} else{// 确保有空间添加新环境变量if (g_envs < MAX_ENVS - 1){g_env[g_envs++] = pwd_str;g_env[g_envs] = NULL; // 确保列表以 NULL 结尾} }return true;
}
以上就使用到chdir,chdir系统调用的作用就是将当前的路径切换为指定的路径
注:在使用chdir时需要引用头文件#include<unostdd.h>,因此要在myshell当中添加上。
以上我们就编写了内建命令当中cd的部分,接下来就编译myshell.cc文件运行myshell看看是否符合我们的要求。
echo指令
要实现内建命令echo就需要处理echo $? 和 echo $环境变量名 若不是这两种情况就只需要将用户输入的第二个参数再输出到命令行上。
要实现echo $? 就需要在myshell当中创建一个全局的变量来存储上一次执行程序的退出码
//上一次执行的程序的退出码
int lastcode=0;
Echo函数实现的代码如下所示:
//执行echo指令
void Echo()
{//判断用户输入的参数个数是否为2if(g_argc==2){//将用户输入的第二个参数存储到opt当中std::string opt=g_argv[1];if(opt=="$?"){//输出错误码lastcode的值std::cout<<lastcode<<std::endl;//重新将lastcode设置为0lastcode=0;}else if(opt[0]=='$'){//输出对应环境变量的数据std::string env_name=opt.substr(1);const char*env_vlue=getenv(env_name.c_str());if(env_vlue)std::cout<<env_vlue<<std::endl;}else{//不为以上的情况就直接将用户输入的第二个参数输出std::cout<<opt<<std::endl;} }
}
export指令
export的作用就是将用户新创建的环境变量导入到环境变量表当中,实现的代码如下所示
//执行export指令
bool Export()
{ char* newenv =(char*)malloc(strlen(g_argv[1])+1); strcpy(newenv,g_argv[1]); //std:: cout<<g_argv[1]<<std::endl; // std:: cout<<newenv<<std::endl; g_env[g_envs++]=newenv; g_env[g_envs]=NULL; return true; }
注:在以上的export当中没有考虑要添加的环境变量之前已经在环境变量表当中存在需要将原来的环境变量覆盖,在此若要实现可自主实现看看。
以上我们就编写了内建命令当中export的部分,接下来就编译myshell.cc文件运行myshell看看是否符合我们的要求。
2.5 执行命令
当输入的命令不是内建命令之后就会执行到最后的步骤5,此时就需要通过创建子进程的方式再结合进程替换来执行普通的命令
在此在Execute内实现普通的命令执行,实现的代码如下所示
//创建子进程执行命令
int Execute()
{ //创建子进程 pid_t pid=fork(); if(pid==0) { //在子进程当中进行进程替换 execvp(g_argv[0],g_argv); exit(1); } //进行进程等待 int status=0; pid_t rid=waitpid(pid,&status,0); if(rid>0) { //设置进程退出码lastcode值 lastcode=WEXITSTATUS(status); } return 0; }
注:在使用fork和wait因此要在myshell当中添加上头文件#include<sys/types.h>和 #include<sys/wait.h>。
在main函数当中调用该函数
int main()
{//从父进程当中获取环境变量表InitEnv();while(true){//1.输出命令行提示符 PrintCommandPrompt();//2.获取用户输入的命令 //创建数组存储用户输出的数据 char commandline[COMMAND_SIZE]; if(!GetCommandLine(commandline,sizeof(commandline))) continue; //3.命令行分析 if(!CommandParse(commandline)) continue; //4.检测并处理内建命令 if(CheckAndExecBuiltin()) continue; //5.执行命令Execute();} return 0;
}
以上我们就编写了执行命令部分,接下来就编译myshell.cc文件运行myshell看看是否符合我们的要求。
3. myshell完整代码
#include<iostream>
#include<cstdio>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<cstring>
#include<sys/wait.h>//上一次执行的程序的退出码
int lastcode=0;#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# " //1.命令行参数表
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc=0;//2.环境变量表
#define MAX_ENVS 200
char* g_env[MAX_ENVS];
int g_envs=0;//使用prev_pwd存储最近一次的路径
std::string prev_pwd;//获取当前登录的用户名
const char* GetUserName()
{const char* name=getenv("USER");return name==NULL?"None":name;
}//获取当前主机名
const char* GetHostName()
{const char* hostname=getenv("HOSTNAME");return hostname==NULL?"None":hostname;}//获取当前路径
const char* GetPwd()
{static char* cur_pwd=nullptr;if(cur_pwd!=NULL){free(cur_pwd);}cur_pwd=getcwd(NULL,0);return cur_pwd==NULL? "None":cur_pwd;
}//得到当前用户的家目录
const char* GetHome()
{const char* home=getenv("HOME");return home==NULL?"":home;
}//导入父进程的环境变量表至当前进程当中
void InitEnv()
{extern char** environ;memset(g_env,0,sizeof(g_env));g_envs=0;//获取环境变量for(int i=0;environ[i];i++){g_env[i]=(char*)malloc(strlen(environ[i])+1);strcpy(g_env[i],environ[i]);g_envs++;}g_env[g_envs]=NULL;//导入环境变量for(int i=0;g_env[i];i++){putenv(g_env[i]);}environ=g_env;}//执行cd指令
bool cd()
{//判断执行的是否是cd -,是的话就获取当前的当前的路径if(!(g_argc==2 && (strcmp(g_argv[1],"-")==0))){//获取当前路径prev_pwd=GetPwd();}//判断命令行参数的个数if(g_argc==1){//命令行参数的个数为1就说明用户输出的命令是cd,此时就直接返回当前用户的家目录即可std::string home=GetHome();if(home.empty())return true;chdir(home.c_str());}else{//使用变量where得到第二个参数std::string where=g_argv[1];//当第二个参数为-时执行的命令是返回最近一次的路径if(where=="-"){std::string tmp=GetPwd();std::cout<<prev_pwd<<std::endl;chdir(prev_pwd.c_str());prev_pwd=tmp;}//当第二个参数是~时执行的命令是返回当前用户的家目录else if(where=="~"){std::string home=GetHome();//std::string homestr=home.substr(0);// std::cout<<home<<std::endl;chdir(home.c_str());}//不是以上的情况就将路径切换为用户指定的路径else{chdir(where.c_str());}}//更新环境变量表int pwd_idx = -1;for(int i = 0; g_env[i] != NULL; i++){if (strncmp(g_env[i], "PWD=", 4) == 0){pwd_idx = i;break;}}// 获取当前工作目录char* cwd = getcwd(NULL, 0);if (cwd == NULL){return "None"; // 错误处理}// 构建新的 PWD 环境变量字符串size_t len = strlen(cwd) + 5; // "PWD=" + 字符串长度 + 1char* pwd_str =(char*) malloc(len);if (pwd_str == NULL){free(cwd);return "None"; // 内存分配失败}snprintf(pwd_str, len, "PWD=%s", cwd);free(cwd);// 更新环境变量表if (pwd_idx != -1){free(g_env[pwd_idx]); // 释放旧的 PWD 条目//printf("%s\n",prev_pwd);g_env[pwd_idx] = pwd_str; // 设置新的 PWD 条目} else{// 确保有空间添加新环境变量if (g_envs < MAX_ENVS - 1){g_env[g_envs++] = pwd_str;g_env[g_envs] = NULL; // 确保列表以 NULL 结尾} }return true;
}//执行echo指令
void Echo()
{//判断用户输入的参数个数是否为2if(g_argc==2){//将用户输入的第二个参数存储到opt当中std::string opt=g_argv[1];if(opt=="$?"){//输出错误码lastcode的值std::cout<<lastcode<<std::endl;lastcode=0;}else if(opt[0]=='$'){//输出对应环境变量的数据std::string env_name=opt.substr(1);const char*env_vlue=getenv(env_name.c_str());if(env_vlue)std::cout<<env_vlue<<std::endl;}else{//不为以上的情况就直接将用户输入的第二个参数输出std::cout<<opt<<std::endl;}}
}//执行export指令
bool Export()
{char* newenv =(char*)malloc(strlen(g_argv[1])+1);strcpy(newenv,g_argv[1]);//std:: cout<<g_argv[1]<<std::endl;// std:: cout<<newenv<<std::endl;g_env[g_envs++]=newenv;g_env[g_envs]=NULL;return true;}//将得到的当前路径当中得到最后的文件名
std::string DirName(const char* pwd)
{//得到当前路径以/之后的文件名
#define SLASH "/"std::string dir=pwd;if(dir==SLASH)return SLASH;auto pos=dir.rfind(SLASH);//当pos的返回值为npos时此时出现bugif(pos==std::string::npos)return "BUG";return dir.substr(pos+1);
}//构建命令行提示符的输出格式
void MakeCommandLine( char cmd_prompt[],int size )
{snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),DirName(GetPwd()).c_str());
}//输出命令行提示符
void PrintCommandPrompt()
{char prompt[COMMAND_SIZE];MakeCommandLine(prompt,sizeof(prompt));printf("%s",prompt);fflush(stdout);
}//从标准输入流当中获取用户输入的内容
bool GetCommandLine(char* out,int size)
{char* c=fgets(out,size,stdin);if(c==NULL)return false;out[strlen(out)-1]=0;if(strlen(out)==0)return false;return true;}//进行对用户输入的命令进行解析
bool CommandParse(char *commandline)
{
#define SEP " "g_argc=0;//使用strtok进行参数的分割,之后再将参数存储至命令行参数表当中g_argv[g_argc++]=strtok(commandline,SEP);while((bool)(g_argv[g_argc++]=strtok(nullptr,SEP)));//最终统计的参数个数会比实际的多一个,此时需要将g_argc减一g_argc--;//最后通过判断g_argc的个数来判断用户是否输入有效的命令return g_argc>0? true:false;
}void Print()
{for(int i=0;g_argv[i];i++){printf("argv[%d]->%s\n",i,g_argv[i]);}printf("argv:%d\n",g_argc);for(int i=0;g_env[i];i++){printf("argv[%d]->%s\n",i,g_env[i]);}
}//检查用户输入的指令是否为内建命令bool CheckAndExecBuiltin()
{std::string cmd=g_argv[0];if(cmd=="cd"){cd(); return true;}else if(cmd=="echo"){Echo();return true;}else if(cmd=="export"){Export();return true;} else{//……}
return false;
}//创建子进程执行命令
int Execute()
{//创建子进程pid_t pid=fork();if(pid==0){//在子进程当中进行进程替换execvp(g_argv[0],g_argv);exit(1);}//进行进程等待int status=0;pid_t rid=waitpid(pid,&status,0);if(rid>0){//设置进程退出码lastcode值lastcode=WEXITSTATUS(status);}return 0;}int main()
{//从父进程当中获取环境变量表InitEnv();while(true){//1.输出命令行提示符PrintCommandPrompt();//2.获取用户输入的命令//创建数组存储用户输出的数据char commandline[COMMAND_SIZE];if(!GetCommandLine(commandline,sizeof(commandline)))continue;//3.命令行分析if(!CommandParse(commandline))continue;// Print();//4.检测并处理内建命令if(CheckAndExecBuiltin())continue;//5.执行命令Execute();// Print();//char arr[1024];//scanf("%s",arr);}return 0;
}
以上就是本篇的全部内容了,接下来我们将开始基础IO的学习,未完待续……