这是有关如何构建Linux Shell的教程的第四部分。在这一部分中,我们将向我们的外壳添加符号表。的符号表是用于由数据结构的编译器和解释器来存储变量如表中的条目。每个条目都包含一个键(变量的名称)和一个关联的值(变量的值)。键通常是唯一的,也就是说,我们不能有两个共享相同键的条目(即,不能有两个共享相同变量名的变量)。
通常,Linux Shell在启动时会填充其符号表。填充符号表后,编译器或解释器可以轻松地在表中搜索变量以检索该变量的值。我们还可以执行类型检查,执行作用域规则(例如,使变量仅对声明其的函数可见),并将shell变量导出到外部命令。
为了填充符号表,外壳程序读取环境变量列表,该环境变量列表从其父进程(通常是登录用户的进程或登录进程的子进程)传递到外壳程序。Shell将每个变量(及其值)添加到符号表中。然后,我们可以使用适当的内置实用程序随意编辑,删除或导出shell变量(我们将在本系列的稍后部分中讨论)。
为什么我们需要符号表?
简而言之,符号表使我们能够定义外壳变量,修改它们的值,在执行变量扩展时使用不同外壳变量的值以及将变量导出到外部命令。在本系列后面的内容中,当我们讨论位置和特殊外壳参数时,符号表也将变得很方便。
每当您要求外壳程序回显,导出或未设置外壳程序变量的值时,您实际上就是在要求外壳程序访问和/或修改其符号表。所有外壳程序都有某种符号表实现,尽管某些外壳程序可能具有不同的名称。
例如,假设您调用了以下命令:
echo $PATH
哪个应该给你类似的输出:
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
您可能知道 echo 该命令与您在屏幕上看到的输出无关,除了以下事实: echo打印出路径。它的外壳究竟是谁明白$PATH 代表外壳变量名称。
也是贝壳代替了单词$PATH 带有实际路径值,然后将其传递给 echo。的echo 命令只是回显了外壳程序传递的参数,这是您在屏幕上看到的可执行路径。
因此,为了能够定义,修改,取消设置和导出Shell变量,我们首先需要实现符号表。让我们看看接下来如何做。
实施符号表
有多种方法可以实现符号表,常见的方法是链表,哈希表和二进制搜索树。每种方法都有优点和缺点,我们没有时间或空间来详细讨论每种方法。为了我们的目的,我们将使用链表,链表是最容易实现的,并且在访问速度和内存使用方面都相当不错。
(注:如果你想使用的外壳任何东西比学习其他的,你应该考虑改变符号表执行到使用哈希表或二进制树可以找到哈希表实现的例子这里)。
现在,让我们来破解该代码。在您的源目录中,创建一个名为symtab (调用 mkdir symtab从您的终端仿真器)。导航到该目录(cd symtab)并创建一个名为 symtab.h。将以下代码添加到刚创建的头文件中:
#ifndef SYMTAB_H
#define SYMTAB_H
#include "../node.h"
#define MAX_SYMTAB 256
/* the type of a symbol table entry's value */
enum symbol_type_e
{
SYM_STR ,
SYM_FUNC,
};
/* the symbol table entry structure */
struct symtab_entry_s
{
char *name;
enum symbol_type_e val_type;
char *val;
unsigned int flags;
struct symtab_entry_s *next;
struct node_s *func_body;
};
/* the symbol table structure */
struct symtab_s
{
int level;
struct symtab_entry_s *first, *last;
};
/* values for the flags field of struct symtab_entry_s */
#define FLAG_EXPORT (1 << 0) /* export entry to forked commands */
/* the symbol table stack structure */
struct symtab_stack_s
{
int symtab_count;
struct symtab_s *symtab_list[MAX_SYMTAB];
struct symtab_s *global_symtab, *local_symtab;
};
struct symtab_s *new_symtab(int level);
struct symtab_s *symtab_stack_push(void);
struct symtab_s *symtab_stack_pop(void);
int rem_from_symtab(struct symtab_entry_s *entry, struct symtab_s *symtab);
struct symtab_entry_s *add_to_symtab(char *symbol);
struct symtab_entry_s *do_lookup(char *str, struct symtab_s *symtable);
struct symtab_entry_s *get_symtab_entry(char *str);
struct symtab_s *get_local_symtab(void);
struct symtab_s *get_global_symtab(void);
struct symtab_stack_s *get_symtab_stack(void);
void init_symtab(void);
void dump_local_symtab(void);
void free_symtab(struct symtab_s *symtab);
void symtab_entry_setval(struct symtab_entry_s *entry, char *val);
#endif
的 symbol_type_e枚举定义了我们的符号表条目的类型。我们将使用类型SYM_STR 表示外壳变量,以及 SYM_FUNC 表示函数(在本系列后面的部分中,我们将介绍shell函数)。
的 struct symtab_entry_s结构代表我们的符号表条目。该结构包含以下字段:
.name =>此条目表示的shell变量(或函数)的名称。
.val_type => SYM_STR 对于外壳变量, SYM_FUNC 用于外壳函数。
.val =>字符串值(仅适用于Shell变量)。
.flags =>表示我们将分配给变量和函数的不同属性,例如export和readonly标志(我们将在本系列的后面部分处理这些标志)。
.next =>指向下一个符号表条目的指针(因为我们将表实现为单链接列表)。
.func_body=>对于外壳函数,是函数主体的抽象语法树或AST(我们在本教程的第一部分中讨论了AST )。
的 struct symtab_s结构表示单个符号表。首先,我们将使用一个符号表,在其中定义所有的shell变量。稍后,当我们讨论外壳函数并开始使用脚本文件时,我们将需要定义更多的符号表。
第零个符号表将是全局表,在其中我们将定义全局变量(shell可以访问的全局变量,以及由它执行的所有函数和脚本)。
符号表中排名第一的符号表是本地表,我们将在其中定义我们的本地变量(这些变量只能由声明了它们的shell函数或脚本访问)。通过以这种方式级联符号表,我们有效地实现了变量作用域。
我们的 struct symtab_s 结构包含以下字段:
.level =>对于全局符号表为0,对于局部符号表为1及更高。
.first, last =>分别指向表的链表中第一个和最后一个条目的指针。
现在,要能够如上所述层叠符号表,我们需要定义并实现符号表栈。甲堆栈是一个后进先出,或LIFO,数据结构,其中的最后一个项目中加入(或推)是移除(或第一项弹出)。的struct symtab_stack_s结构代表我们的符号表堆栈。该结构包含以下字段:
.symtab_count =>当前堆栈中符号表的数量。
.symtab_list=>指向堆栈符号表的指针数组。第零项指向全局符号表,而symtab_count-1项目指向最后一个(或本地)符号表。堆栈最多可容纳MAX_SYMTAB 项,我们在头文件的开头将其定义为256。
.global_symtab, local_symtab =>分别指向全局和局部符号表的指针(为了易于访问)。
我们将在本课程的稍后部分实现堆栈。现在,我们将从编写使用符号表所需的功能开始。
符号表功能
创建 symtab.c 文件(在 symtab 子目录),然后添加以下代码开始:
#include
#include
#include
#include "../shell.h"
#include "../node.h"
#include "../parser.h"
#include "symtab.h"
struct symtab_stack_s symtab_stack;
int symtab_level;
void init_symtab(void)
{
symtab_stack.symtab_count = 1;
symtab_level = 0;
struct symtab_s *global_symtab = malloc(sizeof(struct symtab_s));
if(!global_symtab)
{
fprintf(stderr, "fatal error: no memory for global symbol table ");
exit(EXIT_FAILURE);
}
memset(global_symtab, 0, sizeof(struct symtab_s));
symtab_stack.global_symtab = global_symtab;
symtab_stack.local_symtab = global_symtab;
symtab_stack.symtab_list[0] = global_symtab;
global_symtab->level = 0;
}
首先,我们有两个全局变量:
.symtab_stack =>指向符号表堆栈的指针(每个外壳仅需要一个堆栈)。
.symtab_level =>我们当前在堆栈中的级别(如果正在使用全局符号表,则为0,否则为非零)。
的 init_symtab() 函数初始化符号表堆栈,然后为全局符号表分配内存并进行初始化。
接下来,添加以下功能:
struct symtab_s *new_symtab(int level)
{
struct symtab_s *symtab = malloc(sizeof(struct symtab_s));
if(!symtab)
{
fprintf(stderr, "fatal error: no memory for new symbol table ");
exit(EXIT_FAILURE);
}
memset(symtab, 0, sizeof(struct symtab_s));
symtab->level = level;
return symtab;
}
我们称 new_symtab() 每当我们想要创建一个新的符号表时(例如,当我们要执行一个shell函数时),函数就起作用。
接下来,添加以下功能:
void free_symtab(struct symtab_s *symtab)
{
if(symtab == NULL)
{
return;
}
struct symtab_entry_s *entry = symtab->first;
while(entry)
{
if(entry->name)
{
free(entry->name);
}
if(entry->val)
{
free(entry->val);
}
if(entry->func_body)
{
free_node_tree(entry->func_body);
}
struct symtab_entry_s *next = entry->next;
free(entry);
entry = next;
}
free(symtab);
}
我们称 free_symtab() 当我们完成了符号表的工作后,我们想使用该函数,并希望释放符号表及其条目所使用的内存。
接下来,我们将定义一个调试功能:
void dump_local_symtab(void)
{
struct symtab_s *symtab = symtab_stack.local_symtab;
int i = 0;
int indent = symtab->level * 4;
fprintf(stderr, "%*sSymbol table [Level %d]: ", indent, " ", symtab->level);
fprintf(stderr, "%*s=========================== ", indent, " ");
fprintf(stderr, "%*s No Symbol Val ", indent, " ");
fprintf(stderr, "%*s------ -------------------------------- ------------ ", indent, " ");
struct symtab_entry_s *entry = symtab->first;
while(entry)
{
fprintf(stderr, "%*s[%04d] %-32s '%s' ", indent, " ",
i++, entry->name, entry->val);
entry = entry->next;
}
fprintf(stderr, "%*s------ -------------------------------- ------------ ", indent, " ");
}
此功能打印本地符号表的内容。当我们的外壳启动时,本地和全局符号表将引用同一表。只有在Shell要运行Shell函数或脚本文件时,我们的本地表才与全局表不同。(在本课程后面,我们将编写一个内置实用程序,该实用程序将调用dump_local_symtab() 以帮助我们可视化外壳的全局符号表的内容)。
现在,让我们定义一些函数来帮助我们处理符号表条目。在同一文件中(symtab.c),添加以下功能:
struct symtab_entry_s *add_to_symtab(char *symbol)
{
if(!symbol || symbol[0] == '