Shell 函数和路径变量,第 1 部分
很少有 UNIX 用户过多考虑他们的路径变量。它们通常以“设置后忘记”的方式使用,因此,它们最终往往像杂乱无章的野草一样,杂草丛生,毫无美感。看看这个烂摊子
$ echo $PATH /opt/kde/bin:/localbin:/usr/local/bin:/bin:/usr/bin: /usr/X11R6/bin:/home/stephen /scripts:/home/stephen/bin:/opt/CC/test/bin:/usr/sbin: /usr/bin/X11:/ora01/app /oracle/product/7.3.2/bin:/scripts:/opt/CC/bin:/bin:/usr/bin鉴于这种没有区分的字符流,您需要多长时间才能
列出 PATH 中的所有 bin 目录?(grep 没有帮助——试试看)
交换 /bin 和 /usr/bin 的顺序?
删除那个讨厌的 /opt/CC/test/bin 目录?
去掉重复的目录?
路径变量是由冒号分隔的文本元素组成的任何 shell 或环境变量。您几乎肯定熟悉所谓的搜索路径 PATH,您的 shell 使用它来查找可执行文件,但还有其他标准路径,例如 MANPATH,man 程序使用它来定位 man 页面,以及 LD_LIBRARY_PATH,动态加载器可以使用它来查找共享库。
路径变量由冒号分隔的文本元素组成,我为这些元素使用的(公认的非标准)术语是“路径元素”或简称为“pathel”。(您还会看到术语“路径前缀”被使用,但不是我。)我还将“路径变量”缩写为“pathvar”。
我在此处描述的所有实用程序都假定使用 bash shell(尽管也有 Korn shell 版本),并且它们已经使用 bash 1.14.7 和 bash 2.03.4 进行了测试。
我假设您知道如何在 shell 中设置和访问变量,并且使用过(或见过)shell 控制结构(if、for 和 while)。我还假设您不一定清楚 shell 变量与环境变量或 shell 脚本与 shell 函数之间的区别,特别是,您不知道 eval 是做什么的。
以下是一些路径变量实用程序的简要说明
addpath:仅当在 pathvar 上找不到 pathel 时,才将 pathel 添加到 pathvar(例如,addpath -p NEWP/abc/)。
delpath:从 pathvar 中删除 pathel(例如,delpath -p NEWP /abc/)。
edpath:允许编辑,从而允许对 pathvar 进行任意修改。
listpath:在单独的行上回显 pathvar 的 pathel;然后可以使用 grep 等过滤输出。
uniqpath:从 pathvar 中删除重复的 pathel。
一个好的 shell 实用程序应该为用户提供一些指导,因此,每个 pathvar 实用程序都有一个 -h 选项,该选项将用法信息写入标准输出。此外,一个好的实用程序不应该是脆弱的;它应该尽可能检查其参数的合理性。当重要的变量(如 PATH)被更改时,这一点尤为重要。路径实用程序共享通用的选项处理代码,以简化这种合理性检查。
传统上,shell 脚本以某种临时方式处理它们的选项。脚本中的选项处理代码通常包含围绕 getopts 的手工循环(我稍后会描述);此循环设置变量并根据请求的选项发出错误语句。虽然这种方法很常见,但它需要在编写的每个脚本中复制代码。这是乏味且容易出错的。
选项处理代码通常执行一小组功能(即,设置变量和发出消息),因此我们可以有效地编写 shell 函数来标准化此行为。看看列表 1,一个名为 testoptions 的 shell 脚本。
要运行此脚本,我们可以使该文件可由所有者执行 (chmod u+x testoptions) 并键入其名称。如果您这样做,您应该会看到类似这样的内容
$ testoptions ./testoptions: options: command not found
发生这种情况是因为脚本的第 3 行引用了 options,这是一个 shell 函数,我们尚未告知 shell。当我们这样做时,我们可以再次运行 testoptions,这次带有一些参数
$ testoptions -a -b fred -d opt_a=1 opt_b=fred opt_c= options_missing_arg= options_unknown_option=d options_num_args_left=0现在,shell 函数 options 已经查看了它的第一个参数 (“ab:c”),这是一个编码规范,说明了预期选项的名称和类型。它使用它来解释其剩余的参数,在本例中,这些参数是最初传递给 testoptions 的所有参数(即,-a -b、fred 和 -d,因为 $@ 被转换为脚本的所有参数的带引号列表)。
参数规范 (ab:c) 采用 getopts 命令期望的形式,意思是“我们接受三个选项,-a、-b 和 -c,并且 -b 需要一个参数”。需要参数的事实由冒号表示。
每次 options 函数在其参数列表中看到允许的选项之一时,它都会创建一个新的 shell 变量,指示该参数存在。因此,例如,当检查第二个参数 (-a) 时,options 会创建一个名为 opt_a 的变量并将其值设置为 1。同样,如果传递了非法选项,options 会创建一个名为 options_unknown_option 的变量,并将其值设置为非法选项的名称。正如您从上面显示的输出中看到的那样,如果某个选项需要参数,则提供的参数将用作新变量的值。(Perl 脚本编写者将从 Getopts 模块中识别出此行为,实际上,Getopts 模块是 options 的灵感来源。)
根本问题是 options 无法提前知道它将不得不创建哪些变量名,因此它们不能简单地以某种方式硬编码(至少效率不高)。列表 2 是 options 的代码。前几行通知 shell 接下来是一个 shell 函数。shell 函数是文件中命令的集合,可以通过键入该名称来运行(即,在 shell 中键入 options 会运行该脚本中的命令),并且在调用该函数的 shell 的上下文中运行。最后一部分很重要;也就是说,当 shell 运行函数时,其命令在该 shell 中生效,方式与在交互式 shell 的命令行中键入的命令相同。您应该将此与在 shell 脚本中执行的命令的效果进行比较,在 shell 脚本中,新的 shell 被创建来运行命令。例如,如果您在 shell 函数中执行命令 cd,则当前目录将被更改;在 shell 脚本中,cd 仅在新 shell 中生效,该 shell 由运行脚本创建。当脚本完成时,您将与运行脚本之前位于相同的目录中。shell 函数还具有编号的参数(即,$1、$2 等),就像脚本一样。
options 的下一部分执行一些初始化。前六个可执行行声明变量。由于函数中的代码执行方式就像在调用 shell 中运行一样,因此如果我们在函数中创建变量,它将在函数结束时存在于 shell 中。如果我们不希望出现这种情况,我们可以通过在变量前面加上保留字 typeset 使变量成为函数的本地变量。(在 bash 中,您可以使用 local 代替,但 typeset 也适用于 ksh。)因此,变量 opts 在 options 结束时将不存在,但 options_shift_val 将存在,例如。
在检查参数数量后,我们将 opts 设置为参数 spec 的值,并带有附加的前导冒号。因此,使用我们的 testoptions 值,opts 将包含 :ab:c。前导冒号可防止 getopts 发出虚假的错误消息。然后,第一个参数通过 shift 命令移开。这意味着原本为 $2 的参数变为 $1,$3 变为 $2,依此类推。这是 shell 脚本编写中的常用技巧,当不再需要参数时使用。
函数的主体从行 OPTERR=0 开始。此代码部分执行检查选项和创建变量的工作。我们将选项检查委托给 getopts,并使用 eval 创建变量。
shell 命令 getopts 检查位置参数($1、$2 等)。当您第一次调用它时,它会检查 $1;下次检查 $2,依此类推。当在 while 循环中调用(如列表 2 中所示)时,它将查看所有位置参数,并在完成时返回 false,从而终止循环。请记住,options 期望其第一个参数是 getopts 规范,其余参数是位置参数。但是,我们已经将 getopts 规范移开了,因此当 getopts 检查它们时,$1、$2 等确实是位置参数。getopts 的 $opts 参数告诉它合法参数集,如上所述。
如果 getopts 看到合法的选项,它会将该选项(不带前导 -)存储在 argname 变量中,并且如果该选项需要参数,它会将该参数存储在名为 OPTARG 的变量中。如果看到不正确的选项,getopts 会在 argname 中存储错误代码,并在 OPTARG 中存储不正确选项的名称。有两种类型的不正确选项
选项的名称未在 getopts 规范中列出。在这种情况下,getopts 将 ? 存储在 argname 中。
需要参数的选项,但缺少参数;如果发生这种情况,getopts 将 : 存储在 argname 中。
bash getopts 有一个错误:在这两种情况下,它都存储 ?。列表 2 包含一个解决方法。ksh 没有这个问题。
如果这些问题都没有发生,我们有一个有效的选项,可以继续创建变量。这在循环中的最后一个 if 语句中完成。then 分支处理选项具有参数的情况,else 分支处理没有参数的情况;两者都使用 eval。让我们看一下其中一个
eval opt_$argname=$OPTARG # set option name
假设我们正在处理带有参数 fred 的 -b 选项:argname 将包含 b,而 OPTARG 将包含 fred。我们希望 shell 运行此代码
opt_b=fred我们的第一次尝试可能是
opt_$argname=$OPTARG推理 shell 将用 b 替换 $argname,用 fred 替换 $OPTARG,我们就完成了。尝试不错,但这不起作用。如果您现在坐在 bash shell 提示符前,请尝试这个
$ argname=b $ OPTARG=fred $ opt_$argname=$OPTARG您应该看到此消息:bash: opt_b=fred: command not found。
哪个命令未找到?shell 确实扩展了变量。问题在于,尽管 shell 生成了字符串 opt_b=fred,但它认为它在该行上的工作已经完成,并尝试执行一个名为“opt_b=fred”的程序。尽管处理后的行看起来像 shell 命令,但 shell 不会注意到这一点,因为它每行只处理一次。要解决此问题,我们需要指示 shell 扩展此行中的变量,然后重新开始,就像第一次处理该行一样。这正是行首的 eval 所完成的。当 shell 处理扩展行时,它会将 eval 识别为创建 shell 变量的命令,并将创建一个变量。
请记住,这些变量是在函数中创建的,并且在函数终止后将继续存在。因此,我们可以从脚本(或实际上是另一个函数)调用 options 函数,并以我们喜欢的任何方式使用它创建的变量。
为了节省空间,我没有描述 shell 扩展命令行时执行的所有步骤;有关详细信息,请查阅 O'Reilly & Associates 的 Learning the Bash Shell。
为了确保 shell 知道函数,bash 中有一个选项,ksh 中有两个选项。在 bash 中,您必须在启动脚本(例如 .bash_profile)中“source”包含函数的文件(或等效地,将代码直接包含在启动脚本中)。在 ksh 中,您也可以在启动脚本中 source 文件,或者,将您的函数文件放在目录中(可能称为 $HOME/functions)并将此目录添加到 FPATH 环境变量。当您键入 ksh 未知的命令名称时,它会在 FPATH 中的目录中查找是否有名为该名称的函数文件。如果是,它会读取该文件,记住函数定义并执行它。
