Linux系统下shell的简易实现(C++)(os进程综合实验)

需要了解的预备知识 

        Shell 有时称为命令解释器,用于解释、执行用户命令,是一个用户使用操作 系统的交互界面。在 shell 程序提供的界面中,用户输入要执行的命令,shell 负责执行这个命令。

        操作系统系统创建进程时,会为每个进程自动打开三个标准设备:stdin、 stdout stderr并为其分配三个文件标识符 0、1、2。之后当用户通过系统调用 fd=open(…)fd=creat(….)打开或创建文件时,则会从 3 开始分配文件描述符并返回给 fd。 需要注意的是,系统在为打开或创建的文件分配文件描述符时,会在文件描述 符表项中从索引值 0 开始顺序查找,将搜索到的 第一个未被分配的文件描述符表项 分配给该文件,然后将该表项的索引值返回给 fd,即系统选择索引值最小的空文件描述符表项予以分配。

         例如你使用了系统调用 close (1)关闭了标准输出设备 stdout(显示器),然后再使用 open() 打开一个文件,如 OutFile,这时系统会将该进程的标准输出重定向到文件 OutFile,即你输出到 stdout 的信息会重定向到文件 OutFile 中。例如,如果你用 C 语言编程,函数 printf()的输出会写入到 OutFile 中,而不再输出到屏幕上。

        利用系统调用 dup()可很方便地实现 I/O 重定向

        系统调用 dup() 将一个文件描述符复制到该用户文件描述符表的第一个空的表项中,当复制成功时,返回索引值最小且尚未使用的新的文件描述符。若有错误则 返回-1,errno 会存放错误代码。dup 适用于所有的文件类型。

        示例如下:

int fd=open(“outFile.txt”);
close(1); //或使用下面那种
close(stdout); //关闭标准输出设备 stdout
dup(fd)

        系统将文件 outFile.txt 对应的文件描述符复制到原标准输出设备(stdout)的 文件描述符中,其后的标准输出将重定向到文件 outFile.txt。

        一般的操作系统的 Shell 均支持如下的几个 I/O 重定向命令:>、>>、<、<<及 管道命令“|”。

        输入重定向:

        (1)命令 < 文件:将文件作为命令的标准输入

        (2)命令 << 分界符:从标准输入中读入,直到遇到分界符停止

扫描二维码关注公众号,回复: 15368204 查看本文章

        输出重定向:

        (1)命令 > 文件:将标准输出重定向到文件中(清除原有文件中的数据)

        (2)命令 2> 文件:将错误输出重定向到文件中(清除原有文件中的数据)

        (3)命令 >> 文件:将标准输出重定向到文件中(在原有的内容后追加)

        (4)命令 2>> 文件:将错误输出重定向到文件中(在原有内容后面追加)

        (5)命令 >> 文件 2>&1,或 命令 &>> 文件:标准输出和错误输出共同写入 文件(在原有内容后追加)

        在 Linux 的 Shell 中可以使用管道命令符 "|" 来建立双向的无名管道,它的语法 为 command1 | command2。这个命令符会通过管道将左侧程序的输出作为输入传递 给右侧程序。管道支持多级连接,即支持语法 command1 | command2 | command3 | ... | commandN,左侧的标准输出内容会被一次传递给右侧的下一级,作为它们的 标准输入。

实验思路

        主程序作为父进程,等待读取用户的输入命令行,然后创建子进程pid去执行这个命令。

        如果命令行的最后一个字符不是“&”,即不要求执行该命令的子进程在后台运行,则父进程应该等待子进程执行结束。反之,如果命令行的最后一个字符是“&”,即要求执行该命令的子进程在后台运行,则父进程不需要等待子进程执行,而是继续执行,父子进程即可并发执行。(关于这一点可以理解为,如果有一个程序一直输出1,父进程创建子进程执行这个程序后,如果要求等待子进程结束,那么这时候是无法输入命令行的,程序执行界面会一直输出1;如果要求不等待子进程结束,那么父子进程并行,虽然程序执行界面一直输出1,但是我们仍然可以输入命令行给父进程)

        大致框架如下:

int main()
{
    int background;
    while (1)
    {   
        父进程自定义信号SIGINT,当按下ctrl+c后不终止父进程,终止子进程
        输入ctrl+w触发SIGTSTP,父进程也结束

        background=0;

        //输入一行命令到inputString
        getline(cin,inputString);

        //读用户输入的命令行并分析命令行
        if(命令行含有&)//命令行含有“&”,后台运行用户输入的命令
        {
            background=1;
            命令行裁剪去最后一个字符
        }

        pid=fork();//创建子进程执行命令

        if (pid < 0) //子进程创建失败
        {
            printf("Create process fail!\n");
            exit(EXIT_FAILURE);
        }
        else if(pid==0)//子进程执行命令行输入的命令
        {
            设置信号SIGINT的处理方式为默认SIG_DFL
            按下ctrl+c后终止子进程

            执行输入的命令
            //如果执行命令失败也要记得退出子进程,否则会子进程会进入循环一直创建自己的子进程
        }
        //父进程执行任务
        if(background==0)//父进程等待子进程结束
            waitpid(pid,&status,0);//等待子进程结束
    }
    
}

        因此重点在于:子进程执行输入的命令。

        注:本博客中的代码如果出现了找不到变量或者头文件等,记得看文章末尾的附录标注。

程序中执行简易linux命令

        在这里我们要用到linux进程的exec族函数,详细了解相关函数可以参考此博客

        本实验中使用的都是shell的系统命令(例如"ls"、"ps",可以不必加上文件路径"/bin/"),而且为了方便加入字符串参数(即参数不确定多少,使用参数数组可以自由加入;如果使用参数列表,那么函数的参数个数不好修改),这里使用execvp()。下面是函数原型:

int execvp(const char *file, char *const argv[]);

        举一个简单使用的例子,会输出当前文件下的所有文件(读写权限、日期、文件名等)

    char *inin[] = {"ls", "-la", 0};//不要漏掉最后的0,测试时好像第一个参数不是ls也没影响
    execvp("ls", inin);

        当做到这里,不知道你是不是和我一开始想的一样,直接把输入的所有字符串塞execvp函数里头,那不就解决了。

        事实上,少部分的命令确实可以,包括echo、cat、ls、ps等等。但重定向输入和输出、管道就不行了。我举一个简单例子,在shell命令行输入:

echo hello > a.txt

        上述shell命令正确执行是将hello输出到a.txt文件中,但如果直接放到execvp里,那么它就会把hello > a.txt当作一个字符串,然后在stdout(屏幕)输出了hello > a.txt

        因此可以规定,能直接放到execvp函数里执行的(不含任何重定向和管道)叫做简易命令,像上面那样包括了"<"">"两种重定向的叫做中级命令,不能直接放到execvp里直接执行。

        接着,我们把从一开始键盘输入的字符串叫做高级命令,也就是完完全全没裁剪过的字符串,这种一般包括了管道"|"

        因此给出以下结构:

         ./out1、./out2、./out3分别是可执行文件,input.txt分别是文本文件。

简易命令的实现

        首先command是输入的简易命令(也就是不包含"<"">""|"),我们将其以空格或者缩进符分割,依次存储到string数组中。这里用到字符串流,可以方便的实现(不要问我为啥execvp、readline等基本参数都是char*还选择用基本用c++的string,问就是博主C++使用习惯了C语言的很多东西都忘光了,这就导致了最后还得把这些string挨个换回char*)

    stringstream s(command);            //将string放到string输入流
    string p;                           //存储以‘ ’分割的各个string
    vector<string> para;                //存储每个字符串

    while (s>>p)                        //将命令的每个字符串都分别存储
        para.push_back(p);

         由于execvp接受的是char*和char*[],因此得把string转换回来:

    char* args[MAX_PARA+1]={};          //存储命令和参数
    char** tmp=new char* [MAX_LINE+1];  //二维数组,tmp[i]是一个字符串

    for(int i=0;i<para.size();i++)      //开辟空间,否则strcpy时只有指针,会报段错误
        tmp[i]=new char[MAX_PARA+1];
    // printf("%s\n",args[0]);

    for(int i=0;i<para.size();i++)
    {
        strcpy(tmp[i],para[i].c_str());//将string转化到char*
        args[i]=tmp[i];
    }

        注意不能直接使用strcpy将string的c_str()复制到一个没有分配内存的区域,会报段错误。我这里是先创建二级指针,每一个一级指针开辟空间,指向了一个字符串,然后用strcpy复制c_str过去。(当然可能有更好的方法,能力有限,调试了半天只能想出这个办法,大家可以试一下)

         最后直接作为参数传给execvp()。

    //指令执行时会自己判断该简易指令是否是非法命令
    execvp(args[0],args);//使用参数+文件的exec

         最后要注意,execvp()执行指令时会自己判断该简易指令是否是非法命令,如果输入的这个简易指令非法,那么不会报错(也就是我们看不到),同时不执行,直接返回到子进程下一句继续执行。还记得我们主函数最外层是一个while(1)循环,这时如果指令执行失败了,那么子进程不会把execvp要执行的程序复制进来覆盖子进程,反而进行了一轮while循环,创建自己的子进程,子进程又创建它的子进程……因此为了避免execvp执行失败,我们可以加上一个exit()退出。

        最后是全部的代码(此函数经过封装,调用runCommand(command)就可以执行command):

void runCommand(string command)//执行简易命令
{
    stringstream s(command);            //将string放到string输入流
    string p;                           //存储以‘ ’分割的各个string
    vector<string> para;                //存储每个字符串

    while (s>>p)                        //将命令的每个字符串都分别存储
        para.push_back(p);

    char* args[MAX_PARA+1]={};          //存储命令和参数
    char** tmp=new char* [MAX_LINE+1];  //二维数组,tmp[i]是一个字符串

    for(int i=0;i<para.size();i++)      //开辟空间,否则strcpy时只有指针,会报段错误
        tmp[i]=new char[MAX_PARA+1];
    // printf("%s\n",args[0]);

    for(int i=0;i<para.size();i++)
    {
        strcpy(tmp[i],para[i].c_str());//将string转化到char*
        args[i]=tmp[i];
    }

    // for(int i=0;i<para.size();i++)
    // {
    //     printf("%s\n",args[i]);
    // }
    //指令执行时会自己判断该简易指令是否是非法命令
    execvp(args[0],args);//使用参数+文件的exec
    //程序执行到这里说明execvp返回错误,也就是没有执行相应指令,指令出错
    fprintf(stderr,"%s: 未找到该命令\n",args[0]);
    exit(1);//必须要退出,否则子进程会循环创建自己的子进程

}

 执行中级命令(包含了"<"">")

        首先是重定向输入的判断

        根据重定向的情况:

        (1)命令 < 文件:将文件作为命令的标准输入

        (2)命令 << 分界符:从标准输入中读入,直到遇到分界符停止

        可以写如下函数isReIn(string input,int &type),输入字符串input,返回是否存在“<”、重定向是否出错、存在“<”的话属于上面哪一种。

        返回-1表示不存在“<”

        返回-2表示指令格式错了,比如“<”两边没有输入空格

        返回除了-1和-2以外的值就是"<"在input中的索引位置,此时type就对应了是重定向的哪种情况,(1)或者(2)。

        代码如下:

int isReIn(string input,int &type)
{
    int index = -1;
    for (int i = 0; i < input.size(); i++)//查找"<"在input中的索引位置
        if (input[i] == '<')
        {
            index = i;
            break;
        }

    if(index==-1)//不存在“<”
        return -1;
    else
    {
        if(index+1==input.size())//指令格式错,"<"右边没有了文件名
            return -2;
        else if(input[index+1]=='<')//" <<xxx",“<<”右边没有放空格
        {
            if(index+2==input.size())
                return -2;
            else
            {
                type=2;
                return index;
            }
        }
        else if(input[index+1]==' ')//"< "
        {
            type=1;
            return index;
        }
        else
            return -2;
    }
}

        上述代码规定了要使用重定向必须用标准格式" < ",即"<"两边都要有空格间隔。但实际linux系统的shell没有空格间隔,也能识别出是重定向。

        如以下在linux的shell下是等同的。

./input <file.txt
./input<file.txt
./input < file.txt

        对应的处理如下:

int indexIn = isReIn(devideString, typeIn);
if (indexIn != -1) //存在'<'
{
    if (typeIn == 1) //" < "
    {
        //从“<”后一个位置到结束都读取到字符串流
        string file(devideString.begin() + indexIn + 1, devideString.end());
        //从字符串流里读取第一个字符串,也就是文件名
        stringstream ss(file);
        string fileName;
        ss >> fileName;
        //转化为char数组
        char filename[MAX_LINE + 1];
        strcpy(filename, fileName.c_str());
        //关闭标准输入,dup到标准输出
        fd = open(filename, O_RDONLY);
        close(0);
        dup(fd);
        close(fd);
      }
    else if (typeIn == 2) //" << "没有相应处理
    {

           ;
                
    }

        本代码中没有对"<<"进行处理,因为水平有限,一直对linux分隔符截止输入的实现感到疑惑。。。("<< a"此时以a为结束标志,此时空格不起作用,但"<< aaa"却是以" aaa"而不是以"aaa"作为结束标志,差别在于空格,同时"<< aa "和"<< aa"也不太一样,因此对于"<<"暂时没处理,如果有深入理解的欢迎评论讨论)

        接着是重定向输出的判断

        输出重定向:

        (1)命令 > 文件:将标准输出重定向到文件中(清除原有文件中的数据)

        (2)命令 2> 文件:将错误输出重定向到文件中(清除原有文件中的数据)

        (3)命令 >> 文件:将标准输出重定向到文件中(在原有的内容后追加)

        (4)命令 2>> 文件:将错误输出重定向到文件中(在原有内容后面追加)

        (5)命令 >> 文件 2>&1,或 命令 &>> 文件:标准输出和错误输出共同写入 文件(在原有内容后追加)

        因此函数isReOut(string input, int &type):返回重定向">"在input中的索引位置,如果不存在">"则返回-1,指令格式错误返回-2,type记录处于何种情况

(1):" > "(2):" 2> "(3):" >> "(4):" 2>> "(5):" >> file 2>&1" (6):" &>> "

         实现思路如下,就是基本的字符串判断。

int isReOut(string input, int &type) 
//返回重定向">"在input中的索引位置,如果不存在">"则返回-1,指令格式错误返回-2,type记录处于何种情况
//(1):" > "
//(2):" 2> "
//(3):" >> "
//(4):" 2>> "
//(5):" >> file 2>&1" 
//(6):" &>> "
{
    int index = -1;
    for (int i = 0; i < input.size(); i++)
        if (input[i] == '>')
        {
            index = i;
            break;
        }

    if(index==-1)//不存在">"
        return -1;
    else if(index==0)//即">"处于开头,"> xxx"(这种情况为"> xxx"相当于清空文件xxx)
    {
        if(index+1==input.size())//指令格式错
            return -2;
        else if(input[index+1]=='>')//">>"
        {
            if(index+2==input.size())
                return -2;
            else
            {
                if(input[index+2]==' ')
                {
                    type=3;
                    return 0;
                }
                else
                    return -2;
            }
        }
        else if(input[index+1]==' ')//"> "
        {
            type=1;
            return 0;
        }
        else
            return -2;

    }
    else if(input[index-1]==' ')//" > "或" >> "
    {
        if(index+1==input.size())//指令格式错
            return -2;
        else
        {
            if(input[index+1]==' ')//" > "
            {
                type=1;
                return index;
            }
            else if(input[index+1]=='>')//" >> "
            {
                if(index+2==input.size())
                    return -2;
                else
                {
                    if(input[index+2]==' ')
                    {
                        string t(input.begin()+index+2,input.end());

                        stringstream ss(t);
                        string flag;
                        ss>>flag;
                        string flag2(flag);
                        ss>>flag2;
                        if(flag==flag2)//" >> "
                        {
                            type=3;
                            return index;
                        }
                        else
                        {
                            if(flag2=="2>&1")//" >> file 2>&1"
                            {
                                type=5;
                                return index;
                            }
                            else
                                return -2;
                        }
                    }
                    else
                        return -2;
                }
            }
            else
                return -2;
        }
    }
    else if(input[index-1]=='2')//" 2>" " 2>> "
    {
        if(index+1==input.size())//指令格式错
            return -2;
        else
        {
            if(input[index+1]==' ')//" 2> "
            {
                type=2;
                return index;
            }
            else if(input[index+1]=='>')//" 2>> "||"2>> "
            {
                if(index+2==input.size())
                    return -2;
                else
                {
                    if(input[index+2]==' ')
                    {
                        type=4;
                        return index;
                    }
                    else
                        return -2;
                }
            }
            else
                return -2;
        }
    }
    else if(input[index-1]=='&')//“&>>”
    {
        if (index + 1 == input.size()) //指令格式错
            return -2;
        else
        {
            if (input[index + 1] == '>') //" &>> "
            {
                if (index + 2 == input.size())
                    return -2;
                else
                {
                    if (input[index + 2] == ' ')
                    {
                        type = 6;
                        return index;
                    }
                    else
                        return -2;
                }
            }
            else
                return -2;
        }
    }
    else
        return -2;
}

        然后就是对每种情况的处理:

        因为每种情况都大同小异,差别在于截取的字符串位置从哪开始、打开文件是可写、追加、还是清空、文件权限是可读还是可写。整体框架都一样,就不重复了。

        int indexOut = isReOut(devideString, typeOut);        
        if (indexOut != -1) //存在'>'
        {
            if (typeOut == 1) //" > "
            {
                //截取字符串,最后目的就是为了正确读取到文件名
                string file(devideString.begin() + indexOut + 1, devideString.end());
                stringstream ss(file);
                string fileName;
                ss >> fileName;
                //读取文件名
                char filename[31];
                strcpy(filename, fileName.c_str());
                //以可读可写的方式打开,如果不存在则创建,如果存在就清空
                fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU);
                close(1);
                dup(fd);
                close(fd);
            }
            else if (typeOut == 2) //" 2> "
            {
                ...
            }
            else if (typeOut == 3) // " >> "
            {
                ...
            }
            else if (typeOut == 4) // " 2>> "
            {
                ...
            }
            else if (typeOut == 5 || typeOut == 6) //" >> file 2>&1" || " &>> "
            {
                ...
            }
        }

        这里要注意的点就是打开文件所给的参数,第一个参数是文件名,第二个参数是打开方式,第三个是文件权限。我看了很多资料都说一般情况下第三个可以不写,就是用open的第一种实现方式,但是实验中发现,如果没写第三个参数,会出现一些小问题,如有时候写入一个文件,文件不存在,创建并写入了该文件之后,这个文件是不可以读取的,也就是没有读取权限,没法查看。

        关于open函数,详情可以参考这篇博客

        上面是对存在重定向输入的处理,还有就是对输入格式错误的处理,重定向使用错误如" >3 " " >< "。

        printf("\' < \' 或\' > \' 附近有语法错误\n");
        exit(1);

        把输入输出重定向解决之后,我们就可以调用刚刚写的执行简易命令函数runCommand()来执行这个中级命令,怎么执行呢?

        很简单,就是把">"或"<"前面的那一段字符串提取出来,传给函数runCommand()就行了。这里需要注意如果重定向输入和重定向输出同时出现,应考虑出现的顺序,选择最早出现的那个重定向符号之前的所有字符串。

    if (indexIn == -1 && indexOut == -1) //没有重定向
        runCommand(devideString);
    else //有重定向,要注意重定向出现的顺序,可能先重定向输入,也可能先重定向输出
    {
        if (indexIn == -1) //只重定向输出
        {
            if (typeOut == 2 || typeOut == 4 || typeOut == 6) //"2>>" "&>>",结束位置为“>”前一个
            {
                string command(devideString.begin(), devideString.begin() + indexOut - 1);
                runCommand(command);
            }
            else
            {
                string command(devideString.begin(), devideString.begin() + indexOut);
                runCommand(command);
            }
        }
        else if (indexOut == -1) //只重定向输入
        {
            string command(devideString.begin(), devideString.begin() + indexIn);
            runCommand(command);
        }
        else //重定向了输入与输出,因为重定向输入和输出顺序可以变换,因此找出最小的位置。"2>"和"&>>"实际位置在index-1,也就是2和&也是重定向的一部分
        {
            int realOut;
            if (typeOut == 2 || typeOut == 4 || typeOut == 6)
                realOut = indexOut - 1;
            else
                realOut = indexOut;
            string command(devideString.begin(), devideString.begin() + min(indexIn, realOut));
            runCommand(command);
        }
    }

        这样,我们就完成了中级命令的处理,只要把中级命令作为参数调给整个封装的处理函数就可以完成中级命令的执行。

高级命令的执行

        高级命令就是我们输入的原始字符串。

        高级命令中级命令的区别在于管道符的存在。把输入的字符串按照"|"分割的一个个就是中级命令。当然不能简单的分割然后分别去执行。因为管道是把前面一个命令的输出作为后面一个命令的输入,因此需要用到管道pipe,同时管道还可能出现嵌套,如

command1 | command2 | command3 | ... | commandN

        因此我们还需要用递归来依次处理。

        高级命令执行过程如下:

        首先将输入字符串以"|"为分割符分割成一个个字符串存入vector,如果分割完后的字符串里出现了空,就意味着可能出现了"||"或者以"|"为开头,我对这些的处理都是归类为语法错误。(但在linux系统下的shell "||"不会报错,执行了另外的操作)。如果裁剪后就只有一个字符串,若以"|"结尾,那么说明语法错了;若不以"|"结尾,那说明输入的高级命令不存在"|"

        实现代码如下:

int isPipe(string input,vector<string>& midCommand)
//判断是否管道输入正确,正确则将其每一部分存入midCommand
{
    stringstream ss(input);
    string tmp;
    while (getline(ss,tmp,'|'))
        midCommand.push_back(tmp);
    
    for(int i=0;i<midCommand.size()-1;i++)
        if(midCommand[i].empty())//“||”或者以“|”开头
        {
            printf("\' | \' 附近有语法错误\n");
            exit(1);  
        }
    if(midCommand.size()==1)//裁剪后就是本身,包含以"|"结尾
    {
        if(midCommand[0][midCommand[0].size()-1]=='|')//以“|”结尾
        {
            printf("\' | \' 附近有语法错误\n");
            exit(1);
        }
        return 0;
    }
    else//裁剪后至少有两个字符串
        return 1;

}

        最后如果存在"|"则返回1,不存在"|"则返回0,语法错误则直接结束子进程。

        接下来就是重要部分:递归处理"|"嵌套。

        思路是我们对每一个管道符"|"进行处理,我们每次创建一个子进程去执行"|"左边的中级命令,子进程将输出重定向到管道的1端,父进程等待子进程执行结束,将输入重定向到管道的0端,继续调用递归处理管道"|"右边的部分。要注意的是,父进程自始至终都没有重定向输出,那是在子进程进行的,不会影响父进程。结束条件是,当管道"|"右边部分已经是最后一个中级命令,就不再进入递归创建子进程去执行,因为子进程会重定向输出到管道输出端,我们直接重定向输入端为管道之后就去执行最后一个中级命令。这样既保证了第一个中级命令的输入端没有被重定向,也保证了最后一个中级命令的输出端也没有被重定向,符合管道机制。

        要注意的一点是,如果我们把整个递归过程摊开,就会发现,子进程从管道1(pipe[1])端输出,父进程从管道0(pipe[0])端读入,当下一个递归开始时,子进程又会从管道1端输出,但此时子进程是复制了父进程所有环境,父进程已经把管道pipe[0]作为了输入,也就是子进程会出现:用同一个管道作为输入和输出。而我们知道,管道是具有半双工的特性,不能同时作为一个进程的输入和输出。因此我想到了创建一组管道,每一次递归时用一个管道,下一个递归开始时,用的又是另外一个管道,避免了对同一个管道的同时读写。

        代码如下:

void execPipe(vector<string> midCommand, int index)
{
    pid2 = fork();
    if (pid2 < 0) //子进程创建失败
    {
        printf("Create process fail!\n");
        exit(EXIT_FAILURE);
    }
    else if (pid2 == 0) //子进程2处理管道左部分
    {

        close(1);
        dup(pipe1[index][1]);
        close(pipe1[index][1]);
        close(pipe1[index][0]);
        //执行左边指令

        runMidCommand(midCommand[index]);
    }
    waitpid(pid2, &status, 0);

    // char sss[6];
    // read(pipe1[0], sss, sizeof(char)*4);
    // printf("%s\n",sss);
    close(0);
    dup(pipe1[index][0]);
    close(pipe1[index][1]);
    if (index == midCommand.size() - 2)
    {
        runMidCommand(midCommand[index + 1]);
    }
    execPipe(midCommand, index + 1);//继续执行后部分
}

完整框架

        综合上面说的,调用处理中级命令和递归,就完成了整体代码,逻辑还是比较简单的。

int main()
{
    read_history("_command_recent_.txt");//输入的历史命令存入里面
    int background;
    while (1)
    {   
        //父进程自定义信号SIGINT,当按下ctrl+c后不终止父进程,终止子进程
        signal(SIGINT,(__sighandler_t)kill_child);
        //输入ctrl+w触发SIGTSTP,父进程也结束

        background=0;
        char* p=readline(BEGIN(49,36)BLOD"~$ "CLOSE);//可使用tab补全,接下来p读入一行字符串
        add_history(p);//添加到历史文件,用于上下键
        write_history("_command_recent_.txt");//写到历史文件中,可以实现本次运行时查看上次运行的命令
        inputString=p;

        free(p);//释放内存,因为已经赋值在string

        // printf("~$ ");//命令提示符
        
        //输入一行命令到inputString
        // getline(cin,inputString);

        //读用户输入的命令行并分析命令行
        if(isRunInBackground())//命令行含有“&”,后台运行用户输入的命令
        {
            background=1;
            string tmp(inputString.begin(),inputString.begin()+inputString.size()-1);
            inputString=tmp;
        }

        pid=fork();//创建子进程执行命令

        if (pid < 0) //子进程创建失败
        {
            printf("Create process fail!\n");
            exit(EXIT_FAILURE);
        }
        else if(pid==0)//子进程执行命令行输入的命令
        {
            //设置信号SIGINT的处理方式为默认SIG_DFL
            //按下ctrl+c后终止子进程
            signal(SIGINT,SIG_DFL);

            vector<string> midCommand;
            if(isPipe(inputString,midCommand)==0)//没有管道符
                runMidCommand(inputString);                
            else//有管道
            {
                for(int i=0;i<midCommand.size();i++)//创建多个管道
                {
                    if (pipe(pipe1[i]) < 0) //建立成功返回0,否则返回-1
                    {
                        perror("pipe1 not create");
                        exit(EXIT_FAILURE);
                    }
                }
                execPipe(midCommand,0);//递归执行”|“嵌套
            }
        }
        //父进程执行任务
        if(background==0)//父进程等待子进程结束
            waitpid(pid,&status,0);//等待子进程结束
    }
    
}

        上述代码中出现了一些神奇的东西或许会感到疑惑:

read_history("_command_recent_.txt");//输入的历史命令存入里面
char* p=readline(BEGIN(49,36)BLOD"~$ "CLOSE);//可使用tab补全,接下来p读入一行字符串
add_history(p);//添加到历史文件,用于上下键
write_history("_command_recent_.txt");//写到历史文件中,可以实现本次运行时查看上次运行的命令

        这几句实际上实现的功能是,按下Tab键自动进行命令补全,按上下查看历史命令,左右键移动光标,与shell命令行的功能一致。

        关于readline,有一篇文章说的很清楚透彻,我这种新手都能很快入门,强烈建议参考

        如果不用readline,按上下左右方向键时,大家应该发现了,会出现"^[[A""^[[B"等奇怪字符。(当然有方法可以自己实现,如捕捉输入的方向键信号然后出发相应的处理函数,把历史命令覆盖输出等等,这些比较复杂,我建议还是直接使用readline,readline库十分强大,这里也只是使用了冰山一角)

        下面两句代码加不加对本次程序运行影响不大。

read_history("_command_recent_.txt");//输入的历史命令存入里面
write_history("_command_recent_.txt");//写到历史文件中,可以实现本次运行时查看上次运行的命令

         如果不加上,会出现我这一次运行代码第一次输入时,上一次运行时的历史命令按"↑"是没反应的。而加上之后,就能将每一次运行时的历史命令存到文件"_command_recent_.txt"中,用的时候就从里面读取,实现和shell命令一样,当关掉终端之后下一次打开终端是可以看到上一次的运行命令的。

附:本次实验的所有头文件、预定义、全局变量,函数(略写):

注:"..."略写是因为上面已经有各个部分的代码了,这里为了节省篇幅就不重复了,了解整体框架部分即可。

#include <stdio.h>
#include <sys/types.h>
#include<sys/stat.h>
#include<fcntl.h> 
#include <wait.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <sstream>
#include <string.h>
#include <iostream>
#include <vector>
#include <readline/readline.h>
#include <readline/history.h>

#define CLOSE "\001\033[0m\002"
#define BLOD "\001\033[1m\002"
#define BEGIN(x,y) "\001\033["#x";"#y"m\002" 

#define MAX_LINE 100 //限定用户输入的命令行长度不超过100字符
#define MAX_PARA 30  //限定用户输入命令的参数个数不超过30
using namespace std;

int pid,pid2;//子进程号
int fd;//文件描述符
int pipe1[MAX_PARA][2];//管道数组
int status;//用于waitpid
string inputString;//输入的命令

void kill_child()
{
    kill(pid,SIGKILL);
}

int isRunInBackground()
{
    if(inputString[inputString.length()-1]=='&')
        return 1;
    else
        return 0;
}

int isReOut(string input, int &type) 
//返回重定向">"在input中的索引位置,如果不存在">"则返回-1,指令格式错误返回-2,type记录处于何种情况
//(1):" > "
//(2):" 2> "
//(3):" >> "
//(4):" 2>> "
//(5):" >> file 2>&1" 
//(6):" &>> "
{
    int index = -1;
    for (int i = 0; i < input.size(); i++)
        if (input[i] == '>')
        {
            index = i;
            break;
        }

    if(index==-1)//不存在">"
        return -1;
    else if(index==0)//"> xxx"(这种情况为"> xxx"相当于清空文件xxx)
    {
        if(index+1==input.size())//指令格式错
            return -2;
        else if(input[index+1]=='>')//">>"
        {
            ...
        }
        else if(input[index+1]==' ')//"> "
        {
            ...
        }
        else
            return -2;

    }
    else if(input[index-1]==' ')//" > "或" >> "
    {
        if(index+1==input.size())//指令格式错
            return -2;
        else
        {
            if(input[index+1]==' ')//" > "
            {
                ...
            }
            else if(input[index+1]=='>')//" >> "
            {
                if(index+2==input.size())
                    return -2;
                else
                {
                    ...
                }
            }
            else
                return -2;
        }
    }
    else if(input[index-1]=='2')//" 2>" " 2>> "
    {
        ...
    }
    else if(input[index-1]=='&')//“&>>”
    {
        ...
    }
    else
        return -2;
}

int isReIn(string input,int &type)//输入字符串input,返回是否存在“<”、重定向是否出错、存在“<”的话属于哪一种
//返回-1表示不存在“<”
//返回-2表示指令格式错了,比如“<”两边没有输入空格
//返回除了-1和-2以外的值就是"<"在input中的索引位置
{
    int index = -1;
    for (int i = 0; i < input.size(); i++)//查找"<"在input中的索引位置
        if (input[i] == '<')
        {
            index = i;
            break;
        }

    if(index==-1)//不存在“<”
        return -1;
    else
    {
        if(index+1==input.size())//指令格式错,"<"右边没有了文件名
            return -2;
        else if(input[index+1]=='<')//" <<xxx",“<<”右边没有放空格
        {
            if(index+2==input.size())
                return -2;
            else
            {
                type=2;
                return index;
            }
        }
        else if(input[index+1]==' ')//"< "
        {
            type=1;
            return index;
        }
        else
            return -2;
    }
}
int isPipe(string input,vector<string>& midCommand)//判断是否管道输入正确,正确则将其每一部分存入midCommand
{
    stringstream ss(input);
    string tmp;
    while (getline(ss,tmp,'|'))
        midCommand.push_back(tmp);
    
    for(int i=0;i<midCommand.size()-1;i++)
        if(midCommand[i].empty())//“||”或者以“|”开头
        {
            printf("\' | \' 附近有语法错误\n");
            exit(1);  
        }
    if(midCommand.size()==1)//裁剪后就是本身,包含以“|”结尾
    {
        if(midCommand[0][midCommand[0].size()-1]=='|')//以“|”结尾
        {
            printf("\' | \' 附近有语法错误\n");
            exit(1);
        }
        return 0;
    }
    else//裁剪后至少有两个字符串
        return 1;

}

void runCommand(string command)//执行简易命令
{
    stringstream s(command);            //将string放到string输入流
    string p;                           //存储以‘ ’分割的各个string
    vector<string> para;                //存储每个字符串

    while (s>>p)                        //将命令的每个字符串都分别存储
        para.push_back(p);

    char* args[MAX_PARA+1]={};          //存储命令和参数
    char** tmp=new char* [MAX_LINE+1];  //二维数组,tmp[i]是一个字符串

    for(int i=0;i<para.size();i++)      //开辟空间,否则strcpy时只有指针,会报段错误
        tmp[i]=new char[MAX_PARA+1];
    // printf("%s\n",args[0]);

    for(int i=0;i<para.size();i++)
    {
        strcpy(tmp[i],para[i].c_str());//将string转化到char*
        args[i]=tmp[i];
    }

    // for(int i=0;i<para.size();i++)
    // {
    //     printf("%s\n",args[i]);
    // }
    //指令执行时会自己判断该简易指令是否是非法命令
    execvp(args[0],args);//使用参数+文件的exec
    //程序执行到这里说明execvp返回错误,也就是没有执行相应指令,指令出错
    fprintf(stderr,"%s: 未找到该命令\n",args[0]);
    exit(1);//必须要退出,否则子进程会循环创建自己的子进程

}

void runMidCommand(string devideString)//执行中级命令,即带有">"和"<"的命令
{
    int typeIn, typeOut; //判断类型
    int indexIn = isReIn(devideString, typeIn);
    int indexOut = isReOut(devideString, typeOut);
    // runCommand(inputString);
    //命令行中是否有I/O重定向,即双方是否有符号">","<"
    if (indexIn != -2 && indexOut != -2) //指令规范
    {
        if (indexIn != -1) //存在'<'
        {
            if (typeIn == 1) //" < "
            {
                ...
            }
            else if (typeIn == 2) //" << "没有相应处理
            {
                ...
            }
        }
        if (indexOut != -1) //存在'>'
        {
            if (typeOut == 1) //" > "
            {
                ...
            }
            else if (typeOut == 2) //" 2> "
            {
                ...
            }
            else if (typeOut == 3) // " >> "
            {
                ...
            }
            else if (typeOut == 4) // " 2>> "
            {
                ...
            }
            else if (typeOut == 5 || typeOut == 6) //" >> file 2>&1" || " &>> "
            {
                ...
            }
        }
    }
    else //重定向使用错误,如" >3 " " >< "
    {
        printf("\' < \' 或\' > \' 附近有语法错误\n");
        exit(1);
    }

    if (indexIn == -1 && indexOut == -1) //没有重定向
        runCommand(devideString);
    else //有重定向
    {
        if (indexIn == -1) //只重定向输出
        {
            ...
        }
        else if (indexOut == -1) //只重定向输入
        {
            ...
        }
        else 
        //重定向了输入与输出,因为重定向输入和输出顺序可以变换,因此找出最小的位置。"2>"和"&>>"实际位置在index-1
        {    
            ...
        }
    }
}

void execPipe(vector<string> midCommand, int index)
{
    pid2 = fork();
    if (pid2 < 0) //子进程创建失败
    {
        printf("Create process fail!\n");
        exit(EXIT_FAILURE);
    }
    else if (pid2 == 0) //子进程2处理管道左部分
    {

        close(1);
        dup(pipe1[index][1]);
        close(pipe1[index][1]);
        close(pipe1[index][0]);
        //执行左边指令

        runMidCommand(midCommand[index]);
    }
    waitpid(pid2, &status, 0);

    // char sss[6];
    // read(pipe1[0], sss, sizeof(char)*4);
    // printf("%s\n",sss);
    close(0);
    dup(pipe1[index][0]);
    close(pipe1[index][1]);
    if (index == midCommand.size() - 2)
    {
        runMidCommand(midCommand[index + 1]);
    }
    execPipe(midCommand, index + 1);//继续执行后部分
}

int main()
{
    read_history("_command_recent_.txt");//输入的历史命令存入里面
    int background;
    while (1)
    {   
        //父进程自定义信号SIGINT,当按下ctrl+c后不终止父进程,终止子进程
        signal(SIGINT,(__sighandler_t)kill_child);
        //输入ctrl+w触发SIGTSTP,父进程也结束

        background=0;
        char* p=readline(BEGIN(49,36)BLOD"~$ "CLOSE);//可使用tab补全,接下来p读入一行字符串
        add_history(p);//添加到历史文件,用于上下键
        write_history("_command_recent_.txt");//写到历史文件中,可以实现本次运行时查看上次运行的命令
        inputString=p;

        free(p);//释放内存,因为已经赋值在string

        // printf("~$ ");//命令提示符
        
        //输入一行命令到inputString
        // getline(cin,inputString);

        //读用户输入的命令行并分析命令行
        if(isRunInBackground())//命令行含有“&”,后台运行用户输入的命令
        {
            background=1;
            string tmp(inputString.begin(),inputString.begin()+inputString.size()-1);
            inputString=tmp;
        }

        pid=fork();//创建子进程执行命令

        if (pid < 0) //子进程创建失败
        {
            printf("Create process fail!\n");
            exit(EXIT_FAILURE);
        }
        else if(pid==0)//子进程执行命令行输入的命令
        {
            //设置信号SIGINT的处理方式为默认SIG_DFL
            //按下ctrl+c后终止子进程
            signal(SIGINT,SIG_DFL);

            vector<string> midCommand;
            if(isPipe(inputString,midCommand)==0)//没有管道符
                runMidCommand(inputString);                
            else//有管道
            {
                for(int i=0;i<midCommand.size();i++)//创建多个管道
                {
                    if (pipe(pipe1[i]) < 0) //建立成功返回0,否则返回-1
                    {
                        perror("pipe1 not create");
                        exit(EXIT_FAILURE);
                    }
                }
                execPipe(midCommand,0);//递归执行”|“嵌套
            }
        }
        //父进程执行任务
        if(background==0)//父进程等待子进程结束
            waitpid(pid,&status,0);//等待子进程结束
    }
    
}


写在最后

        本程序可以实现以下功能:

        (1)执行用户输入的合法命令,允许命令携带参数,如 ls -la;

        (2)在命令执行期间,允许用户按下一个给定的组合键结束命令的执行;

        (3)支持 I/O 重定向(>、<)与管道(|);

        (4)支持命令的后台运行;

        (5)保存用户最近输入的 30 个命令,可利用上下方向键进行选择;

        (6)若用户输入非法命令,或找不到命令要执行的文件等,给出错误提示;

        个别地方和Linux的shell命令行有出入,对于各种很极端的shell命令可能还会有bug,希望大家指正。

 演示

猜你喜欢

转载自blog.csdn.net/m0_51653200/article/details/121026390
今日推荐