我们都知道,我们给Linux下发的指令都是shell帮我们处理并完成的,那么他是怎么完成的呢?不难想到他都是通过环境变量以及程序替换来完成的。我们这一篇文章就手把手来教你怎么自己实现一个简单的shell。
目标:
1.要能处理普通命令
2.要能处理内建命令
3.要能帮助我们理解内建命令/本地变量/环境变量这些概念
4.要能帮助我们理解shell的允许原理
思维导图 
打印命令行
这里的打印命令行可不是简单的打印出来,而是要像我们引言里讲的那样使用环境变量和程序替换来打印命令行。

我们可以看到,这个命令行中间包含了用户名,主机名和当前路径,我们需要通过环境变量得到这些信息并打印出来。我们查看环境变量,将我们需要的环境变量圈出来了。
我们就可以在myshell.cc文件里去使用它。如下图所示。
结果图是这样的。
至此,我们就完成了第一步,打印命令行。
获取命令行
打印命令行已经提醒了用户"你可以输入你的命令了!"。所以我们需要获取用户输入的命令,再去执行用户的命令。
首先,定义一个数组用来存放用户的命令,让他1024个字节那么大;然后用fgets来提取用户输入的命令,放在数组里。为了显示成果,我们将数组里存放的用户命令回显出来,用于检测。代码就是这样的:
结果是这样的:
但是呢,我觉得我们的代码有点乱,我们将这些命令封装起来吧。至此,我们完成了第二步。
解析命令行
所谓解析命令行,其实就是将用户输入的每一个命令都分割开来,然后保存在一个数组里面,方便之后去执行命令。
首先我们定义一个全局数组g_argv和一个全局变量g_argc,前者保存命令,后者记录命令个数,然后用strtok函数将命令切割,最后将函数封装起来,之后在main函数里面去调用它。
我们看看代码怎么写的。
这就是解析命令行这个动作的核心,我们来看看结果怎么样。
执行命令
执行命令我们就需要用到程序替换的知识了,需要创建子进程,让父进程获取子进程的结果,最后执行命令。我们在这里回顾复习一下程序替换这一知识点,⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀种exec函数以执⾏另⼀个程序。当进程调⽤⼀种exec函数时,该进程的⽤⼾空间代码和数据完全被新程序替换,从新程序的启动例程开始执⾏。调⽤exec并不创建新进程,所以调⽤exec前后该进程的id并未改变。
而我们这里需要用到的exec函数是execvp,v的含义就是参数是数组,因为我们之前获取用户输入的命令是由数组接收的,所以参数是数组,p的含义就是自动搜索环境变量。我们封装成函数就是这样的。使用execvp函数,父进程使用waitpid函数等待子进程的结果。最后返回就完成了这个任务。
我们看一下结果
但是,我在检测这个自定义的shell的时候,发现了一个问题,可以看一下
这个代码可以看出,cd命令并没有被执行,查看路径并没有变化,这是因为,cd是内建命令,接下来,我们就来谈一谈。
理解内建命令
我们自定义的shell执行命令都是通过创建子进程程序替换来实现的,子进程都有自己的路径,子进程并不能影响bash,所以,我们不能用子进程来执行命令,让bash亲自来执行这个命令。首先我们来梳理一下流程。看看main函数
我们现在要做的流程是第四步,检测并执行内建命令。接下来,我们探讨一下封装的CheckAndExecBuilt函数内部的思路是啥。
内建命令就拿cd来说,我们输入的是cd,或者cd /,或者cd ~等等,来切换目录,当用户输入这个命令的时候就应该被这个函数捕捉到,然后让bash亲自执行。用户输入的命令我们前面已经定义了全局变量g_argv数组,所以函数内部就直接用。我们分析一下,如图所示,用户输入的cd是数组的第一位,有区别的就是后面的,我们先判断是不是输入的内建函数,如果是,那g_argv[1]输入的是空格还是别的,我们这里就不细分作用了,就直接都返回家目录吧。这里要提一下,切换目录的函数时chdir,
chdir
是 C 语言标准库中的一个函数,用于改变当前进程的工作目录(Working Directory)。我们就使用这个函数来回到家目录,具体用法,等下看看代码就懂了。
上面分析了思路,我们看看代码怎么写的。
内建命令的精华就都在代码里了,好好消化一下,理解一下。
内建命令echo
我们上面提到了内建命令cd,我们再来探讨一个内建命令echo,这个命令是返回退出码的,也分好几种,我给大家罗列一下,如图所示,第一种返回字符串"hello world",第二个需要返回退出码,第三个返回环境变量。
首先,我们将这三种情况用if语句来分开,第二种好办,直接返回lastcode,第一种也好办,直接返回字符串,而第三种,先获得用户输入的环境变量是什么,然后再用getenv()这个函数来获取系统路径等。最后打印出来。大体思路就是这样,具体代码怎么写,如下图所示。
做出自己的环境变量表
我们自己自定义了一个shell,并且也自己设置了命令行参数表g_argv,平常只要启动shell,都会自己启动环境变量表,那么我们这个shell是不是也应该有环境变量表呢?我们应该怎么去做呢?
shell启动时,一般是从系统中获得环境变量表,我们这里就可以从父shell中拷贝过来。具体的,首先像命令行参数表一样,定义一个全局的环境变量表g_env,然后将他初始化为0。
接着,通过循环来实现拷贝。最后使用putenv将他导成环境变量,就实现了父shell的拷贝,我们中间加个小环节来验证一下,具体代码如下图所示。
这就证实了,我们用的就是新的环境变量表。
总结
我们这里只是探讨了shell的冰山一角,其实还有很多,但是,一直写就没什么意义了,自定义shell的意义其实就是打通之前所学的知识,将他们串起来,教学意义。
代码汇总
cpp
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <unordered_map>
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "
// 下面是shell定义的全局数据
// 1. 命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0;
// 2. 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;
// 3. 别名映射表
std::unordered_map<std::string, std::string> alias_list;
// for test
char cwd[1024];
char cwdenv[1024];
// last exit code
int lastcode = 0;
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");
const char *pwd = getcwd(cwd, sizeof(cwd));
if(pwd != NULL)
{
snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
putenv(cwdenv);
}
return pwd == NULL ? "None" : 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;
//本来要从配置文件来
//1. 获取环境变量
for(int i = 0; environ[i]; i++)
{
// 1.1 申请空间
g_env[i] = (char*)malloc(strlen(environ[i])+1);
strcpy(g_env[i], environ[i]);
g_envs++;
}
g_env[g_envs++] = (char*)"HAHA=for_test"; //for_test
g_env[g_envs] = NULL;
//2. 导成环境变量
for(int i = 0; g_env[i]; i++)
{
putenv(g_env[i]);
}
environ = g_env;
}
//command
bool Cd()
{
// cd argc = 1
if(g_argc == 1)
{
std::string home = GetHome();
if(home.empty()) return true;
chdir(home.c_str());
}
else
{
std::string where = g_argv[1];
// cd - / cd ~
if(where == "-")
{
// Todu
}
else if(where == "~")
{
// Todu
}
else
{
chdir(where.c_str());
}
}
return true;
}
void Echo()
{
if(g_argc == 2)
{
// echo "hello world"
// echo $?
// echo $PATH
std::string opt = g_argv[1];
if(opt == "$?")
{
std::cout << lastcode << std::endl;
lastcode = 0;
}
else if(opt[0] == '$')
{
std::string env_name = opt.substr(1);
const char *env_value = getenv(env_name.c_str());
if(env_value)
std::cout << env_value << std::endl;
}
else
{
std::cout << opt << std::endl;
}
}
}
// / /a/b/c
std::string DirName(const char *pwd)
{
#define SLASH "/"
std::string dir = pwd;
if(dir == SLASH) return SLASH;
auto pos = dir.rfind(SLASH);
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());
//snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt, sizeof(prompt));
printf("%s", prompt);
fflush(stdout);
}
bool GetCommandLine(char *out, int size)
{
// ls -a -l => "ls -a -l\n" 字符串
char *c = fgets(out, size, stdin);
if(c == NULL) return false;
out[strlen(out)-1] = 0; // 清理\n
if(strlen(out) == 0) return false;
return true;
}
// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " "
g_argc = 0;
// 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
g_argv[g_argc++] = strtok(commandline, SEP);
while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
g_argc--;
return g_argc > 0 ? true:false;
}
void PrintArgv()
{
for(int i = 0; g_argv[i]; i++)
{
printf("argv[%d]->%s\n", i, g_argv[i]);
}
printf("argc: %d\n", g_argc);
}
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")
{
}
else if(cmd == "alias")
{
// std::string nickname = g_argv[1];
// alias_list.insert(k, v);
}
return false;
}
int Execute()
{
pid_t id = fork();
if(id == 0)
{
//child
execvp(g_argv[0], g_argv);
exit(1);
}
int status = 0;
// father
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
}
return 0;
}
int main()
{
// shell 启动的时候,从系统中获取环境变量
// 我们的环境变量信息应该从父shell统一来
InitEnv();
while(true)
{
// 1. 输出命令行提示符
PrintCommandPrompt();
// 2. 获取用户输入的命令
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline, sizeof(commandline)))
continue;
// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
if(!CommandParse(commandline))
continue;
//PrintArgv();
// 检测别名
// 4. 检测并处理内键命令
if(CheckAndExecBuiltin())
continue;
// 5. 执行命令
Execute();
}
//cleanup();
return 0;
}