Shell 函数和路径变量,第 3 部分
在本系列的最后一篇文章中,我将描述剩余的路径处理函数,并指出一些实现问题。但在我这样做之前,我将描述一个名为 makepath 的实用程序。它读取标准输入或其参数列表,从读取的行构建一个冒号分隔的路径变量 (pathvar),并将其回显到标准输出。例如
$ makepath /bin /usr/bin /opt/kde/bin /bin:/usr/bin:/opt/kde/binmakepath 在几个 pathvar 实用程序中用于在路径元素 (pathels) 被更改后重建 pathvar。我不会向您展示 makepath 的内部结构,因为它们与主题有些不相干且相当琐碎。
首先,让我们看看 listpath,它在单独的行上回显组成 pathvar 的 pathels,如下所示
$ listpath -p MANPATH /usr/man /usr/local/man /opt/CC/man
与仅仅回显 $MANPATH 相比,使用 listpath 有两个优点。首先,当 pathels 出现在单独的行上时,更容易阅读;其次,您可以将其输出通过管道传递给 grep
$ listpath | grep bin /opt/kde/bin /usr/local/bin /bin在 addpath 函数中,我们没有看到任何选项处理代码,所以让我们看看主要代码
eval echo这非常简单。我们只是将指定的 pathvar 的内容回显到 colon2line 函数(包含在本文章末尾提到的 tar 文件中),该函数将嵌入的 : 字符转换为换行符。我在第 2 部分中详细描述了这段代码的操作,所以在这里我不再重复。如果您不确定 eval 在这里的原因,请查看该文章 (https://linuxjournal.cn/lj.issues/issue72/3768.html)。
我们已经看到了 addpath 函数,它执行将 pathel 以幂等方式添加到 pathvar 的操作。 delpath 提供了与此行为相反的操作,它从 pathvar 中删除 pathels。因此,例如
delpath /opt/CC/test/bin
将从 $PATH 中删除 /opt/CC/test/bin 目录,并且
delpath -e "(bill|steve)" -p MANPATH将从 $MANPATH 中删除与 egrep 风格的正则表达式 "(bill|steve)" 匹配的所有 pathels。命令
delpath -n从 $PATH 中删除所有不存在的目录。
尽管 delpath 不是您可能经常需要使用的函数,但在一个地方它可能很有用。许多 UNIX 机器都有一个名为 /etc/PATH 的文件,该文件由 /etc/profile 引用。它设置了一个默认的 PATH,其中包含所有用户都需要的目录。但是,通常 /etc/PATH 多年未修改,并且添加的目录要么不再存在,要么并非所有人都真正需要。在这种情况下,您可以在适当的登录脚本(.profile 或 .bash_profile)的开头调用 delpath,以删除您不需要的目录。
让我们看看 delpath 代码。我将跳过大部分选项处理,因为其中大部分与 addpath 中的相同。
MATCH="-x" # default [ -n "$opt_e" ] && MATCH= # make grep use regexps FILTER= # default [ -n "$opt_n" ] && FILTER="| realpath_filter"
在这里,我们看到了选项处理的最后一部分。 MATCH 变量决定了我们是否将提供的路径描述作为正则表达式处理。它稍后用作 grep 的一个选项; grep -x 告诉 grep 执行精确字符串匹配。
FILTER 变量实现了 -n 选项,即“删除不存在的目录”行为。如果用户提供了 -n 选项,则 FILTER 包含一个字符串,该字符串通过管道将先前命令的输出传递给一个名为 realpath_filter 的程序。该程序从其标准输入读取目录名称,并且仅当它是现有目录时才将名称写入标准输出。我将其作为一个简单的练习留给读者来实现这样的过滤器。
delpath 的其余部分如下
eval listpath -p $pathvar $FILTER | grep -v -E $MATCH "$1"> /tmp/makepath_in.$$ eval $pathvar=$(makepath < /tmp/makepath_in.$$) rm /tmp/makepath_in.$$
该函数分三个阶段完成其工作。第一个命令在 /tmp 中生成一个文件,其中包含那些不被删除的目录。第二个命令使用 makepath 从该文件重建 pathvar。最后我们删除该文件;我们不希望它在函数完成后弄乱文件系统。(shell 将 $$ 扩展为运行命令的 shell 的进程 ID;在本文章中,我假设它是 20610。)
让我们看看第一行。本质上,它使用 listpath 将适当的 pathvar 分解为单独的行,并使用 grep 删除我们不想要的行。但是,由于 FILTER 变量的存在,它稍微复杂一些。假设用户输入
delpath -e "^opt"
这意味着“从 $PATH 中删除所有以 opt 字符串开头的目录”。在这种情况下,pathvar 将包含 PATH,而 MATCH 和 FILTER 将为空。因此,第一行将展开为
eval listpath -p PATH | grep -v -E "^opt<" > /tmp/makepath_in.20610这很简单——listpath 将 PATH 中的 pathels 写入 grep 命令,我们使用该命令仅回显非匹配行 (-v)。我们将输出重定向到我们的临时文件,该文件将包含那些不以 opt 开头的 pathels。在这种情况下,前导 eval 是不必要的。但是,如果用户输入
delpath -n从 $PATH 中删除所有不存在的目录,则第一行展开为
eval listpath -p PATH | realpath_filter | grep -v -E "" > /tmp/makepath_in.20610在该行的初始处理期间(即,在 eval 强制重新评估之前),shell 看到了 grep 前面的管道符号,但它没有看到 realpath_filter 前面的管道符号。就目前情况而言,shell 将第一个 | 视为文字字符,并将其作为参数传递给 listpath。发生这种情况是因为 shell 在扩展变量之前查找 | 字符,并且 realpath_filter 前面的 | 字符存储在变量中。由 eval 引起的第二次评估确保了运行 realpath_filter 命令的管道被构建。
现在我们有一个文件,其中仅包含所需的 pathels。 delpath 中的第二行使用以下代码从该文件重建 pathvar
eval $pathvar=$(makepath < /tmp/makepath_in.$$)
这不应该给我们带来太多问题。首先,makepath 只是读取文件中的行,构建一个冒号分隔的 pathvar 并回显它。我们在命令替换模式下运行 makepath(这是我在第 2 部分中描述的 $(...) ),因此 makepath 的输出用作变量赋值的右侧。由于 shell 评估命令的顺序,因此需要初始的 eval。因为它在扩展变量之前查找赋值语句,所以它不会识别出该命令包含有效的赋值。 eval 确保在第二次处理该行时进行赋值。
假设您登录到您的 UNIX 系统并发现,由于您无法控制的原因,PATH 充满了重复条目。(请幽默一下。这种情况确实会发生。也许您的系统管理员不明智地修改了 /etc/PATH)。假设这些重复项使您的 PATH 变得不必要地长。您有什么可以清理的吗?是的,您可以在提示符下键入
$ uniqpath
这将从您的路径中删除任何重复条目,并保持剩余 pathels 的顺序不变。例如
$ NEWP=fred:bill:steve:fred:dave:bill $ uniqpath -p NEWP $ echo $NEWP fred:bill:steve:dave让我们再次跳过选项处理代码,看看核心内容
npath=$(listpath -p $pathvar | awk '{seen[$0]++; if (seen[$0]==1){print}}') eval $pathvar=$(makepath "$npath")像往常一样, $pathvar 包含我们要修改的 pathvar 的名称。该代码与 delpath 的代码非常相似。第一行生成一个变量 (npath),其中包含唯一的路径元素,第二行使用 makepath 从这些元素重建 pathvar。我们不使用外部文件来存储 pathels,而是将所有内容都保留在 shell 变量中。这样做是为了演示一种替代技术——没有更深层次的原因。
第一行运行 listpath 以将 pathvar 分解为单独的行,并通过管道将其传递给一个 awk 过滤器,该过滤器删除重复的 pathels。您可能想知道为什么我们不直接使用 uniq 程序而不是 awk 的魔力。这是因为 uniq 仅当输入中的重复行恰好相邻时才会删除它们。在我们的例子中,重复的 pathels 通常不会相邻,因此 uniq 将不起作用。“啊哈,”您说,“为什么不使用 sort -u?这将对行进行排序并删除重复项。”确实如此,但是,如果我们运行 uniqpath 来更改 PATH,它也可能会修改目录搜索顺序。通常,人们关心其 PATH 目录的搜索顺序,并且修改它是不可取的。
因此,我们有了 awk 解决方案。这使用了一个强大的 awk 功能,称为关联数组或哈希(如果您有 Perl 背景)。如果您是 C 程序员,您就会知道什么是数组:一组相同类型的对象,由整数索引。可以使用诸如 values[0] 或 values[20] 之类的表达式访问数组的内容,它们分别引用第一个和第二十一个元素。哈希有点像一个数组,可以用任意字符串索引。因此,在 awk 表示法中,我们可以写
age["bill"]=27
将 27 分配给名为 age 的哈希中由字符串 bill 索引的哈希元素。让我们看看上面显示的 awk 代码。
在单引号之间,我们有一个代码块,每次 awk 从其标准输入读取新行时都会运行。当 awk 读取一行时,它存储在一个名为 $0 的特殊变量中,我们使用 $0 作为索引放入一个名为 seen 的哈希中。(我们没有在任何地方声明它——这在 awk 中是可以的。变量在代码中出现时会以数值 0 形式出现)。我们使用 seen 哈希来告诉我们 awk 自开始执行以来是否已经看到相同的输入行。让我们看看上面显示的 NEWP 示例中发生了什么。
首先,listpath 将 NEWP 分解为包含以下字符串的行:“fred”、“bill”、“steve”、“fred”、“dave” 和 “bill”,awk 按此顺序读取它们。 awk 将其读取的每一行存储在 $0 中,因此 $0 依次取值 “fred”、“bill” 等。每次读取一行时,都会递增 seen 哈希的相应元素(通过行 seen[$0]++),并且仅当它被看到恰好一次时才打印(通过 if 块中的 print 语句,默认情况下,该语句将 $0 打印到标准输出)。如果我们查看哈希元素 seen["fred"],则最初为 0,然后在 awk 读取第一个 “fred” 行时设置为 1,对于接下来的两行保持为 1,并在 awk 读取第二个 “fred” 行时设置为 2。它仅在第一次看到时打印。 C 程序员应该注意此解决方案在语法上是多么优雅,以及与 C 中的等效解决方案相比,所需的代码量有多么少。
我们要看到的最后一个 pathvar 函数是 edpath。这会将 pathvar 中的 pathels 分解为单独的行,将它们写入临时文件,并在该文件上运行编辑器。您可以随意编辑 pathels,并在完成后退出编辑器。然后,pathvar 将从文件中修改后的行重建。 edpath 允许您对 pathvar 执行任意修改。当我希望交换 PATH 中目录的顺序时,我最常使用它。
edpath 的代码相当简单(再次忽略选项处理的无聊细节)
TEMP=/tmp/edpath.out.$$ VAR=\$$pathvar # VAR="$LIBPATH" for example eval export OLD$pathvar=$VAR # store old path in # e.g. OLDPATH listpath -p $pathvar > $TEMP # write path # elements to file ${EDITOR:-vi} $TEMP # edit the file eval $pathvar=$(makepath < $TEMP) # reconstruct path /bin/rm -f $TEMP # remove temporary file
现在让我们跳过前三行。真正的工作是由以 listpath 开头的代码块完成的。这遵循与 delpath 和 uniqpath 类似的模式。首先,我们使用 listpath 分隔 pathvar 中的 pathels,但是这次,我们将输出重定向到临时文件。下一行编辑该文件。表达式 ${EDITOR:-vi} 可能不熟悉;它的意思是“如果 EDITOR 变量为非空,则使用其值,否则使用 vi。”这允许用户通过设置 EDITOR 环境变量(可能为 Emacs)来指定他最喜欢的编辑器,但如果他没有这样做,则使用 vi。请注意, edit 命令在前台运行,因此 shell 将等待编辑器进程终止,然后才运行 shell 函数中的任何其他命令。发生这种情况时,修改后的 pathvar 将由以 eval 开头的行重建。如果您阅读了上面给出的 delpath 的描述,您就会知道这行代码是如何工作的。
代码的第 2 行和第 3 行是一个安全网。它们将要编辑的 pathvar 的初始值存储在一个新的环境变量中。例如,如果用户正在编辑 PATH,则代码会创建一个名为 OLDPATH 的变量。如果用户对她的 PATH 进行了不需要的修改,她只需键入
$ PATH=$OLDPATH
一切都会好起来的。
UNIX 可以展示令人眼花缭乱的工具和技术,并且任何个人几乎不可能完全熟悉所有这些工具和技术。以我的经验,最好的开发人员随身携带一大堆简单但有用的技术,并且能够快速将它们组合成一个可行的解决方案。您不需要了解每个工具的每个细节即可完成有用的工作,但您确实需要一个您理解的技巧包。
请随时使用我在本系列中描述的任何想法。您可以从 www.netspinner.co.uk/Downloads/pathfunc.tgz 获取 shell 函数的源代码。如果您发现任何错误、想要添加新功能或进行改进,请告诉我。
Stephen Collyer (stephen@twocats.demon.co.uk) 是一名在英国工作的自由软件开发人员。他的兴趣包括脚本语言以及分布式和基于线程的系统。偶尔,他会抽出时间与他的妻子和两个非常有魅力和高度聪明的孩子交谈。