目录
一、模块框架图
二、实现目标
三、实现原理
四、全局变量
五、环境变量函数
六、初始化环境变量表函数
七、输出命令行提示符模块
八、提取命令输入模块
九、填充命令行参数表模块
十、检测并处理内建命令模块
十一、执行命令模块
十二、源码
一、模块框架图
二、实现目标
- 要能处理普通命令
- 要能处理内建命令
- 要能帮助我们理解内建命令/本地变量/环境变量这些概念
- 要能帮助我们理解shell的允许原理
三、实现原理
# 考虑下面这个与shell典型的互动:
[root@localhost epoll]# lsclient.cpp readme.md server.cpp utility.h[root@localhost epoll]# psPID TTY
TIME CMD3451 pts/0
3514 pts/0
00:00:00 bash00:00:00 ps
# ⽤下图的时间轴来表⽰事件的发⽣次序。其中时间从左向右。shell由标识为sh的⽅块代表,它随着时间的流逝从左向右移动。shell从⽤⼾读⼊字符串"ls"。shell建⽴⼀个新的进程,然后在那个进程中运⾏ls程序并等待那个进程结束。
# 然后shell读取新的⼀⾏输⼊,建⽴⼀个新的进程,在这个进程中运⾏程序 并等待这个进程结束。
四、全局变量
- 我们的shell内部有两张表命令行参数表和环境变量表
- 同时我们还要定义一张哈希表方便处理别名
- 定义两个数组用来方便处理记录路径
- lastcode记录上一次的进程退出码、
- 宏定义大小方便开辟数组 以及命令行输出格式
#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;
五、环境变量函数
- 环境标量函数直接调用getenv获取在返回即可
- 注意GetPWD需要ssnprintf格式化写入即可
- DirName直接从后面查找分割符\ 然后返回之后的字符串即可
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;
}
string DirName(const char* pwd)
{
#define SEP "/"string ret = pwd;int index = ret.rfind(SEP);if (ret == SEP){return SEP;}if (index == string::npos){return "BUG?";}string t = ret.substr(index+1);return t;
}
六、初始化环境变量表函数
- 直接把enriron指向的环境变量表拷贝到我们的环境变量表里面 为了区分我们的shell和系统的我们在末尾添加上haha区分
- 再让environ指向我们的环境变量表即可。
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++] = "haha";g_env[g_envs] = NULL;for (int i = 0; g_env[i]; i++){putenv(g_env[i]);cout << g_env[i] << endl;}environ = g_env;
}
七、输出命令行提示符模块
- 先定义一个字符输出存储命令行提示符
- 然后snprintf格式化写入字符数组中 在输出即可。
void PrintCommandPrompt()
{char cmd_prompt[COMMAND_SIZE];MakeCommandLine(cmd_prompt, sizeof(cmd_prompt));std::cout << cmd_prompt;
}
void MakeCommandLine(char cmd_prompt[], int size)
{snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
}
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;
}
string DirName(const char* pwd)
{
#define SEP "/"string ret = pwd;int index = ret.rfind(SEP);if (ret == SEP){return SEP;}if (index == string::npos){return "BUG?";}string t = ret.substr(index+1);return t;
}
八、提取命令输入模块
- 这里先fgets获取标准输入到字符数组中
- 然后构造字符串删除erase\0 然后去map中判断是否为别名
- 如果是直接把value的值拷贝到数组中即可 然后return ture退出
- 不是别名则把用户回车的\n字符消除 同时如果用户只回车此时n==0
- 不做处理返回false 其他返回true;
bool GetCommandLine(char* commandline, int size)
{const char* ret=fgets(commandline, size, stdin);if (ret == NULL){return false;}std::string a = ret;a.erase(a.size() - 1, 1);if (alias_list.count(a)){strcpy(commandline, alias_list[a].c_str());return true;}int n = strlen(commandline);commandline[n - 1] = 0;if (n == 0){return false;}return true;
}
九、填充命令行参数表模块
- 先strtok获取指向第一个空格字符串
- while的那个ret不为空时填充g_argv参数表
- 继续分割填充,直到找不到空格,说明字符串分割完毕
- 最后填充在g_argv表最后填充NULL即可
- 根据g_argv大小判断是否填充成功 成功返回ture 反之返回flase
bool CommandParse(char* commandline)
{
#define SEP " "g_argc = 0;char* ret = strtok(commandline, SEP);if (ret == NULL){return false;}while (ret){g_argv[g_argc++] = ret;ret = strtok(nullptr, SEP);}g_argv[g_argc] = ret;for (int i = 0; g_argv[i]; i++){cout << g_argv[i] << " ";}cout << endl;return g_argc > 0 ? true : false;
}
十、检测并处理内建命令模块
# 先根据g_argv表的第一个命令 判断分流检测处理
- cd命令如果只有cd 那就直接获取家目录的环境变量字符串
- chdir修改当前命令为家目录即可
- 否则直接chdir修改当前目录的路径为g_argv[1]
- echo命令判断分流
- echo $?直接返回lastcode退出码 再设置wield0即可
- echo $xxx 直接获取xxx的环境变量 再输出即可
- echo xxx 直接打印xxx字符串g_argv[1]即可
- export命令先判断是否填写了要导入的环境变量
- 没有直接返回ture不做处理 否则直接putenv导入g_argv[1]环境变量即可
- alias命令 这里只处理不带命令选项的替换
- strtok分割=前后字符串然后 存储到map中即可
bool CheckAndExecBuiltion()
{std::string t = g_argv[0];if (t == "cd"){return Cd();}else if (t == "echo"){return Echo();}else if (t == "export"){return Export();}else if (t == "alias"){cout << "开始替换" << endl;return Alias();}else{return false;}
}
bool Cd()
{std::string t;if (g_argc == 1){t = GetHome();if (t == ""){return true;}chdir(t.c_str());}else{t =g_argv[1];chdir(t.c_str());}return true;
}
bool Echo()
{std::string t = g_argv[1];if (t=="$?"){std::cout << lastcode << std::endl;lastcode = 0;}else if (t[0] == '$'){char* ret = getenv(t.substr(1).c_str());if (ret){cout << ret << endl;}}else{cout << t << endl;}return true;
}
bool Export()
{if (g_argc != 2){return true;}putenv(g_argv[1]);return true;
}
bool Alias()
{
#define SEP "="char* t = g_argv[1];char* ret = strtok(t, SEP);if (ret == NULL){return false;}string a=std::string(ret),b = std::string(strtok(NULL, SEP));alias_list[a] = b;cout << alias_list[a] <<" "<<alias_list.count(a)<<endl;cout << a<< "->" << b << endl;return true;
}
十一、执行命令模块
- 直接创建子进程,子进程通过execvp程序替换执行命令,执行完后exit退出
- 然后父进程waitpid等待子进程,同时把lastcode更新即可
int Execute()
{pid_t id = fork();if (id == 0){execvp(g_argv[0], g_argv);exit(1);}int status = 0;pid_t rid=waitpid(id, &status, 0);if (rid > 0){lastcode = WEXITSTATUS(status);}return 0;
}
十二、源码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#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_testg_env[g_envs] = NULL;//2. 导成环境变量for (int i = 0; g_env[i]; i++){putenv(g_env[i]);std::cout << g_env[i] << std::endl;}environ = g_env;
}//command
bool Cd()
{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 == "-"){// Todo}else if(where == "~"){// Todo}else{chdir(where.c_str());}}return true;
}void Echo()
{if(g_argc == 2){// echo "hello tata"// echo $?// echo $PATHstd::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;}}
}bool Export()
{if (g_argc != 2){return false;}putenv(g_argv[1]);return true;
}bool Alias()
{char* t = g_argv[1];char* ret = strtok(t, "=");if (ret == NULL){return false;}string a=std::string(ret),b = std::string(strtok(NULL, "="));alias_list[a] = b;std::cout << alias_list[a] << " " << alias_list.count(a) << std::endl;std::cout << a << "->" << b << std::endl;return true;
}//绝对路径改为目录名
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); // +1是为了去掉目录名前面的/
}//制作命令行提示符
void MakeCommandLine(char cmd_prompt[], int size)
{snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), Dirname(GetPwd()).c_str());
}//打印命令行提示符
void PrintCommand()
{char prompt[COMMAND_SIZE];MakeCommandLine(prompt, sizeof(prompt));printf("%s", prompt);fflush(stdout);
}//判断输入内容
bool GetCommandLine(char *out, int size)
{//scanf()是以空格作为分隔符的,但是我们的命令是一整个字符串//ls -a -l => "ls -a -l\n”字符串char *c = fgets(out, size, stdin);if(c == NULL) return false;out[strlen(out)-1] = 0; // 清理\nif(strlen(out) == 0) return false;return true;
}//命令行分析 "ls -a -l" => "ls" "-a" "-l"
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--; // NULL不算,去掉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("g_argc:%d\n", g_argc);
}//处理并执行内建命令
bool CheckAndExcuteBuiltn()
{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"){std::cout << "开始替换" << std::endl;return Alias();}elsereturn false;
}//执行命令
int Execute()
{pid_t id = fork();if(id == 0){//childexecvp(g_argv[0], g_argv);exit(1);}int status = 0;//fathepid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);}return 0;
}int main()
{// shell启动的时候需要从系统中获取环境变量// 我们的环境变量信息应该从父shell统一来InitEnv();while(true)//持续运行{// 1. 输出命令行提示符PrintCommand();// 2. 获取用户输入的命令char commandline[COMMAND_SIZE];if(!GetCommandLine(commandline, sizeof(commandline)))continue;//什么都没输入直接回车,继续运行printf("echo %s\n", commandline);// 3. 命令行分析 "ls -a -l" => "ls" "-a" "-l"if(!CommandParse(commandline))continue;//PrintArgv(); //测试// 4. 检测并处理内建命令if(CheckAndExcuteBuiltn())continue;// 5. 执行命令Execute();}// ClearUp();return 0;
}