文章目录
前言
在上一篇文章中,我们详细介绍了进程创建、进程终止、进程等待和进程程序替换的内容,内容还是挺多的,希望大家可以多去练习熟悉一下,那么本篇文章将带大家详细讲解自定义Shell的内容,接下来一起看看吧!
一. shell原理
当我们执行程序创建进程时,我们的shell命令行解释器也就是bash都做了什么工作呢?
- 输出命令行提示符
- 获取并解析输入的指令
- 判断是否为内建命令,如果不是则创建子进程执行命令
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序,并等待这个进程结束。所以要写一个shell,需要循环以下过程:
- 获取命令行
- 解析命令行
- 建立一个子进程(fork)
- 替换子进程(execvp)
- 父进程等待子进程退出(wait)
二. 自定义shell的实现
2.1 输出命令行提示符

bash在每次都会输出命令行提示符,然后等待我们用户输入。
看这个命令行提示符,它包含以下信息:
- 用户名
USER;- 主机名
HOSTNAME;- 当前工作路径
PWD;- 这些在环境变量表中都能够找到,所以就可以使用
getenv来获取。
这样我们需要获取环境变量USER、HOSTNAME、PWD等
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "
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()
{
//connst 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;
}
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());
}
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt,sizeof(prompt));
printf("%s", prompt);
fflush(stdout);
}

可以看到效果就跟上面的差不多了,为了区分系统的shell,所以将$改为#
2.2 获取用户输入的信息
接下来就应该获取命令行信息了

我们输入一行指令,然后再按回车就可以执行命令,所以我们输入的命令中是可以带空格的。我们不应该使用scanf/cin进行输入了,可以使用fgets来输入一行字符串。
bool GetCommandLine(char* out, int size)
{
char* c = fgets(out, size, stdin);
if(c==NULL) return 1;
out[strlen(out)-1] = '\0'; //清理\n
if(strlen(out)==0) return false;
return true;
}
2.3 命令行解析
获取了用户输入的信息,但是获得的只是一个字符串,而我们想执行用户输入的命令,就要先对这个字符串进行解析;生成对应的命令行参数表,才能去执行。
命令行参数个数g_argc,命令行参数表g_argv;我们可以设置成全局的,这样每次只用修改g_argc和g_argv即可。
我们可以使用strtok函数来分割字符串,可以以空格为分隔符进行分割,获得的每一个字符串存储到命令行参数表g_argv,再++命令行参数个数g_argc即可。
char *strtok(char *str, const char *delim);
功能:将字符串 str 按 delim 中的字符分割成若干子串(token)。
特点:修改原字符串,把匹配到的分隔符改成 ‘\0’。
使用流程:
| 步骤 | 调用方式 | 说明 |
|---|---|---|
| 第一次 | strtok(str, delim) | 传入非 NULL 字符串,返回第一个 token |
| 后续 | strtok(NULL, delim) | 传入 NULL,继续从上一次位置往后找 |
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc = 0;
bool CommandParse(char* commandline)
{
#define SEP " "
g_argc = 0;
g_argv[g_argc++] = strtok(commandline, SEP);
while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
g_argc--;
return g_argc > 0 ? true : false;
}
2.4 创建子进程执行命令
解析命令行并且生成命令行参数表之后,现在就可以去执行命令了,那么怎么执行命令呢?
我们的
shell并不是自己去执行,而是创建子进程,然后让子进程去执行命令,shell等待子进程退出
// last exit code
int lastcode = 0;
int Execute()
{
pid_t id = fork();
if(id==0)
{
// child
execvp(g_argv[0], g_argv);
exit(1);
}
// father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
//(void)rid; // rid使用一下
if(rid>0)
{
lastcode = WEXITSTATUS(status);
}
return 0;
}

三. 自定义shell的扩展实现
3.1 环境变量表
上面的shell我们可以运行了,但是没有考虑内建命令、环境变量表等这些东西,所以我们把它完善一下。
在bash启动时,它的环境变量表从我们系统的配置文件中来,但是这里没办法从系统配置文件中读,所以我们这里就只能从父进程bash获取环境变量表,通过environ来获取从父进程继承下来的环境变量表。
但是我们也要有我们自己的一张环境变量表,所以要手动维护一张自己的环境变量表,并且把它们导出来,通过putenv来导出这些环境变量。
#define MAX_ENVS 100
char* g_env[MAX_ENVS+1];
int g_envs = 0;
void InitEnv()
{
extern char** environ;
memset(g_env, 0, sizeof(g_env));
g_envs = 0;
// 本来要从配置文件来
// 1. 获取环境变量
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++] = (char*)"HAHA=for_test"; //for_test
g_env[g_envs] = NULL;
// 2. 导成环境变量
for(int i=0;g_env[i];i++)
{
putenv(g_env[i]);
}
}
3.2 内建命令
内建命令指的是bash不创建子进程去执行,而是bash自己去执行的命令,我们现在知道的内建命令有cd、export、echo、alias等。
cd
cd命令肯定不能让子进程去执行,因为修改的子进程的路径,而不是shell自己的路径,所以要让shell自己去执行,我们可以使用chdir系统调用来修改当前工作路径:

关于cd命令
cd:进入用户的家目录cd ~:进入用户的家目录cd where:进入指定路径cd -:进入上次的工作路径
char cwd[1024];
char cwdenv[1024];
char oldpwd[1024];
const char* GetOldPwd()
{
const char* oldpwd = getenv("OLDPWD");
return oldpwd == NULL ? "None" : oldpwd;
}
const char* GetHome()
{
const char *home = getenv("HOME");
return home == NULL ? "" : home;
}
void UpdateOldPwd()
{
snprintf(oldpwd, sizeof(oldpwd), "OLDPWD=%s", cwd);
//printf("%s\n",oldpwd);
putenv(oldpwd);
}
bool Cd()
{
if(g_argc == 1)
{
std::string home = GetHome();
if(home.empty()) return true;
chdir(home.c_str());
UpdateOldPwd();
GetPWD();
}
else{
std::string where = g_argv[1];
if(where == "-"){
chdir(GetOldPwd());
UpdateOldPwd();
GetPWD();
}else if(where == "~"){
chdir(GetHome());
UpdateOldPwd();
GetPWD();
}else{
chdir(where.c_str());
UpdateOldPwd();
GetPWD();
}
}
return true;
}
echo
echo命令也是内建命令,我们知道,echo $?可以查看最近一次进程退出时的退出码。
查看最近一次进程退出时的退出码,这些退出码在哪里呢?
肯定不会在子进程中,那就在bash中
所以在我们自定义的shell中,我们可以定义一个全局变量,每次执行一次命令就对其进行一次修改。
// last exit code
int lastcode = 0;
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;
}
}
}
export
该命令的作用就是设置环境变量
void Export()
{
std::string addenv = g_argv[1];
auto pos = addenv.find("=");
std::string name = addenv.substr(0,pos);
std::string val = addenv.substr(pos+1);
bool vis = false;
int i = 0;
for(;g_env[i];i++)
{
std::string s = g_env[i];
auto j = s.find('=');
std::string envname = s.substr(0,j);
if(envname == name){
vis = true;
break;
}
}
if(vis)
{
std::string oldenv = g_env[i];
std::string oldval = oldenv.substr(oldenv.find('=')+1);
if(addenv.size() > oldenv.size())
{
free(g_env[i]);
g_env[i] = (char*)malloc(addenv.size()+1);
}
strcpy(g_env[i], addenv.c_str());
putenv(g_env[i]);
}
else
{
if(g_envs < MAX_ENVS)
{
g_env[g_envs] = (char*)malloc(addenv.size()+1);
strcpy(g_env[g_envs], addenv.c_str());
putenv(g_env[g_envs]);
g_env[g_envs++] = NULL;
}
}
}
alias
经过测试我们可以发现,bash支持ll,而我们的shell是不支持的;
我们知道
ll是别名,如果想要我们的shell也支持别名,我们就要在shell中新增一张别名表,维护好这张别名表,就可以支持对其它命令取别名了。
可以使用unordered_map或者map来存储这张别名表。
std::unordered_map<std::string,std::string> alias_list;
void Alias()
{
std::string name = g_argv[1];
for(int i=2;g_argv[i];i++)
{
std::string s = g_argv[i];
std::string ss = " ";
ss += s;
name+=ss;
}
//std::cout << name << std::endl;
auto pos = name.find('=');
std::string name1 = name.substr(0,pos);
std::string name2 = "";
if(name[pos+1]=='"' || name[pos+1]=='\'')
{
name2 = name.substr(pos+2);
name2.pop_back();
}
else{
name2 = name.substr(pos+1);
}
alias_list.insert({name1,name2});
}
bool AliasManage()
{
std::string cmd = g_argv[0];
if(alias_list.count(cmd))
{
return CommandParse((char*)alias_list[cmd].c_str());
}
return true;
}
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 if(cmd == "alias")
{
Alias();
return true;
}
return false;
}
int main()
{
// shell 启动的时候,从系统中获取环境变量
// 我们的环境变量信息应该从父shell统一来
InitEnv();
while(1)
{
// 1. 输出命令行提示符
PrintCommandPrompt();
//printf("[%s@%s %s]# ", GetUserName(), GetHostName(), GetPWD());
// 2. 获取用户输入的命令
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline,sizeof(commandline)))
{
continue;
}
// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
if(!CommandParse(commandline))
continue;
// 4. 别名处理
if(!AliasManage())
continue;
// 5. 检测并处理内建命令
if(CheckAndExecBuiltin())
continue;
// 6. 执行命令
Execute();
//PrintArgv();
//printf("echo %s\n", commandline);
}
return 0;
}

除此之外,还有非常多的内建命令,这里就不一一实现了,大家感兴趣的话可以自己动手试试!
3.3 源代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.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+1];
int g_envs = 0;
// 3. 别名映射表
std::unordered_map<std::string,std::string> alias_list;
char cwd[1024];
char cwdenv[1024];
char oldpwd[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()
{
//connst 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* GetOldPwd()
{
const char* oldpwd = getenv("OLDPWD");
return oldpwd == NULL ? "None" : oldpwd;
}
const char* GetHome()
{
const char *home = getenv("HOME");
return home == NULL ? "" : home;
}
void UpdateOldPwd()
{
snprintf(oldpwd, sizeof(oldpwd), "OLDPWD=%s", cwd);
//printf("%s\n",oldpwd);
putenv(oldpwd);
}
void InitEnv()
{
extern char** environ;
memset(g_env, 0, sizeof(g_env));
g_envs = 0;
// 本来要从配置文件来
// 1. 获取环境变量
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++] = (char*)"HAHA=for_test"; //for_test
g_env[g_envs] = NULL;
// 2. 导成环境变量
for(int i=0;g_env[i];i++)
{
putenv(g_env[i]);
}
}
//command
bool Cd()
{
if(g_argc == 1)
{
std::string home = GetHome();
if(home.empty()) return true;
chdir(home.c_str());
UpdateOldPwd();
GetPWD();
}
else{
std::string where = g_argv[1];
if(where == "-"){
chdir(GetOldPwd());
UpdateOldPwd();
GetPWD();
}else if(where == "~"){
chdir(GetHome());
UpdateOldPwd();
GetPWD();
}else{
chdir(where.c_str());
UpdateOldPwd();
GetPWD();
}
}
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;
}
}
}
void Export()
{
std::string addenv = g_argv[1];
auto pos = addenv.find("=");
std::string name = addenv.substr(0,pos);
std::string val = addenv.substr(pos+1);
bool vis = false;
int i = 0;
for(;g_env[i];i++)
{
std::string s = g_env[i];
auto j = s.find('=');
std::string envname = s.substr(0,j);
if(envname == name){
vis = true;
break;
}
}
if(vis)
{
std::string oldenv = g_env[i];
std::string oldval = oldenv.substr(oldenv.find('=')+1);
if(addenv.size() > oldenv.size())
{
free(g_env[i]);
g_env[i] = (char*)malloc(addenv.size()+1);
}
strcpy(g_env[i], addenv.c_str());
putenv(g_env[i]);
}
else
{
if(g_envs < MAX_ENVS)
{
g_env[g_envs] = (char*)malloc(addenv.size()+1);
strcpy(g_env[g_envs], addenv.c_str());
putenv(g_env[g_envs]);
g_env[g_envs++] = NULL;
}
}
}
void Alias()
{
std::string name = g_argv[1];
for(int i=2;g_argv[i];i++)
{
std::string s = g_argv[i];
std::string ss = " ";
ss += s;
name+=ss;
}
//std::cout << name << std::endl;
auto pos = name.find('=');
std::string name1 = name.substr(0,pos);
std::string name2 = "";
if(name[pos+1]=='"' || name[pos+1]=='\'')
{
name2 = name.substr(pos+2);
name2.pop_back();
}
else{
name2 = name.substr(pos+1);
}
alias_list.insert({name1,name2});
}
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());
}
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 1;
out[strlen(out)-1] = '\0'; //清理\n
if(strlen(out)==0) return false;
return true;
}
// 3. 命令行解析
bool CommandParse(char* commandline)
{
#define SEP " "
g_argc = 0;
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 AliasManage()
{
std::string cmd = g_argv[0];
if(alias_list.count(cmd))
{
return CommandParse((char*)alias_list[cmd].c_str());
}
return true;
}
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 if(cmd == "alias")
{
Alias();
return true;
}
return false;
}
int Execute()
{
pid_t id = fork();
if(id==0)
{
// child
execvp(g_argv[0], g_argv);
exit(1);
}
// father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
//(void)rid; // rid使用一下
if(rid>0)
{
lastcode = WEXITSTATUS(status);
}
return 0;
}
int main()
{
// shell 启动的时候,从系统中获取环境变量
// 我们的环境变量信息应该从父shell统一来
InitEnv();
while(1)
{
// 1. 输出命令行提示符
PrintCommandPrompt();
//printf("[%s@%s %s]# ", GetUserName(), GetHostName(), GetPWD());
// 2. 获取用户输入的命令
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline,sizeof(commandline)))
{
continue;
}
// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
if(!CommandParse(commandline))
continue;
// 4. 别名处理
if(!AliasManage())
continue;
// 5. 检测并处理内建命令
if(CheckAndExecBuiltin())
continue;
// 6. 执行命令
Execute();
//PrintArgv();
//printf("echo %s\n", commandline);
}
return 0;
}
最后
本篇关于自定义Shell的内容到这里就结束了,其中还有很多细节值得我们去探究,需要我们不断地学习。如果本篇内容对你有帮助的话就给一波三连吧,对以上内容有异议或者需要补充的,欢迎大家来讨论!
1607

被折叠的 条评论
为什么被折叠?



