【Linux】模拟实现Shell(下)
【Linux】模拟实现Shell(上)上篇实现的shell能处理一些基本命令和cd、echo这样的内建命令,以及让shell实现好了两张表,环境变量表和命令行参数表。这篇我们会在此基础上对shell的重定向操作进行模拟实现。
1.优化命令行提示符
我们对这个命令行提示符可以做个改进,Ubuntu下Shell的路径这里用~代替了家目录,我们也可以实现成这样。
std::string CurDir()
{std::string home = GetHome();std::string pwd = GetPwd();if(home > pwd) //在 根目录/ 或 /home 路径下return pwd;std::string ret = "~"; //正好在家目录下,直接就使用~if(home == pwd)return ret;int pos = 0;for(int i = 0; home[i]; i++) //找到家目录往后的路径{pos++;}ret += pwd.substr(pos); //往后的路径加在ret里return ret;
}#define FORMAT "%s@%s:%s# " //设置命令行提示符的格式
void PrintCmdPrompt()
{printf(FORMAT,UserName(), HostName(),CurDir().c_str());
}
现在我们的命令行提示符就和Shell更相似了,为了区分,还是用#做结尾,不用$。
2.重定向分析
在命令行输入命令的时候一般就是用> 、<、 >>这样的符号重定向,如"ls -a -l > file.txt",而且这些重定向符号的左右两边可能带空格,也可能不带空格。
2.1 输入重定向
还是以 "ls -a -l < file.txt" 为例:
- 我们需要做的是把 < 的两边拆分,拆成 "ls -a -l" 和 "file.txt" ,< 的左边部分是要执行的命令,右边部分是要打开的目标文件
- 并且我们还需要判断重定向方式,是>,还是<,还是>>。
这个步骤要在命令行分析之前做,叫重定向分析。
int main()
{EnvInit(); //初始化环境变量while(1){//1.打印命令行提示符PrintCmdPrompt();//2.获取命令行参数char commandline[COMMAND_SIZE];if(!GetArguments(commandline, sizeof(commandline))) //获取失败continue;//3.重定向分析RedirCheck(commandline); //4.解析命令行参数if(!CommandParse(commandline))continue;//PrintArg();//5.检测是否为内建命令,是内建命令就执行if(CheckAndExeBuiltinCmd())continue; //6.执行命令Execute();}return 0;
}
实现这个RedirCheck函数之前,我们先定义4个宏,表示重定向的方式,还要一个变量存储重定向到哪个文件。
#define NONE_REDIR 0 //没有重定向
#define INPUT_REDIR 1 //输入重定向
#define OUTPUT_REDIR 2 //输出重定向
#define APPEND_REDIR 3 //追加重定向
int redir_type = NONE_REDIR; //默认设为没有重定向
std::string filename;
然后就可以开始实现RedirCheck函数了,这个函数参数就是commandline,返回值为void。
首先每次都重置重定向的方式,以及清空file里的内容。
void RedirCheck(char* cmd)
{redir_type = NONE_REDIR;filename.clear();}
将重定向符两边的内容分开时,这里我选择从后往前找,先写输入重定向< 的逻辑。从后往前找,找到'<'就停止,并且把 ‘<' 置成0,就可以将两边分开。
bool RedirCheck(char* cmd)
{redir_type = NONE_REDIR;filename.clear();int begin = 0;int end = strlen(cmd)-1;while(begin < end){if(cmd[end] == '<') //输入重定向{redir_type = INPUT_REDIR; cmd[end] = 0; }end--;}
}
<的两边可能有多个空格,也可能没有空格,如果<的左边有多个空格,不用管没因为后续解析命令行参数时用的strtok函数不会返回空串;如果<的右边有多个空格,我们需要清除这些空格,让end指向文件的开头。
void ClearSpaces(char *cmd, int& end) //传地址过去,直接改变end
{while(isspace(cmd[end])){end++;}
}bool RedirCheck(char* cmd)
{redir_type = NONE_REDIR;filename.clear();int begin = 0;int end = strlen(cmd)-1;while(begin < end){if(cmd[end] == '<') //输入重定向{redir_type = INPUT_REDIR; cmd[end] = 0;ClearSpaces(cmd, ++end); //清除空格}end--;}
}
然后文件名的起始地址就是这个数字组的起始地址加上end。
void ClearSpaces(char *cmd, int& end)
{while(isspace(cmd[end])){end++;}
}bool RedirCheck(char* cmd)
{redir_type = NONE_REDIR;filename.clear();int begin = 0;int end = strlen(cmd)-1;while(begin < end){if(cmd[end] == '<') //输入重定向{redir_type = INPUT_REDIR; cmd[end] = 0;ClearSpaces(cmd, ++end); //清除空格filename = cmd+end;break;}end--;}
}
2.2 输出重定向和追加重定向
输出重定向是 >,追加重定向是>>,从后往前扫描时,end停留的前一个位置还是>的话,就是追加重定向,否则是输出重定向。
void ClearSpaces(char *cmd, int& end)
{while(isspace(cmd[end])){end++;}
}bool RedirCheck(char* cmd)
{redir_type = NONE_REDIR;filename.clear();int begin = 0;int end = strlen(cmd)-1;while(begin < end){if(cmd[end] == '<') //输入重定向{redir_type = INPUT_REDIR; cmd[end] = 0;ClearSpaces(cmd, ++end); //清除空格filename = cmd+end;break;}else if(cmd[end] == '>') // > 或 >>{if(cmd[end-1] == '>') // >> 追加重定向{ }else // > 输出重定向{ }break;}end--;}
}
对>>来说,我们把两个>>都置为0,对于>,就是把这一个位置置为0,其他逻辑和输入重定向是一样的,冗余部分整理一下后代码如下。
void ClearSpaces(char *cmd, int& end)
{while(isspace(cmd[end])){end++;}
}bool RedirCheck(char* cmd)
{redir_type = NONE_REDIR;filename.clear();int begin = 0;int end = strlen(cmd)-1;while(begin < end){if(cmd[end] == '<') //输入重定向{redir_type = INPUT_REDIR; cmd[end] = 0;ClearSpaces(cmd, ++end); //清除空格filename = cmd+end;break;}else if(cmd[end] == '>') // > 或 >>{if(cmd[end-1] == '>') // >> 追加重定向{redir_type = APPEND_REDIR;cmd[end-1] = 0;cmd[end] = 0; }else // > 输出重定向{redir_type = OUTPUT_REDIR;cmd[end] = 0; }ClearSpaces(cmd, ++end); //清除空格filename = cmd+end;break;}end--;}
}
我们可以把重定向的类型以及文件名打出来看看。
int main()
{EnvInit(); //初始化环境变量while(1){//1.打印命令行提示符PrintCmdPrompt();//2.获取命令行参数char commandline[COMMAND_SIZE];if(!GetArguments(commandline, sizeof(commandline))) //获取失败continue;//3.重定向分析RedirCheck(commandline); printf("redir:%d, filename:%s\n", redir_type, filename.c_str());//4.解析命令行参数if(!CommandParse(commandline))continue;//PrintArg();//5.检测是否为内建命令,是内建命令就执行if(CheckAndExeBuiltinCmd())continue; //6.执行命令Execute();}return 0;
}
可以看到,不管我们重定向符号左右有多少空格,都是没问题的。
3.重定向执行
重定向不能让父进程重定向,会影响到整个shell,要在子进程重定向,所以就要在子进程里检查重定向情况,直接通过redir_type检查。
int Execute()
{pid_t id = fork();if(id == 0) //子进程:程序替换{if(redir_type == INPUT_REDIR) // <{ }else if(redir_type == OUTPUT_REDIR) // >{ }else if(redir_type == APPEND_REDIR) // >>{}execvp(g_argv[0], g_argv);}//父进程:阻塞等待int status;pid_t rid = waitpid(id, &status, 0);if(rid > 0){if(WIFEXITED(status)) //正常退出{last_exit_code = WEXITSTATUS(status); //获取退出码}elselast_exit_code = 100;}return 0;
}
输入重定向:
重定向文件肯定是先要把文件打开,这里用到函数open,返回值是文件描述符,第一个参数是要打开的文件名,第二个参数flags是打开的方法,第三个参数是如果文件不存在,要设置文件的起始权限,一般是0666。
打开文件之后进行重定向,这里重定向要用到函数dup2,两个参数都是要传文件描述符的。
标准输入(stdin)的文件描述符为0,标准输出(stdout)文件描述符为1,标准错误(stderr)文件描述符为2。输入重定向就是原本要从stdin里获取数据,变成从指定文件获取数据。
重定向:打开文件的方式+dup2
if(id == 0) //子进程:程序替换{int fd = -1;if(redir_type == INPUT_REDIR) // <{fd = open(filename.c_str(), O_RDONLY, 0666); //只读形式打开if(fd < 0) exit(1); //打开失败dup2(fd, 0); //重定向close(fd);}else if(redir_type == OUTPUT_REDIR) // >{ }else if(redir_type == APPEND_REDIR) // >>{}execvp(g_argv[0], g_argv);}
输出重定向:
输出重定向open文件的方式就是创建+写入+覆盖式,所以方式就是O_CREAT | O_WRONLY | O_TRUNC,重定向就是原本像显示器stdout输出数据,变成向文件输出数据。
if(id == 0) //子进程:程序替换{int fd = -1;if(redir_type == INPUT_REDIR) // <{fd = open(filename.c_str(), O_RDONLY, 0666); //只读形式打开if(fd < 0) exit(1); //打开失败dup2(fd, 0); //重定向close(fd); }else if(redir_type == OUTPUT_REDIR) // >{fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd < 0) exit(2);dup2(fd, 1);close(fd);}else if(redir_type == APPEND_REDIR) // >>{ }execvp(g_argv[0], g_argv);}
追加重定向:
追加重定向和输出重定向只有打开方式上的区别,追加重定向打开方式是创建+写入+追加式,所以方式就是O_CREAT | O_WRONLY | O_APPEND。
if(id == 0) //子进程:程序替换{int fd = -1;if(redir_type == INPUT_REDIR) // <{fd = open(filename.c_str(), O_RDONLY, 0666); //只读形式打开if(fd < 0) exit(1); //打开失败dup2(fd, 0); //重定向close(fd);}else if(redir_type == OUTPUT_REDIR) // >{fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd < 0) exit(2);dup2(fd, 1);close(fd);}else if(redir_type == APPEND_REDIR) // >>{fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);if(fd < 0) exit(3);dup2(fd, 1);close(fd);}execvp(g_argv[0], g_
此时shell的重定向操作就完成了,我们来检验一下。
4.结尾
到这里这个简单版的shell就实现好了,别的功能大家自己实现,下面这张图有利于我们理解文件。
程序替换会影响程序替换的结果吗?不会,因为进程有自己的文件描述符表,还有自己的进程地址空间,两者是独立的。
本次分享就到这里了,我们下篇见~