目录
[以下是mini shell的代码实现,仅供参考:](#以下是mini shell的代码实现,仅供参考:)
[6. 总结](#6. 总结)
5.自主Shell命令行解释器



到此为止,代码结构搞定!!

将上面以 .c 结尾的文件,重命名为 .cc 文件。Makefile文件做以下调整:




运行结果:

所以命令行解释器是一个软件 --------- 是一个死循环的软件

运行结果:

但是此时打印出来的结果就不对了,所以要获取用户输入的命令。





此时,在运行程序的话,就会停下来等待用户的输入:

从运行的结果来看,会多一个空行,原因如下:

再次,make:

我们在写打印的时候是没有加 "\n"的,但是从上面的输出结果却自动换行了,是自动加上了 "\n" 的。
这个是因为我们自己在输入 :"ls -a -l -n + 回车",用户按了回车键


此时就是一个纯净的命令行字符串了。

图中的 cmd_str_buff[] 绝对不可能是空字符串的,因为至少要按一下回车键。如果是空串的话,需要进一步进行判断:

运行结果:

这样看,不好看。你给我什么我就返回什么,但是我要给你手动加一个 "\n"



解析出来的命令行参数表必须也得被子进程继承下去,所以将此命令行参数表定义成全局的。









修改myshell.h:


运行结果:

用全局变量的话,需要注意的是,在每次用之前都要进行清空的处理:



运行结果:[基本正确]
但是还是有一点错误:

修改代码:


不想调试了,直接注释掉语句:#define DEBUG

此时将不再回显:









命令行解释器,本质上就是输出命令行,阻塞着等待你做输入,输入对应的命令做解析,解析完之后做fork,让子进程去执行。
最基础的命令行解释器就做好了,结构、完整代码如下:

cpp
//Makefile
myshell:main.cc myshell.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f myshell
cpp
//myshell.h
#ifndef __MYSHELL_H__
#define __MYSHELL_H__
#include <stdio.h>
#define ARGS 64
void InitGlobal();
void PrintCommandPrompt();
bool GetCommandString(char cmd_str_buff[],int len);
bool ParseCommandString(char cmd[]);
void ForkAndExec();
#endif
cpp
//myshell.cc
#include "myshell.h"
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//命令行参数表,故意定义为全局的
char *gargv[ARGS] = {NULL};
int gargc = 0;
void Debug()
{
printf("Hello Shell!\n");
}
static std::string GetUserName() //static不用在.h文件进行声明,仅在本文件使用
{
std::string username = getenv("USER");
return username.empty() ? "None" : username;
//if(username.empty())
// return "None";
//return username;
}
static std::string GetHostName()
{
std::string hostname = getenv("HOSTNAME");
return hostname.empty() ? "None" : hostname;
}
static std::string GetPwd()
{
std::string pwd = getenv("PWD");
return pwd.empty() ? "None" : pwd;
}
//输出提示符
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\0
cmd_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 " "
//"ls -a -l" -> "ls" "-a" "-l"
gargv[gargc++] = strtok(cmd,SEP);
// 整个数组,最后以NULL结尾
while((bool)(gargv[gargc++] = strtok(NULL,SEP)));
// 上一行代码为空时,也加了一次,回退一次,命令行参数的个数
gargc--;
//#define DEBUG
#ifdef DEBUG
printf("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]);
}
#endif
return true;
}
void InitGlobal()
{
gargc = 0;
memset(gargv,'0',sizeof(gargv));
}
void ForkAndExec()
{
pid_t id = fork();
if(id < 0)
{
perror("fork"); //error ---> errstring;
return;
}
else if(id == 0)
{
//子进程 --- 执行对应的命令,程序替换 execvp
execvp(gargv[0],gargv);
exit(0);
}
else
{
//父进程 --- 进程等待
pid_t rid = waitpid(id,nullptr,0);
if(rid > 0)
{
//等待成功
}
}
}
cpp
//main.cc
#include "myshell.h"
#define SIZE 1024
int main()
{
char commandstr[SIZE];
while(true)
{
//0.初始化操作
InitGlobal();
//1.输出命令行提示符
PrintCommandPrompt();
//2.获取用户输入的命令
if(!GetCommandString(commandstr,SIZE))
continue;
// printf("%s\n",commandstr);
//3. "ls -a -l" --> 拆解为 "ls" "-a" "-l"
//对命令字符串,进行解析 -> 解析成命令行参数表
ParseCommandString(commandstr);
//4.执行命令,让子进程来执行,这也是在shell当中创建子进程的原因
//(不敢直接就程序替换,替换之后谁来进行初始化的操作之类呢?)
ForkAndExec();
}
return 0;
}
基于现在的代码来理解历史知识:

为什么 cd .. 命令无法进行回退??


因为是在让子进程执行cd .. ,父进程路径根本就没有变化。
我们之前进行路径切换,本质是父进程bash在进行切换,fork,路径就会被子进程继承下去,pwd,查到新路径!!!
所以像 cd 这样的命令,就不能让子进程运行,而应该让 shell 自己运行!!!





输入cd .. 之后 pwd ,路径确实发生了变化,但是命令行提示符的路径却是一直没有变化的。
此时的pwd如下:

但是在env中查看pwd变量时,并没有变化:


应该如何更新环境变量呢?



再次查看env,同样 pwd也是不变的:

pwd环境变量需要bash自己动态的更新,新的路径
环境变量本质上是一张指针数组的表,由我们的进程真实的记录下来的,有对应的pwd。

曾经说过:环境变量是你的 shell 从配置文件加载进来的,原则上是你自己对应的shell内部就应该有一张 char *genv[]这样的环境变量所对应的表,再加载配置文件的时候,给每个指针new空间,再将每个指针指向对应的空间,把字符串往里面拷,但是在myshell.cc中没有用环境变量表,因为当前的shell环境变量表继承自他的父进程真正的bash进程的环境变量表,没有从配置文件中加载进来。但是又要实现出来一个环境变量更新的情况:






成功!!!
系统的命令行提示的内容只有当前路径的最后一个单词,但是我们自己实现的却有一长串,如何更改代码?

运行结果:

内建命令:是shell自己执行的命令。如同shell执行一个自己的函数
shell是会拿到最近的一个子进程退出时的退出码,我们当前写的命令行解释器还没有支持此项功能,那么echo ? 中的 ? 到底是什么?


echo 命令也是内键命令:

a是本地变量,是无法被子进程继承的。但是,echo $a,也能把子进程访问出来。这说明echo命令也是内键命令,不然无法访问shell内部的变量。


运行结果:

mini shell支持打印环境变量:


运行结果:

以上就是我们mini shell的实现,但是只支持基本功能,还有一些功能未能实现,例如:重定向功能,还会在后续的文章中进行功能的添加。
以下是mini shell的代码实现,仅供参考:
cpp
//myshell.h
#ifndef __MYSHELL_H__
#define __MYSHELL_H__
#include <stdio.h>
#define ARGS 64
void InitGlobal();
void PrintCommandPrompt();
bool GetCommandString(char cmd_str_buff[],int len);
bool ParseCommandString(char cmd[]);
void ForkAndExec();
bool BuiltInCommandExec();
#endif
cpp
//myshell.cc
#include "myshell.h"
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <cstdio>
#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");
}
static std::string GetUserName() //static不用在.h文件进行声明,仅在本文件使用
{
std::string username = getenv("USER");
return username.empty() ? "None" : username;
//if(username.empty())
// return "None";
//returnusername;
}
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自己的环境变量pwd
snprintf(pwd,sizeof(pwd),"PWD=%s",temp);
putenv(pwd); //导出,导成环境变量
// /home/sy/linux
std::string pwd_lable = temp;
const std:: string pathsep = "/";
auto pos = pwd_lable.rfind(pathsep); //倒着去找,找到第一个分割符就停下来
if(pos == std::string::npos) {
return "None";
}
// /linux,从l开始,截取linux这么长
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\0
cmd_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 " "
//"ls -a -l" -> "ls" "-a" "-l"
gargv[gargc++] = strtok(cmd,SEP);
// 整个数组,最后以NULL结尾
while((bool)(gargv[gargc++] = strtok(NULL,SEP)));
// 上一行代码为空时,也加了一次,回退一次,命令行参数的个数
gargc--;
//#define DEBUG
#ifdef DEBUG
bool BuiltInCommandExec();
printf("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]);
}
#endif
return true;
}
void InitGlobal()
{
gargc = 0;
memset(gargv,'0',sizeof(gargv));
}
void ForkAndExec()
{
pid_t id = fork();
if(id < 0)
{
perror("fork"); //error ---> errstring;
return;
}
else if(id == 0)
{
//子进程 --- 执行对应的命令,程序替换execvp
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()
{
//gargv[0]
std::string cmd = gargv[0];
bool ret = false;
if(cmd == "cd")
{
//内键命令
if(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);
//echo本身也是一条命令,所以将lastcode置为0
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;
}
cpp
//main.cc
int main()
{
char commandstr[SIZE];
while(true)
{
//0.初始化操作
InitGlobal();
//1.输出命令行提示符
PrintCommandPrompt();
//2.获取用户输入的命令
if(!GetCommandString(commandstr,SIZE))
continue;
// printf("%s\n",commandstr);
//3. "ls -a -l" --> 拆解为 "ls" "-a" "-l"
//对命令字符串,进行解析 -> 解析成命令行参数表
ParseCommandString(commandstr);
//4.检查命令,内键命令,要让shell自己执行!!!
if(BuiltInCommandExec())
{
continue;
}
//5.执行命令,让子进程来执行,这也是在shell当中创建子进程的原因
//(不敢直接就程序替换,替换之后谁来进行初始化的操作之类呢?)
ForkAndExec();
}
return 0;
}
cpp
//Makefile
myshell:main.cc myshell.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f myshell
6. 总结


运行结果:
