Shell 函数和路径变量,第 2 部分
在我的上一篇文章中,我描述了一个处理命令行选项的 shell 函数。在第 2 部分中,我们将把它用于我承诺描述的路径变量函数中。
每个路径变量 shell 函数的结构都相似。首先,声明局部变量。接下来是选项处理代码,它使用了我们上个月熟悉的 options 函数。最后,实现了代码的主要功能。由于每个函数都具有相同的结构,因此本月我将只详细描述一个函数。下一期将描述其余函数的各种实现特性。
我们必须首先理解进程的“环境”是什么。我们操作的路径变量通常是进程环境中的变量,我们需要我们的函数来更改它们的值(例如,添加或删除目录)。
简而言之,进程的环境是一组命名的变量(类似于 shell 变量),它们被传递给任何创建的子进程。(进程当然是运行程序的实体。例如,如果您在 shell 中键入 ls,这将创建一个进程来运行 ls 程序。)shell 变量可以通过“导出”将其放入环境中。例如,命令
A=fred export $A
创建一个名为 A 的 shell 变量,并将其转换为环境变量。因此,如果您从此 shell 启动一个新进程,它可以检查其环境,找到 A 变量并注意到它的值为 fred。因此,环境变量提供了一个单向信息通道——从父进程到子进程。父进程和子进程不共享环境变量——子进程获得它们的新副本。因此,如果子进程更改了环境变量的值,父进程不会意识到该更改。
现在,我们 希望 修改路径环境变量,以便在运行 shell 实用程序时无法启动新进程。我们的实用程序以函数的形式实现,因为函数在调用进程的上下文中运行。虽然它们没有获得环境变量的副本,但它们可以访问现有的集合。
addpath 的目的是以等幂的方式将 pathel(路径元素)添加到 pathvar(路径变量)中。等幂字面意思是“相等的幂”,比喻意思是“做它 N 次与做一次相同”。因此,例如,
NEWP= addpath -p NEWP /abc addpath -p NEWP /abc
将 /abc 精确地添加到 pathvar NEWP 一次。addpath 检查 pathvar 以查看 pathel 是否已存在。如果不存在,则添加它;如果存在,则不添加。
此函数很有用,因为如果您使用它添加到您的 PATH,例如,您不会在路径中最终得到同一目录的多个副本。执行此操作的代码被拆分为不同的列表显示,以便于讨论。
在列表 1 中,我们创建了一些函数本地的变量。前三行中的 set 用于选项处理;最后 4 行中的 set 包含特定于此函数的变量。选项处理变量在每个使用 options 函数的函数中往往非常相似。
在列表 2 中,我们处理提供给函数的选项。我们通过调用上个月描述的 options 函数来做到这一点。我们告诉它我们准备处理的选项的名称,并给它一个带引号的提供的参数列表。当 options 返回时,它将以它创建的变量的形式向我们提供有关提供的参数的信息。在 addpath 中,我们准备处理 -h、-f、-b 和 -p(-p 选项。-p 选项需要一个参数,它是路径变量的名称,例如 PATH。当 options 返回时,它还会创建一个名为 options_shift_val 的变量。我们可以使用它来移去它已经处理的命令行参数(即,像 -h、-b 等参数)。我们在调用 options 后立即执行此操作。因此,如果用户仅指定了 -h,则 options_shift_val 将设置为 1,我们将移去一个参数;如果指定了 -b 和 -p,我们将移去三个参数(-b、-p 及其所需的路径变量)。
接下来的四个“if”块出现在每个路径变量函数中,因为它们执行以下常见测试
如果 options 创建了一个名为 opt_h 的变量,则提供了 -h 参数,并且用户想要一些帮助。我们通过打印出函数的使用信息并调用 return 来提供此帮助。当函数调用返回时,它会终止,就像 C 中的函数调用返回一样。不要犯在函数中调用 exit 的错误——这将终止调用该函数的 shell 进程,这可能不是您想要做的。
我们检查 options_missing_arg 变量,如果您没有为选项提供必需的参数,options 会创建该变量。如果发生这种情况,我们打印出使用消息,告诉用户出了什么问题并返回。
我们检查 options_unknown_option。options 当您提供我们不准备处理的参数时(即,在本例中不是 -h、-f、-b 或 -p 的参数),将设置此变量。我们在向用户提供一些帮助后返回。
最后,我们查看 options_num_args_left。这为我们提供了在移去已处理的参数后剩余的参数数量的计数。此处的代码将特定于每个函数,但对于 addpath,我们需要用户指定要追加的目录的名称。我们通过在此点没有剩余参数时抱怨来(马虎地)检查这一点。
到目前为止,我们已经执行了选项处理类型,这种类型在接受参数的 shell 脚本和函数中反复出现。我们已经检查了是否提供了任何我们不知道的选项,以及是否提供了必需的参数。我们还通过 -h 处理提供了一个基本的帮助工具。
列表 3 显示了特定于 addpath 函数的选项处理代码。本节中的前两行设置了稍后将使用的变量 COMMAND 和 pathvar 的值。这些行乍一看可能有点神秘,但本质上它们设置了我们将要添加到的默认路径变量 (PATH) 和我们执行以添加到它的默认命令。请注意,COMMAND 的内容周围有单引号。shell 将把引号之间的文字字符串存储到 COMMAND 中,并且不会尝试变量替换。
如果我们键入 addpath /def,则 pathvar 将包含 PATH,sep 将包含 :,而 dirname 将包含 /def。稍后,当我们评估包含 COMMAND 的代码行时,shell 将使用它们的值替换文字字符串 ${pathvar}、${sep} 和 ${dirname},以生成“${PATH}:/def”。请注意,我们在 COMMAND 中使用了三个单独的字符:$、{ 和 }。这确保 shell 按字面意思解释它们,并且它们出现在评估的输出中。
如果给出了 -f 或 -p 选项,则接下来的两行代码会覆盖默认值。如果给出了 -f,则用户希望添加到路径变量的前面,我们相应地设置 COMMAND,将路径变量放在末尾。如果给出了 -p,则用户提供了路径变量的名称;options 函数将其存储在 opt_p 中,我们将 pathvar 变量设置为此值。因此,如果用户键入
addpath -f -p NEWP /def
那么 opt_p 以及因此的 pathvar 将包含 NEWP,并且 COMMAND 将看起来像 $dirname$sep\$$pathvar,其中 dirname 变量位于前面。
接下来,我们设置 sep 变量的值。通常,我们希望它包含 :,因为该字符分隔路径元素。但是,如果要添加到的路径变量最初为空,我们不希望 sep 中有任何内容。这确保我们不会向路径添加前导或尾随 : 字符。以 sep=: 开头的三行代码实现了这一点。
最后,我们将要添加的路径元素的名称存储在 dirname 中。请注意,此时,它将在 $1 中;在 options 返回后立即移去了任何其他命令行参数。
在列表 4 中,我们实际上将路径元素添加到路径变量中。首先要注意的是,函数的真正工作需要五行代码。我们已经执行了所有必需的设置和参数检查,因此几乎没有什么可做的了。本质上,我们检查路径元素是否已存在于 pathvar 中;如果不存在,则添加它。
element=$(eval echo \$$pathvar | colon2line | grep -x "$dirname")
首先,请注意 variable=$(...) 语法。这等效于较旧的 variable="..." 语法,意思是“运行括号内的命令,并将它们写入标准输出的内容用作变量的值。” 这称为命令替换。在我们的示例中,我们有一系列命令管道;最终命令 (grep) 的输出成为 element 的值。让我们看一下管道中的每个命令。请记住,$pathvar 包含我们要添加到的路径变量的名称,而 $dirname 包含要添加的目录的名称。
假设 $pathvar 的值为 PATH,则命令行首先被 shell 扩展为 eval echo pathvar ,即 eval echo $PATH。接下来,由于 eval,shell 重新评估该行。这次,它将 $PATH 扩展为类似“/usr/bin:/bin:/usr/bin/X11”的内容,并将其内容回显到管道中的下一个命令。
colon2line 是另一个 shell 函数,其代码我们尚未见过。它只是在单独的行上打印以冒号分隔的字符串的每个元素(即,它将 : 字符转换为换行符)。这可以通过多种方式完成。例如,这是一个用于此目的的 awk 单行命令
awk `BEGIN{RS=":"}{print}'
此命令告诉 awk 假设 : 分隔一段文本中的记录,然后打印它看到的每个以冒号分隔的记录。然后,管道中的第三个命令读取每个单独的行。
命令 grep -x "$dirname" 用于检查我们要添加的路径元素是否存在。shell 将在执行 grep 之前将 $dirname 替换为其值。它被引号包围,以便我们可以正确处理 $dirname 包含空格的病态情况。
我们告诉 grep 我们只需要完全匹配 (-x)。这确保了如果我们运行以下命令
addpath /abc addpath /ab
我们添加了两个不同的路径元素。如果没有 -x,grep 会报告它在运行第二个命令时看到了匹配项,因为“/ab”是“/abc”的子字符串。
如果 grep 在其输入中看到 $dirname,它会将名称写入标准输出。由于我们在命令替换括号中使用 grep,因此其输出被赋值为 element 变量的值。
简而言之,管道确保如果元素已存在于路径变量中,则该元素具有非空值,否则具有空值。如果元素为空 ([ "$element" = "" ]),我们使用以下命令添加它
eval eval $pathvar=$COMMAND
到现在为止,您应该熟悉 eval 命令的用途:它告诉 shell 重新评估正在处理的行,并且在每次评估时,都会扩展 shell 变量。这里唯一的问题是,我们为什么需要两个 eval?假设用户键入以下内容
addpath /abc进一步假设 PATH 仅包含 /usr/bin,并且 /abc 尚未存在于 $PATH 中。让我们逐步查看此行是如何扩展的。
$pathvar=$COMMAND: 最初
PATH=\$\{${pathvar}\}${sep}${dirname}: 在第一次 shell 扩展后
PATH=${PATH}:/abc: 在第一次 eval 后
PATH=/usr/bin:/abc: 在第二次 eval 后
这结束了对 addpath 函数的描述。其他路径变量函数的结构与 addpath 的结构非常相似,因此在第 3 部分中,我将仅描述关于它们的每个函数的一两个有趣的点。
Stephen Collyer (stephen@twocats.demon.co.uk) 是一位在英国工作的自由软件开发人员。他的兴趣包括脚本语言以及分布式和基于线程的系统。偶尔,他会抽出时间与他的妻子和两个非常迷人且非常聪明的孩子交谈。