shell展开&shell数组
参数展开
大多数的参数展开会用在脚本中,而不是命令行中。
基本参数
The simplest form of parameter expansion is reflected in the ordinary use of variables.
最简单的参数展开形式反映在平常使用的变量上。例如:$a
,当$a
展开后,会变成变量 a 所包含的值。简单参数也可能用花括号引起来:${a}
。
$ a="foo"
$ echo "$a_file"
如果我们执行这个序列,没有任何输出结果,因为 shell 会试着展开一个称为 a_file 的变量,而不是 a。通过 添加花括号可以解决这个问题:
$ echo "${a}_file"
foo_file
我们已经知道通过把数字包裹在花括号中,可以访问大于9的位置参数。例如,访问第十一个位置参数,我们可以这样做:${11}
管理空变量的展开
${parameter:-word}
若 parameter 没有设置(例如,不存在)或者为空,展开结果是 word 的值。若 parameter 不为空,则展开结果是 parameter 的值。
$ foo=
$ echo ${foo:-"substitute value if unset"}
substitute value if unset
$ echo $foo
$ foo=bar
$ echo ${foo:-"substitute value if unset"}
bar
$ echo $foo
bar
${parameter:=word}
若 parameter 没有设置或为空,展开结果是 word 的值。另外,word 的值会赋值给 parameter。 若 parameter 不为空,展开结果是 parameter 的值。
$ foo=
$ echo ${foo:="default value if unset"}
default value if unset
$ echo $foo
default value if unset
$ foo=bar
$ echo ${foo:="default value if unset"}
bar
$ echo $foo
bar
${parameter:?word}
若 parameter 没有设置或为空,这种展开导致脚本带有错误退出,并且 word 的内容会发送到标准错误。若 parameter 不为空, 展开结果是 parameter 的值。
$ foo=
$ echo ${foo:?"parameter is empty"}
bash: foo: parameter is empty
$ echo $?
1
$ foo=bar
$ echo ${foo:?"parameter is empty"}
bar
$ echo $?
0
${parameter:+word}
若 parameter 没有设置或为空,展开结果为空。若 parameter 不为空, 展开结果是 word 的值会替换掉 parameter 的值;然而,parameter 的值不会改变。
$ foo=
$ echo ${foo:+"substitute value if set"}
$ foo=bar
$ echo ${foo:+"substitute value if set"}
substitute value if set
返回变量名的参数展开
${!prefix*}
${!prefix@}
这种展开会返回以 prefix 开头的已有变量名。
$ echo ${!BASH*}
BASH BASH_ARGC BASH_ARGV BASH_COMMAND BASH_COMPLETION
BASH_COMPLETION_DIR BASH_LINENO BASH_SOURCE BASH_SUBSHELL
BASH_VERSINFO BASH_VERSION
字符串展开
${#parameter}
展开成由 parameter 所包含的字符串的长度。通常,parameter 是一个字符串;然而,如果 parameter 是 @ 或者是 * 的话, 则展开结果是位置参数的个数。
$ foo="This string is long."
$ echo "'$foo' is ${#foo} characters long."
'This string is long.' is 20 characters long.
${parameter:offset}
${parameter:offset:length}
这些展开用来从 parameter 所包含的字符串中提取一部分字符。提取的字符始于第 offset 个字符(从字符串开头算起)直到字符串的末尾,除非指定提取的长度。
$ foo="This string is long."
$ echo ${foo:5}
string is long.
$ echo ${foo:5:6}
string
若 offset 的值为负数,则认为 offset 值是从字符串的末尾开始算起,而不是从开头。注意负数前面必须有一个空格, 为防止与 ${parameter:-word} 展开形式混淆。length,若出现,则必须不能小于零。
如果 parameter 是 @,展开结果是 length 个位置参数,从第 offset 个位置参数开始。
$ foo="This string is long."
$ echo ${foo: -5}
long.
$ echo ${foo: -5:2}
lo
${parameter#pattern}
${parameter##pattern}
这些展开会从 paramter 所包含的字符串中清除开头一部分文本,这些字符要匹配定义的 pattern。pattern 是 通配符模式,就如那些用在路径名展开中的模式。这两种形式的差异之处是该 # 形式清除最短的匹配结果, 而该 ## 模式清除最长的匹配结果。
$ foo=file.txt.zip
$ echo ${foo#*.}
txt.zip
$ echo ${foo##*.}
zip
${parameter%pattern}
${parameter%%pattern}
这些展开和上面的 # 和 ## 展开一样,除了它们清除的文本从 parameter 所包含字符串的末尾开始,而不是开头。
$ foo=file.txt.zip
$ echo ${foo%.*}
file.txt
$ echo ${foo%%.*}
file
${parameter/pattern/string}
${parameter//pattern/string}
${parameter/#pattern/string}
${parameter/%pattern/string}
这种形式的展开对 parameter 的内容执行查找和替换操作。如果找到了匹配通配符 pattern 的文本, 则用 string 的内容替换它。在正常形式下,只有第一个匹配项会被替换掉。在该 // 形式下,所有的匹配项都会被替换掉。 该 /# 要求匹配项出现在字符串的开头,而 /% 要求匹配项出现在字符串的末尾。/string 可能会省略掉,这样会 导致删除匹配的文本。
$ foo=JPG.JPG
$ echo ${foo/JPG/jpg}
jpg.JPG
$ echo ${foo//JPG/jpg}
jpg.jpg
$ echo ${foo/#JPG/jpg}
jpg.JPG
$ echo ${foo/%JPG/jpg}
JPG.jpg
#!/bin/bash
# 用参数展开 ${#j} 取代命令 $(echo $j | wc -c)
for i; doif [[ -r $i ]]; thenmax_word=max_len=for j in $(strings $i); dolen=${#j}if (( len > max_len )); thenmax_len=$lenmax_word=$jfidoneecho "$i: '$max_word' ($max_len characters)"fishift
done
下一步,使用 time 命令来比较这两个脚本版本的效率:
$ time longest-word2 dirlist-usr-bin.txt
dirlist-usr-bin.txt: 'scrollkeeper-get-extended-content-list' (38
characters)
real 0m3.618s
user 0m1.544s
sys 0m1.768s
$ time longest-word3 dirlist-usr-bin.txt
dirlist-usr-bin.txt: 'scrollkeeper-get-extended-content-list' (38
characters)
real 0m0.060s
user 0m0.056s
sys 0m0.008s
大小写转换
在我们试图查询数据库之前,把用户的输入转换成标准化。 通过把用户输入的字符全部转换成小写字母或大写字母,并且确保数据库中的条目 按同样的方式规范化。
declare 命令可以用来把字符串规范成大写或小写字符。
#!/bin/bash
declare -u upper
declare -l lower
if [[ $1 ]]; thenupper="$1"lower="$1"echo $upperecho $lower
fi
使用 declare 命令来创建两个变量,upper 和 lower。
把第一个命令行参数的值(位置参数1)赋给每一个变量,然后把变量值在屏幕上显示出来:
$ test aBc
ABC
abc
大小写转换参数展开
格式 | 结果 |
---|---|
$ | 把 parameter 的值全部展开成小写字母。 |
$ | 仅仅把 parameter 的第一个字符展开成小写字母。 |
$ | 把 parameter 的值全部转换成大写字母。 |
$ | 仅仅把 parameter 的第一个字符转换成大写字母(首字母大写)。 |
#!/bin/bash
if [[ $1 ]]; thenecho ${1,,}echo ${1,}echo ${1^^}echo ${1^}
fi
[me@linuxbox ~]$ ul-param aBc
abc
aBc
ABC
ABc
算术求值和展开
算术展开了。它被用来对整数执行各种算术运算。它的基本格式是:
$((expression))
这里的 expression 是一个有效的算术表达式。
数基
在算术表达式中,shell 支持任意进制的整型常量。
指定不同的数基
表示法 | 描述 |
---|---|
number | 默认情况下,没有任何表示法的数字被看做是十进制数(以10为底)。 |
0number | 在算术表达式中,以零开头的数字被认为是八进制数。 |
0xnumber | 十六进制表示法 |
base#number | number 以 base 为底 |
$ echo $((0xff))
255
$ echo $((2#11111111))
255
简单算术
Since the shell’s arithmetic only operates on integers, the results of division are always whole numbers:
因为 shell 算术只操作整型,所以除法运算的结果总是整数:
$ echo $(( 5 / 2 ))
2
$ echo $(( 5 % 2 ))
1
通过使用除法和取模运算符,我们能够确定5除以2得数是2,余数是1。
在循环中计算余数是很有用处的。在循环执行期间,它允许某一个操作在指定的间隔内执行。在下面的例子中, 我们显示一行数字,并高亮显示5的倍数:
#!/bin/bash
for ((i = 0; i <= 20; i = i + 1)); doremainder=$((i % 5))if (( remainder == 0 )); thenprintf "<%d> " $ielseprintf "%d " $ifi
done
printf "\n"
$ modulo
<0> 1 2 3 4 <5> 6 7 8 9 <10> 11 12 13 14 <15> 16 17 18 19 <20>
赋值运算符
=
、+=
、-=
、++
、--
test 命令接受单个 = 运算符 来测试字符串等价性。这也是使用更现代的 [[ ]] 和 (( )) 复合命令来代替 test 命令的另一个原因。
$ foo=1
$ echo $((++foo))
2
$ echo $foo
2
自增 ++ 和 自减 -- 运算符经常和循环操作结合使用。我们将改进我们的 modulo 脚本,让代码更紧凑些:
#!/bin/bash
for ((i = 0; i <= 20; ++i )); doif (((i % 5) == 0 )); thenprintf "<%d> " $ielseprintf "%d " $ifi
done
printf "\n"
位运算符
被用在某类底层的任务中, 经常涉及到设置或读取位标志。
位运算符
运算符 | 描述 |
---|---|
~ | 按位取反。对一个数字所有位取反。 |
<< | 位左移. 把一个数字的所有位向左移动。 |
>> | 位右移. 把一个数字的所有位向右移动。 |
& | 位与。对两个数字的所有位执行一个 AND 操作。 |
^ | 位异或。对两个数字的所有位执行一个异或操作。 |
注意除了按位取反运算符之外,其它所有位运算符都有相对应的赋值运算符(例如,<<=)。
这里我们将演示产生2的幂列表的操作,使用位左移运算符:
[me@linuxbox ~]$ for ((i=0;i<8;++i)); do echo $((1<<i)); done
1
2
4
8
16
32
64
128
逻辑运算符
operators. 复合命令 (( )) 支持各种各样的比较运算符。还有一些可以用来计算逻辑运算。
比较运算符
运算符 | 描述 |
---|---|
<= | 小于或相等 |
>= | 大于或相等 |
< | 小于 |
> | 大于 |
== | 相等 |
!= | 不相等 |
&& | 逻辑与 |
expr1?expr2:expr3 | 条件(三元)运算符。若表达式 expr1 的计算结果为非零值(算术真),则 执行表达式 expr2,否则执行表达式 expr3。 |
表达式的计算结果是零,则认为假,而非零表达式认为真。 该 (( )) 复合命令把结果映射成 shell 正常的退出码:
$ if ((1)); then echo "true"; else echo "false"; fi
true
$ if ((0)); then echo "true"; else echo "false"; fi
false
$ a=0
$ ((a<1?a+=1:a-=1))
bash: ((: a<1?a+=1:a-=1: attempted assignment to non-variable (error token is "-=1")
通过把赋值表达式用括号括起来,可以解决这个错误:
$ ((a<1?(a+=1):(a-=1)))
下一步,我们看一个使用算术运算符更完备的例子,该示例产生一个简单的数字表格:
#!/bin/bash
finished=0
a=0
printf "a\ta**2\ta**3\n"
printf "=\t====\t====\n"
until ((finished)); dob=$((a**2))c=$((a**3))printf "%d\t%d\t%d\n" $a $b $c((a<10?++a:(finished=1)))
done
在这个脚本中,我们基于变量 finished 的值实现了一个 until 循环。首先,把变量 finished 的值设为零(算术假), 继续执行循环之道它的值变为非零。在循环体内,我们计算计数器 a 的平方和立方。在循环末尾,计算计数器变量 a 的值。 若它小于10(最大迭代次数),则 a 的值加1,否则给变量 finished 赋值为1,使得变量 finished 算术为真, 从而终止循环。运行该脚本得到这样的结果:
$ arith-loop
a a**2 a**3
= ==== ====
0 0 0
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
6 36 216
7 49 343
8 64 512
9 81 729
10 100 1000
bc - 一种高精度计算器语言
不能直接用 shell 完成更高级的数学运算或仅使用浮点数。为此,我们需要使用外部程序。
嵌入的 Perl 或者 AWK 程序是一种可能的方案,
另一种方式就是使用一种专业的计算器程序。这样一个程序叫做 bc,在大多数 Linux 系统中都可以找到。bc 程序读取一个用它自己的类似于 C 语言的语法编写的脚本文件。一个 bc 脚本可能是一个分离的文件或者是从标准输入读入。bc 语言支持相当少的功能,包括变量,循环和程序员定义的函数。
编写一个 bc 脚本来执行2加2运算:
/* A very simple bc script */
2 + 2
使用 bc
如果我们把上面的 bc 脚本保存为 foo.bc,然后我们就能这样运行它:
$ bc foo.bc
bc 1.06.94
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software
Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
4
可以通过 -q(quiet)选项禁止这些版权信息。
bc 也能够交互使用:
$ bc -q
2 + 2
4
quit
bc 的 quit 命令结束交互会话。
也可能通过标准输入把一个脚本传递给 bc 程序:
$ bc < foo.bc
4
这种接受标准输入的能力,意味着可以使用 here 文档,here字符串,和管道来传递脚本。这里是一个使用 here 字符串的例子:
[me@linuxbox ~]$ bc <<< "2+2"
4
一个脚本实例
计算每月的还贷金额。使用了 here 文档把一个脚本传递给 bc:
#!/bin/bash
PROGNAME=$(basename $0)
usage () {cat <<- EOFUsage: $PROGNAME PRINCIPAL INTEREST MONTHSWhere:PRINCIPAL is the amount of the loan.INTEREST is the APR as a number (7% = 0.07).MONTHS is the length of the loan's term.EOF
}
if (($# != 3)); thenusageexit 1
fi
principal=$1
interest=$2
months=$3
bc <<- EOFscale = 10i = $interest / 12p = $principaln = $monthsa = p * ((i * ((1 + i) ^ n)) / (((1 + i) ^ n) - 1))print a, "\n"
EOF
$ loan-calc 135000 0.0775 180
475
1270.7222490000
若贷款 135,000 美金,年利率为 7.75%,借贷180个月(15年),这个例子计算出每月需要还贷的金额。 注意这个答案的精确度。这是由脚本中变量 scale 的值决定的。
创建一个数组
数组变量就像其它 bash 变量一样命名,当被访问的时候,它们会被自动地创建。
$ a[1]=foo
$ echo ${a[1]}
foo
通过第一个命令,把数组 a 的元素1赋值为 “foo”。 第二个命令显示存储在元素1中的值。在第二个命令中使用花括号是必需的, 以便防止 shell 试图对数组元素名执行路径名展开操作。
也可以用 declare 命令创建一个数组:
$ declare -a a
使用 -a 选项,declare 命令的这个例子创建了数组 a。
数组赋值
name[subscript]=value
name=(value1 value2 ...)
$ days=(Sun Mon Tue Wed Thu Fri Sat)
$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)
访问数组元素
脚本用来检查一个特定目录中文件的修改次数。显示这些文件最后是在一天中的哪个小时被修改的。这样一个脚本 可以被用来确定什么时段一个系统最活跃。这个脚本,称为 hours,输出这样的结果:
$ hours .
Hour Files Hour Files
---- ----- ---- ----
00 0 12 11
01 1 13 7
02 0 14 1
03 0 15 7
04 1 16 6
04 1 17 5
06 6 18 4
07 3 19 4
08 1 20 1
09 14 21 0
10 2 22 0
11 5 23 0
Total files = 80
当执行该 hours 程序时,指定当前目录作为目标目录。它打印出一张表显示一天(0-23小时)每小时内, 有多少文件做了最后修改。
#!/bin/bash
usage () {echo "usage: $(basename $0) directory" >&2
}
# Check that argument is a directory
if [[ ! -d $1 ]]; thenusageexit 1
fi
# Initialize array
for i in {0..23}; do hours[i]=0; done
# Collect data
for i in $(stat -c %y "$1"/* | cut -c 12-13); doj=${i/#0}((++hours[j]))((++count))
done
# Display data
echo -e "Hour\tFiles\tHour\tFiles"
echo -e "----\t-----\t----\t-----"
for i in {0..11}; doj=$((i + 12))printf "%02d\t%d\t%02d\t%d\n" $i ${hours[i]} $j ${hours[j]}
done
printf "\nTotal files = %d\n" $count
这个脚本由一个函数(名为 usage),和一个分为四个区块的主体组成。
第一部分,我们检查是否有一个命令行参数, 且该参数为目录。如果不是目录,会显示脚本使用信息并退出。
第二部分初始化一个名为 hours 的数组。给每一个数组元素赋值一个0。虽然没有特殊需要在使用之前准备数组,但是我们的脚本需要确保没有元素是空值。注意这个循环构建方式很有趣。通过使用花括号展开({0..23}),我们能很容易为 for 命令产生一系列的数据(words)。
接下来的一部分收集数据,对目录中的每一个文件运行 stat 程序。我们使用 cut 命令从结果中抽取两位数字的小时字段。 在循环里面,我们需要把小时字段开头的零清除掉,因为 shell 将试图(最终会失败)把从 “00” 到 “09” 的数值解释为八进制。 下一步,我们以小时为数组索引,来增加其对应的数组元素的值。最后,我们增加一个计数器的值(count),记录目录中总共的文件数目。
脚本的最后一部分显示数组中的内容。我们首先输出两行标题,然后进入一个循环产生两栏输出。最后,输出总共的文件数目。
数组操作
输出整个数组的内容
下标 *
和 @
可以被用来访问数组中的每一个元素。
$ animals=("a dog" "a cat" "a fish")
$ for i in ${animals[*]}; do echo $i; done
a
dog
a
cat
a
fish
$ for i in ${animals[@]}; do echo $i; done
a
dog
a
cat
a
fish
$ for i in "${animals[*]}"; do echo $i; done
a dog a cat a fish
$ for i in "${animals[@]}"; do echo $i; done
a dog
a cat
a fish
${animals[*]}
和 ${animals[@]}
的行为是一致的直到它们被用引号引起来。
确定数组元素个数
$ a[100]=foo
$ echo ${#a[@]} # number of array elements
1
$ echo ${#a[100]} # length of element 100
3
把字符串赋值给数组元素100, bash 仅仅报告数组中有一个元素。
这不同于一些其它语言的行为,这种行为是数组中未使用的元素(元素0-99)会初始化为空值, 并把它们计入数组长度。
找到数组使用的下标
${!array[*]}
${!array[@]}
这里的 array 是一个数组变量的名字。和其它使用符号 * 和 @ 的展开一样,用引号引起来的 @ 格式是最有用的, 因为它能展开成分离的词。
$ foo=([2]=a [4]=b [6]=c)
$ for i in "${foo[@]}"; do echo $i; done
a
b
c
$ for i in "${!foo[@]}"; do echo $i; done
2
4
6
在数组末尾添加元素
如果我们需要在数组末尾附加数据,那么知道数组中元素的个数是没用的,因为通过 * 和 @ 表示法返回的数值不能 告诉我们使用的最大数组索引。
shell 为我们提供了一种解决方案。通过使用+=
赋值运算符, 我们能够自动地把值附加到数组末尾。这里,我们把三个值赋给数组 foo,然后附加另外三个。
$ foo=(a b c)
$ echo ${foo[@]}
a b c
$ foo+=(d e f)
$ echo ${foo[@]}
a b c d e f
数组排序
#!/bin/bash
a=(f e d c b a)
echo "Original array: ${a[@]}"
a_sorted=($(for i in "${a[@]}"; do echo $i; done | sort))
echo "Sorted array: ${a_sorted[@]}"
$ array-sort
Original array: f e d c b a
Sorted array:
a b c d e f
删除数组
$ foo=(a b c d e f)
$ echo ${foo[@]}
a b c d e f
$ unset foo
$ echo ${foo[@]}
$
也可以使用 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
注意数组元素必须 用引号引起来为的是防止 shell 执行路径名展开操作。
有趣地是,给一个数组赋空值不会清空数组内容:
$ foo=(a b c d e f)
$ foo=
$ echo ${foo[@]}
b c d e f
任何没有下标的对数组变量的引用都指向数组元素0:
$ foo=(a b c d e f)
$ echo ${foo[@]}
a b c d e f
$ foo=A
$ echo ${foo[@]}
A b c d e f
关联数组
现在最新的 bash 版本支持关联数组了。关联数组使用字符串而不是整数作为数组索引。 这种功能给出了一种有趣的新方法来管理数据。例如,我们可以创建一个叫做 “colors” 的数组,并用颜色名字作为索引。
declare -A colors
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"
不同于整数索引的数组,关联数组必须用带有 -A 选项的 declare 命令创建。
echo ${colors["blue"]}