这是有关如何构建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] == '')
{
return NULL;
}
struct symtab_s *st = symtab_stack.local_symtab;
struct symtab_entry_s *entry = NULL;
if((entry = do_lookup(symbol, st)))
{
return entry;
}
entry = malloc(sizeof(struct symtab_entry_s));
if(!entry)
{
fprintf(stderr, "fatal error: no memory for new symbol table entry ");
exit(EXIT_FAILURE);
}
memset(entry, 0, sizeof(struct symtab_entry_s));
entry->name = malloc(strlen(symbol)+1);
if(!entry->name)
{
fprintf(stderr, "fatal error: no memory for new symbol table entry ");
exit(EXIT_FAILURE);
}
strcpy(entry->name, symbol);
if(!st->first)
{
st->first = entry;
st->last = entry;
}
else
{
st->last->next = entry;
st->last = entry;
}
return entry;
}
此功能将新条目添加到本地符号表。请记住,在本课开始时,我曾说过每个条目必须具有唯一的键,这是我们为shell变量或函数指定的名称。为了确保这种唯一性,我们首先通过调用以下命令检查是否存在具有给定名称的条目:do_lookup() (我们将在稍后定义)。
如果存在具有给定名称的条目,我们仅返回现有条目,而无需添加新条目。否则,我们将添加条目,设置其名称,并调整符号表的指针。最后,我们返回新添加的条目。
下一个函数执行相反的工作,即删除键与给定名称匹配的符号表条目:
int rem_from_symtab(struct symtab_entry_s *entry, struct symtab_s *symtab)
{
int res = 0;
if(entry->val)
{
free(entry->val);
}
if(entry->func_body)
{
free_node_tree(entry->func_body);
}
free(entry->name);
if(symtab->first == entry)
{
symtab->first = symtab->first->next;
if(symtab->last == entry)
{
symtab->last = NULL;
}
res = 1;
}
else
{
struct symtab_entry_s *e = symtab->first;
struct symtab_entry_s *p = NULL;
while(e && e != entry)
{
p = e;
e = e->next;
}
if(e == entry)
{
p->next = entry->next;
res = 1;
}
}
free(entry);
return res;
}
此函数释放条目使用的内存,并调整链接列表指针以从符号表中删除条目。
要执行查找(即搜索具有给定名称的变量),我们需要在同一文件中定义以下函数:
struct symtab_entry_s *do_lookup(char *str, struct symtab_s *symtable)
{
if(!str || !symtable)
{
return NULL;
}
struct symtab_entry_s *entry = symtable->first;
while(entry)
{
if(strcmp(entry->name, str) == 0)
{
return entry;
}
entry = entry->next;
}
return NULL;
}
此函数从第一个条目开始搜索给定的符号表。如果条目的键与我们要查找的变量名匹配,则该函数返回该条目。
否则,该函数将按照链接列表指针依次查看每个条目,直到找到键与所需名称匹配的条目为止。如果没有找到匹配项,我们返回NULL。
接下来,添加以下功能:
struct symtab_entry_s *get_symtab_entry(char *str)
{
int i = symtab_stack.symtab_count-1;
do
{
struct symtab_s *symtab = symtab_stack.symtab_list[i];
struct symtab_entry_s *entry = do_lookup(str, symtab);
if(entry)
{
return entry;
}
} while(--i >= 0);
return NULL;
}
此函数搜索其键与给定名称匹配的符号表条目。起初这似乎是多余的,因为我们已经定义了do_lookup()函数搜索本地符号表。区别在于get_symtab_entry()从本地符号表开始搜索整个堆栈。目前,这种区别并不重要,因为我们的局部和全局符号表是指一个和同一表。
只有当我们谈论shell函数和脚本文件时,您才会欣赏到该函数的功能(请紧紧抓住它!)。
最后,添加以下功能:
void symtab_entry_setval(struct symtab_entry_s *entry, char *val)
{
if(entry->val)
{
free(entry->val);
}
if(!val)
{
entry->val = NULL;
}
else
{
char *val2 = malloc(strlen(val)+1);
if(val2)
{
strcpy(val2, val);
}
else
{
fprintf(stderr, "error: no memory for symbol table entry's value ");
}
entry->val = val2;
}
}
此功能释放用于存储旧条目值的内存(如果存在)。然后,它将创建新值的副本,并将其存储在符号表条目中。
符号表功能就是这样。现在让我们编写一些函数来帮助我们处理符号表堆栈。
符号表堆栈功能
将以下代码添加到同一源文件中, symtab.c:
void symtab_stack_add(struct symtab_s *symtab)
{
symtab_stack.symtab_list[symtab_stack.symtab_count++] = symtab;
symtab_stack.local_symtab = symtab;
}
struct symtab_s *symtab_stack_push(void)
{
struct symtab_s *st = new_symtab(++symtab_level);
symtab_stack_add(st);
return st;
}
struct symtab_s *symtab_stack_pop(void)
{
if(symtab_stack.symtab_count == 0)
{
return NULL;
}
struct symtab_s *st = symtab_stack.symtab_list[symtab_stack.symtab_count-1];
symtab_stack.symtab_list[--symtab_stack.symtab_count] = NULL;
symtab_level--;
if(symtab_stack.symtab_count == 0)
{
symtab_stack.local_symtab = NULL;
symtab_stack.global_symtab = NULL;
}
else
{
symtab_stack.local_symtab = symtab_stack.symtab_list[symtab_stack.symtab_count-1];
}
return st;
}
struct symtab_s *get_local_symtab(void)
{
return symtab_stack.local_symtab;
}
struct symtab_s *get_global_symtab(void)
{
return symtab_stack.global_symtab;
}
struct symtab_stack_s *get_symtab_stack(void)
{
return &symtab_stack;
}
以下是上述功能的快速分解:
.symtab_stack_add() 将给定的符号表添加到堆栈中,并将新添加的表分配为本地符号表。
.symtab_stack_push() 创建一个新的空符号表,并将其推入堆栈顶部。
.symtab_stack_pop() 删除(或弹出)堆栈顶部的符号表,并根据需要调整堆栈指针。
.get_local_symtab() 和 get_global_symtab() 分别返回指向本地和全局符号表的指针。
.get_symtab_stack() 返回指向符号表堆栈的指针。
初始化符号表栈
还记得本教程的开头我曾告诉过您,shell需要初始化其全局符号表并将变量从环境列表添加到表中吗?好了,在这一部分中,我们将初始化符号表堆栈和全局符号表。在本系列的下一部分中,我们将阅读环境变量列表并将其添加到符号表中。
继续并创建一个名为 initsh.c 在您的源目录中,并添加以下代码:
#include
#include "shell.h"
#include "symtab/symtab.h"
extern char **environ;
void initsh(void)
{
init_symtab();
struct symtab_entry_s *entry;
char **p2 = environ;
while(*p2)
{
char *eq = strchr(*p2, '=');
if(eq)
{
int len = eq-(*p2);
char name[len+1];
strncpy(name, *p2, len);
name[len] = '';
entry = add_to_symtab(name);
if(entry)
{
symtab_entry_setval(entry, eq+1);
entry->flags |= FLAG_EXPORT;
}
}
else
{
entry = add_to_symtab(*p2);
}
p2++;
}
entry = add_to_symtab("PS1");
symtab_entry_setval(entry, "$ ");
entry = add_to_symtab("PS2");
symtab_entry_setval(entry, "> ");
}
此函数初始化符号表堆栈(包括全局符号表)并扫描环境列表,并将每个环境变量(及其值)添加到表中。最后,该函数添加了两个用于存储提示字符串的变量PS1和PS2(我们在第一部分中讨论了提示字符串)。不要忘记将函数原型添加到您的shell.h 头文件:
void initsh(void);
接下来,我们需要从我们的 main()函数,然后进入REPL循环。为此,将以下行添加到main(),就在循环主体之前:
initsh();
我们需要做的最后一件事是更新提示字符串打印功能,以便它们将使用我们刚刚添加到全局符号表中的PS1和PS2变量。initsh()。我们还将编写第一个内置实用程序,dump。
接下来,将这些更改应用于我们的代码。
更新提示打印功能
打开你的 prompt.c文件。删除主体中的线print_prompt1() 并将其替换为以下代码:
void print_prompt1(void)
{
struct symtab_entry_s *entry = get_symtab_entry("PS1");
if(entry && entry->val)
{
fprintf(stderr, "%s", entry->val);
}
else
{
fprintf(stderr, "$ ");
}
}
新代码检查是否存在名称为PS1的符号表条目。如果存在,我们将使用该条目的值来打印第一个提示字符串。否则,我们将使用默认的内置值,即$ 。
代码更改为 print_prompt2() 功能相似,因此这里不再显示,但是您可以在我在本页顶部提供的GitHub repo链接中进行检查。
不要忘了在文件顶部,紧随 #include "shell.h" 线:
#include "symtab/symtab.h"
定义我们的第一个内置实用程序
甲内置实用程序(也称为内置或内部命令)是一个命令,其代码被编译为可执行本身,即壳不需要执行所述壳的一部分的外部程序,也不会需要叉为了一个新进程来执行命令。
我们每天使用的许多命令,例如 cd, echo, export和 readonly 实际上是内置实用程序。您可以在此POSIX标准链接中了解有关shell内置实用程序的更多信息。
在本教程的过程中,我们将添加不同的内置实用程序来扩展我们的shell。我们将首先定义一个结构,该结构将帮助我们存储有关不同内置实用程序的信息。
打开你的 shell.h 头文件,并在末尾添加以下代码 #endif 指示:
/* shell builtin utilities */
int dump(int argc, char **argv);
/* struct for builtin utilities */
struct builtin_s
{
char *name; /* utility name */
int (*func)(int argc, char **argv); /* function to call to execute the utility */
};
/* the list of builtin utilities */
extern struct builtin_s builtins[];
/* and their count */
extern int builtins_count;
函数原型声明了我们的第一个内置实用程序, dump。的struct builtin_s 结构定义了我们的内置实用程序,并具有以下字段:
.name =>内置实用程序名称,我们将使用它来调用该实用程序。
.func =>函数指针,该指针指向在我们的Shell中实现内置实用程序的函数。
我们将使用 builtins[]数组以存储有关我们的内置实用程序的信息。数组包含builtins_count 元素数量。
现在创建一个新的子目录并命名 builtins。这是我们定义所有内置实用程序的地方。接下来,创建文件builtins.c 并添加以下代码:
#include "../shell.h"
struct builtin_s builtins[] =
{
{ "dump" , dump },
};
int builtins_count = sizeof(builtins)/sizeof(struct builtin_s);
接下来,我们将添加一个内置实用程序,名为 dump,我们将使用它来转储或打印符号表的内容,因此我们知道幕后发生了什么(我主要是编写此实用程序的,因此我们的讨论听起来不会过于抽象和理论化)。
在里面 builtins 子目录,创建文件 dump.c 并添加以下代码:
#include "../shell.h"
#include "../symtab/symtab.h"
int dump(int argc, char **argv)
{
dump_local_symtab();
return 0;
}
简单吧?该功能实现了我们的dump 内置实用程序,可打印本地符号表的内容。
更新执行器
接下来,我们需要告诉执行者新的内置实用程序,以便在输入命令时 dump,shell执行我们的 dump()函数,而不是搜索名称为dump的外部命令。
为此,我们需要修复我们的 do_simple_command() 功能。
在源目录中,打开 executor.c 源文件并导航到 do_simple_command()函数的定义。现在找到两行:
argv[argc] = NULL;
pid_t child_pid = 0;
在这两行之间插入新行,然后输入以下代码:
int i = 0;
for( ; i < builtins_count; i++)
{
if(strcmp(argv[0], builtins[i].name) == 0)
{
builtins[i].func(argc, argv);
free_argv(argc, argv);
return 1;
}
}
现在,我们的外壳将检查要执行的命令的名称是否为内置实用程序的名称,如果是,它将调用实现内置实用程序的函数并返回。
就是这样!现在让我们编译和测试我们的shell。
让我们编译一下shell。打开您喜欢的终端模拟器,导航到源目录,并确保其中有14个文件和2个子目录:
现在,使用以下命令编译shell:
gcc -o shell executor.c initsh.c main.c node.c parser.c prompt.c scanner.c source.c builtins/builtins.c builtins/dump.c symtab/symtab.c
如果一切顺利 gcc 应该不输出任何东西,并且应该有一个名为 shell 在当前目录中:
从输出中可以看到,我们的全局符号表(级别0)包含许多变量(在上面的示例中为67),它们对应于从父进程传递给我们的shell环境变量。最后两个条目代表我们的PS1和PS2变量。尝试输入其他命令,看看我们的shell如何解析和执行简单命令。想了解更多关于Linux的信息,请继续关注吧。