【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); //获取退出码
}
else
last_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就实现好了,别的功能大家自己实现,下面这张图有利于我们理解文件。
程序替换会影响程序替换的结果吗?不会,因为进程有自己的文件描述符表,还有自己的进程地址空间,两者是独立的。
本次分享就到这里了,我们下篇见~