Shell 技巧 - 特殊变量 I:基础

作者:Dave Taylor

当时我正在为这个专栏想主题,当我遇到难题时,我做了我通常做的事情:我在 Twitter 上向我的关注者提出了一个问题。 这次,我得到了一个很好的答案,来自 John Minnihan:“脚本内部的特殊变量怎么样,例如,#!/bin/bash script="${0##*/}" current=`dirname "$0"` cd $current; make?”

这是一个很好的主题,所以让我们深入探讨一下,从本月的基础知识开始,好吗?

简单的特殊变量

shell 中变量的基本表示法是 $varname,但我敢打赌,您已经使用过一些特殊表示法,但没有真正考虑过它。 例如,想知道脚本被调用时您收到了多少个位置参数(又名起始参数)吗? 使用 $# 会为您提供该值

echo "you gave me $# parameters"

想要从起始命令行获取特定的位置参数? 这可以通过其他特殊变量来完成:$1、$2、$3 等等。 这些实际上是相当奇怪的情况,并且 shift 命令将它们全部向下移动一位,因此您可以轻松地解析和修剪命令标志。

试试这个代码片段,看看我在说什么

echo "arg1 = $1" ; shift ; echo "now arg1 = $1"

变量 $0 在这个序列中是一个特殊的变量。 它是被调用的脚本或程序的名称。 这非常有用,因为它意味着您可以使用单个共享脚本库向 Linux shell 添加多个命令。

假设您想添加 “happy” 和 “sad” 作为两个新的命令行选项,但您想在一个脚本中完成它。 简单! 编写脚本,另存为 “happy”,创建一个符号链接,使 “sad” 指向 “happy”,并将此内容放在脚本本身中

if [ "$0" = "happy" ] ; then
  echo "I am so darn happy too, hurray!"
else
  echo "Sorry you're sad. Why not take a walk?"
fi

明白它是如何工作的了吗? 事实证明,这种用法有一个细微之处,因为您通常会在 $0 变量中获得完整路径,因此大多数人使用 $(basename $0) 而不是直接使用 $0。

检查您的状态

您可能遇到的另一个特殊变量是状态变量 $?。 在脚本中,这包含最近执行的(外部)命令的返回值。

在这里,您需要阅读手册页,以便了解成功和失败时的预期结果,但作为一个示例,请考虑 test 命令。 根据手册页,“如果 [表达式] 的计算结果为 true,则它返回零(true)退出状态; 否则,它返回 1(false)。 如果没有表达式,test 也返回 1(false)。”

这意味着您可以这样做

test 1 --eq 3
if $? ; then

快,现在,我们会在这个条件语句中还是不在? 这就是棘手的地方,因为零 = true,非零 = false,这在某种程度上与我们自然而然地思考条件测试的方式相反(好吧,至少我是这样想的)。 实际上,上面的测试将测试 1,因为 “test” 的计算结果将为 false,并且其返回值也将为 false。

现在,像这样使用 test 有点傻,但是如果您想创建一个子目录,然后测试它是否成功呢? 实际上,这是 $? 的完美用法

mkdir $newdir
if [ $? --ne 0 ] ; then
   echo "We failed to make the directory $newdir"

事实证明,您还可以通过让 “if” 直接评估返回代码来简化这类事情

if mkdir $newdir ; then

这是一种更好的编码风格,但如果您习惯于将条件表达式作为值测试,而不是实际执行 某些操作 的命令,则可能会感到困惑。

更多有用的特殊变量

我经常使用一个特殊变量来帮助创建临时文件名,它是 $$,它扩展为系统中的当前进程 ID。 例如

$ echo $$
3243

如果您正在进行大量子 shell 或派生子命令的操作,则另一个有用的变量是 $!,它是最近派生的后台命令的进程 ID。 我从未使用过它在我的任何 shell 脚本中,但您可能会发现它在某些情况下很有用。

我在这里要讲的最后一个例子在您想将起始参数传递给子 shell 时最有用。 两个选项是 $* 和 $@,解释它们之间的区别非常复杂,因此最好直接演示。

让我们从一个小脚本开始,它只报告它获得了多少个参数

#!/bin/sh
echo "I was given $# parameters"
exit 0

我将把它称为 subshellcount.sh 并像这样使用它

#!/bin/sh
echo "you gave me $# variables and the first is $1"
echo "unprotected parameters:"
./subshellcount.sh  $1 $2 $3 $4
echo "or, more succinctly:"
./subshellcount.sh $*
echo "but when we put \$* in quotes:"
./subshellcount.sh "$*"
echo "by comparison, same thing with \$@:"
./subshellcount.sh "$@"

看看当我用三个参数调用它时会发生什么,其中一个参数嵌入了空格

$ sh test.sh I love "Linux Journal"
you gave me 3 variables and the first is I
unprotected parameters:
I was given 4 parameters
or, more succinctly:
I was given 4 parameters
but when we put $* in quotes:
I was given 1 parameters
by comparison, same thing with $@:
I was given 3 parameters

您能看到这里的区别吗? 当我们不努力保护第三个位置参数中的空格时(无论是仅引用 $3 还是在不带引号的情况下使用 $@),它会拆分为子 shell 的两个参数,并且我们得到计数为四。

引号本身也不能解决问题,因为 $@ 和 $* 之间存在差异。 对于后者,所有内容都会展开而不会“跳出”引号,因此 $* 最终成为子 shell 的单个位置参数。 幸运的是,@$ 正好按照我们想要的方式工作,子 shell 获得三个参数,而不是一个,也不是四个。

这看起来有点微不足道,但是当您开始处理文件名中带有空格的文件名时,例如,您很快就会了解到正确地完成所有这些操作有多么棘手!

我将在这里停止,从下个月开始,我们将深入研究更晦涩和复杂的 shell 变量表示法。 这很有趣。

Dave Taylor 是一位拥有 26 年 UNIX 经验的资深人士,The Elm Mail System 的创建者,也是最近畅销书 Wicked Cool Shell ScriptsTeach Yourself Unix in 24 Hours 的作者,以及他的 16 本技术书籍。 他的主要网站是 www.intuitive.com,他还通过 AskDaveTaylor.com 提供技术支持。 您也可以通过 twitter.com/DaveTaylor 在 Twitter 上关注 Dave。

加载 Disqus 评论