Linux下简单Shell实现(二)基本功能---主函数及init()函数

本文详细介绍了Linux Shell的主函数流程和init()函数,包括信号处理、初始化工作、全局变量的获取和更新,以及别名功能的实现。重点讲解了如何处理SIGINT信号,以保持Shell的鲁棒性,并展示了相关函数的功能和效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

主函数流程

介绍完了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信号,为什么这么做呢?理由有两点:

  1. 如果在Shell中直接点击ctrl+c发送SIGINT信号,如果不处理的话会导致Shell直接退出,然而我们可以看到在bash中发送SIGINT信号时,反应是这样的:
    这里写图片描述
    如果发送要给SIGINT直接退出的话,就太不鲁棒了。

  2. 当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()函数的实现完全讲完。。。我发现讲自己的代码在干嘛比写代码痛苦了不是一点点。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值