Shell 变量(一)

bash shell 编程和其他编程语言差不多,同样包含变量(存放字符串和数值的容器,可以进行修改、比较、传递)。在引用 bash 变量时,可以使用一些非常特殊的运算符。bash 还拥有内建变量,这些变量可以提供有关脚本中其他变量的重要信息。下面介绍 bash 变量和一些特殊的变量引用机制,展示如何将其运用于你自己的脚本。

1、shell 变量基础知识

bash 脚本中的变量名称通常采用全大写,但这并非强制性的,只是一种常见做法而已。变量不用事先声明,直接使用就行了。变量基本上都是字符串类型,不过有些运算符能够将变量内容视为数字。变量的实际用法如下所示。

# 使用shell变量的普通脚本
MYVAR="something"
echo $MYVAR
# 写法类似,但没有引号
MY_2ND=anotherone
echo $MY_2ND
# 这里因为包含空客,需要使用引号:
MYOTHER="more stuff to echo"
echo $MYOTHER

bash 变量的语法有两处要点,但可能不那么一目了然。

MYVAR = something

此时 shell 很难区分出到底是要调用命令还是要给变量赋值。对于能够以 = 为参数的命令(如 test)更是如此。所以,还是让事情简单点吧:变量赋值时,shell 不允许在 = 两侧出现空白字符。该规定的另一方面也值得注意,不要在文件名中使用 =。

2、记录脚本

详细讨论 shell 脚本或变量前,我们还得说说如何记录脚本。毕竟,你得能看明白自己的脚本,即便是在编写完的几个月后。

用注释记录脚本。# 代表注释的开始。该行上随后的所有字符都会被shell 忽略。

#
# 这是一行注释
#
# 多用注释
# 注释是你的好朋友

如果您是java开发工作者,你会发现,这就是我们平时常说的代码注释。

3、将变量名与周围的文本分开

如果你需要输出变量以及其他文本。引用变量要用到 $,但是该怎么区分变量名与紧随其后的其他文本呢?例如,你想要用 shell 变量作为文件名的一部分,如下所示:

for FN in 1 2 3 4 5
do
 somescript /tmp/rep$FNport.txt #执行某个脚本,把文件当作执行参数,如cat
done

shell 会怎么理解这段代码?它会认为变量名从 $ 开始,到点号结束。换句话说,它将 $FNport 视为变量名,而非我们想要的 $FN。

那么,我们如何让shell知道我们的变量是FN呢?

使用完整的变量引用语法,不仅要包括 $,还要在变量名周围加上花括号,如下:

somescript /tmp/rep${FN}port.txt

因为 shell 变量名中只能包含字母、数字以及下划线,所以很多时候并不需要使用花括号。任何空白字符或标点符号(下划线除外)都足以提示变量名的结束位置。但只要有疑问,就应该用花括号。

4、导出变量

你在某个脚本中定义了一个变量,但在调用其他脚本时,该脚本并不知道这个变量的存在。为了解决这个问题,我们需要将传给其他脚本的变量导出。如下所示:

export MYVAR
export NAME=value

要想查看所有已导出的变量,敲入命令 env(或者内建命令 export-p)就能列出各个变量及其值。当脚本运行时,这些变量都可供使用,其中很多是 bash 启动脚本已经设置好的,如$PATH。

可以在 export 后面跟上变量赋值,不过这种写法不适用于比较老的 shell 版本。然后导出之后,就可以随意给变量赋值,不用重复导出。因此,有时你会看到下列语句:

# 导出变量
export FNAME
export SIZE
export MAX
# 为变量赋值
MAX=2048
SIZE=64
FNAME=/tmp/scratch

注意,导出的变量实际上是按值调用的。在被调用脚本中修改导出变量的值并不会改变调用脚本中该变量的值。

对于导出的变量,我们该如何删除呢?

# 删除变量
unset myvar

Shell 变量(二)

你希望用户能在调用脚本时指定参数。可以要求用户设置一个 shell变量,但这种做法似乎不够灵活。另外还需要向其他脚本传递数据。这可以通过环境变量实现,但会使两个脚本之间的联系过于紧密。因此,此处我们可以用到脚本参数。

1、在shell脚本中使用参数

使用命令行参数。在命令行上,出现在脚本名之后的任意单词都可以在脚本中作为编号变量(numbered variable)被访问。假设有下列脚本 simplest.sh。

# 一个简单的shell脚本
echo $1

该脚本会显示在命令行上被调用时所指定的第一个参数。我们来看一种实际用法。

$ cat simplest.sh
# 一个简单的shell脚本
echo ${1}
$ ./simplest.sh you see what I mean
you
$ ./simplest.sh one more time
one
$

其他参数的可用形式分别为 ${2}、${3}、${4}、${5} 等。单个数位的数字用不着花括号,除非要区分变量名与其后出现的文本。典型的脚本只用到少部分参数,但如果涉及 ${10},那就得使用花括号了,否则 shell 会将 $10 理解为 ${1} 后面紧跟着字符串 0,如下所示。

$ cat tricky.sh
echo $1 $10 ${10}
$ ./tricky.sh I II III IV V VI VII VIII IX X XI
I I0 X   #注意观察第二个输出
$

第 10 个参数的值是 X,但如果在脚本中写成 $10,那么你在 echo语句中得到的会是第一个参数 $1,后面紧跟着一个字符串 0。

因为第三个使用了${},所以三个${10}可以正常输出X。

2、遍历传入脚本的参数: $*

如果你想对指定的一系列参数执行某些操作。在编写 shell 脚本时,对单个参数进行处理不是什么问题,只需要用 $1 引用这个参数即可。但如果面对的是一大批文件呢?你可能想这样调用脚本。

./actall *.txt

shell 会进行模式匹配,生成匹配 *.txt 模式(以 .txt 结尾的文件名)的文件名列表。对于脚本而言,我们永远无法预估传入的参数的个数,那么我们就无法通过${数字}获取所有参数,那么${数字}方式将不再适用。

特殊的 shell 变量 $* 能够引用所有的参数,可以将其用于 for 循环,如下所示:

#!/usr/bin/env bash
# 实例文件:actall.sh
# 批量修改文件权限
#
for FN in $*
do
 echo changing $FN
 chmod 0750 $FN
done

变量 $FN 是我们自己挑选的;使用别的变量名也没有任何问题。$*引用的是命令行上出现的所有参数。假如用户输入

./actall abc.txt another.txt allmynotes.txt

调用该脚本时,$1 等于 abc.txt、$2 等于 another.txt、$3 等于allmynotes.txt,而 $* 等于整个参数列表。换句话说,shell 替换for 语句中的 $* 后,脚本就变成了如下这样:

for FN in abc.txt another.txt allmynotes.txt
do
 echo changing $FN
 chmod 0750 $FN
done

for 循环从列表中获取第一个值,并将其赋给变量 $FN,然后执行do 和 done 之间的语句。列表中的其他值会重复执行该过程。

3、处理包含空格的参数: “”

你编写了一个可以接受文件名作为参数的脚本,看起来一切正常,但有一次脚本出现了问题,结果发现是因为文件名中带有空格。你得仔细将所有可能包含文件名的命令参数全部加上引号。引用变量时,将其放入双引号中。

在 shell 脚本中,曾经简单的写作 ls -l $1 的地方,现在最好给参数加上引号,改写成 ls -l "$1"。否则,如果参数包含空格,那么会被 shell 解析成两个单词,$1 中只会包含部分文件名。如下:

$ cat simpls.sh
# 一个简单的shell脚本
ls -l ${1}
$
$ ./simple.sh Oh the Waste
ls: Oh: No such file or directory
$

如果调用脚本时没有将文件名放进引号,那么 bash 会看到 3 个参数并将 $1 替换成第 1 个参数(Oh)。ls 命令运行时只有一个参数Oh,结果就是无法找到该文件。

接下来,我们在调用脚本时给文件名加上引号。

$ ./simpls.sh "Oh the Waste"
ls: Oh: No such file or directory
ls: the: No such file or directory
ls: Waste: No such file or directory
$

还是不行。bash 得到了一个包含 3 个单词的文件名,并将 ls 命令中的 $1 替换成了该文件名。到目前一切都还好。但是,我们并没有将脚本中的变量引用放入引号,因此 ls 将文件名中的各个单词视为单独的参数(作为单独的文件名)。结果还是无法找到这些文件,相当执行命令:

ls -l Oh the Waste

因此,我们需要将我们变量引用放进引号,修改脚本内容如下:

$ cat simpls.sh
# 一个简单的shell脚本,注意此处${1}与第一次脚本里的区别,多了双引号
ls -l "${1}"
$
$ ./simple.sh "Oh the Waste"
$

4、处理包含空格的参数列表:$@

对于第二节,我们通过$*,可以获取参数列表,那么如果这个时候我们传入的参数列表包含空格会不会有问题呢? 如下所示:

$ ./actall.sh  "Oh the Waste"
changing Oh        
chmod: 无 法 访 问 "Oh": 没 有 那 个 文 件 或 目 录 
changing the
chmod: 无 法 访 问 "the": 没 有 那 个 文 件 或 目 录 
changing Waste
chmod: 无 法 访 问 "Waste": 没 有 那 个 文 件 或 目 录
$

按照上节中的建议,你给变量加上引号,但是仍然出现了错误。如下:

#!/usr/bin/env bash
#实例文件:actall.sh
#批量修改文件权限
#
for FN in $*
do
   echo changing "$FN"
   chmod 0750 "$FN"
done

如果文件名中带有空格,就会报错,报错的原因与 for 循环中使用的 $* 有关。在这个示例中,我们需要用到另一个不同但相关的 shell 变量 $@。如果该变量出现在引号中,则会得到一个命令行参数列表,其中每个参数都会被单独引用起来。修改后的 shell 脚本如下:

#!/usr/bin/env bash
# 实例文件:chmod_all.2
# 在文件名包含空格时选择更好的引号添加方式,批量修改文件权限
#
for FN in "$@"
do
 chmod 0750 "$FN"
done

如果不加引号,$* 和 $@ 没什么两样。但当两者出现在引号中时,bash 就会区别对待了。"$*" 得到的是整个参数列表,而"$@" 得到的可不是一个字符串,而是与各个参数对应的带有引号的字符串列表。

如果你知道文件名中没有空格,沿用老的 $ 语法基本没什么大碍。对于那些更稳健的脚本而言,安全起见,建议使用 "$@"*

Shell 变量(三)

1、统计参数数量

你想知道调用脚本时使用了多少个参数。使用 shell 内建变量 $#。如下,展示了一个严格要求3个参数的脚本:

#!/usr/bin/env bash
# 实例文件:check_arg_count
#
# 检查正确的参数数量:
# 使用下列语法或者:if [ $# -lt 3 ]
if (( $# < 3 ))
then
 printf "%b" "Error. Not enough arguments.\n" >&2
 printf "%b" "usage: myscript file1 op file2\n" >&2
 exit 1
elif (( $# > 3 ))
then
 printf "%b" "Error. Too many arguments.\n" >&2
 printf "%b" "usage: myscript file1 op file2\n" >&2
 exit 2
else
 printf "%b" "Argument count correct. Proceeding...\n"
fi

以下分别是参数过多和参数数量正好时的运行情况。

$ ./myscript myfile is copied into yourfile
Error. Too many arguments.
usage: myscript file1 op file2
$ ./myscript myfile copy yourfile
Argument count correct. Proceeding...

我们用 if 测试所提供的参数数量(保存在 $# 中)是否大于 3。如果答案是肯定的,则输出一条错误信息,提醒用户正确的脚本用法,然后退出。

标准提示错误信息会被重定向到标准错误(>&2)。这种做法符合标准错误的本意:作为所有错误信息的通道。

2、丢弃参数:shift

所有的正式脚本可能都要有两种参数:修改脚本行为的选项以及要处理的真正参数。你需要用一种方法在处理完选项后将其丢弃。例如,现在有如下脚本:

for FN in "$@"
do
 echo changing $FN
 chmod 0750 "$FN"
done

脚本内容非常简单,它会显示正在处理的文件名,然后修改文件权限。但有时你希望脚本静悄悄地工作,不要显示文件名,而有时又希望显示文件名。如何在保留for 循环的同时添加一个能够关闭文件名显示的选项呢?

用 shift 删除处理过的参数,如下:

# 自定义变量
VERBOSE=0
# 判断第一个参数的值
if [[ $1 = -v ]]
then
 # 使用变量保存参数值
 VERBOSE=1
 # 删除参数
 shift
fi
# 此时for拿到的参数已经少了$1,从$2开始读取
for FN in "$@"
do
 if (( VERBOSE == 1 ))
 then
 echo changing $FN
 fi
 chmod 0750 "$FN"
done

我们添加了标记变量 $VERBOSE,借此了解是否应该输出所处理的文件名。可是一旦 shell 脚本发现 -v 选项并设置好标记,我们就用不着参数列表中的 -v 了。shift 语句告诉 bash 将命令行参数挪动一个位置,使 $2 变成 $1、$3 变成 $2,以此类推,这样就丢弃了第一个参数($1)。如此一来,当 for 循环启动时,参数列表($@)中就再也没有 -v,剩下的是紧随其后的那些参数。

运行结果如下:

# 执行脚本,带-v参数,输出修改的文件名
$ ./shift_test.sh -v error.out  
changing error.out
# 执行脚本,不带-v参数,悄悄执行,不输出文件名
$./shift_test.sh  error.out                                                                                                
$   

3、获取默认值:${:-}

有一个可以接受命令行参数的 shell 脚本。如你希望能够提供默认值,这样就不用每次都让用户输入那些频繁用到的值了。

用 ${:-} 语法引用参数并提供默认值,如下所示:

FILEDIR=${1:-/tmp}

在引用 shell 变量时,有多种特殊运算符可用。:- 运算符的意思是,如果指定参数(这里是 $1)不存在或为空,则将运算符之后的内容(本例为 /tmp)作为值。否则,使用已经设置好的参数值。该运算符可用于任何 shell 变量,并不局限于位置参数$1、$2、$3等。

当然,你也可以用更多的代码来实现:用 if 语句检查变量是否为空或不存在,但在 shell 脚本中,此类处理司空见惯,:- 运算符可谓是一种颇受欢迎的便捷写法。

4、设置环境变量默认值: ${HOME:=/tmp}

你的脚本依赖于某些常用(如 $USER)或业务特定的环境变量。要想构建一个稳健的 shell 脚本,就得确保这些变量都有合理的默认值。那么该如何确保呢?

首次引用 shell 变量时,如果该变量没有值,则使用赋值运算符为其赋值,如下:

cd ${HOME:=/tmp}

示例中所引用的 $HOME 会返回其当前值,除非它为空或者压根就没设置。对于后两种情况(为空或没有设置),返回 /tmp,该值还会被赋给 $HOME,随后再引用 $HOME 的话,返回的就是这个新值。如下所示,

注意:下面的例子会改变环境变量HOME,请慎重执行

$ echo ${HOME:=/tmp}
/home/uid002
$ unset HOME # 删除环境变量值
$ echo ${HOME:=/tmp} # 重新获取,此时不存在,将重新赋值并返回新值
/tmp
$ echo $HOME # 此时再查看变量,输出新设置的值
/tmp
$ cd;pwd
/tmp
$

赋值运算符有一个重要的例外:不能对位置参数(如 $1 或$)赋值。在这种情况下,可以使用 :-(如 ${1:-default*}),该表达式只返回值,但不进行赋值。

${VAR:=value} 和 ${VAR:-value} 在形式上的差异,也许可以帮助你记忆这两种让人抓狂的符号。:= 执行赋值操作,同时返回运算符右侧的值。:- 只做了前者一半的工作:返回值,但不赋值。因此,它的符号也只有等号的一半(一个横杠,而不是两个)。

Shell 变量(四)

1、获得某个数的绝对值

变量中的数值可能是负数,也可能是零或正数。你想得到它的大小(也就是绝对值),但 bash 似乎没有求绝对值的功能。但是,我们可以利用字符串操作。如下:

${MYVAR#-}

这是一种简单的字符串操作。# 从字符串起始位置开始搜索负号(-)。如果找到,则将其删除;如果没有找到,就保留原值。不管是哪种情况,最后得到的都是不包含负号的绝对值。

然而,我们也可以使用 if/then/else 按照数学方法来实现。如下:

# 通过判断数值于0的关系,并且通过与-1相乘
if (( MYVAR < 0 ))
then
 let MYVAR=MYVAR*-1
fi

对比上面2种方法,明显第一种更简单,所以推荐第一种。

2、选取CSV的替换值

你想制作一个由逗号分隔的值列表,但不希望开头或结尾处出现逗号,然后这是我们日常工作中很普遍的需求。如果在循环内部用 LIST="${LIST},${NEWVAL}" 构建该列表,那么第一次循环(此时 LIST 为空)过后会得到一个前导逗号。你可以对 LIST 进行特殊的初始化处理,以便它在进入循环前就先得到第一个值,但如果觉得这种方法不实用,或是为了避免重复代码(用于得到新值),你可以改用 bash 中的 ${:+} 语法。如下:

LIST="${LIST}${LIST:+,}${NEWVAL}"

如果 {LIST} 为空或不存在,那么 $LIST 的两个表达式不会产生任何内容。这就意味着,第一次循环过后,LIST 中保存的只有NEWVAL 的值。如果 LIST 不为空,则第二个表达式 ${LIST:+,}会被替换为逗号,将先前的值与新值分隔开来。

下面的代码片段用于读取并构建 CSV 列表。

LIST=""
for NEWVAL in "$@" 
do
LIST="${LIST}${LIST:+,}${NEWVAL}"
done 
echo ${LIST}  

3、使用数组变量

到目前为止,我们已经见识了不少使用变量的脚本,但是 bash 能不能处理数组呢?当然可以,bash 有专门的一维数组语法。

如果编写脚本时已经知道具体的值,则初始化数组就很容易了。格式如下:

MYRA=(first second third home)

注意:数组是用(),这同java里的数组符号[]不同。

括号内列表的每个单词都对应着一个数组元素。你可以像下面这样引用各个元素:

echo runners on ${MYRA[0]} and ${MYRA[2]}

输出结果如下:

runners on first and third

注意:如果只写 $MYRA,那么只会得到第一个数组元素,相当于${MYRA[0]}。

4、转换大小写

bash 4.0 中的几个运算符可以在引用变量名时转换其大小写。如果变量 $FN 中包含一个需要转换成小写的文件名(字符串),那么${FN,,} 会返回全部是小写形式的字符串。与此类似,${FN^^} 会返回全部是大写形式的字符串。甚至还有 ${FN~~},它可以切换大小写,将所有的小写字母转换成大写,大写字母转换成小写。

以下的 for 循环会将所有参数更改成小写字母。

for FN in "$@"
do
 echo "${FN}" 转为小写的结果为:"${FN,,}"
done

或者写成单行脚本:

for FN in "$@";  do  echo "${FN}" 转为小写的结果为:"${FN,,}" ; done

5、对不存在的参数输出错误消息

有时你需要强制用户指定某个值,否则就无法继续往下进行。用户有可能会遗漏某个参数,因为他们确实不知道该怎样调用脚本。你希望能给用户点提示,省得他们自己瞎猜。相较于堆砌成堆的 if 语句,有没有更简洁的方法来检查各个参数?

引用参数时使用 ${:?} 语法。如果指定参数不存在或为空,那么 bash 会输出错误消息并退出。

#!/usr/bin/env bash
# 实例文件:check_unset_parms
#
USAGE="usage: myscript scratchdir sourcefile conversion"
FILEDIR=${1:?"Error. You must supply a scratch directory."}
FILESRC=${2:?"Error. You must supply a source file."}
CVTTYPE=${3:?"Error. ${USAGE}"}

如果执行脚本时没有指定足够的参数,则会出现下列结果。

$ ./myscript /tmp /dev/null
./myScript.sh:行11: 3: Error. usage: myscript scratchdir sourcefile conversion
$

bash 会测试各个参数,如果参数不存在或为空,则输出错误信息并退出。$3 所对应的错误消息中使用了另一个 shell 变量。

如果 $3 不存在,则错误消息中会包含短语 "Error."、变量$USAGE 的值。

另一方面,${:?} 生成的错误信息包含 shell 脚本文件名和行号。

本文由传智教育博学谷教研团队发布。

如果本文对您有帮助,欢迎关注点赞;如果您有任何建议也可留言评论私信,您的支持是我坚持创作的动力。

转载请注明出处!

发表回复