之前为家介绍了关于如何构建一个Linux Shell的一、二、三、四。今天这里为大家介绍的是关于如何构建Linux Shell的教程的第五部分。正如我们在前面的部分中所看到的,一个简单的命令由一个或多个参数组成。第一个单词包含我们要执行的命令的名称,其余单词包含命令的参数。在外壳程序执行命令之前,它将对命令字执行字扩展。

单词扩展是shell提取命令字,检查它是否包含变量名,路径名,命令和算术表达式,然后用其值替换每个名称/命令/表达式的过程。

通常长于原始单词的结果单词可以在称为字段拆分的过程中分解为一个或多个子单词。

在这一部分中,我们将实现POSIX定义的7个词扩展,它们是:波浪号扩展,参数扩展,算术扩展,命令替换,字段拆分,路径名扩展和引号删除。还有其他单词扩展,例如大括号扩展和流程替换,这些没有由POSIX定义,因此我们将不在这里讨论。完成本课程后,如果您通过实现非POSIX单词扩展来扩展Shell,将是一个很好的练习。

  单词扩展过程

当外壳程序执行单词扩展时,它将检查命令行中的每个单词以查看其是否包含可能的单词扩展。扩展可以出现在单词的任何位置:在单词的开头,中间或结尾。扩展可能还包括整个单词。

单词扩展之前有一个$标志。后面的字符$符号指示外壳要执行的扩展类型。这些字符由外壳解释如下:

一个或多个数字,指示位置参数的变量扩展。

之一@,*,#,?,-,$,!,要么0,它指示特殊参数的变量扩展。

一个或多个字母数字字符和/或下划线,以字母或下划线开头,表示外壳变量名称。

括号内的变量名{和}。

算术展开,被(和)。

命令替换,由((和))。

Shell首先执行波浪号扩展,参数扩展,命令替换和算术扩展,然后进行字段拆分和路径名扩展。最后,shell从已扩展单词中删除了已成为原始单词一部分的所有引号字符。

  使用文字

当外壳程序执行单词扩展时,该过程可能会导致零个,一个或多个单词。

structword_s

{

char*data;

intlen;

structword_s*next;

};

  该结构包含以下字段:

data=>表示此单词的字符串。

len=>的长度data领域。

next=>指向下一个单词的指针,或者NULL如果这是最后一个单词。

  当然,我们需要一些函数来分配和释放我们的structword_s结构。为此,我们将使用以下功能:

structword_s*make_word(char*str);

voidfree_all_words(structword_s*first);

第一个函数为该结构分配内存,创建单词字符串的副本,然后返回新分配的单词。第二个功能释放单词结构列表使用的内存。您可以在我们的网站中阅读功能代码wordexp.c源文件。

  定义一些助手功能

如前所述,词扩展是一个复杂的过程,为此,我们需要定义许多不同的功能。在深入研究单词扩展的细节之前,让我们先定义一些辅助函数。

以下列表显示了我们的辅助函数的函数原型,所有这些我们将在wordexp.c源文件:

char*wordlist_to_str(structword_s*word);

voiddelete_char_at(char*str,size_tindex);

intis_name(char*str);

size_tfind_closing_quote(char*data);

size_tfind_closing_brace(char*data);

char*substitute_str(char*s1,char*s2,size_tstart,size_tend);

intsubstitute_word(char**pstart,char**p,size_tlen,char*(func)(char*),intadd_quotes);

以下是这些函数的功能细目:

wordlist_to_str()=>将扩展单词的链接列表转换为单个字符串。

delete_char_at()=>从字符串中删除给定索引处的字符。

is_name()=>检查字符串是否表示有效的变量名。

find_closing_quote()=>当单词扩展包含开头的引号",',要么`,我们需要找到匹配的用引号引起来的字符,该字符将引号引起来的字符串括起来。此函数返回单词中结束引号字符的从零开始的索引。

find_closing_brace()=>与上面类似,不同之处在于它找到匹配的右括号。也就是说,如果左括号是{,(,要么[,此函数返回匹配项的从零开始的索引},),要么]字符。查找引号对对于处理参数扩展,算术扩展和命令替换很重要。

substitute_str()=>替换的子字符串s1,从位置上的角色开始start到那个位置end,s2串。当单词扩展是较长单词的一部分例如,${PATH}/ls,在这种情况下,我们只需要扩展${PATH},然后附加/ls到扩展字符串的末尾。

substitute_word()=>一个帮助程序函数,它调用其他单词扩展功能,我们将在以下各节中对其进行定义。

另外,我们将定义一些函数来帮助我们处理字符串。我们将在strings.c源文件:

char*strchr_any(char*string,char*chars);

char*quote_val(char*val,intadd_quotes);

intcheck_buffer_bounds(int*count,int*len,char***buf);

voidfree_buffer(intlen,char**buf);

  这些功能的作用如下:

strchr_any()=>类似于strchr(),除了它会在给定的字符串中搜索任何给定的字符。

quote_val()=>执行与引号删除相反的操作,即将字符串转换为带引号的字符串。

的check_buffer_bounds()和free_buffer()函数将使我们的后端执行器支持可变数量的命令参数,而不是我们在第二部分中设置的硬限制255。

现在,让我们编写函数来处理每种类型的单词扩展。

  波浪号扩展

在波浪符号扩展期间,外壳程序将波浪符号字符替换为用户主目录的路径名。例如,~和~/波浪线扩展到当前用户的主目录,而~john被波浪扩展到用户John的主目录,依此类推。除后面的所有字符之外,代字号字符被称为代字号前缀

要执行波浪线扩展,我们将定义tilde_expand()函数,具有以下原型:

char*tilde_expand(char*s);

该函数接受一个参数:我们要扩展的代字号前缀。如果扩展成功,该函数返回一个的malloc表示波浪线扩展前缀“d字符串。否则返回NULL。下面是该函数为扩展代字号前缀所做的工作的快速分解:

如果前缀是~,获得$HOME外壳变量。如果$HOME被定义而不是NULL,返回其值。否则,通过调用获取当前的用户ID(UID)getuid(),然后将UID传递给getpwuid()获取与当前用户相对应的密码数据库条目。的pw_dir密码数据库条目的“字段”包含函数返回的主目录的路径名。

如果前缀包含其他字符除了前导字符~,我们将这些字母作为要获取其主目录的用户的名称。我们称之为getpwnam(),将其传递给用户名,然后返回pw_dir领域。

如果我们无法检索主目录,则返回NULL。否则,我们将返回主目录路径的malloc副本。

  参数扩展

在参数扩展中,外壳程序用变量的值替换外壳程序变量的名称。参数扩展使外壳程序可以执行诸如echo$PATH。在此示例中,外壳程序对$PATH变量,将其替换为实际的可执行路径。

为了向shell发出我们要扩展shell变量的信号,我们在变量名称前添加一个$标志。也就是说,扩大PATH,USER和SHELL变量,我们需要传递$PATH,$USER和$SHELL将单词分别传递给shell或者,我们可以将这些变量扩展传递给shellshell,如下所示:${PATH},${USER}和${SHELL}。Shell变量名称可以包含字母,数字和下划线的任意组合。名称可以包含大写字母或小写字母,尽管按照惯例,大写名称保留用于标准Shell变量。

我们可以使用参数扩展修饰符来控制外壳如何执行参数扩展,该修饰符告诉外壳我们要扩展值的哪一部分,以及在没有给定名称的外壳变量的情况下该怎么做。下表总结了参数扩展修饰符由POSIX定义的修饰符在“描述”列中由POSIX单词标记。大多数外壳程序都支持其他修饰符,我们将不在此处讨论。有关非POSIX修饰符的更多信息,请参见您的Shell的手册页。

要执行参数扩展,我们将定义var_expand()函数,具有以下原型:

char*var_expand(char*orig_var_name);

该函数接受一个参数:我们要扩展的参数。如果扩展成功,该函数将返回一个包含扩展值的malloc'd字符串。否则返回NULL。下面是该函数为扩展变量名以获取其值而执行的操作的快速细分:

如果变量名用大括号括起来,请删除大括号,因为它们不是变量名本身的一部分。如果名称以#,我们需要获取变量名称的长度。

如果变量名称包含冒号,我们将使用它来将名称与单词或模式分开。单词或图案的使用如上表所示。获取具有给定变量名称的符号表条目。获取符号表条目的值。

如果该值为空或为空,请使用扩展中提供的替代词。

如果该值不为空,则将该值用作扩展结果。要使外壳执行模式匹配${parameter#word}和${parameter%word}扩展,我们需要两个帮助函数:match_suffix()和match_prefix()。我们不会在这里讨论这些功能,但是您可以从此链接中阅读它们的代码。

如果扩展修饰符为${parameter:=word},我们需要将符号表条目的值设置为刚刚扩展的值。

如果扩展以#,获取扩展值的长度并将其用作最终结果。返回扩展值的malloc'd副本或其长度,视情况而定。

  命令替换

在命令替换中,shell派生一个进程来运行命令,然后用命令的输出替换命令替换扩展。例如,在以下循环中:

foriin$(ls);doecho$i;done

外壳分叉一个过程,其中ls命令运行。该命令的输出是当前目录中的文件列表。Shell获取该输出,将其拆分为单词列表,然后一次将这些单词提供给循环。在循环的每次迭代中,变量$i被分配了列表中下一个文件的名称。

此名称将传递给echo命令,该命令在单独的行上输出名称。

命令替换可以写成$(command),要么`command`。要执行命令替换,我们将定义command_substitute()函数,具有以下原型:

char*command_substitute(char*orig_cmd);

该函数接受一个参数:我们要执行的命令。如果扩展成功,则该函数将返回一个malloc'd字符串,表示命令的输出。如果扩展失败,或者命令没有输出任何内容,则函数返回NULL。

下面是该函数为扩展命令替换而执行的操作的快速细分:

根据使用的格式,我们首先删除$()或反引号“。这给我们留下了我们需要执行的命令。

呼叫popen()创建一个管道。我们将要执行的命令传递给popen(),我们得到一个指向FILE流,我们将从中读取命令的输出。

呼叫fread()从管道读取命令的输出。将读取的字符串存储在缓冲区中。

删除所有尾随换行符。

关闭管道,并使用命令输出返回缓冲区。

  算术扩展

使用算术扩展,我们可以让外壳执行不同的算术运算,并将结果用于执行其他命令。尽管POSIX要求外壳程序仅支持带符号的长整数算法,但许多外壳程序都支持浮点算法。

此外,尽管大多数外壳程序都不需要外壳程序来支持任何数学函数。对于简单的shell,我们将仅支持带符号的长整数算法,而没有数学函数支持。

算术扩展写为$((expression))。

要执行扩展,我们将定义arithm_expand()函数,具有以下原型:

char*arithm_expand(char*expr);

的arithm_expand()函数接收包含算术表达式的字符串,执行必要的计算,然后以malloc'd字符串的形式返回结果。该函数及其相关的帮助器函数既复杂又冗长,但这是主要亮点:

算术表达式转换为反向波兰表示法,更易于分析和计算。RPN由一系列算术运算组成,其中运算符遵循其操作数。例如,RPNx-y是xy-,以及3+4×(2−1)是3421−×+。

在转换过程中,算术运算符被压入一个运算符堆栈,我们将从中弹出每个运算符并稍后执行其运算。同样,操作数被添加到它们自己的堆栈中。

一次将操作员从堆栈中弹出,然后对操作员进行检查。根据运算符的类型,一个或两个操作数从堆栈中弹出。控制此过程的规则是调车场算法的规则,您可以在此处阅读。

结果将转换为字符串,然后将其返回给调用方。

  场分裂

在字段拆分期间,shell会获取参数扩展,命令替换和算术扩展的结果,并将它们拆分为一个或多个部分,我们将其称为字段。该过程取决于$IFS外壳变量。IFS是一个历史术语,代表内部字段分隔符,它起源于Unixshell没有内置数组类型的时间。

作为解决方法,早期的Unixshell必须找到另一种表示多成员数组的方式。外壳程序将以单个字符串将数组成员连接在一起,并以空格分隔。当外壳程序需要检索数组成员时,它将字符串分成一个或多个字段。的$IFS变量告诉外壳程序在何处确切地中断该字符串。

  外壳解释$IFS如下字符:

如果值$IFS是空格,制表符和换行符,或者如果未设置变量,则在输入的开头或结尾处的空格,制表符或换行符的任何序列都将被忽略,并且输入中这些字符的任何序列应定界领域。

如果值$IFS为null,不得执行任何字段拆分。

否则,应依次适用以下规则:(a)$IFS在输入的开头和结尾应忽略空格。(b)输入中的每次出现$IFS不是的字符$IFS空白,以及任何相邻的空白$IFS如前所述,空白应界定一个字段。(c)非零长度$IFS空白应界定一个字段。

要执行扩展,我们将定义field_split()函数,具有以下原型:

structword_s*field_split(char*str);

路径名扩展

在扩展路径名期间,shell将以给定的模式匹配一个或多个文件名。除特殊字符外,该模式还可以包含普通字符*,?和[],也称为“通配符”。

星号*匹配任意长度的字符,匹配一个字符,并且方括号引入正则表达式(RE)括号表达式。扩展的结果是名称与模式匹配的文件列表。

要执行扩展,我们将定义pathnames_expand()函数,具有以下原型:

structword_s*pathnames_expand(structword_s*words);

此函数接受一个参数:指向我们要路径名扩展的单词的链接列表中第一个单词的指针。对于每个单词,该函数执行以下操作:

检查单词是否包含任何通配符*,?和[],通过调用辅助函数has_glob_chars(),我们将在源文件中定义pattern.c。如果单词包含通配符,我们将其视为需要匹配的模式;否则,我们移至下一个单词。

获取名称与模式匹配的文件列表,不包括特殊名称.和..。我们将模式匹配委托给另一个帮助函数,get_filename_matches(),我们将在同一源文件中定义pattern.c。

将匹配的文件名添加到最终列表。

移至下一个单词并循环。

返回与所有给定单词匹配的文件名列表。

  删除报价

单词扩展过程的最后一步是删除引号。引用用于删除某些字符到shell的特殊含义。外壳会以特殊方式处理某些字符,例如反斜杠和引号。要禁止这种行为,我们需要引用那些字符以强制外壳将它们视为普通字符。

我们可以使用以下三种方式之一对字符进行引用:使用反斜杠,单引号或双引号。反斜杠字符用于保留反斜杠后面的字符的字面意思。这类似于我们用C语言转义字符的方式。

单引号保留引号内所有字符的字面含义,即外壳程序不尝试对单引号字符串进行单词扩展。

双引号与单引号类似,不同之处在于外壳可以识别反引号,反斜杠和$标志。也就是说,外壳程序可以在双引号字符串内执行单词扩展。

要执行报价删除,我们将定义remove_quotes()函数,具有以下原型:

voidremove_quotes(structword_s*wordlist)。

  放在一起

现在我们有了词扩展功能,是时候将其结合在一起了。在本节中,我们将编写主要的单词扩展功能,我们将调用该功能来执行单词扩展。反过来,此函数将调用其他函数来执行单词扩展的各个步骤。

我们的主要功能是word_expand(),我们将在源文件中定义wordexp.c:

structword_s*word_expand(char*orig_word);

这是为了对作为唯一参数传递的单词执行单词扩展的功能:

创建原始单词的副本。我们将在此副本上执行单词扩展,以便在出现任何问题时将原始单词保留完整。逐字扫描单词,寻找特殊字符~,",',`,=,和$。如果找到上述字符之一,请致电substitute_word(),这将调用相应的单词扩展功能。

跳过任何没有特殊含义的字符。完成单词扩展后,通过调用执行字段拆分field_split()。通过调用执行路径名扩展pathnames_expand()。通过调用执行报价删除remove_quotes()。返回扩展单词的列表。

更新扫描仪

在本教程的第二部分中,我们编写了tokenize()函数,我们用来获取输入令牌。到目前为止,我们的tokenize()函数不知道如何处理带引号的字符串和转义字符。要添加此功能,我们需要更新代码。打开scanner.c文件,并将以下代码添加到tokenize()功能之后switch声明的开头括号:

case'"':

case''':

case'`':

add_to_buf(nc);

i=find_closing_quote(src->buffer+src->curpos);

if(!i)

{

src->curpos=src->bufsize;

fprintf(stderr,"error:missingclosingquote'%c'
",nc);

return&eof_token;

}

while(i–)

{

add_to_buf(next_char(src));

}

break;

case'\':

nc2=next_char(src);

if(nc2=='
')

{

break;

}

add_to_buf(nc);

if(nc2>0)

{

add_to_buf(nc2);

}

break;

case'$':

add_to_buf(nc);

nc=peek_char(src);

if(nc=='{'||nc=='(')

{

i=find_closing_brace(src->buffer+src->curpos+1);

if(!i)

{

src->curpos=src->bufsize;

fprintf(stderr,"error:missingclosingbrace'%c'
",nc);

return&eof_token;

}

while(i–)

{

add_to_buf(next_char(src));

}

}

elseif(isalnum(nc)||nc=='*'||nc=='@'||nc=='#'||

nc=='!'||nc=='?'||nc=='$')

{

add_to_buf(next_char(src));

}

break;

现在,我们的词法扫描器知道如何识别和跳过带引号的字符串,转义字符和其他单词扩展构造。在此链接中查看更新的词法扫描程序代码。

  更新执行器最后,我们需要更新后端执行程序,以便可以:

在执行命令之前,对命令的参数执行单词扩展。每个命令支持超过255个参数。打开executor.c文件,导航到do_simple_command()函数并找到以下几行:

intargc=0;

longmax_args=255;

char*argv[max_args+1];

char*str;

while(child)

{

}

argv[argc]=NULL;

并用以下代码替换它们:

intargc=0;

inttargc=0;

char**argv=NULL;

char*str;

while(child)

{

str=child->val.str;

structword_s*w=word_expand(str);

if(!w)

{

child=child->next_sibling;

continue;

}

structword_s*w2=w;

while(w2)

{

if(check_buffer_bounds(&argc,&targc,&argv))

{

str=malloc(strlen(w2->data)+1);

if(str)

{

strcpy(str,w2->data);

argv[argc++]=str;

}

}

w2=w2->next;

}

free_all_words(w);

child=child->next_sibling;

}

if(check_buffer_bounds(&argc,&targc,&argv))

{

argv[argc]=NULL;

}

使用此代码,执行程序调用word_expand()在每个命令自变量上,并将扩展的单词添加到自变量列表,我们最终将其传递给命令。该列表可以根据需要增长,这要归功于我们check_buffer_bounds()函数,根据需要将内存分配给缓冲区。

现在剩下的就是在执行完命令后释放参数列表,然后返回调用者。为此我们致电:

free_buffer(argc,argv);

在三个不同的位置:执行内置实用程序后,如果fork()返回错误状态,然后waitpid()已经回来了。在此链接中查看更新的执行程序代码。

  编译外壳

让我们编译一下shell。打开您喜欢的终端模拟器,导航到源目录,并确保其中有19个文件和2个子目录:

现在,使用以下命令编译外壳程序:

  make

如果一切顺利gcc不应输出任何内容,并且应该有一个名为shell在当前目录中:

现在通过运行来调用shell./shell,然后尝试使用我们的单词扩展功能并检查结果:$echo*Makefilebuildbuiltinsexecutor.cexecutor.hinitsh.cmain.cnode.cnode.hparser.cparser.hpattern.cprompt.cscanner.cscanner.hshellshell.hshunt.csource.csource.hstrings.csymtabwordexp.c

$echo'*'

*

$echo~

/home/user

$echo~/Downloads

/home/user/Downloads

$echo${A=value}

value

$echo$A

value

$echo$((2+7))

9

就是今天。我们的外壳现在可以处理各种单词扩展。玩弄外壳,看看从不同类型的单词扩展中可以得到什么结果。将结果与从默认Shell获得的结果进行比较。

在此,我们已经取得了长足的进步,提供了许多代码,其中大多数代码没有时间或空间来详细研究。您可能需要花一些时间阅读我们存储库中的代码,以使自己熟悉单词扩展过程。想了解更多关于Linux
Shell 的信息,请继续关注中培教育。