构建Linux Shell [第三部分]

这是有关如何构建Linux Shell的教程的第三部分。 您可以通过以下链接阅读本教程的前两部分:第一部分第二部分

注意 :您可以从此GitHub存储库下载第二部分和第三部分的完整源代码。

解析简单命令

在本教程的上半部分,我们实现了词法扫描器。 现在,让我们将目光转向解析器。

回顾一下,解析器是命令行解释器的一部分,它调用词法扫描器以检索令牌,然后从这些令牌中构造一个抽象语法树AST 。 这个AST是我们将传递给执行者的东西,好吧,被执行。

我们的解析器将仅包含一个函数parse_simple_command() 。 在本教程的后续部分中,我们将添加更多功能以使我们的Shell能够解析循环和条件表达式。

因此,让我们开始对解析器进行编码。 您可以首先在源目录中创建一个名为parser.h的文件,并在其中添加以下代码:

# ifndef PARSER_H
# define PARSER_H

# include "scanner.h"    /* struct token_s */
# include "source.h"     /* struct source_s */

struct node_s * parse_simple_command (struct token_s *tok) ;

# endif

没什么,只是声明我们唯一的解析器功能。

接下来,创建parser.c并向其中添加以下代码:

# include <unistd.h>
# include "shell.h"
# include "parser.h"
# include "scanner.h"
# include "node.h"
# include "source.h"


struct node_s * parse_simple_command (struct token_s *tok)
 {
    if (!tok)
    {
        return NULL ;
    }
    
    struct node_s * cmd = new_node ( NODE_COMMAND );
    if (!cmd)
    {
        free_token(tok);
        return NULL ;
    }
    
    struct source_s * src = tok -> src ;
    
    do
    {
        if (tok->text[ 0 ] == '\n' )
        {
            free_token(tok);
            break ;
        }

        struct node_s * word = new_node ( NODE_VAR );
        if (!word)
        {
            free_node_tree(cmd);
            free_token(tok);
            return NULL ;
        }
        set_node_val_str(word, tok->text);
        add_child_node(cmd, word);

        free_token(tok);

    } while ((tok = tokenize(src)) != &eof_token);

    return cmd;
}

很简单,对吧? 要解析一个简单的命令,我们只需要调用tokenize()来逐个检索输入令牌,直到获得换行符(我们在读取的行中对其进行测试: if(tok->text[0] == '\n') ),或者我们到达输入的末尾(我们知道这是在我们获得eof_token令牌时发生的。请参阅上一清单底部的循环条件表达式)。 我们使用输入令牌来创建AST,它是一个树状结构 ,其中包含有关命令组件的信息。 详细信息应足以使执行程序正确执行命令。 例如,下图显示了简单命令的AST外观。

命令的AST中的每个节点都必须包含有关其表示的输入令牌的信息(例如原始令牌的文本)。 该节点还必须包含指向其子节点(如果该节点是根节点)及其兄弟节点(如果该节点是子节点)的指针。 因此,我们需要定义另一个结构struct node_s ,我们将使用它来表示AST中的节点。

继续创建一个新文件node.h ,并向其中添加以下代码:

# ifndef NODE_H
# define NODE_H

enum node_type_e
{
    NODE_COMMAND,           /* simple command */
    NODE_VAR,               /* variable name (or simply, a word) */
};

enum val_type_e
{
    VAL_SINT = 1 ,       /* signed int */
    VAL_UINT,           /* unsigned int */
    VAL_SLLONG,         /* signed long long */
    VAL_ULLONG,         /* unsigned long long */
    VAL_FLOAT,          /* floating point */
    VAL_LDOUBLE,        /* long double */
    VAL_CHR,            /* char */
    VAL_STR,            /* str (char pointer) */
};

union symval_u
{
    long               sint;
    unsigned long      uint;
    long long          sllong;
    unsigned long long ullong;
    double             sfloat;
    long double        ldouble;
    char               chr;
    char              *str;
};

struct node_s
{
    enum   node_type_e type;    /* type of this node */
    enum   val_type_e val_type; /* type of this node's val field */
    union  symval_u val;        /* value of this node */
    int    children;            /* number of child nodes */
    struct node_s * first_child ; /* first child node */
    struct node_s * next_sibling , * prev_sibling ; /*
                                                 * if this is a child node, keep
                                                 * pointers to prev/next siblings
                                                 */
};

struct  node_s * new_node ( enum node_type_e type) ;
void    add_child_node (struct node_s *parent, struct node_s *child) ;
void    free_node_tree (struct node_s *node) ;
void    set_node_val_str (struct node_s *node, char *val) ;

# endif

node_type_e枚举定义了我们的AST节点的类型。 目前,我们只需要两种类型。 第一种类型表示简单命令的AST的根节点,而第二种类型表示简单命令的子节点(包含命令名称参数 )。 在本教程的下一部分中,我们将向该枚举添加更多节点类型。

val_type_e枚举表示我们可以存储在给定节点结构中的值的类型。 对于简单的命令,我们将仅使用字符串( VAL_STR枚举类型)。 在本系列的稍后部分,我们将在处理其他类型的复杂命令时使用其他类型。

symval_u联合表示我们可以存储在给定节点结构中的值。 每个节点只能具有一种类型的值,例如字符串或数字值。 我们通过引用适当的联合成员来访问节点的值( sint表示带符号的长整数, str表示字符串,等等)。

struct node_s结构表示AST节点。 它包含一些字段,这些字段告诉我们有关节点类型,节点值的类型以及值本身的信息。 如果这是根节点,则children字段包含子节点的数量,并且first_child指向第一个子节点(否则它将为NULL )。 如果这是一个子节点,则可以通过遵循next_siblingprev_sibling指针来遍历AST节点。

如果要检索节点的值,则需要检查val_type字段,并根据在该字段中找到的内容访问val字段的适当成员。 对于简单命令,所有节点都将具有以下属性:

  • type => NODE_COMMAND (根节点)或NODE_VAR (命令名称和参数列表)
  • val_type => VAL_STR
  • val.str =>指向字符串值的指针

现在让我们编写一些函数来帮助我们处理节点结构。

创建一个名为node.c的文件,并添加以下代码:

# include <stdlib.h>
# include <string.h>
# include <stdio.h>
# include <errno.h>
# include "shell.h"
# include "node.h"
# include "parser.h"


struct node_s * new_node ( enum node_type_e type)
 {
    struct node_s * node = malloc ( sizeof ( struct node_s ));

    if (!node)
    {
        return NULL ;
    }
    
    memset (node, 0 , sizeof (struct node_s));
    node->type = type;
    
    return node;
}


void add_child_node (struct node_s *parent, struct node_s *child)
 {
    if (!parent || !child)
    {
        return ;
    }

    if (!parent->first_child)
    {
        parent->first_child = child;
    }
    else
    {
        struct node_s *sibling = parent->first_child;
    
    	while (sibling->next_sibling)
        {
            sibling = sibling->next_sibling;
        }
    
    	sibling->next_sibling = child;
        child->prev_sibling = sibling;
    }
    parent->children++;
}


void set_node_val_str (struct node_s *node, char *val)
 {
    node->val_type = VAL_STR;

    if (!val)
    {
        node->val.str = NULL ;
    }
    else
    {
        char *val2 = malloc ( strlen (val)+ 1 );
    
    	if (!val2)
        {
            node->val.str = NULL ;
        }
        else
        {
            strcpy (val2, val);
            node->val.str = val2;
        }
    }
}


void free_node_tree (struct node_s *node)
 {
    if (!node)
    {
        return ;
    }

    struct node_s * child = node -> first_child ;
    
    while (child)
    {
        struct node_s * next = child -> next_sibling ;
        free_node_tree(child);
        child = next;
    }
    
    if (node->val_type == VAL_STR)
    {
        if (node->val.str)
        {
            free (node->val.str);
        }
    }
    free (node);
}

new_node()函数创建一个新节点并设置其type字段。

add_child_node()函数通过添加新的子节点并增加根节点的children节点字段来扩展简单命令的AST。 如果根节点没有子节点,则将新的子节点分配给根节点的first_child字段。 否则,该孩子将被添加到孩子列表的末尾。

set_node_val_str()函数将节点的值设置为给定的字符串。 它将字符串复制到新分配的内存空间,然后相应地设置val_typeval.str字段。 将来,我们将定义类似的函数,以使我们可以将节点值设置为不同的数据类型,例如整数和浮点数。

free_node_tree()函数释放节点结构使用的内存。 如果节点有子节点,则以递归方式调用该函数以释放每个子节点。

解析器就这些了。 现在让我们编写命令执行器。

执行简单命令

与我们的解析器类似,执行程序将仅包含一个函数do_simple_command() 。 在本教程的后续部分中,我们将添加更多功能以使我们能够执行各种命令,例如循环和条件表达式。

创建一个名为executor.h的文件,并添加以下代码:

# ifndef BACKEND_H
# define BACKEND_H

# include "node.h"

char * search_path ( char *file) ;
int do_exec_cmd ( int argc, char **argv) ;
int do_simple_command (struct node_s *node) ;

# endif

只是一些功能原型。 现在创建executor.c并定义以下功能:

# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <string.h>
# include <errno.h>
# include <sys/stat.h>
# include <sys/wait.h>
# include "shell.h"
# include "node.h"
# include "executor.h"


char * search_path ( char *file)
 {
    char *PATH = getenv( "PATH" );
    char *p    = PATH;
    char *p2;
    
    while (p && *p)
    {
        p2 = p;

        while (*p2 && *p2 != ':' )
        {
            p2++;
        }
        
	int  plen = p2-p;
        if (!plen)
        {
            plen = 1 ;
        }
        
        int  alen = strlen (file);
        char path[plen+ 1 +alen+ 1 ];
        
	strncpy (path, p, p2-p);
        path[p2-p] = '\0' ;
        
	if (p2[ -1 ] != '/' )
        {
            strcat (path, "/" );
        }

        strcat (path, file);
        
	struct stat st ;
        if (stat(path, &st) == 0 )
        {
            if (!S_ISREG(st.st_mode))
            {
                errno = ENOENT;
                p = p2;
                if (*p2 == ':' )
                {
                    p++;
                }
                continue ;
            }

            p = malloc ( strlen (path)+ 1 );
            if (!p)
            {
                return NULL ;
            }
            
	    strcpy (p, path);
            return p;
        }
        else    /* file not found */
        {
            p = p2;
            if (*p2 == ':' )
            {
                p++;
            }
        }
    }

    errno = ENOENT;
    return NULL ;
}


int do_exec_cmd ( int argc, char **argv)
 {
    if ( strchr (argv[ 0 ], '/' ))
    {
        execv(argv[ 0 ], argv);
    }
    else
    {
        char *path = search_path(argv[ 0 ]);
        if (!path)
        {
            return 0 ;
        }
        execv(path, argv);
        free (path);
    }
    return 0 ;
}


static inline void free_argv ( int argc, char **argv)
 {
    if (!argc)
    {
        return ;
    }

    while (argc--)
    {
        free (argv[argc]);
    }
}


int do_simple_command (struct node_s *node)
 {
    if (!node)
    {
        return 0 ;
    }

    struct node_s * child = node -> first_child ;
    if (!child)
    {
        return 0 ;
    }
    
    int argc = 0 ;
    long max_args = 255 ;
    char *argv[max_args+ 1 ];     /* keep 1 for the terminating NULL arg */
    char *str;
    
    while (child)
    {
        str = child->val.str;
        argv[argc] = malloc ( strlen (str)+ 1 );
        
	if (!argv[argc])
        {
            free_argv(argc, argv);
            return 0 ;
        }
        
	strcpy (argv[argc], str);
        if (++argc >= max_args)
        {
            break ;
        }
        child = child->next_sibling;
    }
    argv[argc] = NULL ;

    pid_t child_pid = 0 ;
    if ((child_pid = fork()) == 0 )
    {
        do_exec_cmd(argc, argv);
        fprintf ( stderr , "error: failed to execute command: %s\n" , strerror(errno));
        if (errno == ENOEXEC)
        {
            exit ( 126 );
        }
        else if (errno == ENOENT)
        {
            exit ( 127 );
        }
        else
        {
            exit (EXIT_FAILURE);
        }
    }
    else if (child_pid < 0 )
    {
        fprintf ( stderr , "error: failed to fork command: %s\n" , strerror(errno));
        return 0 ;
    }

    int status = 0 ;
    waitpid(child_pid, &status, 0 );
    free_argv(argc, argv);
    
    return 1 ;
}

search_path()函数采用命令的名称,然后搜索$PATH变量中列出的目录,以尝试查找命令的可执行文件。 $PATH变量包含逗号分隔的目录列表,例如/bin:/usr/bin 。 对于每个目录,我们通过在目录名称后附加命令名称来创建路径名,然后调用stat()以查看是否存在具有给定路径名的文件(为简单起见,我们不检查该文件是否实际可执行) ,或者我们是否有足够的权限执行它)。 如果文件存在,则假定它包含我们要执行的命令,然后返回该命令的完整路径名。 如果在第一个$PATH目录中找不到该文件,则搜索第二个,第三个以及$PATH目录的其余部分,直到找到可执行文件。 如果我们无法通过搜索$PATH中的所有目录来找到命令,则返回NULL (这通常意味着用户键入了无效的命令名称)。

do_exec_cmd()函数通过调用execv()来执行命令,以用新的命令可执行文件替换当前过程映像。 如果命令名称包含任何斜杠字符,我们会将其视为路径名,然后直接调用execv() 。 否则,我们尝试通过调用search_path()来找到命令,该命令应返回将传递给execv()的完整路径名。

free_argv()函数释放用于存储上次执行命令的参数列表的内存。

do_simple_command()函数是执行程序中的主要函数。 它获取命令的AST并将其转换为参数列表(请记住,第零个参数argv[0]包含我们要执行的命令的名称)。

然后,该函数派生一个新的子进程。 在子进程中,我们通过调用do_exec_cmd()执行命令。 如果命令执行成功,则此调用不应返回。 如果返回,则表示发生了错误(例如,找不到命令,文件不可执行,内存不足等)。 在这种情况下,我们将打印适当的错误消息并以非零退出状态退出

在父进程中,我们调用waitpid()等待子进程完成执行。 然后,我们释放用于存储参数列表的内存并返回。

现在,为了将新代码合并到我们现有的shell中,您需要首先通过删除读取printf("%s\n", cmd);的行来更新main()函数printf("%s\n", cmd); 并将其替换为以下行:

struct source_s src ;
        src.buffer   = cmd;
        src.bufsize  = strlen (cmd);
        src.curpos   = INIT_SRC_POS;
        parse_and_execute(&src);

现在,在关闭main.c文件之前,请转到文件的开头,并在最后一个#include指令之后添加以下行:

# include "source.h"
# include "parser.h"
# include "backend.h"

然后转到文件末尾(在read_cmd()函数定义之后),并添加以下函数:

int parse_and_execute (struct source_s *src)
 {
    skip_white_spaces(src);

    struct token_s * tok = tokenize ( src );

    if (tok == &eof_token)
    {
        return 0 ;
    }

    while (tok && tok != &eof_token)
    {
        struct node_s * cmd = parse_simple_command ( tok );

        if (!cmd)
        {
            break ;
        }

        do_simple_command(cmd);
        free_node_tree(cmd);
        tok = tokenize(src);
    }

    return 1 ;
}

此函数使我们的Read-Eval-Print-LoopREPL )的Eval-Print部分脱离main()函数。 它首先跳过任何前导空格字符,然后解析并执行简单命令,一次执行一个命令,直到输入被消耗为止,然后再将控制返回给main()函数中的REPL循环。

最后,不要忘了在shell.h #endif指令之前将以下包含和函数原型添加到shell.h文件中:

# include "source.h"
int  parse_and_execute (struct source_s *src) ;

就是这样! 现在让我们编译我们的shell。

编译外壳

让我们编译一下shell。 打开您喜欢的终端仿真器,导航到源目录,并确保其中包含13个文件:

现在,使用以下命令编译shell:

gcc -o shell main.c source.c scanner.c parser.c node.c executor.c prompt.c

如果一切顺利, gcc将不会输出任何内容,并且在当前目录中应该有一个名为shell的可执行文件:

现在,通过运行./shell调用Shell,并尝试输入一些命令,例如lsls -lecho Hello World!

如您所见,无论我们在命令行中传递多少参数,我们的Shell现在都可以解析和执行简单的命令。 好极了!

但是,如果尝试执行命令echo $PATH ,则会发现结果与预期不符! 这是因为我们的外壳不知道如何访问其环境,如何存储外壳变量以及如何执行单词扩展。 这是我们将在本教程的下一部分中解决的问题。

下一步是什么

为了能够进行单词扩展,我们首先需要访问shell的环境并找到一种动态的方式来存储shell变量。 我们将在下一部分中进行此操作。

敬请关注!

先前发布在 https://medium.com/swlh/lets-build-a-linux-shell-part-iii-a472c0102849

翻译自: https://hackernoon.com/building-a-linux-shell-part-iii-wzo3uoi

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值