5.1 Shell脚本的第一行"#!"(Shebang)
#!(Shebang)是一个由井号‘#’和叹号‘!’构成的字符序列。它是出现在Shell脚本文件第一行的前两个字符。脚本中的#!行(第一行)用于指示一个解释程序。
#!行的语法格式类似如下所示:
#! INTERPRETER [OPTION]…
注意:INTERPRETER必须是一个程序的绝对路径。
在Linux系统中,当一个内容以#!开头的脚本作为一个程序运行时,程序加载器会将脚本第一行的#!之后的内容解析为一个解释程序,然后用这个指定的解释程序替代其运行,并将脚本的路径作为第一个参数传递给解释程序。例如,一个脚本的路径名是"path/to/script",并且它的内容以如下开头:
#!/bin/bash
那么程序加载器被指示用解释程序"/bin/bash"替代其运行,并将路径"path/to/script"作为第一个参数传递给解释程序"/bin/bash"。
5.2 Shell中的注释
Shell脚本中,井号‘#’是注释标识符。如果脚本的某行含有#或以#开头(除了$#),那么这一行在#之后的所有内容都将被解释程序忽略,#之后的这些内容被称为注释。
Shell脚本的注释用于解释脚本及其相关语句的用途和含义,使这些脚本源代码更容易被别人或自己(很长一段时间之后的自己)读懂和理解,使以后对脚本的维护和更新更容易。
我们来看下面这个脚本:
$ cat seeDate_IP_Hostname.sh #!/bin/bash # A Simple Shell Script To Get Linux Date & Hostname & Network Information # Liu Yantao - 2013-10-07 echo "Current date : $(date) @ $(hostname)" echo "Network configuration" /sbin/ifconfig –a
在Shell脚本中,还可以使用Bash的HERE DOCUMENT特性添加多行的注释内容,如下:
#!bin/bash echo "Say something" <<COMMENT comment line 1 comment line 2 comment line n COMMENT echo "Do something else"
5.3 如何设置脚本的权限和执行脚本
在你运行一个Shell脚本之前,你要确保你的Shell脚本文件具有可执行的权限,否则当你直接运行脚本时,会得到“Permission denied”的错误信息。类似如下所示:
$ ./multicomments.sh -bash: ./multicomments.sh: Permission denied
如果遇到上述错误,你就需要给脚本文件添加可执行的权限。chmod命令给文件添加执行权限:
$ chmod u+x ./multicomments.sh
如果你想给所有用户执行此脚本的权限,则使用如下的命令:
$ chmod +x ./multicomments.sh
运行一个Shell脚本,使用绝对路径或相对路径两种方式都可以:
使用绝对路径运行Shell脚本的方法如下所示:
[li@hadoop102 bin]$ /home/li/bin/helloworld.sh helloworld!
使用相对路径运行Shell脚本的示例如下:
[li@hadoop102 bin]$ cd /home/li [li@hadoop102 ~]$ ./bin/helloworld.sh helloworld!
也可以像运行一个命令一样运行一个Shell脚本,即不需要指定绝对路径或相对路径,只需要输入脚本名称即可。要实现这一目的,需要将Shell脚本所在目录的路径添加到你的环境变量PATH中。例如,如果将目录路径"/home/li/bin/"加入到PATH环境变量中,那么就可以在任何路径下直接运行目录"/home/li/bin/"下的Shell脚本。
临时设置环境变量:
export PATH=/gg/gg/:$PATH
永久设置环境变量:
vim /etc/profile.d/my_env.sh
立即生效
source /etc/profile.d
5.4 Shell变量进阶
5.4.1 Bash中的参数扩展
参数是一个存储数值的实体,并由名称、数字或特定符号所引用。
- 被名称引用的参数称作变量。
- 被数字引用的参数称作位置参数。
- 被特定符号引用的参数具有特殊的含义和用途,被做为Bash的特殊内部变量引用。
参数扩展是从引用的实体取值的过程,就像扩展变量打印它的值。字符“$”会引导参数扩展。将要扩展的参数名或符号可以放在大括号中。大括号虽然是可选的,但却可以保护待扩展的变量,使得紧跟在大括号后面的内容不会被扩展。我们通过下面的列表来了解一下参数扩展的各种形式:
基本的参数扩展:
{PARAMETER}
如果参数名后面还紧连着其它字符,这时使用大括号{}是必须的,否则紧接在参数名后面的字符串会被解释为参数名的一部分。例如我们想打印一个单词后跟字母‘s’:
[li@hadoop102 ~]$ WORD=car [li@hadoop102 ~]$ W=c [li@hadoop102 ~]$ O=a [li@hadoop102 ~]$ echo $WORDs [li@hadoop102 ~]$ echo ${WORD}s cars
另外,对于访问$9之后的位置参数也同样需要使用大括号(关于位置参数的详细内容将在5.4.3节中介绍),比如下面的示例:
$ echo "Argument 1 is: $1" $ echo "Argument 10 is: ${10}"
注意:参数名是大小写敏感的。
间接参数扩展:
${!PARAMETER}
上述语句中,被引用的参数不是PARAMETER自身,而是PARAMETER的值。比如,如果参数PARAMETER的值是“TEMP”,则${!PARAMETER}将扩展为参数TEMP的值。
[li@hadoop102 ~]$ PARAMETER=TEMP [li@hadoop102 ~]$ TEMP="It's indirect" [li@hadoop102 ~]$ echo ${PARAMETER} TEMP [li@hadoop102 ~]$ echo ${!PARAMETER} It's indirect [li@hadoop102 ~]$ echo $PARAMETER TEMP [li@hadoop102 ~]$
大小写修改(Bash 4.0的新特性):
{PARAMETER^^}
{PARAMETER,,}
{PARAMETER~~}
上述语句中的这些扩展操作符修改参数值中的字母的大小写。操作符‘’将参数值得第一个字符改为大写,操作符‘,’将参数值的第一个字符改为小写。当使用双重模式(^和,,)时,参数值得所有字符都将被转换。
[li@hadoop102 ~]$ varName=varValue [li@hadoop102 ~]$ echo ${varName,,} varvalue [li@hadoop102 ~]$ echo ${varName^^} VARVALUE [li@hadoop102 ~]$ echo ${varName^} VarValue
变量名扩展
{!PREFIX@}
这种参数扩展将列出以字符串PREFIX开头的所有变量名。默认情况下,列出的这些变量名用空格分隔。例如,列出以BASH开头的所有已定义的变量名:
[li@hadoop102 ~]$ echo ${!BASH*} BASH BASHOPTS BASHPID BASH_ALIASES BASH_ARGC BASH_ARGV BASH_CMDS BASH_COMMAND BASH_COMPLETION_COMPAT_DIR BASH_LINENO BASH_REMATCH BASH_SOURCE BASH_SUBSHELL BASH_VERSINFO BASH_VERSION
字符串移除
{PARAMETER##PATTERN}
{PARAMETER%%PATTERN}
这种参数扩展可以只扩展参数值的一部分,用指定的模式来描述从参数值字符串中移除的内容。上述的语法格式中,前两个语句用于移除从参数值的开头匹配指定模式的字符串,而后两个语句与之相反,用于从参数值的末尾匹配指定模式的字符串。操作符‘#’和‘%’表示将移除匹配指定模式的最短文本,而操作符‘##’和‘%%’表示移除匹配指定模式的最长文本。
[li@hadoop102 ~]$ MYSTRING="This is used for removing string" [li@hadoop102 ~]$ echo ${MYSTRING#* } # 最后有一个空格 is used for removing string [li@hadoop102 ~]$ echo ${MYSTRING##* } string [li@hadoop102 ~]$ echo ${MYSTRING% *} This is used for removing [li@hadoop102 ~]$ echo ${MYSTRING%% *} This
可能这种参数扩展最常用的用途是提取文件名的一部分:
[li@hadoop102 ~]$ FILENAME=linux_bash.txt [li@hadoop102 ~]$ echo ${FILENAME%.*} # 移除文件名的后缀 linux_bash [li@hadoop102 ~]$ echo ${FILENAME##*.} # 移除文件名,保留后缀 txt [li@hadoop102 ~]$ FILENAME=/home/li/linux_bash.txt [li@hadoop102 ~]$ echo ${FILENAME%/*} # 移除文件名,保留文件目录 /home/li [li@hadoop102 ~]$ echo ${FILENAME##*/} # 移除文件目录,保留文件名 linux_bash.txt
字符串搜索与替换
{PARAMETER//PATTERN/STRING}
{PARAMETER//PATTERN}
这种参数扩展可以替换参数值中匹配指定模式的子字符串。操作符‘/’表示只替换一个匹配的字符串,而操作符‘//’表示替换所有匹配的字符串。如果没有指定替换字符串STRING,那么匹配的内容将被替换为空字符串,即被删除。
示例如下:
[li@hadoop102 ~]$ MYSTRING="This is used for replacing string or removing string" [li@hadoop102 ~]$ echo ${MYSTRING/string/characters} This is used for replacing characters or removing string [li@hadoop102 ~]$ echo ${MYSTRING//string/characters } This is used for replacing characters or removing characters [li@hadoop102 ~]$ echo ${MYSTRING/string} This is used for replacing or removing string [li@hadoop102 ~]$ echo ${MYSTRING/string} This is used for replacing or removing string [li@hadoop102 ~]$ echo ${MYSTRING//string/ } This is used for replacing or removing
求字符串长度
${#PARAMETER}
此参数扩展格式将得到参数值的长度:
[li@hadoop102 ~]$ MYSTRING="Hello World" [li@hadoop102 ~]$ echo ${#MYSTRING} 11
子字符串扩展
{PARAMETER:OFFSET:LENGTH}
这种参数扩展格式将扩展参数值的一部分,从指定的位置开始截取指定的长度的字符串,如果省略LENGTH,将截取到参数值的末尾。
[li@hadoop102 ~]$ MYSTRING="This is used for substring expansion." [li@hadoop102 ~]$ echo ${MYSTRING:8} used for substring expansion.
下面指定LENGTH的值:
[li@hadoop102 ~]$ echo ${MYSTRING:8:10} used for s
使用默认值:
{PARAMETER-WORD}
如果参数PARAMETER是未定义,或为null时,这种模式会扩展WORD,否则将扩展参数PARAMETER。如果在PARAMETER和WORD之间略去了符号‘:’,即上述语法中的第二种格式,则只有参数PARAMETER是未定义时,才会使用WORD。
[li@hadoop102 ~]$ unset MYSTRING [li@hadoop102 ~]$ echo $MYSTRING [li@hadoop102 ~]$ echo ${MYSTRING:-Hello World} Hello World [li@hadoop102 ~]$ echo $MYSTRING [li@hadoop102 ~]$ MYSTRING=Hi [li@hadoop102 ~]$ echo ${MYSTRING:-Hello World} Hi
指定默认值:
{PARAMENTER:=WORD}
{PARAMENTER=WORD}
这种模式与使用默认值的模式类似,但其区别在于,此种模式不仅扩展WORD,还将WORD赋值给参数PARAMENTER,作为PARAMENTER的值。如下实例:
[li@hadoop102 ~]$ unset MYSTRING [li@hadoop102 ~]$ echo $MYSTRING [li@hadoop102 ~]$ echo ${MYSTRING:=Hello World} Hello World [li@hadoop102 ~]$ echo $MYSTRING Hello World
使用替代值
{PARAMETER:+WORD}
{PARAMETER+WORD}
如果参数PARAMETER是未定义,或其值为空时,这种模式将不扩展任何内容。如果参数PARAMETER是定义的,且其值不为空,这种模式将扩展WORD,而不是扩展为参数PARAMETER的值。
$ MYSTRING="" $ echo ${MYSTRING:+ NOTE: M YSTRING seems to be set.} $ MYSTRING="Hi" $ echo ${MYSTRING:+ NOTE: M YSTRING seems to be set.} NOTE: MYSTRING seems to be set.
5.4.2 Bash的内部变量
Bash的内部变量会影响Bash脚本的行为。在本节中我们将介绍几个比较常用的Bash内部变量:
- $BASH变量:用于引用bash实例的全路径名
- $HOME变量:当前用户的home目录
- $IFS变量:IFS是内部字段分隔符的缩写
- $OSTYPE变量:操作系统的类型
- $SECONDS变量:脚本已经运行的秒数
- TMOUT变量被指定了一个非零的值,此值就会被bash的内部命令read作为默认的超时秒数。在一个交互式的Shell中,$TMOUT的值被作为命令行提示符等待输入的秒数,如果在指定的秒数内没有输入,Bash将自动被终结
- $UID变量:当前用户的账号标识码(ID号),与/etc/passwd中记录的相同
[li@hadoop102 ~]$ echo $BASH /bin/bash [li@hadoop102 ~]$ echo $HOME /home/li [li@hadoop102 ~]$ echo $IFS [li@hadoop102 ~]$ echo $OSTYPE linux-gnu [li@hadoop102 ~]$ echo $SECONDS 268 [li@hadoop102 ~]$ echo $UID 1000
5.4.3 Bash中的位置参数和特殊参数
Bash中的位置参数是由除0以外的一个或多个数字表示的参数。
位置参数是当Shell或Shell的函数被引用时由Shell或Shell函数的参数赋值,并且可以使用Bash的内部命令set来重新赋值。位置参数N可以被引用为N:
[li@hadoop102 ~]$ set 1 2 3 four five six 7 8 9 ten [li@hadoop102 ~]$ echo "$1 $2 $3 $4 $5 $6 $7 $8 $9 ${10}" 1 2 3 four five six 7 8 9 ten
位置参数不能通过赋值语句来赋值,而只能通过Bash的内部命令set和shift来设置和取消它们。当Shell函数运行时,位置参数会被临时地替换。
echo "Argument 1: $1" echo "Argument 2: $2" echo "Argument 3: $3" echo "Argument 4: $4" echo "Argument 5: $5" $ ./show_positional_param.sh one two three four five Argument 1: one Argument 2: two Argument 3: three Argument 4: four Argument 5: five
Bash对一些参数的处理比较特殊。这些参数只能被引用,但不能修改它们的值。这些特殊参数分别是*、、!、0和_。
**特殊参数***,将扩展为从1开始的所有位置参数。如果扩展发生在双引号内,即“”,则扩展为包含每个参数值的单词,每个参数值用特殊变量IFS的第一个字符分隔。也就是说,“$”等价于“$1c$2c…”,其中,c是特殊变量IFS的第一个字符。如果变量IFS没有定义,则参数之间默认用空格分隔。如果IFS为空,则参数直接相连,中间没有分隔。
[li@hadoop102 ~]$ set one two three [li@hadoop102 ~]$ echo "$*" one two three
特殊参数@,也将扩展为从1开始的所有位置参数。但当它的扩展发生在双引号内时,每个参数都扩展为分隔的单词。也就是说,“$@”等价于“$1” “$2” …。参数@与*之间的区别将在for循环的调用中明显地显现出来。
[li@hadoop102 test]$ set one two three [li@hadoop102 test]$ echo $@ one two three
特殊参数#,将扩展为位置参数的个数,用十进制表示。
[li@hadoop102 ~]$ set one two three [li@hadoop102 ~]$ echo $# 3
特殊参数?,将扩展为最近一个在前台执行的命令的退出状态。可以使用它来检查你的Shell脚本是否已成功地执行,通常退出状态0表示命令已经没有任何错误的结束运行。比如,我们创建一个文件,并使用ls命令列出这个文件,这些命令成功执行的话,则退出状态将是0,否则将是其它数值。
[li@hadoop102 ~]$ mkdir test [li@hadoop102 ~]$ cd test [li@hadoop102 test]$ touch newfile [li@hadoop102 test]$ echo $? 0 # 退出状态0表示命令已经没有任何错误地结束运行。 [li@hadoop102 test]$ ls newfile newfile [li@hadoop102 test]$ echo $? 0 [li@hadoop102 test]$ rm -f newfile [li@hadoop102 test]$ echo $? 0 [li@hadoop102 test]$ ls newfile ls: 无法访问newfile: 没有那个文件或目录 [li@hadoop102 test]$ echo $? 2 [li@hadoop102 test]$
特殊参数-,将扩展为当前的选项标志。这些选项是在调用时、或由内部命令set指定,或由Shell自身指定。
[li@hadoop102 test]$ echo $- himBH
特殊参数$,将扩展为当前Shell的进程号。在一个子Shell中,它扩展为调用Shell的进程号,而不是子Shell的。如下所示,打印当前Shell的进程号。
[li@hadoop102 test]$ echo $$ 2687
特殊参数!,将扩展为最近一次执行的后台命令的进程号。
[li@hadoop102 test]$ sleep 10 & [1] 3251 [li@hadoop102 test]$ echo $! 3251 [li@hadoop102 test]$
特殊参数0,将扩展为Shell或Shell脚本的名称。它是在Shell初始化时设置。如果Bash调用时带有脚本文件作为参数,$0就设置为脚本的文件名。
[li@hadoop102 test]$ echo $0 /bin/bash
特殊参数_,在Shell启动时,它被设为开始运行的Shell或Shell脚本的路径。随后,扩展为前一个命令的最后一个参数。
echo "The \$_is $_" uname -a echo $_ $ ./param_underscore.sh The $_is ./param_underscore.sh Linux localhost 2.6.18-238.9.1.el5PAE #1 SMP Tue Apr 12 19:28:32 EDT 2011 i686 i686 i386 GNU/Linux -a $ bash./param_underscore.sh The $_is /bin/bash Linux localhost 2.6.18-238.9.1.el5PAE #1 SMP Tue Apr 12 19:28:32 EDT 2011 i686 i686 i386 GNU/Linux -a
5.4.4 使用declare指定变量的类型
declare命令是Bash的内部命令,用于声明变量和修改变量的属性。它与Bash的另一个内部命令typeset的用法和用途完全相同。
如果直接使用declare命令,不指定变量名,将显示所有变量的值。
[li@hadoop102 test]$ declare ABRT_DEBUG_LOG=/dev/null BASH=/bin/bash BASHOPTS=checkwinsize:cmdhist:expand_aliases:extglob:extquote:force_fignore:histappend:interactive_comments:progcomp:promptvars:sourcepath BASH_ALIASES=() BASH_ARGC=() BASH_ARGV=() BASH_CMDS=() ...
使用-r选项,declare命令将把指定的变量定义为只读变量,这些变量将不能再被赋予新值或被清除。
[li@hadoop102 test]$ declare -r var=1 [li@hadoop102 test]$ var=2 bash: var: 只读变量
使用-i选项,declare命令将把指定的变量定义为整数型变量,赋予整数型变量的任何类型的值都将被转换成整数,下面通过实例来了解一下整数型变量的赋值。
[li@hadoop102 test]$ declare -i NUMBER [li@hadoop102 test]$ NUMBER=1.5 bash: 1.5: 语法错误: 无效的算术运算符 (错误符号是 ".5") [li@hadoop102 test]$ NUMBER=1 [li@hadoop102 test]$ NUMBER=one [li@hadoop102 test]$ echo $NUMBER 0
使用-x选项,declare命令将把指定的变量通过环境输出到后续命令。
使用-p选项,declare命令将显示指定变量的属性和值。
[li@hadoop102 test]$ declare -p NUMBER declare -i NUMBER="0"
5.4.5 Bash中的数组变量
一个数组是包含多个值的变量。任何变量也可以作为一个数组使用。数组的大小没有限制,也不需要成员变量是连续分配的。数组的索引是从0开始的,即第一个元素的索引是0。
间接声明一个数组变量的语法如下所示:
ARRAYNAME[INDEX]=value
INDEX是一个正数,或是一个值为正数的算术表达式。
显式声明一个数组变量是使用Bash的内部命令declare:
declare -a ARRAYNAME
带有一个索引编号的声明也是接受的,但索引编号将被忽略。数组的属性可以使用Bash的内部命令declare和readonly指定,这些属性将被应用到数组中的所有变量。
使用declare命令定义一个数组变量:
[li@hadoop102 test]$ declare -a linux=('Debin' 'Redhat' 'Suse' 'Fedora')
数组变量还可以使用复合赋值的格式:
ARRAYNAME=(value1 value2 … valueN)
如果引用数组时,不指定索引编号,则引用的将是数组中的第一元素,即使用索引编号0。
若要引用数组中某一项的内容,必须要使用大括号{}。如果索引编号是"@"或"*",那么数组的所有成员都将被引用。示例如下:
[li@hadoop102 test]$ echo ${linux[@]} Debin Redhat Suse Fedora [li@hadoop102 test]$ echo ${linux[*]} Debin Redhat Suse Fedora [li@hadoop102 test]$ [li@hadoop102 test]$ echo ${linux[2]} Suse
使用unset命令可以消除一个数组或数组的成员变量。
[li@hadoop102 ~]$ arr1=(one two three) [li@hadoop102 ~]$ unset arr1[1] [li@hadoop102 ~]$ echo ${arr1[@]} one three [li@hadoop102 ~]$ unset arr1 [li@hadoop102 ~]$ echo ${arr1[@]}
Bash的各种参数扩展也可以应用于数组变量。
5.5 Shell算术运算
Shell可以对算术表达式求值,它可以是Shell算术扩展,也可以由内部命令let来实现。求值时使用固定宽度的整数,并且不检查溢出,但是它可以捕获除以0的情况并报错。
5.5.1 Bash的算术运算符
Bash中的算术运算符以及它们的优先级、结合性和值都与C语言相同。
操作符 | 用途 |
id++ ,id-- | 变量后递增和后递减 |
++id,--id | 变量前递增和前递减 |
-,+ | 单目负号和正号 |
!~ | 逻辑取反,按位取反 |
** | 求幂 |
*,/,% | 乘,除,求余 |
+,- | 加,减 |
<<,>> | 按位左移,按位右移 |
<=,>=,<,> | 比较运算符 |
==,!= | 相等,不等 |
& | 按位与 |
^ | 按位异或 |
| | 按位或 |
&& | 逻辑与 |
|| | 逻辑或 |
expr?expr:expr | 条件运算符 |
=,*=,/=,%=,+=,-=, | |
<<=,>>=,&=,^=,|= | 赋值 |
expr1,expr2 | 逗号运算 |
下面我们通过几个实例,来了解几个运算符的使用。
求幂运算符:**
$ let var=5**2 $ echo $var 25
求余运算符:%
$let var=9%2 $echo $var 1
相加赋值运算符:+=
$echo $var 1 $let var+=10 $echo $var 11
相乘赋值运算符:*=
$echo $var 11 $let var*=5 $echo $var 55
逻辑与&&和逻辑或||运算符:
$echo $(( 2 && 3 )) 1 $echo $(( 2 && 0 )) 0 $echo $(( 2 || 3 )) 1 $echo $(( 0 || 0 )) 0
逗号运算符将两个或更多的算术运算连接在一起,所有的运算都被求值,但只有最后一个运算的值被返回。示例如下:
$let var=(2+3, 10-5, 20-6) $echo $var 14 $let var=(var1=10, 10%3) $echo $var 1 $echo $var1 10
逗号运算符只要在for循环中使用。
5.5.2 数字常量
默认情况下,Shell算术表达式都是使用十进制数,除非这个数字有特定的前缀或标记。以0开头的常量将被当作八进制数解释,而以"0x"或"0X"开头的数值将被解释为十六进制数。此外,如果数值的格式是BASE#NUMBER,BASE是介于2~64之间的十进制数,表示算术进制基数,比如,BASE是数字12,那么12#NUMBER就表示十二进制数,NUMBER即为此进制中的数值。
示例:
$ let dec=20 #默认为十进制数 s echo "Decimal number: $dec" Decimal number:20 $ let oct=020 #以0开头的八进制数 echo "Octal number: $oct" #换算的十进制为:2*(8的1次方)+0*(8的0次方)=16,其他进制的数学算法相同 Octal number: 16 $ let hex=0x20 #以0x开头的十六进制数 $ echo "Hexadecimal number: $hex" Hexadecimal number: 32 $ let bin=2#111 #符号“#”之前的数字2表示此数值为二进制 $ echo "Binary number: $bin" Binary number: 7 $ let base32=32#20 #三十二进制数,数值为20 $ echo "Base-32 number: $base32" Base-32 number:64 $ let base64=64#@ _ #在六十四进制中,十进制的0~9即用0~9表示,10-35这26个数依次用小写字母a~z表示, 36~61这26个数依次用大写字母A~z表示,最后剩余的62 和63分别用@和表示 $ echo "Base-64 number: $base64" Base-64number:4031
5.5.3 使用算术扩展和let进行算术运算
算术扩展可以对算术表达式求值并替换成所求得的值。它的格式是:
$ $((算术表达式))
注意:算术扩展中的运算数只能是整数,算术扩展不能对浮点数进行算术运算。
算术表达式中的所有符号都会进行参数扩展,字符串扩展、命令替换和引用去除。算术表达式也可以是嵌套的。如果算术表达式无效,Bash将打印指示错误的信息,并且不会进行任何替换。
$var=5 $var=$(( $var + 8)) $echo $var 13 $ x=17 $ y=2 $ z=$(( x%y )) $echo $z 1 $echo $(( 10>3 )) 1 $ a=28 $ b=25 $ c=$(( $(( a>b ))?a:b )) echo $c 28
let命令是Bash的内部命令,它也同样可以用于算术表达式的求值。let命令按照从左到右的顺序将提供给它的每一个参数进行算术表达式的求值。求值运算只能使用固定宽度的整数,并且不会检查溢出,但是它可以捕获除以0的情况并报错。当最后一个参数的求值结果为真时,let命令返回退出码0,否则返回退出码1。
let命令的功能和算数扩展基本相同。但let语句要求默认情况下在任何操作符两边不能含有空格,即所有算术表达式要连接在一起。若要在算术表达式中的符号之间使用空格就必须使用双引号将算术表达式括起。
let i=i+1 echo $i 5 let i=i +5 bash: let: +: syntax error: operand expected (error token is"+") echo $i 5 let "i=i + 5" echo $i 10 let i=1 let "i <<= 3" echo $i 8 let i++ echo $i 9 let "i = i<6 ? i : 6" echo $i 6
5.5.4 使用expr命令
expr命令是一个用于对表达式进行求值并输出相应结果的命令行工具。它同样也只支持整数运算数,不支持浮点运算数的运算。
与let命令相反,使用expr命令时,表达式中的运算符左右必须包含空格,如果没有空格,而是将运算符与运算数直接相连,expr命令将不会对表达式进行求值,而直接输出算术表达式。
使用expr命令时,对于某些运算符,还需要使用符号‘\’进行转义,否则提示语法错误。
使用expr命令给变量赋值时,需要使用Shell扩展中的命令替换。
expr 6 + 8 14 expr 6+8 6+8 expr 6 * 8 expr:syntax error expr 6 \* 8 48 expr 1 \< 2 1 expr 2 \> 5 0 a=15 b=35 expr $a \* $b 525 c='expr $a \* $b' echo $c 525 c=$( expr $a \* $b ) echo $c 525
5.6 退出脚本
每一个命令都会返回一个退出状态(有时被称作返回状态或退出码)。一个运行成功的命令会返回一个0。而不成功的会返回一个非0的值,它通常可以被解释为一个错误代码。功能良好的Linux命令、程序或工具当成功完成时,会返回退出状态码0。
同样地,Shell脚本和它里面的函数也会返回一个退出状态码。在Shell脚本或函数中,最后执行的一条命令决定其退出状态。
你可以通过检查Bash的特殊变量$?(请参见5.4.3节)来查看上一条命令运行后的退出状态码。
在你的脚本中检查你调用的程序的退出状态是非常重要的。当你的脚本运行完成时,返回一个有意义的退出状态也同样是非常重要的。
5.6.1 退出状态码
每一个命令都会返回一个退出状态(有时被称作返回状态或退出码)。一个运行成功的命令会返回一个0。而不成功的会返回一个非0的值,它通常可以被解释为一个错误代码。功能良好的Linux命令、程序或工具当成功完成时,会返回退出状态码0。
同样地,Shell脚本和它里面的函数也会返回一个退出状态码。在Shell脚本或函数中,最后执行的一条命令决定其退出状态。
你可以通过检查Bash的特殊变量$?(请参见5.4.3节)来查看上一条命令运行后的退出状态码。
在你的脚本中检查你调用的程序的退出状态是非常重要的。当你的脚本运行完成时,返回一个有意义的退出状态也同样是非常重要的。
5.6.2 使用exit命令
exit命令的语法如下所示:
$ exit N
exit命令语句用于从shell脚本中退出并返回指定的退出状态码N,来指示Shell脚本是否成功结束。当错误发生时,使用exit命令语句可以终结脚本的运行。当N为0时,表示脚本成功运行正常退出;而当N为非0时,表示脚本运行失败,由于错误而退出运行。
退出状态码N可以被其它命令或脚本使用来采取它们自己的行为。如果退出状态码N被省略,则将把最后一条运行的命令的退出状态作为脚本的退出状态码。
cd $SOME_DIR if [ $? -eq 0 ]; then rm -rf * else echo 'Cannot change directory!' exit 1 fi
上述代码中我们检查了cd命令的退出状态,如果其不为0,将打印一个错误信息,并使用exit命令终结脚本运行,返回退出状态码1。
再来看一个完整的备份脚本的示例:
#!/bin/bash BAK=/data TAPE=/dev/st0 echo "Trying to backup ${BAK} directory to tape device ${TAPE}.." [ ! -d $BAK ] && { echo "Source backup directory $BAK not found."; exit 1; } # 检查目录BAK是否存在,若不存在,则打印错误信息,并结束脚本,同时返回退出状态码1 [ ! -b $TAPE ] && {echo "Backup tape drive $TAPE not found or configured!"; exit 2;} # 检查磁带设备是否存在,若不存在,则打印错误信息,并结束脚本,同时返回退出状态码2 tar cvf $TAPE %BAK >& /tmp/error.log #开始备份 if [ $? -ne 0 ] # 如果备份失败,打印错误信息,并结束脚本,返回退出状态码3 then echo "An error occurred while making a tape backup, see /tmp/error.logfile." exit 3 fi exit 0 # 如果备份成功,则返回退出状态码0
在Shell脚本中对调用的程序进行退出状态检查,并根据退出状态做出相应的处理,当脚本退出运行时,明确地返回一个退出状态码,这对一个完善的Shell脚本来说,是不可或缺的。
5.7 调式脚本
Shell脚本调试的主要工作是发现引发脚本错误的原因,以及在脚本中定位发生错误的行。Bash提供了多种脚本调试的功能。但最常用的脚本调试方法是使用Bash的-x选项启动一个子Shell,它将以调试模式运行整个脚本,使Shell在执行脚本的过程中把实际执行的每一个命令行显示出来,并且在命令行的行首显示一个‘+’号,‘+’号后面显示的是经过了参数扩展之后的命令行的内容,有助于分析实际执行的是什么命令。
bash +x param_underscore.sh
上述输出结果中,前面有"+"号的行是Shell脚本实际执行的命令,其他行则是Shell脚本的打印输出信息。这一调试功能在自3.0以后的Bash大多数现代版本中可用。
Bash的执行选项除了可以在启动Shell是指定外,也可以在脚本中用set命令来指定。“set -选项”表示启动某选项,“set +选项”表示关闭某选项。所以我们可以在Shell脚本中使用“set -x”和“set +x”命令来调试脚本中的某一段代码。
set -x uname -a set +x
Bash中还有一个“-v”选项,该选项将激活详细输出模式,在这一模式中,由Bash读入的脚本的每一个命令行都将在执行前被打印输出。
$bash -V param_underscore.sh
通常,将-v选项和-x选项同时使用,可以得到更为详细的脚本调试信息
$bash -xV param_underscore.sh
从上面的几个实例你可能发现,-x选项虽然使用起来比较方便,但它输出的调试信息仅限于参数扩展之后的每一条实际执行的命令以及行首的一个‘+’号,但却没有代码行的行号这样的重要信息,这对于调试复杂的Shell脚本来说,是很不方便的。幸运的是,Bash的一些内部环境变量可以用来增强-x选项的输出信息,下面介绍几个有用的Bash内部环境变量:
- $LINENO:表示Shell脚本的当前行号。
- {FUNCNAME[0]}代表当前正在执行的Shell函数的名称,{FUNCNAME[0]}的函数的名字,以此类推。
- PS4的默认值。
利用变量PS4,我们就可以增强-x选项的输出信息。
$export PS4='+{$LINENO:${FUNCNAME[0]}}'
然后使用Bash的-xv选项来调试脚本param_underscore.sh:
$bash -xV param_underscore.sh
由于上面实例的脚本中没有函数,所以BASH_SOURCE、PS4,从而达到增强Bash的-x选项的输出信息的目的。
Bash中还有一个执行选项-n,它可用于测试Shell脚本中是否存在语法错误,它会读取脚本中的命令但不会执行它们。在编写完Shell脚本后,实际执行之前,最好首先使用-n选项来测试脚本中是否存在语法错误,这是一个好的习惯。因为某些Shell脚本在执行时会对系统环境产生影响,如果在实际执行时才发现语法错误,你可能不得不手工地做一些恢复工作才能继续测试这个脚本。
5.8 Shell脚本编程风格
脚本编程也应该符合一定的标准:
- 每个代码行不多于80个字符。
- 保持一致的缩进深度。程序结构的缩进应与逻辑嵌套深度保持一致。在每一个代码块之间留一个空行,可以提高脚本的可读性。
- 每个脚本文件必须要有一个文件头注释,任何一个不简短的且不显而易见的函数都需要注释,脚本中任何复杂的、不是显而易见的、以及重要的代码部分都需要注释。文件头提供文件名和它的内容等一些信息。
- 自定义的变量名或函数名使用小写字母,使用下划线‘_’分隔单词。
- 程序和脚本的返回值需要使用变量$?进行验证。
5.9 小结
下面我们总结一下本章所学的主要知识:
- #!(Shebang)是一个由井号‘#’和叹号‘!’构成的字符序列。它是出现在Shell脚本文件第一行的前两个字符。脚本中的#!行(第一行)用于指示一个解释程序。‘#!’后必须是解释程序的绝对路径。
- Shell脚本中,井号‘#’是注释标识符。如果脚本的某行含有#或以#开头,那么这一行在#之后的所有内容都将被解释程序忽略,#之后的这些内容被称为注释。
- 在运行Shell脚本前,要确保此Shell脚本具有可执行的权限。
- 参数扩展是从引用的实体取值的过程。字符“$”会引导参数扩展。将要扩展的参数名或符号可以放在大括号中。大括号虽然是可选的,但却可以保护待扩展的变量,使得紧跟在大括号后面的内容不会被扩展。
- 参数扩展的形式有多种,包括:基本的参数扩展、间接参数扩展、大小写修改、变量名扩展、字符串移除、字符串搜索与替换、求字符串长度、子字符串扩展、使用默认值、指定默认值、使用替代值等。
- Bash中的位置参数是由除0以外的一个或多个数字表示的参数。多于一个数字的位置参数在扩展时必须放在大括号中。比如,位置参数10在扩展时使用${10}。
- Bash对一些参数的处理比较特殊。这些参数只能被引用,但不能修改它们的值。这些特殊参数分别是*、@、#、?、-、$、!、0和_。
- declare命令是Bash的内部命令,用于声明变量和修改变量的属性。它与Bash的另一个内部命令typeset的用法和用途完全相同。
- 默认情况下,Shell算术表达式都是使用十进制数,除非这个数字有特定的前缀或标记。以0开头的常量将被当作八进制数解释,而已“0x”或“0X”开头的数值将被解释为十六进制数。
- 算术扩展可以对算术表达式求值并替换成所求得的值。算术扩展中的运算数只能是整数,算术扩展不能对浮点数进行算术运算。
- let命令是Bash的内部命令,它也同样可以用于算术表达式的求值。求值运算只能使用固定宽度的整数,并且不会检查溢出,但是它可以捕获除以0的情况并报错。
- Shell脚本调试的主要工作是发现引发脚本错误的原因,以及在脚本中定位发生错误的行。最常用的脚本调试方法是使用Bash的-x选项启动一个子Shell,它将以调试模式运行整个脚本。
- 对一个程序来说,只有当它的结构和作用能被除编写者之外的其他人简单地理解时,它才能够被维护,对它的成功修改才能在合理的时间内完成。若要满足这些需求,就要在脚本编程中使用好的编程风格。