关于bash:检查环境变量是否在Unix shell脚本中设置的简洁方法是什么?

What's a concise way to check that environment variables are set in a Unix shell script?

我有一些unix shell脚本,在这些脚本中,我需要在开始执行操作之前检查是否设置了某些环境变量,因此我执行以下操作:

1
2
3
4
5
6
7
8
9
if [ -z"$STATE" ]; then
    echo"Need to set STATE"
    exit 1
fi  

if [ -z"$DEST" ]; then
    echo"Need to set DEST"
    exit 1
fi

这是很多打字。是否有更优雅的习惯用法来检查是否设置了一组环境变量?

编辑:我应该提到这些变量没有有意义的默认值——如果有任何未设置的值,脚本应该出错。


参数展开

显而易见的答案是使用参数扩展的一种特殊形式:

1
2
: ${STATE?"Need to set STATE"}
: ${DEST:?"Need to set DEST non-empty"}

或者更好(请参见下面的"双引号位置"部分):

1
2
:"${STATE?Need to set STATE}"
:"${DEST:?Need to set DEST non-empty}"

第一个变量(仅使用?)要求设置状态,但state="(空字符串)是正常的,这不是您想要的,而是替代的和旧的符号。

第二种变体(使用:?)要求dest设置为非空。

如果不提供消息,shell将提供默认消息。

${var?}结构可移植回7版Unix和BourneShell(1978年左右)。${var:?}结构稍微更新一些:我认为它大约在1981年在System III Unix中,但在此之前它可能在pwb Unix中。因此,它在korn shell中,在posix shell中,尤其是bash中。

它通常记录在shell的手册页中的一个称为参数扩展的部分中。例如,bash手册说:

1
${parameter:?word}

Display Error if Null or Unset. If parameter is null or unset, the expansion of word (or a message to that effect if word is not present) is written to the standard error and the shell, if it is not interactive, exits. Otherwise, the value of parameter is substituted.

colon命令

我可能应该补充一点,冒号命令只是计算了它的参数,然后成功了。它是原始的shell注释符号(在'#'之前到行尾)。很长一段时间以来,BourneShell脚本的第一个字符是冒号。C shell将读取一个脚本,并使用第一个字符来确定它是用于C shell(一个'#散列)还是用于bourne shell(一个':冒号)。然后内核加入了该法案,并添加了对"EDOCX1"(12)的支持,bourne shell得到了"EDOCX1"(9)的评论,而colon约定则顺其自然。但是如果你遇到一个以冒号开头的脚本,现在你就知道为什么了。

双引号的位置

Blong在评论中问道:

Any thoughts on this discussion? https://github.com/koalaman/shellcheck/issues/380#issuecomment-145872749

讨论的要点是:

… However, when I shellcheck it (with version 0.4.1), I get this message:

1
2
3
In script.sh line 13:
: ${FOO:?"The environment variable 'FOO' must be set and non-empty"}
  ^-- SC2086: Double quote to prevent globbing and word splitting.

Any advice on what I should do in this case?

简短的回答是"照shellcheck所建议的做":

1
2
:"${STATE?Need to set STATE}"
:"${DEST:?Need to set DEST non-empty}"

为了说明原因,请学习以下内容。请注意,:命令不回显其参数(但shell会评估这些参数)。我们希望看到这些参数,因此下面的代码使用printf"%s
"
代替:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ mkdir junk
$ cd junk
$ > abc
$ > def
$ > ghi
$
$ x="*"
$ printf"%s
"
${x:?You must set x}    # Careless; not recommended
abc
def
ghi
$ unset x
$ printf"%s
"
${x:?You must set x}    # Careless; not recommended
bash: x: You must set x
$ printf"%s
"
"${x:?You must set x}"  # Careful: should be used
bash: x: You must set x
$ x="*"
$ printf"%s
"
"${x:?You must set x}"  # Careful: should be used
*
$ printf"%s
"
${x:?"You must set x"}  # Not quite careful enough
abc
def
ghi
$ x=
$ printf"%s
"
${x:?"You must set x"}  # Not quite careful enough
bash: x: You must set x
$ unset x
$ printf"%s
"
${x:?"You must set x"}  # Not quite careful enough
bash: x: You must set x
$

请注意,当整个表达式不是双引号时,$x中的值如何扩展到第一个*,然后扩展到文件名列表。这是shellcheck建议的应该修正的。我还没有证实它并不反对表达式用双引号括起来的形式,但这是一个合理的假设,即它是可以的。


试试这个:

1
[ -z"$STATE" ] && echo"Need to set STATE" && exit 1;


你的问题取决于你使用的外壳。

BourneShell对你的追求几乎没有影响。

但是…

它确实有效,几乎无处不在。

试着远离CSH。与波恩贝壳相比,它对钟和口哨很有好处,但现在它真的吱吱作响。如果你不相信我,试着在CSH中分离出stderr!(-)

这里有两种可能性。上面的示例,即使用:

1
${MyVariable:=SomeDefault}

第一次需要引用$myvariable。这需要环境。var myvariable,如果当前未设置,则将somedefault的值分配给该变量供以后使用。

您还可以:

1
${MyVariable:-SomeDefault}

它只是用某个默认值替换您正在使用此构造的变量。它不会将值somedefault赋给变量,遇到此语句后,myvariable的值仍然为空。


当然,最简单的方法是将-u开关添加到shebang(脚本顶部的行),假设您使用的是bash

#!/bin/sh -u

如果有任何未绑定的变量隐藏在脚本中,这将导致脚本退出。


1
${MyVariable:=SomeDefault}

如果设置了MyVariable且不为空,则将重置变量值(=无任何情况发生)。否则,MyVariable设置为SomeDefault

上面将尝试执行${MyVariable},因此如果您只想设置变量do:

1
MyVariable=${MyVariable:=SomeDefault}


在我看来,最简单和最兼容的检查!/BI/SH为:

1
2
3
4
5
6
if ["$MYVAR" ="" ]
then
   echo"Does not exist"
else
   echo"Exists"
fi

同样,这是针对/bin/sh的,并且在旧的Solaris系统上也兼容。


bash4.2引入了-v运算符,用于测试名称是否设置为任何值,甚至空字符串。

1
2
3
4
5
6
7
8
$ unset a
$ b=
$ c=
$ [[ -v a ]] && echo"a is set"
$ [[ -v b ]] && echo"b is set"
b is set
$ [[ -v c ]] && echo"c is set"
c is set

我经常使用:

1
if ["x$STATE" =="x" ]; then echo"Need to set State"; exit 1; fi

恐怕没那么简洁。

在CSH下你有美元?状态。


对于像我这样的未来人,我想向前迈出一步,参数化var名称,这样我就可以循环访问变量大小的变量名称列表:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
declare -a vars=(NAME GITLAB_URL GITLAB_TOKEN)

for var_name in"${vars[@]}"
do
  if [ -z"$(eval"echo \$$var_name")" ]; then
    echo"Missing environment variable $var_name"
    exit 1
  fi
done

我们可以编写一个很好的断言来同时检查一系列变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#
# assert if variables are set (to a non-empty string)
# if any variable is not set, exit 1 (when -f option is set) or return 1 otherwise
#
# Usage: assert_var_not_null [-f] variable ...
#
function assert_var_not_null() {
  local fatal var num_null=0
  [["$1" ="-f" ]] && { shift; fatal=1; }
  for var in"$@"; do
    [[ -z"${!var}" ]] &&
      printf '%s
'
"Variable '$var' not set">&2 &&
      ((num_null++))
  done

  if ((num_null > 0)); then
    [["$fatal" ]] && exit 1
    return 1
  fi
  return 0
}

示例调用:

1
2
3
4
5
6
7
one=1 two=2
assert_var_not_null one two
echo test 1: return_code=$?
assert_var_not_null one two three
echo test 2: return_code=$?
assert_var_not_null -f one two three
echo test 3: return_code=$? # this code shouldn't execute

输出:

1
2
3
4
test 1: return_code=0
Variable 'three' not set
test 2: return_code=1
Variable 'three' not set

以上所有解决方案都不能满足我的目的,部分原因是我在环境中检查了一个开放的变量列表,这些变量在开始一个冗长的过程之前需要设置。最后我得到了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mapfile -t arr < variables.txt

EXITCODE=0

for i in"${arr[@]}"
do
   ISSET=$(env | grep ^${i}= | wc -l)
   if ["${ISSET}" ="0" ];
   then
      EXITCODE=-1
      echo"ENV variable $i is required."
   fi
done

exit ${EXITCODE}


这也是一种方式:

1
2
3
if (set -u; : $HOME) 2> /dev/null
...
...

http://unstableme.blogspot.com/2007/02/checks-when-envvar-is-set-or-not.html


我倾向于在登录shell中加载函数,而不是使用外部shell脚本。我使用类似这样的辅助函数来检查环境变量,而不是任何设置变量:

1
2
3
4
5
6
7
8
is_this_an_env_variable ()
    local var="$1"
    if env |grep -q"^$var"; then
       return 0
    else
       return 1
    fi
 }


$?语法非常简洁:

1
2
3
4
5
if [ $?BLAH == 1 ]; then
    echo"Exists";
else
    echo"Does not exist";
fi