1.程序目标
通过观察shell,我们发现它是由打印命令行内容,获取命令行内容,分析命令,执行命令 这四个步骤组成的;而在启动shell之后就会有一个bash,简单来说,bash 是一个程序,更准确地说,是一个命令行解释器。当你登录系统、打开终端时,系统就会为你创建一个 bash 进程。它是你的"管家":你输入的所有命令,比如 ls、cd、g++,都是由这个 bash 进程负责接收和处理的。它是"父进程":绝大多数命令(比如你之前想编译的 myshell)在执行时,bash 都会通过一种叫 fork 的方式,创建一个和自己几乎一模一样的子进程来实际运行这些命令。而你面前的 bash 则会进入一种"休眠"等待的状态,直到子进程运行结束,它才会重新醒过来,等待你的下一条指令。这就像一个经理把具体工作分配给下属,自己等着汇报一样。我们所在的路径,本质上就是由最顶层的 bash 进程所定义和管理的。每当你在 Shell 中敲下命令,无论是改变目录还是执行程序,都围绕着这个"工作目录"在转。你在编写 myshell 时,其实就是在尝试模拟这个过程:你的 myshell 进程会成为新的"父进程",负责接收命令、创建子进程去执行,并维护它自己的工作目录。这也是为什么你需要处理 cd 这样的内建命令,因为它必须由你的 myshell 自己来执行,才能改变它自身的"工作目录",进而影响后续创建的子进程;总的来说就是我们的myshell其实就是在模拟bash的操作;
现在我们先来实现它的命令行吧!
1.打印命令行
1.1重点解析

我们要能打印出来的也就是我标红的地方,这三个参数分别是用户名、主机名、当前所处的路径;而想要得到这三个参数,最好的方法就是使用env,因为在env中存储与此相光的数据;因此我们就需要使用下面的C函数,getenv( )

使用getenv()函数将我们需要的用户名,主机号,以及路径都可以得到,但是如果你的服务器是ubuntu( )主机号就不是通过getenv获得的了,因为不存储再env()中;snprintf() 是 C 语言中安全的字符串格式化函数,可以防止缓冲区溢出。也就是把我们指定格式的字符串,安全的写入指定大小的缓冲区内;

1.2代码实现
cpp
string GetUserName()
{
string name =getenv("LOGNAME");
return name.empty()?"None":name;
}
string GetHostName()
{
char hostname[64+1];
if(gethostname(hostname,sizeof(hostname))==0)
{
string Hostname(hostname);
return Hostname;
}
else
{
return "None";
}
}
string GetPwd()
{
//放在局部的话,函数调用结束,这个缓冲区就会自动被释放
// char pwd[basesize];
if(nullptr == getcwd(pwd,sizeof(pwd)) ) return "None";
//修改环境变量
snprintf(pwdenv,sizeof(pwdenv),"PWD=%s",pwd);
putenv(pwdenv);
return pwd;
// string pwd =getenv("PWD");
// return pwd.empty()?"None":pwd;
}
//为了将路径可以省略成~的进行省略
string LastDir()
{
string curr=GetPwd();
if(curr=="/" || curr =="None")
return curr;
size_t pos =curr.rfind("/");
return "~"+curr.substr(pos+1);
}
//构建一个输出的命令行
string MakeCommandLine()
{
//root@VM-4-2-ubuntu:~/test#
char command_line[basesize];
snprintf(command_line,basesize,"%s@%s:%s# ",\
GetUserName().c_str(),GetHostName().c_str(),LastDir().c_str());
return command_line;
}
//1.打印命令行
void PrintCommandLine()
{
printf("%s",MakeCommandLine().c_str());
//没有\n,刷新不出来,用函数fflush;
fflush(stdout);
}
2.获取命令
2.1重点分析
从输入流中获取我们想要的字符串可以使用fgets( )函数,

我们发现我们在输入命令之后还输入了一个回车(\n),它会被fgets( )读入字符串,需要手动去掉;不去掉的话后面无论是计算字符串长度等等,结果都会被影响;

2.2代码实现
cpp
bool GetCommandLine(char command_buffer[],int basesize)
{
char *result = fgets(command_buffer,basesize,stdin);//从标准输入中获取
if(result)
{
command_buffer[strlen(command_buffer)-1]=0;
//strlen计算的是字符串的长度,数组下标从0开始,
//将最后一个字符置为0就是将\n置为0
if(strlen(command_buffer)==0)
{
return false; //这个就是在判断只有换行的清空,让它不能再在argv里面++
}
return true;
}
return false;
}
3.解析命令行
3.1重点分析
当我们在电脑屏幕上输入内容之后,我们该怎么将 ls -a -l 这样的一行字符串进行解析呢?还得让他们具体的每一个找到他们对应的操作,是的,我们需要把他们按照空格分开,而strtok( )函数的作用就是将字符串按照指定的符号进行分割;但是这个函数非常奇怪,让需要你两次调用,第一次你str传你想要分割的字符串,第二次如果还是这个字符串的话,你需要再调用他一次,但是这次的str要传NULL;该分割的都分割了之后它返回一个NULL给你;它的最后可以自动返回nullptr的特性和正好我们的命令行参数->argv的最后一个元素是nullptr一样,所以我们选用它;
而我们定义一个变量gargc,让他来记录我们的命令被分割了多少个,char*gargv[ ]中每一个地址对应一个参数,这样我们后面实行起来执行起来命令不就简单多了,这个时候我们再想如果刚才我们没有将最后一个换行符减去,现在不久要多执行一个换行;




3.2代码实现
cpp
//3,解析字符串
void ParseCommandLine(char command_buffer[])
{
//因为打印和获取命令的操作一直在循环做,所以我们要将其清空
memset(gargv ,0,sizeof(gargv));
gargc=0;
//printf("%p\n",&gargc);
const char * sep =" ";
gargv[gargc++]=strtok(command_buffer,sep);
while(gargv[gargc++]=strtok(nullptr,sep));//这里传nullptr表示我要切的是上一个被切的字符串
gargc--;
}
4.执行命令行
4.1重点分析
所谓的执行命令到底是谁在执行呢?肯定是fork出来的子进程了,因为每一个bash都是一个进程,你让父进程我们自己的shell执行命令不就乱套了吗?因此我们现在需要用到fork来实现了,而什么ls -a 、pwd、cd code .......这样的命令其实都是在我们的Linux系统的PATH的环境变量中存储着,我们自己实现还是有点困难的,因此我们直接程序替换成Linux系统中的就简单多了;而我们最后选用的是execvpe,e代表的使环境变量,这是因为我们还要实现env这样的内建命令,所谓的内建命令就是应该让父进程实现,子进程继承的命令,比如cd ..,只有父进程实现,后面的子进程才能查上一个路径的是时候查到呀,还有echo $?,查看上一个进程的退出码,这个也得父进程保存子进程继承才能得到呀,还有export,向环境变量中插入一个新的环境变量,你说环境变量是不是子进程继承父进程的?呢增加环境变量也应该父进程是下了;

使用execvp施行程序替换





环境变量是一个需要维护的变量,pwd是环境变量,所以需要我们自己维护;
所以使用系统调用接口;

永远不要忘记我们的shell是系统shell的子进程,会继承父进程的环境变量表,但是我们子进程对父进程的变量表修改之后,会反正写时拷贝,我们的子进程会拥有一张和父进程一模一样的表,这个时候我们就可以对我们的环境变量表做修改了;





4.2 代码实现
cpp
//4.执行命令
bool ExecuteCommand()
{
//让子进程执行命令
pid_t id =fork();
if(id<0)
{
return false;
}
else if(id == 0)
{
//子进程
//1.执行命令
execvpe(gargv[0],gargv,genv);
//2.退出
exit(1); //只要退出码是1就退出进程,证明程序替换就失败了
}
int status =0;
pid_t rid=waitpid(id,&status,0);
if(rid > 0)
{
//等待成功
if(WIFEXITED(status))
{
lastcode=WEXITSTATUS(status);
}
else
{
lastcode =3;
}
return true;
}
return false;
}
//5.处理内建命令
//shell自己执行命令本质是shell调用自己的函数
void AddEnv(char * item)
{
int index=0;
while(genv[index])
{
index++;
}
genv[index]=(char*)malloc(strlen(item)+1);
strncpy(genv[index],item,strlen(item)+1);
genv[++index]=nullptr;
}
bool CheckAndExecBuiltCommand()
{
if(strcmp(gargv[0],"cd")==0)
{
//printf("%d",gargc);
if(gargc==2)
{
chdir(gargv[1]);
lastcode =0;
}
else
{
lastcode=1;
}
return true;
}
else if(strcmp(gargv[0],"export")==0)
{
if(gargc==2)
{
AddEnv(gargv[1]);
lastcode=0;
}
else
{
lastcode =2;
}
return true;
}
else if(strcmp(gargv[0],"env")==0)
{
for(int i=0; genv[i];i++)
{
printf("%s\n",genv[i]);
}
lastcode =0;
return true;
}
else if(strcmp(gargv[0],"echo")==0)
{
if(gargc==2)
{
//echo $?
if(gargv[1][0] =='$')
{
if(gargv[1][1] =='?')
{
printf("%d\n",lastcode);
lastcode=0;
}
}
else
{
printf("%s\n",gargv[1]);
lastcode =0;
}
}
else
{
lastcode= 3;
}
return true;
}
return false;
}
//处理自己的环境变量表
//我们今天就直接从父进程中获环境变量表
//环境变量在我们的虚拟地址空间表中
void InitEnv()
{
extern char ** environ;
int index=0;
while(environ[index])
{
genv[index]=(char*)malloc(strlen(environ[index]+1));
strncpy(genv[index],environ[index],strlen(environ[index])+1);
index++;
}
genv[index]=nullptr;
}
5.完整代码实现
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
const int basesize = 1024;
const int argvnum = 64;
const int envnum=64;
char * gargv[argvnum];
char pwd[basesize];
int gargc=0;
int lastcode=0;
//我自己系统的环境变量
char * genv[envnum];
char pwdenv[basesize];
string GetUserName()
{
string name =getenv("LOGNAME");
return name.empty()?"None":name;
}
string GetHostName()
{
char hostname[64+1];
if(gethostname(hostname,sizeof(hostname))==0)
{
string Hostname(hostname);
return Hostname;
}
else
{
return "None";
}
}
string GetPwd()
{
//放在局部的话,函数调用结束,这个缓冲区就会自动被释放
// char pwd[basesize];
if(nullptr == getcwd(pwd,sizeof(pwd)) ) return "None";
//修改环境变量
snprintf(pwdenv,sizeof(pwdenv),"PWD=%s",pwd);
putenv(pwdenv);
return pwd;
// string pwd =getenv("PWD");
// return pwd.empty()?"None":pwd;
}
string LastDir()
{
string curr=GetPwd();
if(curr=="/" || curr =="None")
return curr;
size_t pos =curr.rfind("/");
return "~"+curr.substr(pos+1);
}
//构建一个输出的命令行
string MakeCommandLine()
{
//root@VM-4-2-ubuntu:~/test#
char command_line[basesize];
snprintf(command_line,basesize,"%s@%s:%s# ",\
GetUserName().c_str(),GetHostName().c_str(),LastDir().c_str());
return command_line;
}
//1.打印命令行
void PrintCommandLine()
{
printf("%s",MakeCommandLine().c_str());
//没有\n,刷新不出来,用函数fflush;
fflush(stdout);
}
//2.获取字符串
bool GetCommandLine(char command_buffer[],int basesize)
{
char *result = fgets(command_buffer,basesize,stdin);//从标准输入中获取
if(result)
{
command_buffer[strlen(command_buffer)-1]=0;
//strlen计算的是字符串的长度,数组下标从0开始,
//将最后一个字符置为0就是将\n置为0
if(strlen(command_buffer)==0)
{
return false; //这个就是在判断只有换行的清空,让它不能再在argv里面++
}
return true;
}
return false;
}
//3,解析字符串
void ParseCommandLine(char command_buffer[])
{
//因为打印和获取命令的操作一直在循环做,所以我们要将其清空
memset(gargv ,0,sizeof(gargv));
gargc=0;
//printf("%p\n",&gargc);
const char * sep =" ";
gargv[gargc++]=strtok(command_buffer,sep);
while(gargv[gargc++]=strtok(nullptr,sep));//这里传nullptr表示我要切的是上一个被切的字符串
gargc--;
}
// void debug()
// {
// printf("argc: %d\n",gargc);
// for(int i=0;gargv[i];i++)
// {
// printf("argv[%d]:%s\n",i,gargv[i]);
// }
// }
//4.执行命令
bool ExecuteCommand()
{
//让子进程执行命令
pid_t id =fork();
if(id<0)
{
return false;
}
else if(id == 0)
{
//子进程
//1.执行命令
execvpe(gargv[0],gargv,genv);
//2.退出
exit(1); //只要退出码是1就退出进程,证明程序替换就失败了
}
int status =0;
pid_t rid=waitpid(id,&status,0);
if(rid > 0)
{
//等待成功
if(WIFEXITED(status))
{
lastcode=WEXITSTATUS(status);
}
else
{
lastcode =3;
}
return true;
}
return false;
}
//5.处理内建命令
//shell自己执行命令本质是shell调用自己的函数
void AddEnv(char * item)
{
int index=0;
while(genv[index])
{
index++;
}
genv[index]=(char*)malloc(strlen(item)+1);
strncpy(genv[index],item,strlen(item)+1);
genv[++index]=nullptr;
}
bool CheckAndExecBuiltCommand()
{
if(strcmp(gargv[0],"cd")==0)
{
//printf("%d",gargc);
if(gargc==2)
{
chdir(gargv[1]);
lastcode =0;
}
else
{
lastcode=1;
}
return true;
}
else if(strcmp(gargv[0],"export")==0)
{
if(gargc==2)
{
AddEnv(gargv[1]);
lastcode=0;
}
else
{
lastcode =2;
}
return true;
}
else if(strcmp(gargv[0],"env")==0)
{
for(int i=0; genv[i];i++)
{
printf("%s\n",genv[i]);
}
lastcode =0;
return true;
}
else if(strcmp(gargv[0],"echo")==0)
{
if(gargc==2)
{
//echo $?
if(gargv[1][0] =='$')
{
if(gargv[1][1] =='?')
{
printf("%d\n",lastcode);
lastcode=0;
}
}
else
{
printf("%s\n",gargv[1]);
lastcode =0;
}
}
else
{
lastcode= 3;
}
return true;
}
return false;
}
//处理自己的环境变量表
//我们今天就直接从父进程中获环境变量表
//环境变量在我们的虚拟地址空间表中
void InitEnv()
{
extern char ** environ;
int index=0;
while(environ[index])
{
genv[index]=(char*)malloc(strlen(environ[index]+1));
strncpy(genv[index],environ[index],strlen(environ[index])+1);
index++;
}
genv[index]=nullptr;
}
int main()
{
InitEnv();
char command_buffer[basesize];
while(true)
{
PrintCommandLine(); //打印命名行
// printf("\n");
// sleep(1);
if( !GetCommandLine(command_buffer,basesize)) //获取命令行
{
continue;
}
// printf("%s\n",command_buffer);
ParseCommandLine(command_buffer); //分析命令行
// debug();
//检测并执行内建命令
if(CheckAndExecBuiltCommand())
{
continue;
}
ExecuteCommand(); //执行命令行
}
return 0;
}
