开始学习Bash(下)
11.read 命令
用法
有时,脚本需要在执行过程中,由用户提供一部分数据,这时可以使用read
命令。它将用户的输入存入一个变量,方便后面的代码使用。用户按下回车键,就表示输入结束。
read
命令的格式如下。
read [-options] [variable...]
上面语法中,options
是参数选项,variable
是用来保存输入数值的一个或多个变量名。如果没有提供变量名,环境变量REPLY
会包含用户输入的一整行数据。
下面是一个例子demo.sh
。
#!/bin/bash
echo -n "输入一些文本 > "
read text
echo "你的输入:$text"
上面例子中,先显示一行提示文本,然后会等待用户输入文本。用户输入的文本,存入变量text
,在下一行显示出来。
$ bash demo.sh
输入一些文本 > 你好,世界
你的输入:你好,世界
read
可以接受用户输入的多个值。
#!/bin/bash
echo Please, enter your firstname and lastname
read FN LN
echo "Hi! $LN, $FN !"
上面例子中,read
根据用户的输入,同时为两个变量赋值。
如果用户的输入项少于read
命令给出的变量数目,那么额外的变量值为空。如果用户的输入项多于定义的变量,那么多余的输入项会包含到最后一个变量中。
如果read
命令之后没有定义变量名,那么环境变量REPLY
会包含所有的输入。
#!/bin/bash
# read-single: read multiple values into default variable
echo -n "Enter one or more values > "
read
echo "REPLY = '$REPLY'"
上面脚本的运行结果如下。
$ read-single
Enter one or more values > a b c d
REPLY = 'a b c d'
read
命令除了读取键盘输入,可以用来读取文件。
#!/bin/bash
filename='/etc/hosts'
while read myline
do
echo "$myline"
done < $filename
上面的例子通过read
命令,读取一个文件的内容。done
命令后面的定向符<
,将文件内容导向read
命令,每次读取一行,存入变量myline
,直到文件读取完毕。
参数
read
命令的参数如下。
(1)-t 参数
read
命令的-t
参数,设置了超时的秒数。如果超过了指定时间,用户仍然没有输入,脚本将放弃等待,继续向下执行。
#!/bin/bash
echo -n "输入一些文本 > "
if read -t 3 response; then
echo "用户已经输入了"
else
echo "用户没有输入"
fi
上面例子中,输入命令会等待3秒,如果用户超过这个时间没有输入,这个命令就会执行失败。if
根据命令的返回值,转入else
代码块,继续往下执行。
环境变量TMOUT
也可以起到同样作用,指定read
命令等待用户输入的时间(单位为秒)。
$ TMOUT=3
$ read response
上面例子也是等待3秒,如果用户还没有输入,就会超时。
(2)-p 参数
-p
参数指定用户输入的提示信息。
read -p "Enter one or more values > "
echo "REPLY = '$REPLY'"
上面例子中,先显示Enter one or more values >
,再接受用户的输入。
(3)-a 参数
-a
参数把用户的输入赋值给一个数组,从零号位置开始。
$ read -a people
alice duchess dodo
$ echo ${people[2]}
dodo
上面例子中,用户输入被赋值给一个数组people
,这个数组的2号成员就是dodo
。
(4)-n 参数
-n
参数指定只读取若干个字符作为变量值,而不是整行读取。
$ read -n 3 letter
abcdefghij
$ echo $letter
abc
上面例子中,变量letter
只包含3个字母。
(5)-e 参数
-e
参数允许用户输入的时候,使用readline
库提供的快捷键,比如自动补全。具体的快捷键可以参阅《行操作》一章。
#!/bin/bash
echo Please input the path to the file:
read -e fileName
echo $fileName
上面例子中,read
命令接受用户输入的文件名。这时,用户可能想使用 Tab 键的文件名“自动补全”功能,但是read
命令的输入默认不支持readline
库的功能。-e
参数就可以允许用户使用自动补全。
(6)其他参数
-d delimiter
:定义字符串delimiter
的第一个字符作为用户输入的结束,而不是一个换行符。-r
:raw 模式,表示不把用户输入的反斜杠字符解释为转义字符。-s
:使得用户的输入不显示在屏幕上,这常常用于输入密码或保密信息。-u fd
:使用文件描述符fd
作为输入。
IFS 变量
read
命令读取的值,默认是以空格分隔。可以通过自定义环境变量IFS
(内部字段分隔符,Internal Field Separator 的缩写),修改分隔标志。
IFS
的默认值是空格、Tab 符号、换行符号,通常取第一个(即空格)。
如果把IFS
定义成冒号(:
)或分号(;
),就可以分隔以这两个符号分隔的值,这对读取文件很有用。
#!/bin/bash
# read-ifs: read fields from a file
FILE=/etc/passwd
read -p "Enter a username > " user_name
file_info="$(grep "^$user_name:" $FILE)"
if [ -n "$file_info" ]; then
IFS=":" read user pw uid gid name home shell <<< "$file_info"
echo "User = '$user'"
echo "UID = '$uid'"
echo "GID = '$gid'"
echo "Full Name = '$name'"
echo "Home Dir. = '$home'"
echo "Shell = '$shell'"
else
echo "No such user '$user_name'" >&2
exit 1
fi
上面例子中,IFS
设为冒号,然后用来分解/etc/passwd
文件的一行。IFS
的赋值命令和read
命令写在一行,这样的话,IFS
的改变仅对后面的命令生效,该命令执行后IFS
会自动恢复原来的值。如果不写在一行,就要采用下面的写法。
OLD_IFS="$IFS"
IFS=":"
read user pw uid gid name home shell <<< "$file_info"
IFS="$OLD_IFS"
另外,上面例子中,<<<
是 Here 字符串,用于将变量值转为标准输入,因为read
命令只能解析标准输入。
如果IFS
设为空字符串,就等同于将整行读入一个变量。
#!/bin/bash
input="/path/to/txt/file"
while IFS= read -r line
do
echo "$line"
done < "$input"
上面的命令可以逐行读取文件,每一行存入变量line
,打印出来以后再读取下一行。
12.条件判断
本章介绍 Bash 脚本的条件判断语法。
if 结构
if
是最常用的条件判断结构,只有符合给定条件时,才会执行指定的命令。它的语法如下。
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
这个命令分成三个部分:if
、elif
和else
。其中,后两个部分是可选的。
if
关键字后面是主要的判断条件,elif
用来添加在主条件不成立时的其他判断条件,else
则是所有条件都不成立时要执行的部分。
if test $USER = "foo"; then
echo "Hello foo."
else
echo "You are not foo."
fi
上面的例子中,判断条件是环境变量$USER
是否等于foo
,如果等于就输出Hello foo.
,否则输出其他内容。
if
和then
写在同一行时,需要分号分隔。分号是 Bash 的命令分隔符。它们也可以写成两行,这时不需要分号。
if true
then
echo 'hello world'
fi
if false
then
echo 'it is false' # 本行不会执行
fi
上面的例子中,true
和false
是两个特殊命令,前者代表操作成功,后者代表操作失败。if true
意味着命令部分总是会执行,if false
意味着命令部分永远不会执行。
除了多行的写法,if
结构也可以写成单行。
$ if true; then echo 'hello world'; fi
hello world
$ if false; then echo "It's true."; fi
注意,if
关键字后面也可以是一条命令,该条命令执行成功(返回值0
),就意味着判断条件成立。
$ if echo 'hi'; then echo 'hello world'; fi
hi
hello world
上面命令中,if
后面是一条命令echo 'hi'
。该命令会执行,如果返回值是0
,则执行then
的部分。
if
后面可以跟任意数量的命令。这时,所有命令都会执行,但是判断真伪只看最后一个命令,即使前面所有命令都失败,只要最后一个命令返回0
,就会执行then
的部分。
$ if false; true; then echo 'hello world'; fi
hello world
上面例子中,if
后面有两条命令(false;true;
),第二条命令(true
)决定了then
的部分是否会执行。
elif
部分可以有多个。
#!/bin/bash
echo -n "输入一个1到3之间的数字(包含两端)> "
read character
if [ "$character" = "1" ]; then
echo 1
elif [ "$character" = "2" ]; then
echo 2
elif [ "$character" = "3" ]; then
echo 3
else
echo 输入不符合要求
fi
上面例子中,如果用户输入3
,就会连续判断3次。
test 命令
if
结构的判断条件,一般使用test
命令,有三种形式。
# 写法一
test expression
# 写法二
[ expression ]
# 写法三
[[ expression ]]
上面三种形式是等价的,但是第三种形式还支持正则判断,前两种不支持。
上面的expression
是一个表达式。这个表达式为真,test
命令执行成功(返回值为0
);表达式为伪,test
命令执行失败(返回值为1
)。注意,第二种和第三种写法,[
和]
与内部的表达式之间必须有空格。
$ test -f /etc/hosts
$ echo $?
0
$ [ -f /etc/hosts ]
$ echo $?
0
上面的例子中,test
命令采用两种写法,判断/etc/hosts
文件是否存在,这两种写法是等价的。命令执行后,返回值为0
,表示该文件确实存在。
实际上,[
这个字符是test
命令的一种简写形式,可以看作是一个独立的命令,这解释了为什么它后面必须有空格。
下面把test
命令的三种形式,用在if
结构中,判断一个文件是否存在。
# 写法一
if test -e /tmp/foo.txt ; then
echo "Found foo.txt"
fi
# 写法二
if [ -e /tmp/foo.txt ] ; then
echo "Found foo.txt"
fi
# 写法三
if [[ -e /tmp/foo.txt ]] ; then
echo "Found foo.txt"
fi
判断表达式
if
关键字后面,跟的是一个命令。这个命令可以是test
命令,也可以是其他命令。命令的返回值为0
表示判断成立,否则表示不成立。因为这些命令主要是为了得到返回值,所以可以视为表达式。
常用的判断表达式有下面这些。
文件判断
以下表达式用来判断文件状态。
[ -a file ]
:如果 file 存在,则为true
。[ -b file ]
:如果 file 存在并且是一个块(设备)文件,则为true
。[ -c file ]
:如果 file 存在并且是一个字符(设备)文件,则为true
。[ -d file ]
:如果 file 存在并且是一个目录,则为true
。[ -e file ]
:如果 file 存在,则为true
。[ -f file ]
:如果 file 存在并且是一个普通文件,则为true
。[ -g file ]
:如果 file 存在并且设置了组 ID,则为true
。[ -G file ]
:如果 file 存在并且属于有效的组 ID,则为true
。[ -h file ]
:如果 file 存在并且是符号链接,则为true
。[ -k file ]
:如果 file 存在并且设置了它的“sticky bit”,则为true
。[ -L file ]
:如果 file 存在并且是一个符号链接,则为true
。[ -N file ]
:如果 file 存在并且自上次读取后已被修改,则为true
。[ -O file ]
:如果 file 存在并且属于有效的用户 ID,则为true
。[ -p file ]
:如果 file 存在并且是一个命名管道,则为true
。[ -r file ]
:如果 file 存在并且可读(当前用户有可读权限),则为true
。[ -s file ]
:如果 file 存在且其长度大于零,则为true
。[ -S file ]
:如果 file 存在且是一个网络 socket,则为true
。[ -t fd ]
:如果 fd 是一个文件描述符,并且重定向到终端,则为true
。 这可以用来判断是否重定向了标准输入/输出/错误。[ -u file ]
:如果 file 存在并且设置了 setuid 位,则为true
。[ -w file ]
:如果 file 存在并且可写(当前用户拥有可写权限),则为true
。[ -x file ]
:如果 file 存在并且可执行(有效用户有执行/搜索权限),则为true
。[ FILE1 -nt FILE2 ]
:如果 FILE1 比 FILE2 的更新时间更近,或者 FILE1 存在而 FILE2 不存在,则为true
。[ FILE1 -ot FILE2 ]
:如果 FILE1 比 FILE2 的更新时间更旧,或者 FILE2 存在而 FILE1 不存在,则为true
。[ FILE1 -ef FILE2 ]
:如果 FILE1 和 FILE2 引用相同的设备和 inode 编号,则为true
。
下面是一个示例。
#!/bin/bash
FILE=~/.bashrc
if [ -e "$FILE" ]; then
if [ -f "$FILE" ]; then
echo "$FILE is a regular file."
fi
if [ -d "$FILE" ]; then
echo "$FILE is a directory."
fi
if [ -r "$FILE" ]; then
echo "$FILE is readable."
fi
if [ -w "$FILE" ]; then
echo "$FILE is writable."
fi
if [ -x "$FILE" ]; then
echo "$FILE is executable/searchable."
fi
else
echo "$FILE does not exist"
exit 1
fi
上面代码中,$FILE
要放在双引号之中,这样可以防止变量$FILE
为空,从而出错。因为$FILE
如果为空,这时[ -e $FILE ]
就变成[ -e ]
,这会被判断为真。而$FILE
放在双引号之中,[ -e "$FILE" ]
就变成[ -e "" ]
,这会被判断为伪。
字符串判断
以下表达式用来判断字符串。
[ string ]
:如果string
不为空(长度大于0),则判断为真。[ -n string ]
:如果字符串string
的长度大于零,则判断为真。[ -z string ]
:如果字符串string
的长度为零,则判断为真。[ string1 = string2 ]
:如果string1
和string2
相同,则判断为真。[ string1 == string2 ]
等同于[ string1 = string2 ]
。[ string1 != string2 ]
:如果string1
和string2
不相同,则判断为真。[ string1 '>' string2 ]
:如果按照字典顺序string1
排列在string2
之后,则判断为真。[ string1 '<' string2 ]
:如果按照字典顺序string1
排列在string2
之前,则判断为真。
注意,test
命令内部的>
和<
,必须用引号引起来(或者是用反斜杠转义)。否则,它们会被 shell 解释为重定向操作符。
下面是一个示例。
#!/bin/bash
ANSWER=maybe
if [ -z "$ANSWER" ]; then
echo "There is no answer." >&2
exit 1
fi
if [ "$ANSWER" = "yes" ]; then
echo "The answer is YES."
elif [ "$ANSWER" = "no" ]; then
echo "The answer is NO."
elif [ "$ANSWER" = "maybe" ]; then
echo "The answer is MAYBE."
else
echo "The answer is UNKNOWN."
fi
上面代码中,首先确定$ANSWER
字符串是否为空。如果为空,就终止脚本,并把退出状态设为1
。注意,这里的echo
命令把错误信息There is no answer.
重定向到标准错误,这是处理错误信息的常用方法。如果$ANSWER
字符串不为空,就判断它的值是否等于yes
、no
或者maybe
。
注意,字符串判断时,变量要放在双引号之中,比如[ -n "$COUNT" ]
,否则变量替换成字符串以后,test
命令可能会报错,提示参数过多。另外,如果不放在双引号之中,变量为空时,命令会变成[ -n ]
,这时会判断为真。如果放在双引号之中,[ -n "" ]
就判断为伪。
整数判断
下面的表达式用于判断整数。
[ integer1 -eq integer2 ]
:如果integer1
等于integer2
,则为true
。[ integer1 -ne integer2 ]
:如果integer1
不等于integer2
,则为true
。[ integer1 -le integer2 ]
:如果integer1
小于或等于integer2
,则为true
。[ integer1 -lt integer2 ]
:如果integer1
小于integer2
,则为true
。[ integer1 -ge integer2 ]
:如果integer1
大于或等于integer2
,则为true
。[ integer1 -gt integer2 ]
:如果integer1
大于integer2
,则为true
。
下面是一个用法的例子。
#!/bin/bash
INT=-5
if [ -z "$INT" ]; then
echo "INT is empty." >&2
exit 1
fi
if [ $INT -eq 0 ]; then
echo "INT is zero."
else
if [ $INT -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi
if [ $((INT % 2)) -eq 0 ]; then
echo "INT is even."
else
echo "INT is odd."
fi
fi
上面例子中,先判断变量$INT
是否为空,然后判断是否为0
,接着判断正负,最后通过求余数判断奇偶。
正则判断
[[ expression ]]
这种判断形式,支持正则表达式。
[[ string1 =~ regex ]]
上面的语法中,regex
是一个正则表示式,=~
是正则比较运算符。
下面是一个例子。
#!/bin/bash
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
echo "INT is an integer."
exit 0
else
echo "INT is not an integer." >&2
exit 1
fi
上面代码中,先判断变量INT
的字符串形式,是否满足^-?[0-9]+$
的正则模式,如果满足就表明它是一个整数。
test 判断的逻辑运算
通过逻辑运算,可以把多个test
判断表达式结合起来,创造更复杂的判断。三种逻辑运算AND
,OR
,和NOT
,都有自己的专用符号。
AND
运算:符号&&
,也可使用参数-a
。OR
运算:符号||
,也可使用参数-o
。NOT
运算:符号!
。
下面是一个AND
的例子,判断整数是否在某个范围之内。
#!/bin/bash
MIN_VAL=1
MAX_VAL=100
INT=50
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if [[ $INT -ge $MIN_VAL && $INT -le $MAX_VAL ]]; then
echo "$INT is within $MIN_VAL to $MAX_VAL."
else
echo "$INT is out of range."
fi
else
echo "INT is not an integer." >&2
exit 1
fi
上面例子中,&&
用来连接两个判断条件:大于等于$MIN_VAL
,并且小于等于$MAX_VAL
。
使用否定操作符!
时,最好用圆括号确定转义的范围。
if [ ! \( $INT -ge $MIN_VAL -a $INT -le $MAX_VAL \) ]; then
echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
echo "$INT is in range."
fi
上面例子中,test
命令内部使用的圆括号,必须使用引号或者转义,否则会被 Bash 解释。
算术判断
Bash 还提供了((...))
作为算术条件,进行算术运算的判断。
if ((3 > 2)); then
echo "true"
fi
上面代码执行后,会打印出true
。
注意,算术判断不需要使用test
命令,而是直接使用((...))
结构。这个结构的返回值,决定了判断的真伪。
如果算术计算的结果是非零值,则表示判断成立。这一点跟命令的返回值正好相反,需要小心。
$ if ((1)); then echo "It is true."; fi
It is true.
$ if ((0)); then echo "It is true."; else echo "it is false."; fi
It is false.
上面例子中,((1))
表示判断成立,((0))
表示判断不成立。
算术条件((...))
也可以用于变量赋值。
$ if (( foo = 5 ));then echo "foo is $foo"; fi
foo is 5
上面例子中,(( foo = 5 ))
完成了两件事情。首先把5
赋值给变量foo
,然后根据返回值5
,判断条件为真。
注意,赋值语句返回等号右边的值,如果返回的是0
,则判断为假。
$ if (( foo = 0 ));then echo "It is true.";else echo "It is false."; fi
It is false.
下面是用算术条件改写的数值判断脚本。
#!/bin/bash
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if ((INT == 0)); then
echo "INT is zero."
else
if ((INT < 0)); then
echo "INT is negative."
else
echo "INT is positive."
fi
if (( ((INT % 2)) == 0)); then
echo "INT is even."
else
echo "INT is odd."
fi
fi
else
echo "INT is not an integer." >&2
exit 1
fi
只要是算术表达式,都能用于((...))
语法,详见《Bash 的算术运算》一章。
普通命令的逻辑运算
如果if
结构使用的不是test
命令,而是普通命令,比如上一节的((...))
算术运算,或者test
命令与普通命令混用,那么可以使用 Bash 的命令控制操作符&&
(AND)和||
(OR),进行多个命令的逻辑运算。
$ command1 && command2
$ command1 || command2
对于&&
操作符,先执行command1
,只有command1
执行成功后, 才会执行command2
。对于||
操作符,先执行command1
,只有command1
执行失败后, 才会执行command2
。
$ mkdir temp && cd temp
上面的命令会创建一个名为temp
的目录,执行成功后,才会执行第二个命令,进入这个目录。
$ [ -d temp ] || mkdir temp
上面的命令会测试目录temp
是否存在,如果不存在,就会执行第二个命令,创建这个目录。这种写法非常有助于在脚本中处理错误。
[ ! -d temp ] && exit 1
上面的命令中,如果temp
子目录不存在,脚本会终止,并且返回值为1
。
下面就是if
与&&
结合使用的写法。
if [ condition ] && [ condition ]; then
command
fi
下面是一个示例。
#! /bin/bash
filename=$1
word1=$2
word2=$3
if grep $word1 $filename && grep $word2 $filename
then
echo "$word1 and $word2 are both in $filename."
fi
上面的例子只有在指定文件里面,同时存在搜索词word1
和word2
,就会执行if
的命令部分。
下面的示例演示如何将一个&&
判断表达式,改写成对应的if
结构。
[[ -d "$dir_name" ]] && cd "$dir_name" && rm *
# 等同于
if [[ ! -d "$dir_name" ]]; then
echo "No such directory: '$dir_name'" >&2
exit 1
fi
if ! cd "$dir_name"; then
echo "Cannot cd to '$dir_name'" >&2
exit 1
fi
if ! rm *; then
echo "File deletion failed. Check results" >&2
exit 1
fi
case 结构
case
结构用于多值判断,可以为每个值指定对应的命令,跟包含多个elif
的if
结构等价,但是语义更好。它的语法如下。
case expression in
pattern )
commands ;;
pattern )
commands ;;
...
esac
上面代码中,expression
是一个表达式,pattern
是表达式的值或者一个模式,可以有多条,用来匹配多个值,每条以两个分号(;
)结尾。
#!/bin/bash
echo -n "输入一个1到3之间的数字(包含两端)> "
read character
case $character in
1 ) echo 1
;;
2 ) echo 2
;;
3 ) echo 3
;;
* ) echo 输入不符合要求
esac
上面例子中,最后一条匹配语句的模式是*
,这个通配符可以匹配其他字符和没有输入字符的情况,类似if
的else
部分。
下面是另一个例子。
#!/bin/bash
OS=$(uname -s)
case "$OS" in
FreeBSD) echo "This is FreeBSD" ;;
Darwin) echo "This is Mac OSX" ;;
AIX) echo "This is AIX" ;;
Minix) echo "This is Minix" ;;
Linux) echo "This is Linux" ;;
*) echo "Failed to identify this OS" ;;
esac
上面的例子判断当前是什么操作系统。
case
的匹配模式可以使用各种通配符,下面是一些例子。
a)
:匹配a
。a|b)
:匹配a
或b
。[[:alpha:]])
:匹配单个字母。???)
:匹配3个字符的单词。*.txt)
:匹配.txt
结尾。*)
:匹配任意输入,通过作为case
结构的最后一个模式。
#!/bin/bash
echo -n "输入一个字母或数字 > "
read character
case $character in
[[:lower:]] | [[:upper:]] ) echo "输入了字母 $character"
;;
[0-9] ) echo "输入了数字 $character"
;;
* ) echo "输入不符合要求"
esac
上面例子中,使用通配符[[:lower:]] | [[:upper:]]
匹配字母,[0-9]
匹配数字。
Bash 4.0之前,case
结构只能匹配一个条件,然后就会退出case
结构。Bash 4.0之后,允许匹配多个条件,这时可以用;;&
终止每个条件块。
#!/bin/bash
# test.sh
read -n 1 -p "Type a character > "
echo
case $REPLY in
[[:upper:]]) echo "'$REPLY' is upper case." ;;&
[[:lower:]]) echo "'$REPLY' is lower case." ;;&
[[:alpha:]]) echo "'$REPLY' is alphabetic." ;;&
[[:digit:]]) echo "'$REPLY' is a digit." ;;&
[[:graph:]]) echo "'$REPLY' is a visible character." ;;&
[[:punct:]]) echo "'$REPLY' is a punctuation symbol." ;;&
[[:space:]]) echo "'$REPLY' is a whitespace character." ;;&
[[:xdigit:]]) echo "'$REPLY' is a hexadecimal digit." ;;&
esac
执行上面的脚本,会得到下面的结果。
$ test.sh
Type a character > a
'a' is lower case.
'a' is alphabetic.
'a' is a visible character.
'a' is a hexadecimal digit.
可以看到条件语句结尾添加了;;&
以后,在匹配一个条件之后,并没有退出case
结构,而是继续判断下一个条件。
参考链接
- The Linux Command Line, William Shotts
13.循环
Bash 提供三种循环语法for
、while
和until
。
while 循环
while
循环有一个判断条件,只要符合条件,就不断循环执行指定的语句。
while condition; do
commands
done
上面代码中,只要满足条件condition
,就会执行命令commands
。然后,再次判断是否满足条件condition
,只要满足,就会一直执行下去。只有不满足条件,才会退出循环。
循环条件condition
可以使用test
命令,跟if
结构的判断条件写法一致。
#!/bin/bash
number=0
while [ "$number" -lt 10 ]; do
echo "Number = $number"
number=$((number + 1))
done
上面例子中,只要变量$number
小于10,就会不断加1,直到$number
等于10,然后退出循环。
关键字do
可以跟while
不在同一行,这时两者之间不需要使用分号分隔。
while true
do
echo 'Hi, while looping ...';
done
上面的例子会无限循环,可以按下 Ctrl + c 停止。
while
循环写成一行,也是可以的。
$ while true; do echo 'Hi, while looping ...'; done
while
的条件部分也可以是执行一个命令。
$ while echo 'ECHO'; do echo 'Hi, while looping ...'; done
上面例子中,判断条件是echo 'ECHO'
。由于这个命令总是执行成功,所以上面命令会产生无限循环。
while
的条件部分可以执行任意数量的命令,但是执行结果的真伪只看最后一个命令的执行结果。
$ while true; false; do echo 'Hi, looping ...'; done
上面代码运行后,不会有任何输出,因为while
的最后一个命令是false
。
until 循环
until
循环与while
循环恰好相反,只要不符合判断条件(判断条件失败),就不断循环执行指定的语句。一旦符合判断条件,就退出循环。
until condition; do
commands
done
关键字do
可以与until
不写在同一行,这时两者之间不需要分号分隔。
until condition
do
commands
done
下面是一个例子。
$ until false; do echo 'Hi, until looping ...'; done
Hi, until looping ...
Hi, until looping ...
Hi, until looping ...
^C
上面代码中,until
的部分一直为false
,导致命令无限运行,必须按下 Ctrl + c 终止。
#!/bin/bash
number=0
until [ "$number" -ge 10 ]; do
echo "Number = $number"
number=$((number + 1))
done
上面例子中,只要变量number
小于10,就会不断加1,直到number
大于等于10,就退出循环。
until
的条件部分也可以是一个命令,表示在这个命令执行成功之前,不断重复尝试。
until cp $1 $2; do
echo 'Attempt to copy failed. waiting...'
sleep 5
done
上面例子表示,只要cp $1 $2
这个命令执行不成功,就5秒钟后再尝试一次,直到成功为止。
until
循环都可以转为while
循环,只要把条件设为否定即可。上面这个例子可以改写如下。
while ! cp $1 $2; do
echo 'Attempt to copy failed. waiting...'
sleep 5
done
一般来说,until
用得比较少,完全可以统一都使用while
。
for...in 循环
for...in
循环用于遍历列表的每一项。
for variable in list
do
commands
done
上面语法中,for
循环会依次从list
列表中取出一项,作为变量variable
,然后在循环体中进行处理。
关键词do
可以跟for
写在同一行,两者使用分号分隔。
for variable in list; do
commands
done
下面是一个例子。
#!/bin/bash
for i in word1 word2 word3; do
echo $i
done
上面例子中,word1 word2 word3
是一个包含三个单词的列表,变量i
依次等于word1
、word2
、word3
,命令echo $i
则会相应地执行三次。
列表可以由通配符产生。
for i in *.png; do
ls -l $i
done
上面例子中,*.png
会替换成当前目录中所有 PNG 图片文件,变量i
会依次等于每一个文件。
列表也可以通过子命令产生。
#!/bin/bash
count=0
for i in $(cat ~/.bash_profile); do
count=$((count + 1))
echo "Word $count ($i) contains $(echo -n $i | wc -c) characters"
done
上面例子中,cat ~/.bash_profile
命令会输出~/.bash_profile
文件的内容,然后通过遍历每一个词,计算该文件一共包含多少个词,以及每个词有多少个字符。
in list
的部分可以省略,这时list
默认等于脚本的所有参数$@
。但是,为了可读性,最好还是不要省略,参考下面的例子。
for filename; do
echo "$filename"
done
# 等同于
for filename in "$@" ; do
echo "$filename"
done
在函数体中也是一样的,for...in
循环省略in list
的部分,则list
默认等于函数的所有参数。
for 循环
for
循环还支持 C 语言的循环语法。
for (( expression1; expression2; expression3 )); do
commands
done
上面代码中,expression1
用来初始化循环条件,expression2
用来决定循环结束的条件,expression3
在每次循环迭代的末尾执行,用于更新值。
注意,循环条件放在双重圆括号之中。另外,圆括号之中使用变量,不必加上美元符号$
。
它等同于下面的while
循环。
(( expression1 ))
while (( expression2 )); do
commands
(( expression3 ))
done
下面是一个例子。
for (( i=0; i<5; i=i+1 )); do
echo $i
done
上面代码中,初始化变量i
的值为0,循环执行的条件是i
小于5。每次循环迭代结束时,i
的值加1。
for
条件部分的三个语句,都可以省略。
for ((;;))
do
read var
if [ "$var" = "." ]; then
break
fi
done
上面脚本会反复读取命令行输入,直到用户输入了一个点(.
)为止,才会跳出循环。
break,continue
Bash 提供了两个内部命令break
和continue
,用来在循环内部跳出循环。
break
命令立即终止循环,程序继续执行循环块之后的语句,即不再执行剩下的循环。
#!/bin/bash
for number in 1 2 3 4 5 6
do
echo "number is $number"
if [ "$number" = "3" ]; then
break
fi
done
上面例子只会打印3行结果。一旦变量$number
等于3,就会跳出循环,不再继续执行。
continue
命令立即终止本轮循环,开始执行下一轮循环。
#!/bin/bash
while read -p "What file do you want to test?" filename
do
if [ ! -e "$filename" ]; then
echo "The file does not exist."
continue
fi
echo "You entered a valid file.."
done
上面例子中,只要用户输入的文件不存在,continue
命令就会生效,直接进入下一轮循环(让用户重新输入文件名),不再执行后面的打印语句。
select 结构
select
结构主要用来生成简单的菜单。它的语法与for...in
循环基本一致。
select name
[in list]
do
commands
done
Bash 会对select
依次进行下面的处理。
select
生成一个菜单,内容是列表list
的每一项,并且每一项前面还有一个数字编号。- Bash 提示用户选择一项,输入它的编号。
- 用户输入以后,Bash 会将该项的内容存在变量
name
,该项的编号存入环境变量REPLY
。如果用户没有输入,就按回车键,Bash 会重新输出菜单,让用户选择。 - 执行命令体
commands
。 - 执行结束后,回到第一步,重复这个过程。
下面是一个例子。
#!/bin/bash
# select.sh
select brand in Samsung Sony iphone symphony Walton
do
echo "You have chosen $brand"
done
执行上面的脚本,Bash 会输出一个品牌的列表,让用户选择。
$ ./select.sh
1) Samsung
2) Sony
3) iphone
4) symphony
5) Walton
#?
如果用户没有输入编号,直接按回车键。Bash 就会重新输出一遍这个菜单,直到用户按下Ctrl + c
,退出执行。
select
可以与case
结合,针对不同项,执行不同的命令。
#!/bin/bash
echo "Which Operating System do you like?"
select os in Ubuntu LinuxMint Windows8 Windows10 WindowsXP
do
case $os in
"Ubuntu"|"LinuxMint")
echo "I also use $os."
;;
"Windows8" | "Windows10" | "WindowsXP")
echo "Why don't you try Linux?"
;;
*)
echo "Invalid entry."
break
;;
esac
done
上面例子中,case
针对用户选择的不同项,执行不同的命令。
参考链接
- Bash Select Command, Fahmida Yesmin
14.Bash 函数
本章介绍 Bash 函数的用法。
简介
函数(function)是可以重复使用的代码片段,有利于代码的复用。它与别名(alias)的区别是,别名只适合封装简单的单个命令,函数则可以封装复杂的多行命令。
函数总是在当前 Shell 执行,这是跟脚本的一个重大区别,Bash 会新建一个子 Shell 执行脚本。如果函数与脚本同名,函数会优先执行。但是,函数的优先级不如别名,即如果函数与别名同名,那么别名优先执行。
Bash 函数定义的语法有两种。
# 第一种
fn() {
# codes
}
# 第二种
function fn() {
# codes
}
上面代码中,fn
是自定义的函数名,函数代码就写在大括号之中。这两种写法是等价的。
下面是一个简单函数的例子。
hello() {
echo "Hello $1"
}
上面代码中,函数体里面的$1
表示函数调用时的第一个参数。
调用时,就直接写函数名,参数跟在函数名后面。
$ hello world
Hello world
下面是一个多行函数的例子,显示当前日期时间。
today() {
echo -n "Today's date is: "
date +"%A, %B %-d, %Y"
}
删除一个函数,可以使用unset
命令。
unset -f functionName
查看当前 Shell 已经定义的所有函数,可以使用declare
命令。
$ declare -f
上面的declare
命令不仅会输出函数名,还会输出所有定义。输出顺序是按照函数名的字母表顺序。由于会输出很多内容,最好通过管道命令配合more
或less
使用。
declare
命令还支持查看单个函数的定义。
$ declare -f functionName
declare -F
可以输出所有已经定义的函数名,不含函数体。
$ declare -F
参数变量
函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的。
$1
~$9
:函数的第一个到第9个的参数。$0
:函数所在的脚本名。$#
:函数的参数总数。$@
:函数的全部参数,参数之间使用空格分隔。$*
:函数的全部参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,但是可以自定义。
如果函数的参数多于9个,那么第10个参数可以用${10}
的形式引用,以此类推。
下面是一个示例脚本test.sh
。
#!/bin/bash
# test.sh
function alice {
echo "alice: $@"
echo "$0: $1 $2 $3 $4"
echo "$# arguments"
}
alice in wonderland
运行该脚本,结果如下。
$ bash test.sh
alice: in wonderland
test.sh: in wonderland
2 arguments
上面例子中,由于函数alice
只有第一个和第二个参数,所以第三个和第四个参数为空。
下面是一个日志函数的例子。
function log_msg {
echo "[`date '+ %F %T'` ]: $@"
}
使用方法如下。
$ log_msg "This is sample log message"
[ 2018-08-16 19:56:34 ]: This is sample log message
return 命令
return
命令用于从函数返回一个值。函数执行到这条命令,就不再往下执行了,直接返回了。
function func_return_value {
return 10
}
函数将返回值返回给调用者。如果命令行直接执行函数,下一个命令可以用$?
拿到返回值。
$ func_return_value
$ echo "Value returned by function is: $?"
Value returned by function is: 10
return
后面不跟参数,只用于返回也是可以的。
function name {
commands
return
}
全局变量和局部变量,local 命令
Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取。这一点需要特别小心。
# 脚本 test.sh
fn () {
foo=1
echo "fn: foo = $foo"
}
fn
echo "global: foo = $foo"
上面脚本的运行结果如下。
$ bash test.sh
fn: foo = 1
global: foo = 1
上面例子中,变量$foo
是在函数fn
内部声明的,函数体外也可以读取。
函数体内不仅可以声明全局变量,还可以修改全局变量。
#! /bin/bash
foo=1
fn () {
foo=2
}
fn
echo $foo
上面代码执行后,输出的变量$foo
值为2。
函数里面可以用local
命令声明局部变量。
#! /bin/bash
# 脚本 test.sh
fn () {
local foo
foo=1
echo "fn: foo = $foo"
}
fn
echo "global: foo = $foo"
上面脚本的运行结果如下。
$ bash test.sh
fn: foo = 1
global: foo =
上面例子中,local
命令声明的$foo
变量,只在函数体内有效,函数体外没有定义。
参考链接
- How to define and use functions in Linux Shell Script, by Pradeep Kumar
15.数组
数组(array)是一个包含多个值的变量。成员的编号从0开始,数量没有上限,也没有要求成员被连续索引。
创建数组
数组可以采用逐个赋值的方法创建。
ARRAY[INDEX]=value
上面语法中,ARRAY
是数组的名字,可以是任意合法的变量名。INDEX
是一个大于或等于零的整数,也可以是算术表达式。注意数组第一个元素的下标是0, 而不是1。
下面创建一个三个成员的数组。
$ array[0]=val
$ array[1]=val
$ array[2]=val
数组也可以采用一次性赋值的方式创建。
ARRAY=(value1 value2 ... valueN)
# 等同于
ARRAY=(
value1
value2
value3
)
采用上面方式创建数组时,可以按照默认顺序赋值,也可以在每个值前面指定位置。
$ array=(a b c)
$ array=([2]=c [0]=a [1]=b)
$ days=(Sun Mon Tue Wed Thu Fri Sat)
$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)
只为某些值指定位置,也是可以的。
names=(hatter [5]=duchess alice)
上面例子中,hatter
是数组的0号位置,duchess
是5号位置,alice
是6号位置。
没有赋值的数组元素的默认值是空字符串。
定义数组的时候,可以使用通配符。
$ mp3s=( *.mp3 )
上面例子中,将当前目录的所有 MP3 文件,放进一个数组。
先用declare -a
命令声明一个数组,也是可以的。
$ declare -a ARRAYNAME
read -a
命令则是将用户的命令行输入,存入一个数组。
$ read -a dice
上面命令将用户的命令行输入,存入数组dice
。
读取数组
读取单个元素
读取数组指定位置的成员,要使用下面的语法。
$ echo ${array[i]} # i 是索引
上面语法里面的大括号是必不可少的,否则 Bash 会把索引部分[i]
按照原样输出。
$ array[0]=a
$ echo ${array[0]}
a
$ echo $array[0]
a[0]
上面例子中,数组的第一个元素是a
。如果不加大括号,Bash 会直接读取$array
首成员的值,然后将[0]
按照原样输出。
读取所有成员
@
和*
是数组的特殊索引,表示返回数组的所有成员。
$ foo=(a b c d e f)
$ echo ${foo[@]}
a b c d e f
这两个特殊索引配合for
循环,就可以用来遍历数组。
for i in "${names[@]}"; do
echo $i
done
@
和*
放不放在双引号之中,是有差别的。
$ activities=( swimming "water skiing" canoeing "white-water rafting" surfing )
$ for act in ${activities[@]}; \
do \
echo "Activity: $act"; \
done
Activity: swimming
Activity: water
Activity: skiing
Activity: canoeing
Activity: white-water
Activity: rafting
Activity: surfing
上面的例子中,数组activities
实际包含5个成员,但是for...in
循环直接遍历${activities[@]}
,导致返回7个结果。为了避免这种情况,一般把${activities[@]}
放在双引号之中。
$ for act in "${activities[@]}"; \
do \
echo "Activity: $act"; \
done
Activity: swimming
Activity: water skiing
Activity: canoeing
Activity: white-water rafting
Activity: surfing
上面例子中,${activities[@]}
放在双引号之中,遍历就会返回正确的结果。
${activities[*]}
不放在双引号之中,跟${activities[@]}
不放在双引号之中是一样的。
$ for act in ${activities[*]}; \
do \
echo "Activity: $act"; \
done
Activity: swimming
Activity: water
Activity: skiing
Activity: canoeing
Activity: white-water
Activity: rafting
Activity: surfing
${activities[*]}
放在双引号之中,所有成员就会变成单个字符串返回。
$ for act in "${activities[*]}"; \
do \
echo "Activity: $act"; \
done
Activity: swimming water skiing canoeing white-water rafting surfing
所以,拷贝一个数组的最方便方法,就是写成下面这样。
$ hobbies=( "${activities[@]}" )
上面例子中,数组activities
被拷贝给了另一个数组hobbies
。
这种写法也可以用来为新数组添加成员。
$ hobbies=( "${activities[@]}" diving )
上面例子中,新数组hobbies
在数组activities
的所有成员之后,又添加了一个成员。
默认位置
如果读取数组成员时,没有读取指定哪一个位置的成员,默认使用0
号位置。
$ declare -a foo
$ foo=A
$ echo ${foo[0]}
A
上面例子中,foo
是一个数组,赋值的时候不指定位置,实际上是给foo[0]
赋值。
引用一个不带下标的数组变量,则引用的是0
号位置的数组元素。
$ foo=(a b c d e f)
$ echo ${foo}
a
$ echo $foo
a
上面例子中,引用数组元素的时候,没有指定位置,结果返回的是0
号位置。
数组的长度
要想知道数组的长度(即一共包含多少成员),可以使用下面两种语法。
${#array[*]}
${#array[@]}
下面是一个例子。
$ a[100]=foo
$ echo ${#a[*]}
1
$ echo ${#a[@]}
1
上面例子中,把字符串赋值给100
位置的数组元素,这时的数组只有一个元素。
提问,某些索引存在空值时,空值所对应的索引会被计数到数组长度吗?
注意,如果用这种语法去读取具体的数组成员,就会返回该成员的字符串长度。这一点必须小心。
$ a[100]=foo
$ echo ${#a[100]}
3
上面例子中,${#a[100]}
实际上是返回数组第100号成员a[100]
的值(foo
)的字符串长度。
提取数组序号
${!array[@]}
或${!array[*]}
,可以返回数组的成员序号,即哪些位置是有值的。
$ arr=([5]=a [9]=b [23]=c)
$ echo ${!arr[@]}
5 9 23
$ echo ${!arr[*]}
5 9 23
上面例子中,数组的5、9、23号位置有值。
利用这个语法,也可以通过for
循环遍历数组。
arr=(a b c d)
for i in ${!arr[@]};do
echo ${arr[i]}
done
提取数组成员
${array[@]:position:length}
的语法可以提取数组成员。
$ food=( apples bananas cucumbers dates eggs fajitas grapes )
$ echo ${food[@]:1:1}
bananas
$ echo ${food[@]:1:3}
bananas cucumbers dates
上面例子中,${food[@]:1:1}
返回从数组1号位置开始的1个成员,${food[@]:1:3}
返回从1号位置开始的3个成员。
如果省略长度参数length
,则返回从指定位置开始的所有成员。
$ echo ${food[@]:4}
eggs fajitas grapes
上面例子返回从4号位置开始到结束的所有成员。
提问:position的值和length的值为负数时,是不是变为反方向操作?
追加数组成员
数组末尾追加成员,可以使用+=
赋值运算符。它能够自动地把值追加到数组末尾。否则,就需要知道数组的最大序号,比较麻烦。
$ foo=(a b c)
$ echo ${foo[@]}
a b c
$ foo+=(d e f)
$ echo ${foo[@]}
a b c d e f
删除数组
删除一个数组成员,使用unset
命令。
$ foo=(a b c d e f)
$ echo ${foo[@]}
a b c d e f
$ unset foo[2]
$ echo ${foo[@]}
a b d e f
上面例子中,删除了数组中的第三个元素,下标为2。
将某个成员设为空值,可以从返回值中“隐藏”这个成员。
$ foo=(a b c d e f)
$ foo[1]=''
$ echo ${foo[@]}
a c d e f
上面例子中,将数组的第二个成员设为空字符串,数组的返回值中,这个成员就“隐藏”了。
注意,这里是“隐藏”,而不是删除,因为这个成员仍然存在,只是值变成了空值。
$ foo=(a b c d e f)
$ foo[1]=''
$ echo ${#foo[@]}
6
$ echo ${!foo[@]}
0 1 2 3 4 5
上面代码中,第二个成员设为空值后,数组仍然包含6个成员。
由于空值就是空字符串,所以下面这样写也有隐藏效果,但是不建议这种写法。
$ foo[1]=
上面的写法也相当于“隐藏”了数组的第二个成员。
直接将数组变量赋值为空字符串,相当于“隐藏”数组的第一个成员。
$ foo=(a b c d e f)
$ foo=''
$ echo ${foo[@]}
b c d e f
上面的写法相当于“隐藏”了数组的第一个成员。
unset ArrayName
可以清空整个数组。
$ unset ARRAY
$ echo ${ARRAY[*]}
<--no output-->
关联数组
Bash 的新版本支持关联数组。关联数组使用字符串而不是整数作为数组索引。
declare -A
可以声明关联数组。
declare -A colors
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"
关联数组必须用带有-A
选项的declare
命令声明创建。相比之下,整数索引的数组,可以直接使用变量名创建数组,关联数组就不行。
访问关联数组成员的方式,几乎与整数索引数组相同。
echo ${colors["blue"]}
16.set 命令,shopt 命令
set
命令是 Bash 脚本的重要环节,却常常被忽视,导致脚本的安全性和可维护性出问题。本章介绍set
的基本用法,帮助你写出更安全的 Bash 脚本。
简介
我们知道,Bash 执行脚本时,会创建一个子 Shell。
$ bash script.sh
上面代码中,script.sh
是在一个子 Shell 里面执行。这个子 Shell 就是脚本的执行环境,Bash 默认给定了这个环境的各种参数。
set
命令用来修改子 Shell 环境的运行参数,即定制环境。一共有十几个参数可以定制,官方手册有完整清单,本章介绍其中最常用的几个。
以下内容来自官方手册
set [--abefhkmnptuvxBCEHPT] [-o选项名称] [参数...] set [+abefhkmnptuvxBCEHPT] [+o选项名称] [参数…]
如果未提供任何选项或参数,则set显示所有 shell 变量和函数的名称和值,根据当前语言环境排序,其格式可重复用作设置或重置当前设置变量的输入。只读变量不能被重置。在POSIX模式下,仅列出 shell 变量。
提供选项时,它们会设置或取消设置 shell 属性。选项(如果指定)具有以下含义:
顺便提一下,如果命令行下不带任何参数,直接运行
set
,会显示所有的环境变量和 Shell 函数。
-a
创建或修改的每个变量或函数都被赋予导出属性并标记为导出到后续命令的环境。-b
导致立即报告已终止后台作业的状态,而不是在打印下一个主要提示之前。-e
如果可能由单个简单命令(请参阅简单命令)、列表(请参阅列表)或复合命令(请参阅复合命令)组成的管道(请参阅管道)返回非零状态,则立即退出。如果失败的命令是紧跟在or关键字之后的命令列表的一部分、语句中测试的一部分、在 a或列表中执行的任何命令的一部分(除了最后一个or之后的命令、在一个管道但最后一个,或者如果命令的返回状态正在与whileuntilif&&||&&||!. 如果子 shell 以外的复合命令由于命令失败而返回非零状态-e被忽略,shell 不会退出。如果设置了trap ERR,则会在 shell 退出之前执行。此选项分别适用于 shell 环境和每个子 shell 环境(请参阅命令执行环境),并可能导致子 shell 在执行子 shell 中的所有命令之前退出。
如果复合命令或 shell 函数在上下文中执行 -e被忽略时,在复合命令或函数体中执行的任何命令都不会受到-e设置,即使-e设置并且命令返回失败状态。如果复合命令或 shell 函数设置-e在上下文中执行时-e被忽略,在复合命令或包含函数调用的命令完成之前,该设置将不起作用。
-f
禁用文件名扩展(通配符)。-h
查找并记住(散列)命令,因为它们被查找执行。默认情况下启用此选项。-k
赋值语句形式的所有参数都放置在命令的环境中,而不仅仅是命令名称之前的参数。-m
作业控制已启用(请参阅作业控制)。所有进程都在一个单独的进程组中运行。当后台作业完成时,shell 会打印一行包含其退出状态的行。-n
读取命令但不执行它们。这可用于检查脚本的语法错误。交互式 shell 会忽略此选项。-o option-name
设置与option-name对应的选项:allexport
与 相同-a。braceexpand
与 相同-B。emacs
使用emacs-style 行编辑界面(请参阅命令行编辑)。这也会影响用于read -e.errexit
与 相同-e。errtrace
与 相同-E。functrace
与 相同-T。hashall
与 相同-h。histexpand
与 相同-H。history
启用命令历史记录,如Bash History Facilities中所述。默认情况下,此选项在交互式 shell 中处于启用状态。ignoreeof
读取 EOF 后,交互式 shell 不会退出。keyword
与 相同-k。monitor
与 相同-m。noclobber
与 相同-C。noexec
与 相同-n。noglob
与 相同-f。nolog
目前忽略。notify
与 相同-b。nounset
与 相同-u。onecmd
与 相同-t。physical
与 相同-P。
pipefail
如果设置,则管道的返回值是以非零状态退出的最后一个(最右边)命令的值,如果管道中的所有命令成功退出,则返回零。默认情况下禁用此选项。posix
在默认操作与POSIX标准不同的地方更改 Bash 的行为以匹配标准(请参阅Bash POSIX 模式)。这是为了让 Bash 成为该标准的严格超集。privileged
与 相同-p。verbose
与 相同-v。vi
使用vi-style 行编辑界面。这也会影响用于read -e.xtrace
与 相同-x。p
开启特权模式。在此模式下,不处理\(BASH_ENVand\)ENV文件,不从环境继承 shell 函数,并且 如果出现在环境中的 , 和 变量将被SHELLOPTS忽略。如果 shell 启动时有效用户(组)id 不等于真实用户(组)id,并且BASHOPTSCDPATHGLOBIGNORE-p未提供选项时,将执行这些操作并将有效用户 ID 设置为真实用户 ID。如果-p选项在启动时提供,有效用户 ID 不会重置。关闭此选项会导致有效用户和组 ID 设置为真实用户和组 ID。-t
读取并执行一条命令后退出。-u
处理未设置的变量和特殊参数以外的参数 '@' 或者 '*' 作为执行参数扩展时的错误。将向标准错误写入错误消息,并且将退出非交互式 shell。-v
在读取 shell 输入行时打印它们。-x
在展开之后和执行之前打印简单命令、for命令、case 命令、select命令和算术for命令及其参数或相关单词列表的跟踪。变量的值PS4 被扩展,结果值在命令及其扩展参数之前打印。-B
shell 将执行大括号扩展(请参阅大括号扩展)。此选项默认开启。
-C
使用 ' 防止输出重定向>', '>&', 和 '<>' 覆盖现有文件。-E
如果设置,任何trap ERR都将由 shell 函数、命令替换和在子 shell 环境中执行的命令继承。在这种情况下,ERR trap 通常不会被继承。-H
使能够 '!' 样式历史替换(请参阅历史交互)。对于交互式 shell,此选项默认启用。-P
cd如果设置,则在执行更改当前目录等命令时不解析符号链接 。而是使用物理目录。默认情况下,Bash 在执行更改当前目录的命令时遵循目录的逻辑链。例如,如果/usr/sys是一个符号链接/usr/local/sys 然后:
$ cd /usr/sys; echo $PWD /usr/sys $ cd ..; password /usr
如果set -P打开,则:
$ cd /usr/sys; echo $PWD /usr/local/sys $ cd ..; password /usr/local
-T
如果设置,则任何trap 都将被 shell 函数、命令替换和在子 shell 环境中执行的命令继承DEBUG。RETURN在这种情况下,DEBUG和RETURN trap 通常不会被继承。--
如果此选项后没有参数,则未设置位置参数。否则,位置参数将设置为 arguments,即使其中一些以 ' 开头-'。
发出选项结束的信号,使所有剩余的参数 分配给位置参数。这-X 和-v 选项被关闭。如果没有参数,则位置参数保持不变。
使用 '+' 而不是 '-' 导致这些选项被关闭。这些选项也可以在调用 shell 时使用。当前的选项集可以在 中找到$-。
剩下的 N个参数是位置参数,并按顺序分配给$1, $2, ... $N。特殊参数#设置为 N。
除非提供了无效选项,否则返回状态始终为零。
$ set
set -u
执行脚本时,如果遇到不存在的变量,Bash 默认忽略它。
#!/usr/bin/env bash
echo $a
echo bar
上面代码中,$a
是一个不存在的变量。执行结果如下。
$ bash script.sh
bar
可以看到,echo $a
输出了一个空行,Bash 忽略了不存在的$a
,然后继续执行echo bar
。大多数情况下,这不是开发者想要的行为,遇到变量不存在,脚本应该报错,而不是一声不响地往下执行。
set -u
就用来改变这种行为。脚本在头部加上它,遇到不存在的变量就会报错,并停止执行。
#!/usr/bin/env bash
set -u
echo $a
echo bar
运行结果如下。
$ bash script.sh
bash: script.sh:行4: a: 未绑定的变量
可以看到,脚本报错了,并且不再执行后面的语句。
-u
还有另一种写法-o nounset
,两者是等价的。
set -o nounset
set -x
默认情况下,脚本执行后,只输出运行结果,没有其他内容。如果多个命令连续执行,它们的运行结果就会连续输出。有时会分不清,某一段内容是什么命令产生的。
set -x
用来在运行结果之前,先输出执行的那一行命令。
#!/usr/bin/env bash
set -x
echo bar
执行上面的脚本,结果如下。
$ bash script.sh
+ echo bar
bar
可以看到,执行echo bar
之前,该命令会先打印出来,行首以+
表示。这对于调试复杂的脚本是很有用的。
-x
还有另一种写法-o xtrace
。
set -o xtrace
脚本当中如果要关闭命令输出,可以使用set +x
。
#!/bin/bash
number=1
set -x
if [ $number = "1" ]; then
echo "Number equals 1"
else
echo "Number does not equal 1"
fi
set +x
上面的例子中,只对特定的代码段打开命令输出。
Bash 的错误处理
如果脚本里面有运行失败的命令(返回值非0
),Bash 默认会继续执行后面的命令。
#!/usr/bin/env bash
foo
echo bar
上面脚本中,foo
是一个不存在的命令,执行时会报错。但是,Bash 会忽略这个错误,继续往下执行。
$ bash script.sh
script.sh:行3: foo: 未找到命令
bar
可以看到,Bash 只是显示有错误,并没有终止执行。
这种行为很不利于脚本安全和除错。实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。这时,一般采用下面的写法。
command || exit 1
上面的写法表示只要command
有非零返回值,脚本就会停止执行。
如果停止执行之前需要完成多个操作,就要采用下面三种写法。
# 写法一
command || { echo "command failed"; exit 1; }
# 写法二
if ! command; then echo "command failed"; exit 1; fi
# 写法三
command
if [ "$?" -ne 0 ]; then echo "command failed"; exit 1; fi
另外,除了停止执行,还有一种情况。如果两个命令有继承关系,只有第一个命令成功了,才能继续执行第二个命令,那么就要采用下面的写法。
command1 && command2
set -e
上面这些写法多少有些麻烦,容易疏忽。set -e
从根本上解决了这个问题,它使得脚本只要发生错误,就终止执行。
#!/usr/bin/env bash
set -e
foo
echo bar
执行结果如下。
$ bash script.sh
script.sh:行4: foo: 未找到命令
可以看到,第4行执行失败以后,脚本就终止执行了。
set -e
根据返回值来判断,一个命令是否运行失败。但是,某些命令的非零返回值可能不表示失败,或者开发者希望在命令失败的情况下,脚本继续执行下去。这时可以暂时关闭set -e
,该命令执行结束后,再重新打开set -e
。
set +e
command1
command2
set -e
上面代码中,set +e
表示关闭-e
选项,set -e
表示重新打开-e
选项。
还有一种方法是使用command || true
,使得该命令即使执行失败,脚本也不会终止执行。
#!/bin/bash
set -e
foo || true
echo bar
上面代码中,true
使得这一行语句总是会执行成功,后面的echo bar
会执行。
-e
还有另一种写法-o errexit
。
set -o errexit
set -o pipefail
set -e
有一个例外情况,就是不适用于管道命令。
所谓管道命令,就是多个子命令通过管道运算符(|
)组合成为一个大的命令。Bash 会把最后一个子命令的返回值,作为整个命令的返回值。也就是说,只要最后一个子命令不失败,管道命令总是会执行成功,因此它后面命令依然会执行,set -e
就失效了。
请看下面这个例子。
#!/usr/bin/env bash
set -e
foo | echo a
echo bar
执行结果如下。
$ bash script.sh
a
script.sh:行4: foo: 未找到命令
bar
上面代码中,foo
是一个不存在的命令,但是foo | echo a
这个管道命令会执行成功,导致后面的echo bar
会继续执行。
set -o pipefail
用来解决这种情况,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行。
#!/usr/bin/env bash
set -eo pipefail
foo | echo a
echo bar
运行后,结果如下。
$ bash script.sh
a
script.sh:行4: foo: 未找到命令
可以看到,echo bar
没有执行。
set -E
一旦设置了-e
参数,会导致函数内的错误不会被trap
命令捕获(参考《trap 命令》一章)。-E
参数可以纠正这个行为,使得函数也能继承trap
命令。
#!/bin/bash
set -e
trap "echo ERR trap fired!" ERR
myfunc()
{
# 'foo' 是一个不存在的命令
foo
}
myfunc
上面示例中,myfunc
函数内部调用了一个不存在的命令foo
,导致执行这个函数会报错。
$ bash test.sh
test.sh:行9: foo:未找到命令
但是,由于设置了set -e
,函数内部的报错并没有被trap
命令捕获,需要加上-E
参数才可以。
#!/bin/bash
set -Eeuo pipefail
trap "echo ERR trap fired!" ERR
myfunc()
{
# 'foo' 是一个不存在的命令
foo
}
myfunc
执行上面这个脚本,就可以看到trap
命令生效了。
$ bash test.sh
test.sh:行9: foo:未找到命令
ERR trap fired!
其他参数
set
命令还有一些其他参数。
set -n
:等同于set -o noexec
,不运行命令,只检查语法是否正确。set -f
:等同于set -o noglob
,表示不对通配符进行文件名扩展。set -v
:等同于set -o verbose
,表示打印 Shell 接收到的每一行输入。set -o noclobber
:防止使用重定向运算符>
覆盖已经存在的文件。
上面的-f
和-v
参数,可以分别使用set +f
、set +v
关闭。
set 命令总结
上面重点介绍的set
命令的几个参数,一般都放在一起使用。
# 写法一
set -Eeuxo pipefail
# 写法二
set -Eeux
set -o pipefail
这两种写法建议放在所有 Bash 脚本的头部。
另一种办法是在执行 Bash 脚本的时候,从命令行传入这些参数。
$ bash -euxo pipefail script.sh
shopt 命令
shopt
命令用来调整 Shell 的参数,跟set
命令的作用很类似。之所以会有这两个类似命令的主要原因是,set
是从 Ksh 继承的,属于 POSIX 规范的一部分,而shopt
是 Bash 特有的。
直接输入shopt
可以查看所有参数,以及它们各自打开和关闭的状态。
$ shopt
shopt
命令后面跟着参数名,可以查询该参数是否打开。
$ shopt globstar
globstar off
上面例子表示globstar
参数默认是关闭的。
(1)-s
-s
用来打开某个参数。
$ shopt -s optionNameHere
(2)-u
-u
用来关闭某个参数。
$ shopt -u optionNameHere
举例来说,histappend
这个参数表示退出当前 Shell 时,将操作历史追加到历史文件中。这个参数默认是打开的,如果使用下面的命令将其关闭,那么当前 Shell 的操作历史将替换掉整个历史文件。
$ shopt -u histappend
(3)-q
-q
的作用也是查询某个参数是否打开,但不是直接输出查询结果,而是通过命令的执行状态($?
)表示查询结果。如果状态为0
,表示该参数打开;如果为1
,表示该参数关闭。
$ shopt -q globstar
$ echo $?
1
上面命令查询globstar
参数是否打开。返回状态为1
,表示该参数是关闭的。
这个用法主要用于脚本,供if
条件结构使用。下面例子是如果打开了这个参数,就执行if
结构内部的语句。
if (shopt -q globstar); then
...
fi
参考链接
17.脚本除错
本章介绍如何对 Shell 脚本除错。
常见错误
编写 Shell 脚本的时候,一定要考虑到命令失败的情况,否则很容易出错。
#! /bin/bash
dir_name=/path/not/exist
cd $dir_name
rm *
上面脚本中,如果目录$dir_name
不存在,cd $dir_name
命令就会执行失败。这时,就不会改变当前目录,脚本会继续执行下去,导致rm *
命令删光当前目录的文件。
如果改成下面的样子,也会有问题。
cd $dir_name && rm *
上面脚本中,只有cd $dir_name
执行成功,才会执行rm *
。但是,如果变量$dir_name
为空,cd
就会进入用户主目录,从而删光用户主目录的文件。
下面的写法才是正确的。
[[ -d $dir_name ]] && cd $dir_name && rm *
上面代码中,先判断目录$dir_name
是否存在,然后才执行其他操作。
如果不放心删除什么文件,可以先打印出来看一下。
[[ -d $dir_name ]] && cd $dir_name && echo rm *
上面命令中,echo rm *
不会删除文件,只会打印出来要删除的文件。
bash
的-x
参数
bash
的-x
参数可以在执行每一行命令之前,打印该命令。一旦出错,这样就比较容易追查。
下面是一个脚本script.sh
。
# script.sh
echo hello world
加上-x
参数,执行每条命令之前,都会显示该命令。
$ bash -x script.sh
+ echo hello world
hello world
上面例子中,行首为+
的行,显示该行是所要执行的命令,下一行才是该命令的执行结果。
下面再看一个-x
写在脚本内部的例子。
#! /bin/bash -x
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ]; then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
上面的脚本执行之后,会输出每一行命令。
$ trouble
+ number=1
+ '[' 1 = 1 ']'
+ echo 'Number is equal to 1.'
Number is equal to 1.
输出的命令之前的+
号,是由系统变量PS4
决定,可以修改这个变量。
$ export PS4='$LINENO + '
$ trouble
5 + number=1
7 + '[' 1 = 1 ']'
8 + echo 'Number is equal to 1.'
Number is equal to 1.
另外,set
命令也可以设置 Shell 的行为参数,有利于脚本除错,详见《set 命令》一章。
环境变量
有一些环境变量常用于除错。
LINENO
变量LINENO
返回它在脚本里面的行号。
#!/bin/bash
echo "This is line $LINENO"
执行上面的脚本test.sh
,$LINENO
会返回3
。
$ ./test.sh
This is line 3
FUNCNAME
变量FUNCNAME
返回一个数组,内容是当前的函数调用堆栈。该数组的0号成员是当前调用的函数,1号成员是调用当前函数的函数,以此类推。
#!/bin/bash
function func1()
{
echo "func1: FUNCNAME0 is ${FUNCNAME[0]}"
echo "func1: FUNCNAME1 is ${FUNCNAME[1]}"
echo "func1: FUNCNAME2 is ${FUNCNAME[2]}"
func2
}
function func2()
{
echo "func2: FUNCNAME0 is ${FUNCNAME[0]}"
echo "func2: FUNCNAME1 is ${FUNCNAME[1]}"
echo "func2: FUNCNAME2 is ${FUNCNAME[2]}"
}
func1
执行上面的脚本test.sh
,结果如下。
$ ./test.sh
func1: FUNCNAME0 is func1
func1: FUNCNAME1 is main
func1: FUNCNAME2 is
func2: FUNCNAME0 is func2
func2: FUNCNAME1 is func1
func2: FUNCNAME2 is main
上面例子中,执行func1
时,变量FUNCNAME
的0号成员是func1
,1号成员是调用func1
的主脚本main
。执行func2
时,变量FUNCNAME
的0号成员是func2
,1号成员是调用func2
的func1
。
BASH_SOURCE
变量BASH_SOURCE
返回一个数组,内容是当前的脚本调用堆栈。该数组的0号成员是当前执行的脚本,1号成员是调用当前脚本的脚本,以此类推,跟变量FUNCNAME
是一一对应关系。
下面有两个子脚本lib1.sh
和lib2.sh
。
# lib1.sh
function func1()
{
echo "func1: BASH_SOURCE0 is ${BASH_SOURCE[0]}"
echo "func1: BASH_SOURCE1 is ${BASH_SOURCE[1]}"
echo "func1: BASH_SOURCE2 is ${BASH_SOURCE[2]}"
func2
}
# lib2.sh
function func2()
{
echo "func2: BASH_SOURCE0 is ${BASH_SOURCE[0]}"
echo "func2: BASH_SOURCE1 is ${BASH_SOURCE[1]}"
echo "func2: BASH_SOURCE2 is ${BASH_SOURCE[2]}"
}
然后,主脚本main.sh
调用上面两个子脚本。
#!/bin/bash
# main.sh
source lib1.sh
source lib2.sh
func1
执行主脚本main.sh
,会得到下面的结果。
$ ./main.sh
func1: BASH_SOURCE0 is lib1.sh
func1: BASH_SOURCE1 is ./main.sh
func1: BASH_SOURCE2 is
func2: BASH_SOURCE0 is lib2.sh
func2: BASH_SOURCE1 is lib1.sh
func2: BASH_SOURCE2 is ./main.sh
上面例子中,执行函数func1
时,变量BASH_SOURCE
的0号成员是func1
所在的脚本lib1.sh
,1号成员是主脚本main.sh
;执行函数func2
时,变量BASH_SOURCE
的0号成员是func2
所在的脚本lib2.sh
,1号成员是调用func2
的脚本lib1.sh
。
BASH_LINENO
变量BASH_LINENO
返回一个数组,内容是每一轮调用对应的行号。${BASH_LINENO[$i]}
跟${FUNCNAME[$i]}
是一一对应关系,表示${FUNCNAME[$i]}
在调用它的脚本文件${BASH_SOURCE[$i+1]}
里面的行号。
下面有两个子脚本lib1.sh
和lib2.sh
。
# lib1.sh
function func1()
{
echo "func1: BASH_LINENO is ${BASH_LINENO[0]}"
echo "func1: FUNCNAME is ${FUNCNAME[0]}"
echo "func1: BASH_SOURCE is ${BASH_SOURCE[1]}"
func2
}
# lib2.sh
function func2()
{
echo "func2: BASH_LINENO is ${BASH_LINENO[0]}"
echo "func2: FUNCNAME is ${FUNCNAME[0]}"
echo "func2: BASH_SOURCE is ${BASH_SOURCE[1]}"
}
然后,主脚本main.sh
调用上面两个子脚本。
#!/bin/bash
# main.sh
source lib1.sh
source lib2.sh
func1
执行主脚本main.sh
,会得到下面的结果。
$ ./main.sh
func1: BASH_LINENO is 7
func1: FUNCNAME is func1
func1: BASH_SOURCE is main.sh
func2: BASH_LINENO is 8
func2: FUNCNAME is func2
func2: BASH_SOURCE is lib1.sh
上面例子中,函数func1
是在main.sh
的第7行调用,函数func2
是在lib1.sh
的第8行调用的。
18.mktemp 命令,trap 命令
Bash 脚本有时需要创建临时文件或临时目录。常见的做法是,在/tmp
目录里面创建文件或目录,这样做有很多弊端,使用mktemp
命令是最安全的做法。
临时文件的安全问题
直接创建临时文件,尤其在/tmp
目录里面,往往会导致安全问题。
首先,/tmp
目录是所有人可读写的,任何用户都可以往该目录里面写文件。创建的临时文件也是所有人可读的。
$ touch /tmp/info.txt
$ ls -l /tmp/info.txt
-rw-r--r-- 1 ruanyf ruanyf 0 12月 28 17:12 /tmp/info.txt
上面命令在/tmp
目录直接创建文件,该文件默认是所有人可读的。
其次,如果攻击者知道临时文件的文件名,他可以创建符号链接,链接到临时文件,可能导致系统运行异常。攻击者也可能向脚本提供一些恶意数据。因此,临时文件最好使用不可预测、每次都不一样的文件名,防止被利用。
最后,临时文件使用完毕,应该删除。但是,脚本意外退出时,往往会忽略清理临时文件。
生成临时文件应该遵循下面的规则。
- 创建前检查文件是否已经存在。
- 确保临时文件已成功创建。
- 临时文件必须有权限的限制。
- 临时文件要使用不可预测的文件名。
- 脚本退出时,要删除临时文件(使用
trap
命令)。
mktemp 命令的用法
mktemp
命令就是为安全创建临时文件而设计的。虽然在创建临时文件之前,它不会检查临时文件是否存在,但是它支持唯一文件名和清除机制,因此可以减轻安全攻击的风险。
直接运行mktemp
命令,就能生成一个临时文件。
$ mktemp
/tmp/tmp.4GcsWSG4vj
$ ls -l /tmp/tmp.4GcsWSG4vj
-rw------- 1 ruanyf ruanyf 0 12月 28 12:49 /tmp/tmp.4GcsWSG4vj
上面命令中,mktemp
命令生成的临时文件名是随机的,而且权限是只有用户本人可读写。
Bash 脚本使用mktemp
命令的用法如下。
#!/bin/bash
TMPFILE=$(mktemp)
echo "Our temp file is $TMPFILE"
为了确保临时文件创建成功,mktemp
命令后面最好使用 OR 运算符(||
),保证创建失败时退出脚本。
#!/bin/bash
TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"
为了保证脚本退出时临时文件被删除,可以使用trap
命令指定退出时的清除操作。
#!/bin/bash
trap 'rm -f "$TMPFILE"' EXIT
TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"
mktemp 命令的参数
-d
参数可以创建一个临时目录。
$ mktemp -d
/tmp/tmp.Wcau5UjmN6
-p
参数可以指定临时文件所在的目录。默认是使用$TMPDIR
环境变量指定的目录,如果这个变量没设置,那么使用/tmp
目录。
$ mktemp -p /home/ruanyf/
/home/ruanyf/tmp.FOKEtvs2H3
-t
参数可以指定临时文件的文件名模板,模板的末尾必须至少包含三个连续的X
字符,表示随机字符,建议至少使用六个X
。默认的文件名模板是tmp.
后接十个随机字符。
$ mktemp -t mytemp.XXXXXXX
/tmp/mytemp.yZ1HgZV
trap 命令
trap
命令用来在 Bash 脚本中响应系统信号。
最常见的系统信号就是 SIGINT(中断),即按 Ctrl + C 所产生的信号。trap
命令的-l
参数,可以列出所有的系统信号。
$ trap -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
trap
的命令格式如下。
$ trap [动作] [信号1] [信号2] ...
上面代码中,“动作”是一个 Bash 命令,“信号”常用的有以下几个。
- HUP:编号1,脚本与所在的终端脱离联系。
- INT:编号2,用户按下 Ctrl + C,意图让脚本终止运行。
- QUIT:编号3,用户按下 Ctrl + 斜杠,意图退出脚本。
- KILL:编号9,该信号用于杀死进程。
- TERM:编号15,这是
kill
命令发出的默认信号。- EXIT:编号0,这不是系统信号,而是 Bash 脚本特有的信号,不管什么情况,只要退出脚本就会产生。
trap
命令响应EXIT
信号的写法如下。
$ trap 'rm -f "$TMPFILE"' EXIT
上面命令中,脚本遇到EXIT
信号时,就会执行rm -f "$TMPFILE"
。
trap 命令的常见使用场景,就是在 Bash 脚本中指定退出时执行的清理命令。
#!/bin/bash
trap 'rm -f "$TMPFILE"' EXIT
TMPFILE=$(mktemp) || exit 1
ls /etc > $TMPFILE
if grep -qi "kernel" $TMPFILE; then
echo 'find'
fi
上面代码中,不管是脚本正常执行结束,还是用户按 Ctrl + C 终止,都会产生EXIT
信号,从而触发删除临时文件。
注意,trap
命令必须放在脚本的开头。否则,它上方的任何命令导致脚本退出,都不会被它捕获。
如果trap
需要触发多条命令,可以封装一个 Bash 函数。
function egress {
command1
command2
command3
}
trap egress EXIT
参考链接
- Working with Temporary Files and Directories in Shell Scripts, Steven Vona
- Using Trap to Exit Bash Scripts Cleanly
- Sending and Trapping Signals
19.Bash 启动环境
Session
用户每次使用 Shell,都会开启一个与 Shell 的 Session(对话)。
Session 有两种类型:登录 Session 和非登录 Session,也可以叫做 login shell 和 non-login shell。
登录 Session
登录 Session 是用户登录系统以后,系统为用户开启的原始 Session,通常需要用户输入用户名和密码进行登录。
登录 Session 一般进行整个系统环境的初始化,启动的初始化脚本依次如下。
/etc/profile
:所有用户的全局配置脚本。/etc/profile.d
目录里面所有.sh
文件~/.bash_profile
:用户的个人配置脚本。如果该脚本存在,则执行完就不再往下执行。~/.bash_login
:如果~/.bash_profile
没找到,则尝试执行这个脚本(C shell 的初始化脚本)。如果该脚本存在,则执行完就不再往下执行。~/.profile
:如果~/.bash_profile
和~/.bash_login
都没找到,则尝试读取这个脚本(Bourne shell 和 Korn shell 的初始化脚本)。
Linux 发行版更新的时候,会更新/etc
里面的文件,比如/etc/profile
,因此不要直接修改这个文件。如果想修改所有用户的登陆环境,就在/etc/profile.d
目录里面新建.sh
脚本。
如果想修改你个人的登录环境,一般是写在~/.bash_profile
里面。下面是一个典型的.bash_profile
文件。
# .bash_profile
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin
PATH=$PATH:$HOME/bin
SHELL=/bin/bash
MANPATH=/usr/man:/usr/X11/man
EDITOR=/usr/bin/vi
PS1='\h:\w\$ '
PS2='> '
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
export PATH
export EDITOR
可以看到,这个脚本定义了一些最基本的环境变量,然后执行了~/.bashrc
。
bash
命令的--login
参数,会强制执行登录 Session 会执行的脚本。
$ bash --login
bash
命令的--noprofile
参数,会跳过上面这些 Profile 脚本。
$ bash --noprofile
非登录 Session
非登录 Session 是用户进入系统以后,手动新建的 Session,这时不会进行环境初始化。比如,在命令行执行bash
命令,就会新建一个非登录 Session。
非登录 Session 的初始化脚本依次如下。
/etc/bash.bashrc
:对全体用户有效。~/.bashrc
:仅对当前用户有效。
对用户来说,~/.bashrc
通常是最重要的脚本。非登录 Session 默认会执行它,而登录 Session 一般也会通过调用执行它。每次新建一个 Bash 窗口,就相当于新建一个非登录 Session,所以~/.bashrc
每次都会执行。注意,执行脚本相当于新建一个非互动的 Bash 环境,但是这种情况不会调用~/.bashrc
。
bash
命令的--norc
参数,可以禁止在非登录 Session 执行~/.bashrc
脚本。
$ bash --norc
bash
命令的--rcfile
参数,指定另一个脚本代替.bashrc
。
$ bash --rcfile testrc
.bash_logout
~/.bash_logout
脚本在每次退出 Session 时执行,通常用来做一些清理工作和记录工作,比如删除临时文件,记录用户在本次 Session 花费的时间。
如果没有退出时要执行的命令,这个文件也可以不存在。
启动选项
为了方便 Debug,有时在启动 Bash 的时候,可以加上启动参数。
-n
:不运行脚本,只检查是否有语法错误。-v
:输出每一行语句运行结果前,会先输出该行语句。-x
:每一个命令处理之前,先输出该命令,再执行该命令。
$ bash -n scriptname
$ bash -v scriptname
$ bash -x scriptname
键盘绑定
Bash 允许用户定义自己的快捷键。全局的键盘绑定文件默认为/etc/inputrc
,你可以在主目录创建自己的键盘绑定文件.inputrc
文件。如果定义了这个文件,需要在其中加入下面这行,保证全局绑定不会被遗漏。
$include /etc/inputrc
.inputrc
文件里面的快捷键,可以像这样定义,"\C-t":"pwd\n"
表示将Ctrl + t
绑定为运行pwd
命令。
20.命令提示符
用户进入 Bash 以后,Bash 会显示一个命令提示符,用来提示用户在该位置后面输入命令。
环境变量 PS1
命令提示符通常是美元符号$
,对于根用户则是井号#
。这个符号是环境变量PS1
决定的,执行下面的命令,可以看到当前命令提示符的定义。
$ echo $PS1
Bash 允许用户自定义命令提示符,只要改写这个变量即可。改写后的PS1
,可以放在用户的 Bash 配置文件.bashrc
里面,以后新建 Bash 对话时,新的提示符就会生效。要在当前窗口看到修改后的提示符,可以执行下面的命令。
$ source ~/.bashrc
命令提示符的定义,可以包含特殊的转义字符,表示特定内容。
\a
:响铃,计算机发出一记声音。\d
:以星期、月、日格式表示当前日期,例如“Mon May 26”。\h
:本机的主机名。\H
:完整的主机名。\j
:运行在当前 Shell 会话的工作数。\l
:当前终端设备名。\n
:一个换行符。\r
:一个回车符。\s
:Shell 的名称。\t
:24小时制的hours:minutes:seconds
格式表示当前时间。\T
:12小时制的当前时间。\@
:12小时制的AM/PM
格式表示当前时间。\A
:24小时制的hours:minutes
表示当前时间。\u
:当前用户名。\v
:Shell 的版本号。\V
:Shell 的版本号和发布号。\w
:当前的工作路径。\W
:当前目录名。\!
:当前命令在命令历史中的编号。\#
:当前 shell 会话中的命令数。\$
:普通用户显示为$
字符,根用户显示为#
字符。\[
:非打印字符序列的开始标志。\]
:非打印字符序列的结束标志。
举例来说,[\u@\h \W]\$
这个提示符定义,显示出来就是[user@host ~]$
(具体的显示内容取决于你的系统)。
[user@host ~]$ echo $PS1
[\u@\h \W]\$
改写PS1
变量,就可以改变这个命令提示符。
$ PS1="\A \h \$ "
17:33 host $
注意,$
后面最好跟一个空格,这样的话,用户的输入与提示符就不会连在一起。
颜色
默认情况下,命令提示符是显示终端预定义的颜色。Bash 允许自定义提示符颜色。
使用下面的代码,可以设定其后文本的颜色。
\033[0;30m
:黑色\033[1;30m
:深灰色\033[0;31m
:红色\033[1;31m
:浅红色\033[0;32m
:绿色\033[1;32m
:浅绿色\033[0;33m
:棕色\033[1;33m
:黄色\033[0;34m
:蓝色\033[1;34m
:浅蓝色\033[0;35m
:粉红\033[1;35m
:浅粉色\033[0;36m
:青色\033[1;36m
:浅青色\033[0;37m
:浅灰色\033[1;37m
:白色
举例来说,如果要将提示符设为红色,可以将PS1
设成下面的代码。
PS1='\[\033[0;31m\]<\u@\h \W>\$'
但是,上面这样设置以后,用户在提示符后面输入的文本也是红色的。为了解决这个问题, 可以在结尾添加另一个特殊代码\[\033[00m\]
,表示将其后的文本恢复到默认颜色。
PS1='\[\033[0;31m\]<\u@\h \W>\$\[\033[00m\]'
除了设置前景颜色,Bash 还允许设置背景颜色。
\033[0;40m
:蓝色\033[1;44m
:黑色\033[0;41m
:红色\033[1;45m
:粉红\033[0;42m
:绿色\033[1;46m
:青色\033[0;43m
:棕色\033[1;47m
:浅灰色
下面是一个带有红色背景的提示符。
PS1='\[\033[0;41m\]<\u@\h \W>\$\[\033[0m\] '
环境变量 PS2,PS3,PS4
除了PS1
,Bash 还提供了提示符相关的另外三个环境变量。
环境变量PS2
是命令行折行输入时系统的提示符,默认为>
。
$ echo "hello
> world"
上面命令中,输入hello
以后按下回车键,系统会提示继续输入。这时,第二行显示的提示符就是PS2
定义的>
。
环境变量PS3
是使用select
命令时,系统输入菜单的提示符。
环境变量PS4
默认为+
。它是使用 Bash 的-x
参数执行脚本时,每一行命令在执行前都会先打印出来,并且在行首出现的那个提示符。
比如下面是脚本test.sh
。
#!/bin/bash
echo "hello world"
使用-x
参数执行这个脚本。
$ bash -x test.sh
+ echo 'hello world'
hello world
上面例子中,输出的第一行前面有一个+
,这就是变量PS4
定义的。
目前我零零碎碎花了几周时间看完了阮一峰老师的bash教程,还有有一丢丢心得的,只不过我的机械硬盘有点卡,浏览器也装在机械硬盘里的,文章篇幅有这么长,写点心得卡得受不了,所以就没有写自己的看法了