主函数流程
介绍完了Shell的基本情况,就开始描述代码了。首先如下代码段所示为主函数的内容,一共只有几句话,但却是本Shell的一个基本工作流程。
//main.cpp
#include"tlsh.h"
int main()
{
init(); //初始化
queue<string> task_queue; //任务队列
while(true)
{
cout << getPrefix() << flush; //输出前缀
getTaskList(task_queue); //获取任务队列
executeTaskList(task_queue); //执行任务队列
}
return 0;
}
上述代码片段转换成流程图就是下面这个样子:
接下来我们一个函数一个函数的跟进,看看这几步里面到底在做什么?
init()函数
以下代码段为init函数的内容:
//init() @ internal.cpp
void init()
{
//处理SIGINT信号
if(signal_(SIGINT, intHandler) == SIG_ERR)
{
cout << "Signal_ Error!" << endl;
exit(1);
}
getIfDetach() = false;
get_stdin_fd() = dup(STDIN_FILENO);
getPrefix() = updatePrefix();
get_alias()["ls"] = "ls --color=auto";
get_alias()["grep"] = "grep --color=auto";
}
init函数顾名思义就是进行了一些初始化的工作。调用的各种函数会在下方逐个解释。
其中最重要的是,我们使用了自己实现的信号处理函数signal_来手动处理SIGINT信号,为什么这么做呢?理由有两点:
如果在Shell中直接点击ctrl+c发送SIGINT信号,如果不处理的话会导致Shell直接退出,然而我们可以看到在bash中发送SIGINT信号时,反应是这样的:
如果发送要给SIGINT直接退出的话,就太不鲁棒了。当Shell进程fork出来一个新的进程用于装载另一个程序时,如果我们给前台的fork出来的程序发送SIGINT,而后台的Shell不处理SIGINT的话,默认情况下这个SIGINT函数会导致后台的Shell也接收到这个信号,到时fork出来的程序会不会终止不知道,反正Shell是默默地退出了,所以这个信号我们必须手动处理。而本Shell处理方式尽量和bash相似。
另外需要一提的是,在本程序中,大部分的全局变量使用的方式是定义一个名为getXXX()的函数,其中包含一个static型的变量,函数行为是返回该变量的引用。
如其中的
getIfDetach() = false;
get_stdin_fd() = dup(STDIN_FILENO);
getPrefix() = updatePrefix();
signal_()函数
该函数为按照APUE中所描述的自己实现的行为可预知的信号处理函数,函数的具体定义如下所示:
void (*signal_(int signo, void(*func)(int))) (int)
{
struct sigaction act, oact;
act.sa_handler = func;
act.sa_flags = 0;
act.sa_flags |= SA_RESTART;
sigemptyset(&act.sa_mask);
if(sigaction(signo, &act, &oact) < 0)
{
return SIG_ERR;
}
return oact.sa_handler;
}
其中sa_handler是信号处理函数,sa_flags中开启了自动重启系统调用的标识,清空了信号掩码,其他的没有更多的配置。
在本Shell中,使用的信号处理函数为intHandler(见init()源码),定义如下:
static void intHandler(int signo)
{
cout << endl;
cout << getPrefix() << flush;
}
操作很简单,类似bash中的行为,换行+重新输出前缀(前缀稍后再讲),工作效果如下:
和bash里面差不多吧?
getIfDetach()函数
该函数返回一个全局变量,如下所示:
bool& getIfDetach()
{
static bool Detach;
return Detach;
}
Detach变量用于记录命令是否后台运行,即类似bash中的’&’字符的效果。初始化为false。
get_stdin_fd()函数和getPrefix()函数
get_stdin_fd()函数用于返回标准输入描述符,在重定向到管道和恢复中会用到。
int& get_stdin_fd()
{
static int stdin_fd;
return stdin_fd;
}
getPrefix()函数用于返回输出前缀,如下:
string& getPrefix()
{
static string Prefix;
return Prefix;
}
updatePrefix()函数
用于更新前缀,每次进行改变前缀的操作后,都需要调用该函数更新getPrefix()中的全局变量。函数定义如下所示:
string updatePrefix()
{
char buffer[4096];
uid_t user_id = getuid();
struct utsname uname_info;
uname(&uname_info);
getcwd(buffer, 4096);//获取当前工作目录
string user_name = getpwuid(user_id)->pw_name;
string temp(buffer);
string homeUser = "/home/" + user_name;
string computer_name(uname_info.nodename);//获取当前用户信息
user_name = "\033[1;37m" + user_name;
if(temp.size() >= homeUser.size())
{
if(string(temp.begin(), temp.begin() + homeUser.size()) == homeUser)
temp.replace(0, homeUser.size(), "~");
}//如果目录为/home目录下的当前用户目录下的子目录,则将home以前的目录用~来代替
return user_name + "@ " + temp + "> " + "\033[0m";
}
通过该代码片段,可得到本Shell前缀命名规则为:
用户名@ 当前工作目录>
效果如下图所示
本机用户名为tl,可以看到当目录中包含/home/tl时,该字段被自动替换为~,这与bash中的逻辑也是相似的。
get_alias()函数
在正常的Shell中,alias命令用于对某个命令取别名。我并不知道真实的Shell是怎么实现这个功能的,但我能想到的最直观的方法就是使用哈希表。所以在本Shell中,get_alias()返回的就是这个记录别名的哈希表,如下:
map<string, string>& get_alias(){
static map<string, string> alias;
return alias;
}
虽说本意是用哈希表,但是map实际上是一颗红黑树,当然了我认为用unordered_map也就是真正的哈希,也可以完成相同的任务,甚至查找效率会高点儿?
而
get_alias()["ls"] = "ls --color=auto";
get_alias()["grep"] = "grep --color=auto";
这两行代码则是用于在哈希表中插入两个键值对,用来完成ls命令和grep命令的高亮显示。
至此,init()函数的实现完全讲完。。。我发现讲自己的代码在干嘛比写代码痛苦了不是一点点。。。。