read命令
read
将用户的输入存入一个变量,方便后面的代码使用:
read [-options] [variable...]
options是参数选项,variable是用来保存输入数值的一个或多个变量名。如果没有提供变量名,环境变量REPLY会包含用户输入的一整行数据。
比如:
#!/bin/bash
echo -n "输入一些文本 > "
read text
echo "你的输入:$text"
执行结果如下:
$ bash demo.sh
输入一些文本 > 你好,世界
你的输入:你好,世界
使用read接受多个输入:
#!/bin/bash
echo Please, enter your firstname and lastname
read FN LN #输入时两个输入用空格隔开
echo "Hi! $LN, $FN !"
如果用户的输入项少于read命令给出的变量数目,那么额外的变量值为空。如果用户的输入项多于定义的变量,那么多余的输入项会包含到最后一个变量中:
#read-single文件
#!/bin/bash
# read-single: read multiple values into default variable
echo -n "Enter one or more values > "
read
echo "REPLY = '$REPLY'"
#执行read-single
$ 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的参数:
-t
:设置了超时的秒数。如果超过了指定时间,用户仍然没有输入,脚本将放弃等待,继续向下执行。
环境变量TMOUT也可以起到同样作用:
$ TMOUT=3
$ read response
-p
:指定用户输入的提示信息。
-a
:把用户的输入赋值给一个数组,从零号位置开始:
$ read -a people
alice duchess dodo
$ echo ${people[2]}
dodo
-n
:参数指定只读取若干个字符作为变量值,而不是整行读取:
$ read -n 3 letter
abcdefghij
$ echo $letter
abc
-e
:参数允许用户输入的时候,使用readline库提供的快捷键,比如自动补全:
#!/bin/bash
echo Please input the path to the file:
read -e fileName
echo $fileName
上面例子中,read命令接受用户输入的文件名。这时,用户可能想使用 Tab 键的文件名“自动补全”功能,但是read命令的输入默认不支持readline库的功能。-e
参数就可以允许用户使用自动补全。
read命令读取的值,默认是以空格分隔。可以通过自定义环境变量IFS(内部字段分隔符,Internal Field Separator 的缩写),修改分隔标志。
IFS的默认值是空格、Tab 符号、换行符号,通常取第一个(即空格)。
如下,通过修改IFS的值为:
,自动分析/etc/passwd文件:
#!/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的赋值命令和read命令写在一行,这样的话,IFS的改变仅对后面的命令生效,该命令执行后IFS会自动恢复原来的值。
<<<
是 Here 字符串,用于将变量值转为标准输入,因为read命令只能解析标准输入。
如果IFS设为空字符串,就等同于将整行读入一个变量:
#!/bin/bash
input="/path/to/txt/file"
while IFS= read -r line #-r:raw 模式,表示不把用户输入的反斜杠字符解释为转义字符
do
echo "$line"
done < "$input"
注意,上述代码最后一段done < "$input"
,即done < "/path/to/txt/file"
,该字符串并不是标准输入,无法传入read,而该字符串表示的文件为标准输入,所以read读取的是对应文件的内容。若改成:
#!/bin/bash
input="/path/to/txt/file"
while IFS= read -r line #-r:raw 模式,表示不把用户输入的反斜杠字符解释为转义字符
do
echo "$line"
done <<< "$input"
输出的则是/path/to/txt/file
,因为<<<
可以将字符串转换为标准输入。
条件判断
if语句结构如下:
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
#or
if commands
then
commands
[elif commands
then
commands...]
[else
commands]
fi
比如:
if test $USER = "foo"; then #test用于检测后面语句是否成立
echo "Hello foo."
else
echo "You are not foo."
fi
可以直接使用true
或者false
进行判断:
$ 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后面可以跟任意数量的命令。这时,所有命令都会执行,但是判断真伪只看最后一个命令,即使前面所有命令都失败,只要最后一个命令返回0,就会执行then的部分:
$ if false; true; then echo 'hello world'; fi
hello world
test语句
if结构的判断条件,一般使用test命令,有三种形式:
# 写法一
test expression
# 写法二
[ expression ]
# 写法三
[[ expression ]] #只有该形式支持正则判断
第二种和第三种写法,[和]与内部的表达式之间必须有空格。
比如:
$ test -f /etc/hosts
$ echo $?
0 #真
$ [ -f /etc/hosts ] #与上一种等价
$ echo $?
0 #真
以下为常用的判断文件状态的语句:
[ -a file ]:如果 file 存在,则为true
[ -d file ]:如果 file 存在并且是一个目录,则为true
[ -e file ]:如果 file 存在,则为true
[ -f file ]:如果 file 存在并且是一个普通文件,则为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
注意,字符串判断时,变量要放在双引号之中,比如[ -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
read INT
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,接着判断正负,最后通过求余数判断奇偶。
正则判断语句:
[[ string1 =~ 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判断表达式结合起来,创造更复杂的判断。三种逻辑运算AND,OR,和NOT,都有自己的专用符号(逻辑判断需要使用[[...]]
):
AND运算:符号&&,也可使用参数-a。
OR运算:符号||,也可使用参数-o。
NOT运算:符号!。
比如:
#!/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
使用否定操作符!
时,最好用圆括号确定转义的范围:
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 还提供了((...))
作为算术条件,进行算术运算的判断(无需使用test语句):
if ((3 > 2)); then
echo "true"
fi
如果算术计算的结果是非零值,则表示判断成立。这一点跟命令的返回值正好相反,需要小心:
$ 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.
算术条件((...))
也可以用于变量赋值:
$ 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
一些命令的混用:
#测试目录temp是否存在,如果不存在,就会执行第二个命令,创建这个目录
$ [ -d temp ] || mkdir temp
#如果temp子目录不存在,脚本会终止,并且返回值为1
[ ! -d temp ] && exit 1
比如:只有在指定文件里面,同时存在搜索词word1和word2,就会执行if的命令部分:
#! /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
比如:将一个&&
判断表达式,改写成对应的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 expression in
pattern )
commands ;;
pattern )
commands ;;
...
esac
比如:
#!/bin/bash
echo -n "输入一个1到3之间的数字(包含两端)> "
read character
case $character in
1 ) echo 1
;;
2 ) echo 2
;;
3 ) echo 3
;;
* ) echo 输入不符合要求
esac
上面例子中,最后一条匹配语句的模式是*
,这个通配符可以匹配其他字符和没有输入字符的情况,类似if的else部分。
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
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结构,而是继续判断下一个条件。
循环
while循环:
while condition; do
commands
done
比如:
#!/bin/bash
number=0
while [ "$number" -lt 10 ]; do
echo "Number = $number"
number=$((number + 1))
done
while的条件部分可以执行任意数量的命令,但是执行结果的真伪只看最后一个命令的执行结果:
#以下代码运行后,不会有任何输出,因为while的最后一个命令是false。
$ while true; false; do echo 'Hi, looping ...'; done
until循环:
until循环与while循环恰好相反,只要不符合判断条件(判断条件失败),就不断循环执行指定的语句。一旦符合判断条件,就退出循环:
until condition; do
commands
done
比如:
number=0
until [ "$number" -ge 10 ]; do
echo "Number = $number"
number=$((number + 1))
done
until的条件部分也可以是一个命令,表示在这个命令执行成功之前,不断重复尝试:
until cp $1 $2; do
echo 'Attempt to copy failed. waiting...'
sleep 5
done
转换为while循环:
while ! cp $1 $2; do
echo 'Attempt to copy failed. waiting...'
sleep 5
done
for...in循环:
for...in循环用于遍历列表的每一项:
for variable in list; do
commands
done
比如:
for i in word1 word2 word3; do
echo $i
done
列表可以由通配符产生:
for i in *.png; do
ls -l $i
done
in list的部分可以省略,这时list默认等于脚本的所有参数$@
。
列表也可以通过子命令产生:
#!/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
利用wc指令我们可以计算文件的Byte数、字数、或是列数,若不指定文件名称、或是所给予的文件名为"-",则wc指令会从标准输入设备读取数据。-c
或--bytes
或--chars
只显示Bytes数。
echo -n
:选项-n
表示输出文字后不换行,否则计数会多一个回车符。
for循环还支持 C 语言的循环语法:
for (( expression1; expression2; expression3 )); do
commands
done
比如:
for (( i=0; i<5; i=i+1 )); do
echo $i
done
注意,循环条件放在双重圆括号之中。另外,圆括号之中使用变量,不必加上美元符号$
。
for条件部分的三个语句,都可以省略:
for ((;;))
do
read var
if [ "$var" = "." ]; then
break
fi
done
上面脚本会反复读取命令行输入,直到用户输入了一个点(.)为止,才会跳出循环。
break语句示例:
#!/bin/bash
for number in 1 2 3 4 5 6
do
echo "number is $number"
if [ "$number" = "3" ]; then
break
fi
done
#执行结果如下:
number is 1
number is 2
number is 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
select语句执行流程:
1.select生成一个菜单,内容是列表list的每一项,并且每一项前面还有一个数字编号。
2.Bash 提示用户选择一项,输入它的编号。
3.用户输入以后,Bash 会将该项的内容存在变量name,该项的编号存入环境变量REPLY。如果用户没有输入,就按回车键,Bash 会重新输出菜单,让用户选择。
4.执行命令体commands。
5.执行结束后,回到第一步,重复这个过程。
比如:
#!/bin/bash
# select.sh
select brand in Samsung Sony iphone symphony Walton
do
echo "You have chosen $brand"
done
执行时会让用户进行选择:
$ ./select.sh
1) Samsung
2) Sony
3) iphone
4) symphony
5) Walton
#?
如果用户没有输入编号,直接按回车键。Bash 就会重新输出一遍这个菜单,直到用户按下Ctrl + c,退出执行。
如果用户选择1,则结果如下:
#? 1
You have chosen Samsung
#?
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
执行结果如下:
Which Operating System do you like?
1) Ubuntu
2) LinuxMint
3) Windows8
4) Windows10
5) WindowsXP
#? 1
I also use Ubuntu.
#? 5
Why don't you try Linux?
#? 0
Invalid entry.
函数
Bash 函数定义的语法有两种:
# 第一种
fn() {
# codes
}
# 第二种
function fn() {
# codes
}
比如:
hello() {
echo "Hello $1" #$1表示函数调用时的第一个参数
}
如何调用:
$ hello world
Hello world
比如:
today() {
echo -n "Today's date is: "
date +"%A, %B %-d, %Y"
}
调用:
$ today
Today's date is: Monday, October 17, 2022
删除一个函数,可以使用unset命令:
unset -f functionName
查看当前 Shell 已经定义的所有函数(包括函数名和定义),可以使用declare命令:
$ declare -f
declare命令还支持查看单个函数的定义:
$ declare -f functionName
指输出所有已定义的函数名:
$ declare -F
函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的:
$1~$9:函数的第一个到第9个的参数。
$0:函数所在的脚本名。
$#:函数的参数总数。
$@:函数的全部参数,参数之间使用空格分隔。
$*:函数的全部参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。
比如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
再比如:
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示例:
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
}
全局变量和局部变量:
Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取。这一点需要特别小心:
# 脚本 test.sh
fn () {
foo=1
echo "fn: foo = $foo"
}
fn
echo "global: foo = $foo"
运行结果如下:
$ bash test.sh
fn: foo = 1
global: foo = 1
函数体内不仅可以声明全局变量,还可以修改全局变量:
#! /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
变量,只在函数体内有效,函数体外没有定义。
数组
数组的创建方式:
#单个元素赋值:
$ 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)
#使用通配符,将当前目录的所有 MP3 文件,放进一个数组
$ mp3s=( *.mp3 )
#先用declare -a命令声明一个数组,也是可以的
$ declare -a ARRAYNAME
#将用户的命令行输入,存入一个数组
$ read -a 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 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
当@
在双引号中,则不会出现这种问题:
$ for act in "${activities[@]}"; \
do \
echo "Activity: $act"; \
done
Activity: swimming
Activity: water skiing
Activity: canoeing
Activity: white-water rafting
Activity: 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
当*
在双引号中,所有成员会被合并为单个字符串返回:
$ for act in "${activities[*]}"; \
do \
echo "Activity: $act"; \
done
Activity: swimming water skiing canoeing white-water rafting surfing
所以,拷贝一个数组的最方便方法,是使用在双引号中的@
:
$ hobbies=( "${activities[@]}" )
为数组添加新成员:
$ hobbies=( "${activities[@]}" diving )
数组的默认位置为index=0的位置:
$ declare -a foo
$ foo=A #赋值不指定位置,默认在0位置
$ echo ${foo[0]}
A
$ foo=(a b c d e f)
$ echo ${foo} #引用不带下表的数组,默认也是0位置
a
$ echo $foo
a
返回数组长度:
${#array[*]}
${#array[@]}
比如把字符串赋值给100位置的数组元素,这时的数组只有一个元素。:
$ a[100]=foo
$ echo ${#a[*]}
1
$ echo ${#a[@]}
1
如果用这种语法去读取具体的数组成员,就会返回该成员的字符串长度:
$ a[100]=foo
$ echo ${#a[100]}
3
${!array[@]}
或${!array[*]}
,可以返回数组的成员序号,即哪些位置是有值的:
$ arr=([5]=a [9]=b [23]=c)
$ echo ${!arr[@]}
5 9 23
$ echo ${!arr[*]}
5 9 23
基于此可以遍历稀疏数组:
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
如果省略长度参数length,则返回从指定位置开始的所有成员:
$ echo ${food[@]:4}
eggs fajitas grapes
数组末尾追加成员,可以使用+=赋值运算符。它能够自动地把值追加到数组末尾:
$ foo=(a b c)
$ echo ${foo[@]}
a b c
$ foo+=(d e f)
$ echo ${foo[@]}
a b c d e f
#也可以直接拼接
$ foo=(${foo[*]} 1 2 3)
$ echo ${foo[*]}
a b c d e f 1 2 3
删除一个数组成员,使用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
unset与将某一数组元素设置为空值不同。将某个成员设为空值,可以从返回值中“隐藏”这个成员:
$ 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[@]} #1号位置仍然有元素
0 1 2 3 4 5
直接将数组变量赋值为空字符串,相当于“隐藏”数组的第一个成员:
$ 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"]}
set & shopt
Bash 执行脚本时,会创建一个子 Shell,Bash 默认给定了这个子Shell环境的各种参数。set命令用来修改子 Shell 环境的运行参数,即定制环境。
显示所有的环境变量和 Shell 函数:
$ set
set -u
:碰到不存在的变量会报错。
未设置set -u
时如下,输出不存在的变量会输出一个空行(echo输出会在行尾加换行符):
#!/usr/bin/env bash
echo $a
echo bar
#运行结果如下
$ bash script.sh
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
用来在运行结果之前,先输出执行的那一行命令:
#!/usr/bin/env bash
set -x
echo bar
上述代码运行结果如下:
$ bash script.sh
+ echo bar
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
上面的例子中,只对特定的代码段打开命令输出。
如果脚本里面有运行失败的命令(返回值非0),Bash 默认会继续执行后面的命令,这在有些开发环境下是不友好的。
#!/usr/bin/env bash
foo
echo bar
上述代码执行结果如下:
$ bash script.sh
script.sh:行3: foo: 未找到命令
bar
实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。这时,一般采用下面的写法:
command || exit 1 #如果command出错则exit 1
如果停止执行之前需要完成多个操作,就要采用下面三种写法:
# 写法一
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
set -e
使得脚本只要发生错误,就终止执行:
#!/usr/bin/env bash
set -e
foo
echo bar
上述代码执行结果如下:
$ bash script.sh
script.sh:行4: foo: 未找到命令
set -e
根据返回值来判断,一个命令是否运行失败。但是,某些命令的非零返回值可能不表示失败,或者开发者希望在命令失败的情况下,脚本继续执行下去。这时可以暂时关闭set -e
,该命令执行结束后,再重新打开set -e
:
set +e
command1
command2
set -e
还有一种方法是使用command || true,使得该命令即使执行失败,脚本也不会终止执行:
#!/bin/bash
set -e
foo || true
echo bar
上面代码中,true
使得这一行语句总是会执行成功,后面的echo bar
会执行。
-e
还有另一种写法-o errexit
:
set -o errexit
set -e
有一个例外情况,就是不适用于管道命令:
#!/usr/bin/env bash
set -e
foo | echo a
echo bar
执行结果如下:
$ bash script.sh
a
script.sh:行4: foo: 未找到命令
bar
可以看到尽管报错了,但是还是继续执行了管道命令的后半句。
Bash 会把最后一个子命令的返回值,作为整个命令的返回值。也就是说,只要最后一个子命令不失败,管道命令总是会执行成功,因此它后面命令依然会执行,set -e
就失效了。
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
没有执行。
一旦设置了-e
参数,会导致函数内的错误不会被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 -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
-s
:用来打开某个参数:
$ shopt -s optionNameHere
-u
:用来关闭某个参数:
$ shopt -u optionNameHere
-q
:也是用来查询某个参数是否打开,但不是直接输出查询结果,而是通过命令的执行状态$?
表示查询结果。如果状态为0,表示该参数打开;如果为1,表示该参数关闭:
$ shopt -q globstar
$ echo $? #返回状态为1,表示该参数是关闭的
1
这个用法主要用于脚本,供if条件结构使用。下面例子是如果打开了这个参数,就执行if结构内部的语句:
if (shopt -q globstar); then
...
fi