这是有关如何构建Linux Shell的教程的第三部分。在本教程的上半部分,我们实现了词法扫描器。现在,让我们将目光转向解析器。回顾一下,解析器是命令行解释器的一部分,它调用词法扫描器以检索令牌,然后从这些令牌中构造一个抽象语法树或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
#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] == ' ')
{
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] == ' ')),或者我们到达输入的结尾(我们知道当我们收到 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_uunion表示我们可以存储在给定节点结构中的值。每个节点只能具有一种类型的值,例如字符串或数字值。我们通过引用适当的联合成员来访问节点的值(sint 对于带符号的长整数, str 用于字符串等)。
的 struct node_s结构代表AST节点。它包含一些字段,这些字段告诉我们有关节点类型,节点值的类型以及值本身的信息。如果这是根节点,则children 字段包含子节点的数量,并且 first_child 指向第一个子节点(否则它将是 NULL)。如果这是一个子节点,我们可以按照以下步骤遍历AST节点:next_sibling 和 prev_sibling 指针。
如果要检索节点的值,则需要检查 val_type 字段,然后根据我们在其中找到的内容,访问 val领域。对于简单命令,所有节点都将具有以下属性:
.type => NODE_COMMAND (根节点)或 NODE_VAR (命令名称和参数列表)
.val_type => VAL_STR
.val.str =>指向字符串值的指针
现在让我们编写一些函数来帮助我们处理节点结构。
创建一个名为 node.c 并添加以下代码:
#include
#include
#include
#include
#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() 函数通过添加新的子节点并增加根节点的扩展来扩展简单命令的AST children领域。如果根节点没有子节点,则将新的子节点分配给first_child根节点的字段。否则,该孩子将被添加到孩子列表的末尾。
的 set_node_val_str()函数将节点的值设置为给定的字符串。它将字符串复制到新分配的内存空间,然后设置val_type 和 val.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
#include
#include
#include
#include
#include
#include
#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] = '