实现的功能

  1. 类 UNIX 操作系统控制台的提示信息

    • 当前用户信息
    • 当前工作目录
  2. 获取命令字符串

    • 分号分隔
    • 多行输入
  3. 解析命令

    • 分析指令及其参数
    • 管道
    • 输入输出重定向
    • 变量存储和替换
  4. 指令执行

    • exec 函数族实现前台执行
    • 实现部分内建命令

大致框架

我们使用的 shell 一般有以下流程:初始化,打印提示符,解析命令,执行内置命令,执行外部命令,循环上述过程。所以可以根据这个流程来写出大致框架:

int main()
{
    while (true)
    {
        init();
        fflush(stdin);
        print_prompt();
        int len = get_input();

        if (len <= 0)
            continue;

        parse_token();

        for (auto i = 0; i < cmdNum; i++)
        {
            cmd_info* curCmd = cmds + i;

            if (exec_inner(curCmd) == 1) // if not shell builtin
            {
                // execvp will not return if it runs successfully
                // so use child proc to execute other command.
                pid_t pid = fork();
                if (pid < 0)
                    printf("[Error] Fork error in main.");
                else if (pid == 0) {
                    exec_outer(curCmd);
                    continue;
                }

                if ((cmds[i].type & BACKGROUND) == 0)
                    waitpid(pid, nullptr, 0);
            }

            // free cmd node
            init_node(curCmd);
        }
    }
}

功能实现

头文件和全局变量等

#include <cstdio>
#include <cstdlib>
#include <cerrno>
#include <cstring>
#include <fcntl.h>
#include <pwd.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>

#define MAX_CMD 100             // Maximum number of read commands per line
#define MAX_VAR_NUM 100         // Maximum number of variables
#define BUFFER_SIZE 1024        // Maximum buffer size
#define MAX_PATH_LENGTH 1024    // Maximum length of file path

struct cmd_info cmds[MAX_CMD];
char cmdStr[BUFFER_SIZE];
int cmdNum = 0;
char envKey[MAX_VAR_NUM][MAX_PATH_LENGTH];
char envVal[MAX_VAR_NUM][MAX_PATH_LENGTH];
int varNum = 0;

cmd_info 结构体

首先我们需要一个结构体来保存命令的信息。根据要实现的功能,这个结构体应该保存有以下信息:

  • 指令名
  • 指令参数
  • 输入输出重定向
  • 管道信息

所以我设计结构体如下:

// type of command, use binary
#define BACKGROUND              1
#define LEFT_REDIRECT           2
#define RIGHT_REDIRECT          4
#define REDIRECT_APPEND         8

struct cmd_info
{
    // command and parameters
    int argc = 0;
    char** argv{};

    // Input and output redirection
    char* input{};
    char* output{};

    // next command
    struct cmd_info* next = nullptr;

    // command attributes
    int type = 0;
};

argcargv 记录命令和参数个数;inputoutput 记录输入输出重定向;next 指针记录管道的下一个指令;type 记录重定向类型。

输出提示符

一般入门编程学的就是这个,就不需要讲什么了吧。

void print_prompt()
{
    char host_name[MAX_PATH_LENGTH];  // user's hostname
    char path_name[MAX_PATH_LENGTH];  // current working path
    passwd* pwd = getpwuid(getuid());  // user's information
    getcwd(path_name, MAX_PATH_LENGTH);

    // get user's hostname
    if (gethostname(host_name, MAX_PATH_LENGTH))
        strcpy(host_name, "unknown");

    // if current directory is not user's home, print it directory
    if (strlen ( path_name ) < strlen ( pwd->pw_dir ) || strncmp(path_name , pwd->pw_dir , strlen ( pwd->pw_dir)) != 0)
        printf("\033[;32m%s@%s\033[0m:\033[;34m%s\033[0m", pwd->pw_name, host_name, path_name);
    else // else use '~' instead of user's home
        printf("\033[;32m%s@%s\033[0m:\033[;34m~%s\033[0m", pwd->pw_name, host_name, path_name + strlen(pwd->pw_dir));

    // if the current user is root
    if (getuid() == 0)
        printf("# "); // is root
    else
        printf("$ "); // isn't root
}

解析命令字符串

这个功能代码量比较大,具体代码见文末吧。

这些函数解析命令字符串, 能支持多个空格,支持多行输入,支持多条命令 ;,支持了变量$, 支持引号' ,同时为重定向 <,>,<<,以及管道 |做好准备。

这一部分有些可能处理不是很恰当,参考一下就好了吧 qwq

内建命令

对于内建命令, 比如 cd, pwd, exit, echo, export, unset 可以直接执行
在代码中, 内建命令的实现都在 exec_inner 函数中, 如果不是内建命令, 则返回 1 , 然后会调用执行外部命令的函数 exec_outer

外部命令

实现的函数是 exec_outer,里面包括了重定向,管道,下面再介绍。

对于外部命令, 可以 fork 一个子进程,然后使用 exec 函数族让程序在子进程执行并返回。这里我使用 execvp 函数。

如果当前命令 的 nextnullptr,即没有下一条管道命令,那么直接将标准文件描述符传给 setIO 处理好文件 IO,然后调用 execvp 执行外部命令即可。

如果不为nullptr,说明有管道。建立管道,并用 fork 来新建子进程执行管道命令。这时传递到 setIO 函数的是对应管道文件描述符的输入输出, 然后如果有多个管道,可以递归地调用 execOuter函数。

void exec_outer(cmd_info* cmd)
{
    // the last command runs directly and exit
    if (cmd->next == nullptr)
    {
        setIO(cmd, STDIN_FILENO, STDOUT_FILENO);
        execvp(cmd->argv[0], cmd->argv);

        printf("[Error] Execute command %s error.\n", cmd->argv[0]);
        return;
    }

    // pipe
    int fd[2];
    pipe(fd);
    pid_t pid = fork();

    if (pid < 0)
    {
        printf("[Error] Fork error in exec_outer.");
        return;
    }
    else if (pid == 0)
    {
        close(fd[0]);
        setIO(cmd, STDIN_FILENO, fd[1]);
        execvp(cmd->argv[0], cmd->argv);

        // if execute success, the following code will not be executed
        printf("[Error] Execute command %s error.\n", cmd->argv[0]);
        return;
    }
    else
    {
        wait(nullptr);
        close(fd[1]);

        // execute all commands recursively
        cmd = cmd->next;
        setIO(cmd, fd[0], STDOUT_FILENO);
        exec_outer(cmd);
    }
}

管道与重定向

重定向的 I/O 以及管道的 I/O,我都放在 setIO 函数中处理,如下.
这个函数接受的参数包括一个命令指针 cmd (以;分隔的, 包括管道中的命令), 以及 一个输入文件描述符rfd,一个输出文件描述符wfd.

文件重定向

如果命令的 type 属性中的 LEFT_REDIRECTION/ RIGHT_REDIRECTION 标志位设置为 1 的话,就打开重定向的文件得到其文件描述符, 然后将标准输入/输出文件描述符关闭,再修改为此文件描述符。

注意最后用完此该文件描述符要用 close 关闭

管道重定向

分别检查传入的文件描述符参数是否是标准输入,输出。如果不是, 说明传递的是管道的文件描述符, 就将相应的 标准输入/输出 关闭 ,再复制到 rfd/wfd, 最后`close rfd/wfd

void setIO(cmd_info* cmd, const int rfd, const int wfd)
{
    int type = cmd->type;
    // > or >>
    if ((type & RIGHT_REDIRECT) != 0)
    {
        int mode = 0;
        if ((type & REDIRECT_APPEND) != 0)
            mode = O_WRONLY | O_TRUNC | O_CREAT;
        else
            mode = O_WRONLY | O_APPEND | O_CREAT;

        // change output file
        int wport = open(cmd->output, mode);
        dup2(wport, STDOUT_FILENO);
        close(wport);
    }

    // <
    if ((type & LEFT_REDIRECT) != 0)
    {
        int rport = open(cmd->input, O_RDONLY);
        dup2(rport, STDIN_FILENO);
        close(rport);
    }

    // pipe
    if (rfd != STDIN_FILENO)
    {
        dup2(rfd, STDIN_FILENO);
        close(rfd);
    }

    if (wfd != STDOUT_FILENO)
    {
        dup2(wfd, STDOUT_FILENO);
        close(wfd);
    }
}

完整代码

Github

最后修改:2020 年 04 月 27 日
如果觉得我的文章对你有用,请随意赞赏