第34章:字符串和数字
计算机程序总是和数据一起工作。在上一章中,我们聚焦在文件级别的数据处理。然而,很多程序问题需要在更小单位的数据上处理,如字符串和数字。
本章中,我们要学习几个操纵字符串和数字的 shell 功能。shell 提供了多种参数扩展来执行对字符串的操作。除了算术扩展(我们在第 7 章「 如 Shell 般看世界 」中接触过),还有一个广为人知的命令行程序 bc
,用来执行高级别数学计算。
参数扩展
尽管在第 7 章里已经学习了参数扩展,但因为大多数参数扩展是用在脚本中而非命令行中的,所以,还有很多细节没有接触到。我们已经用过一些形式的参数扩展了,例如,shell 变量。shell 还提供了更多形式。
注意:永远要记得把参数扩展放在双引号内,以防不需要的分词,除非有特别的缘由不这么做。特别是在处理文件名的时候,因为文件名经常会包含空格和其它杂七杂八的符号。
基本参数
最简单的参数扩展形式体现在普通的变量的使用上。例如:
$a
当其展开时,成为变量 a
所包含的内容。简单参数可能还会被括在花括号内。
${a}
这不影响扩展,但是如果在该变量和其它文本连在一起、可能会使 shell 困惑的情况下,就是必须的了。在这个例子中,我们尝试用字符串 _file
附加到变量 a
的内容后,以创建一个文件名。
[me@linuxbox ~]$ a="foo"
[me@linuxbox ~]$ echo "$a_file"
如果我们执行该命令序列,结果为空,因为 shell 会尝试扩展一个名为 a_file
的变量,而非 a
变量。这就可以通过把「真的」变量名括在花括号中来解决该问题。
[me@linuxbox ~]$ echo "${a}_file"
foo_file
我们也已经看到大于 9 的位置参数可以通过括号内的数字来访问。例如要访问第 11 个位置参数,可以这么做:
${11}
管理空变量的扩展
有几个位置参数是用来处理不存在的和空的变量的。这些扩展便于处理缺失的位置参数和分配默认值给参数。
${parameter:-word}
${parameter:-word}
如果 parameter 未设置(即,不存在)或为空,则该扩展的结果就是 word。如果 parameter 不为空,则扩展的结果就是 parameter 的值。
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:-"substitute value if unset"}
substitute value if unset
[me@linuxbox ~]$ echo $foo
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:-"substitute value if unset"}
bar
[me@linuxbox ~]$ echo $foo
bar
${parameter:=word}
${parameter:=word}
如果 parameter 未设置或为空,则该扩展的结果就是 word。另外,word 的值被赋予给 parameter。如果 parameter 不为空,则扩展的结果就是 parameter 的值。
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:="default value if unset"}
default value if unset
[me@linuxbox ~]$ echo $foo
default value if unset
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:="default value if unset"}
bar
[me@linuxbox ~]$ echo $foo
bar
注意:位置和其它特殊参数不能用此方式赋值。
${parameter:?word}
${parameter:?word}
如果 parameter 未设置或为空,则该扩展导致脚本出错并退出,word 的内容被发送到标准错误。如果 parameter 不为空,则扩展的结果就是 parameter 的值。
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:?"parameter is empty"}
bash: foo: parameter is empty
[me@linuxbox ~]$ echo $?
1
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:?"parameter is empty"}
bar
[me@linuxbox ~]$ echo $?
0
${parameter:+word}
${parameter:+word}
如果 parameter 未设置或为空,则该扩展的结果为空。如果 parameter 不为空,则 word 的值被用来替代 parameter;不过,parameter 的值不会被改变。
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:+"substitute value if set"}
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:+"substitute value if set"}
substitute value if set
返回变量名的扩展
shell 具有返回变量名的能力。该功能用于一些相当奇特的场景中。
${!prefix*}
${!prefix@}
该扩展返回名称中以 prefix
开头的现有变量。根据 bash
文档,这两种形式的扩展执行效果完全一致。这里,我们列出了系统环境中以 BASH
开头的所有变量名:
[me@linuxbox ~]$ 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
是 @
或 *
,则扩展结果是位置参数的个数。
[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo "'$foo' is ${#foo} characters long."
'This string is long.' is 20 characters long.
${parameter:offset}
${parameter:offset:length}
${parameter:offset}
${parameter:offset:length}
这两个扩展用来提取包含在 parameter
内的一部分字符串。从 offset
字符开始,一直到字符串的末尾,除非指定了 length
。
[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo ${foo:5}
string is long.
[me@linuxbox ~]$ echo ${foo:5:6}
string
如果 offset
是负数,意味着从字符串的末尾而不是从开头开始计算。注意负数必须用一个空格开始,以防止和 ${parameter:-word}
扩展相混淆。length
,如果有的话,一定不能小于零。
如果 parameter
是 @
,则扩展的结果为从 offset
开始的位置参数长度。
[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo ${foo: -5}
long.
[me@linuxbox ~]$ echo ${foo: -5:2}
lo
${parameter#pattern}
${parameter##pattern}
${parameter#pattern}
${parameter##pattern}
这两个扩展会移除 parameter
字符串中由 pattern
定义的前半部分。pattern
是一个通配符模式,就像那些用在路径名扩展中的那样。上面两种形式的差别在于 #
只删除最短匹配,而 ##
则删除最长匹配。
[me@linuxbox ~]$ foo=file.txt.zip
[me@linuxbox ~]$ echo ${foo#*.}
txt.zip
[me@linuxbox ~]$ echo ${foo##*.}
zip
${parameter%pattern}
${parameter%%pattern}
${parameter%pattern}
${parameter%%pattern}
这两个扩展和前面那两个 #
和 ##
扩展一样,不过这两个是从字符串后面开始移除字符,而非从开头开始移除。
[me@linuxbox ~]$ foo=file.txt.zip
[me@linuxbox ~]$ echo ${foo%.*}
file.txt
[me@linuxbox ~]$ echo ${foo%%.*}
file
${parameter/pattern/string}
${parameter//pattern/string}
${parameter/#pattern/string}
${parameter/%pattern/string}
${parameter/pattern/string}
${parameter//pattern/string}
${parameter/#pattern/string}
${parameter/%pattern/string}
这组扩展根据 parameter
的内容执行一次搜索替换操作。如果找到匹配通配符 pattern
的文本,就会用 string
中的内容替换。在普通形式中,只替换第一次发现的 pattern
。在 //
形式中,会替换所有发现的 pattern
。/#
形式要求该匹配必须发生在字符串开端处,/%
形式则要求匹配必须发生在字符串末尾。在每一种形式中,都可以省略 /string
,结果就会导致匹配于 pattern
的文本会被删除。
[me@linuxbox ~]$ foo=JPG.JPG
[me@linuxbox ~]$ echo ${foo/JPG/jpg}
jpg.JPG
[me@linuxbox ~]$ echo ${foo//JPG/jpg}
jpg.jpg
[me@linuxbox ~]$ echo ${foo/#JPG/jpg}
jpg.JPG
[me@linuxbox ~]$ echo ${foo/%JPG/jpg}
JPG.jpg
参数扩展是值得去理解的。字符操作扩展可以用来替换其它命令,如 sed
和 cut
。扩展可以避免使用外部程序,从而提高脚本的效率。作为一个示例,我们将修改上一章中讨论过的 longest-word
程序,使用参数扩展 ${#j}
以取代命令替换 $(echo -n $j | wc -c)
,并产生子 shell,如下:
#!/bin/bash
# longest-word3: find longest string in a file
for i; do
if [[ -r "$i" ]]; then
max_word=
max_len=0
for j in $(strings $i); do
len="${#j}"
if (( len > max_len )); then
max_len="$len"
max_word="$j"
fi
done
echo "$i: '$max_word' ($max_len characters)"
fi
done
随后,我们用 time
命令比较两个版本的效率。
[me@linuxbox ~]$ 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
[me@linuxbox ~]$ 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
原始版本的脚本使用 3.618 秒扫描文本文件,而使用参数扩展的新版本,仅使用了 0.06 秒——一个显著的改进。
Case 转换
bash
有四种参数扩展和两种 declare
命令选项支持字符串的大小写转换。
那么,大小写转换有什么用呢?除了显而易见的美观价值之外,在编程中具有重要的作用。让我们考虑数据库查找的一个案例。想象有个用户,已经在要在数据库中查找的数据输入字段中输入了字符串。可能该用户会全用大写字母或小写字母键入,也可能是二者的组合。我们当然不会用每种可能的大小写排列去填充数据库。要做什么?
该问题常见的解决方法是归一化(normalize)用户的输入。即,在尝试数据库检索前,将用户的输入转换为标准形式。我们将用户输入的所有字符转换为大小写中的任何一种,并保证数据库条目也是以同样的方式归一化。
declare
命令可以用来将字符串归一化为大小写中的任一形式。使用 declare
,我们可以强制一个变量总是包含想要的格式,无论其被赋值的是什么。
#!/bin/bash
# ul-declare: demonstrate case conversion via declare
declare -u upper
declare -l lower
if [[ $1 ]]; then
upper="$1"
lower="$1"
echo "$upper"
echo "$lower"
fi
在上面的脚本中,我们用 declare
创建了两个变量,upper
和 lower
。我们将第一个命令行参数(位置参数 1)的值分配给每个变量,然后在屏幕上显示它们。
[me@linuxbox ~]$ ul-declare aBc
ABC
abc
可以看到,命令行参数(aBc
)已经得到了归一化。
除了 declare
,有四个参数扩展也可以执行大小写转换的,如表 34-1 所示。
表 34-1:大小写转换参数扩展
格式
结果
${parameter,,pattern}
将 parameter
的值全部扩展为小写。pattern
是一个可选的 shell 模式(例如 [A-F]),可以限制哪些字符被转换。请查阅 bash
手册页以获取完整的 pattern
说明。
${parameter,pattern}
将 parameter
值中的首字母扩展为小写。
${parameter^^pattern}
将 parameter
的值全部扩展为大写字母。
${parameter^pattern}
将 parameter
值中的首字母扩展为大写。
下面的这个脚本演示了这几个扩展:
#!/bin/bash
# ul-param: demonstrate case conversion via parameter expansion
if [[ "$1" ]]; then
echo "${1,,}"
echo "${1,}"
echo "${1^^}"
echo "${1^}"
fi
脚本运行结果如下:
[me@linuxbox ~]$ ul-param aBc
abc
aBc
ABC
ABc
我们又一次处理了命令行中第一个参数,并输出了四种由参数扩展支持的变形。这个脚本使用了第一个位置参数,parameter
可以是任意字符串、变量或字符串表达式。
算术评估和扩展
我们曾在第 7 章中学习过算术扩展。用来对整数执行各种算术操作。其基本形式如下:
$((expression))
其中 expression
是一个合法的算术表达式。
这就联系到第 27 章中学过的用来做算术评估(真值测试)的复合命令 (( ))
。
在前面几章中,我们学习了一些常见类型的表达式和操作符。这里,我们将学习一个更完整的列表。
基数
在第 9 章中,我们学习了八进制(基数为 8)和十六进制(基数为 16)。在算术表达式中,shell 支持任意基数的整数常量。表 34-2 中列出了用指定基数的计数法。
表 34-2:指定不同的基数
记号
描述
number
默认情况下,不带任何记号的数字被看作是十进制(基数是 10)整数。
0number
在算术表达式中,带一个前置零的被认为是八进制。
0xnumber
十六进制表示法。
base#number
基数是 base
的数字。
这里有些示例:
[me@linuxbox ~]$ echo $((0xff))
255
[me@linuxbox ~]$ echo $((2#11111111))
255
在上例中,我们打印了十六进制数字 ff
(最大的两位数)和最大的八位数二进制(基数为 2)数字。
单目运算符
有两个单目运算符,+
和 -
,分别用来指示一个数字是正值还是负值。例如 -5
。
简单算术
普通的算术运算符罗列在表 34-3 中。
表 34-3:算术运算符
运算符
描述
+
加法
-
减法
*
乘法
/
整除
**
乘方
%
模(取余)
大多数的运算符都是不言而喻的,不过整除和模还需要更进一步讨论。
由于 shell 算术运算符仅计算整数,所以除法的结果也永远是整数。
[me@linuxbox ~]$ echo $(( 5/2 ))
2
这使得确定除法运算中的余数更为重要。
[me@linuxbox ~]$ echo $(( 5 % 2 ))
1
使用整除和模运算符,我们可以确定 5 除以 2 的结果是 2,余数为 1。
计算余数在循环中很有用。它允许一个运算符在循环执行期间按指定间隔被执行。在下面的示例中,我们显示了一行数字,高亮显示了 5 的倍数:
#!/bin/bash
# modulo: demonstrate the modulo operator
for ((i = 0; i <= 20; i = i + 1)); do
remainder=$((i % 5))
if (( remainder == 0 )); then
printf "<%d> " "$i"
else
printf "%d " "$i"
fi
done
printf "\n"
当执行时,结果如下:
[me@linuxbox ~]$ modulo
<0> 1 2 3 4 <5> 6 7 8 9 <10> 11 12 13 14 <15> 16 17 18 19 <20>
赋值
尽管其用途可能不是立即显而易见的,但算术表达式可以执行赋值。我们已经执行了许多赋值,尽管在不同的上下文中。每次,我们给一个变量一个值,我们正在执行赋值。我们还可以在算术表达式中赋值。
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo $foo
[me@linuxbox ~]$ if (( foo = 5 )); then echo "It is true."; fi
It is true.
[me@linuxbox ~]$ echo $foo
5
上面这个例子中,首先给变量 foo
赋了一个空值,并验证其确实为空。然后执行了一个带复合命令 (( foo = 5 ))
的复合命令。该进程做了两件有趣的事情:它将 5
分配到了变量 foo
,并评估为真,因为 foo
已经被分配了一个非零的数值。
注意:记住上面这个表达式中
=
的确切意义非常重要。单个的=
执行赋值。foo = 5
的意思是「使foo
等于5
」,而==
评估相等性。foo == 5
的意思是「foo
等于5
吗?」这是许多编程语言中都共有的特性。在 shell 中,这会导致一点小小的困惑,因为test
命令接收单个的=
作为字符串等价判断。这也是使用更现代的[[ ]]
和(( ))
复合命令以取代test
的另一个理由。
除了 =
表示法,shell 还提供了一些表示法,以执行非常有用的赋值,如表 34-4 所示。
表 34-4:赋值运算符
记号
描述
parameter = value
简单的赋值。将 value
赋值给 parameter
。
parameter += value
加法。使 parameter
等于 parameter
+ value
。
parameter -= value
减法。使 parameter
等于 parameter
- value
。
parameter *= value
乘法。使 parameter
等于 parameter
* value
。
parameter /= value
整除。使 parameter
等于 parameter
/ value
。
parameter %= value
取模。使 parameter
等于 parameter
% value
。
parameter++
变量递增。使 parameter
= parameter
+ 1(参看下方讨论)。
parameter--
变量递减。使 parameter
= parameter
- 1。
++parameter
变量递增。使 parameter
= parameter
+ 1。
--parameter
变量递减。使 parameter
= parameter
- 1。
这些赋值运算符为许多常见的算术任务提供了方便的简写。特别有趣的是递增(++
)和递减(--
)运算符,可以使它们参数的值自增一或自减一。这种风格的表示法来自 C 编程语言,且已经被整合到许多程序语言之中,包括 bash
。
递增递减运算符可以出现在一个参数的前面或后面。无论前后,都会使得该参数递增或递减,不过这前后位置有微妙的差别。如果运算符在参数之前,在返回参数之前,该参数会被自增(或自减)。如果在参数之后,则该操作会在参数被返回后被执行。这一点相当奇怪,然而确是预期的行为。这里有一个演示:
[me@linuxbox ~]$ foo=1
[me@linuxbox ~]$ echo $((foo++))
1
[me@linuxbox ~]$ echo $foo
2
如果我们把 1
赋值给变量 foo
,然后在参数名后用 ++
运算符自增,foo
返回的值为 1
。然后如果我们第二次查看该变量的值,就会看到其已经被自增了。如果将 ++
运算符放在参数前面,则会得到更多预期的行为。
[me@linuxbox ~]$ foo=1
[me@linuxbox ~]$ echo $((++foo))
2
[me@linuxbox ~]$ echo $foo
2
对大多数 shell 应用程序,前置运算符最有用。
++
和 --
运算符常与循环一起使用。我们来改进一下取模的脚本,使其更紧凑一点。
#!/bin/bash
# modulo2: demonstrate the modulo operator
for ((i = 0; i <= 20; ++i )); do
if (((i % 5) == 0 )); then
printf "<%d> " "$i"
else
printf "%d " "$i"
fi
done
printf "\n"
位运算
有一类运算符以一种不寻常的途径操纵数字。这类运算符在位级别(bit level)工作。它们被用在某种低级别任务中,经常参与设置和读取位标识(bit flags)。这些位运算符见表 34-5。
表 34-5:位运算符
操作
描述
~
按位求反。对数字中的所有位取反。
<<
左移。将数字中的所有位向左移动。
>>
右移。将数字中的所有位向右移动。
&
位与。对两个数字上的所有位执行与运算。
|
位或。对两个数字上的所有位执行或运算。
^
位异或。对两个数字上的所有位执行异或运算。
注意还可以对上面所有的运算符做相应的赋值运算,除了按位取反以外。
下面,我们来演示使用左移运算符,来产生一个 2 的幂的列表。
[me@linuxbox ~]$ for ((i=0;i<8;++i)); do echo $((1<<i)); done
1
2
4
8
16
32
64
128
逻辑
如我们在第 27 章中学到的,(( ))
复合命令支持各种比较运算符。还有更多可以用在评估逻辑的运算符。表 34-6 提供了一个完整列表。
表 34-6:比较运算符
运算符
描述
<=
小于等于。
>=
大于等于。
<
小于。
>
大于。
==
等于。
!=
不等于。
&&
逻辑与。
||
逻辑或。
expr1?expr2:expr3
比较(三元)运算符。如果表达式 expr1
的评估结果位非零(算术真),则执行 expr2
;否则执行 expr3
。
当用在逻辑运算符时,表达式遵循如下算术逻辑准则,即,表达式评估为零的,被认为是假,非零的,则为真。(( ))
复合命令将结果映射到 shell 的正常的退出代码。
[me@linuxbox ~]$ if ((1)); then echo "true"; else echo "false"; fi
true
[me@linuxbox ~]$ if ((0)); then echo "true"; else echo "false"; fi
false
最奇怪的逻辑运算符就是三元运算符(ternary operator)。该运算符(模仿 C 程序语言中的一个)执行一个独立的逻辑测试。它可以用作一种 if/then/else
语句。它作用于三个算术表达式(字符串不起作用),如果第一个表达式的结果为真(或非零),就执行第二个表达式。否则就执行第三个表达式。可以在命令行上尝试一下:
[me@linuxbox ~]$ a=0
[me@linuxbox ~]$ ((a<1?++a:--a))
[me@linuxbox ~]$ echo $a
1
[me@linuxbox ~]$ ((a<1?++a:--a))
[me@linuxbox ~]$ echo $a
0
这里我们看到一个三元运算符在工作。该示例实施了一个拨动效果。每次执行这个运算,变量 a
的值就从 0
变为 1
,或者相反变动。
请注意在表达式中赋值,不那么简单明了。当尝试时,bash
会报告一个错误。
[me@linuxbox ~]$ a=0
[me@linuxbox ~]$ ((a<1?a+=1:a-=1))
bash: ((: a<1?a+=1:a-=1: attempted assignment to non-variable (error token is "-=1")
这个问题可以通过在赋值表达式周围使用括号来解决。
[me@linuxbox ~]$ ((a<1?(a+=1):(a-=1)))
接下来,是一个更完整的使用算术运算符的示例脚本,用来生成一个简单的数字表格。
#!/bin/bash
# arith-loop: script to demonstrate arithmetic operators
finished=0
a=0
printf "a\ta**2\ta**3\n"
printf "=\t====\t====\n"
until ((finished)); do
b=$((a**2))
c=$((a**3))
printf "%d\t%d\t%d\n" "$a" "$b" "$c"
((a<10?++a:(finished=1)))
done
在脚本中,实施了一个基于 finished
变量的值的 until
循环。首先,变量被设置为零(算术的假),随后开始循环,直到其变为非零。在循环中,我们计算了计数变量 a
的平方和立方。在循环末,会评估计数变量的值,如果小于 10(迭代的最大数),则会自增一,否则就给 finished
变量赋值为 1
,使得 finished
成为算术的真,从而终止循环。运行该脚本得到如下结果:
[me@linuxbox ~]$ 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 是如何处理各类整数计算的了,但是如果我们需要执行更高级点的数学或者仅仅是使用浮点数呢?答案是,不能。起码不能用 shell 直接计算。要做的话,就需要使用一款外部程序。我们可以用的办法有好几种。嵌入式的 Perl 或者 AWK 程序是一个可能的解决方案,不过不幸的是,这超出了本书的范畴。
另一个途径是使用特制的计算程序。在众多 Linux 系统中可以找到的一款程序,叫 bc
。
bc
程序读取一份由其自身类 C 语言写成的文件并执行。一份 bc
脚本可以是一个独立的文件,或从标准输入读取。bc
语言支持许多特性,包括变量、循环和程序员定义的函数。这里,我们不会完整地学习 bc
,仅仅是浅尝一二。bc
自己的手册页就很完善了。
让我们用一个简单的示例开始。来写一个 2 加 2 的 bc
脚本。
/* A very simple bc script */
2 + 2
脚本的第一行是一条注释。bc
使用和 C 语言一样的注释句法。注释,可以跨越多行,以 /*
开始,以 */
结束。
使用 bc
如果我们将之前的那个 bc
脚本保存为 foo.bc
,可以这样运行:
[me@linuxbox ~]$ 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
还可以交互式使用。
[me@linuxbox ~]$ bc -q
2 + 2
4
quit
当交互式使用 bc
时,可以简单的键入我们想要执行的计算,而结果即刻就会被显示。bc
命令 quit
结束了交互式会话。
还可以将一个脚本通过标准输入传递给 bc
。
[me@linuxbox ~]$ bc < foo.bc
4
获取标准输入的能力意味着我们可以使用 here 文档、here 字符串和管道来传递脚本。这里是一个 here 字符串示例:
[me@linuxbox ~]$ bc <<< "2+2"
4
一个示例脚本
作为一个真实世界的示例,我们将构建一个执行常见计算的脚本,月度贷款支付。在下面的脚本中,我们使用了一个 here 文档将一个脚本传递给 bc
:
#!/bin/bash
# loan-calc: script to calculate monthly loan payments
PROGNAME="${0##*/}" # Use parameter expansion to get basename
usage () {
cat <<- EOF
Usage: $PROGNAME PRINCIPAL INTEREST MONTHS
Where:
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)); then
usage
exit 1
fi
principal=$1
interest=$2
months=$3
bc <<- EOF
scale = 10
i = $interest / 12
p = $principal
n = $months
a = p * ((i * ((1 + i) ^ n)) / (((1 + i) ^ n) - 1))
print a, "\n"
EOF
当执行时,结果如下:
[me@linuxbox ~]$ loan-calc 135000 0.0775 180
1270.7222490000
这个示例计算了对一笔 $135,000 贷款的月度还款,年利率 7.75%,分 180 期(15 年)还清。注意该答案的精度。这取决于 bc
脚本中特殊的 scale
变量。bc
手册页提供了一份完整的 bc
脚本语言的描述。除了有一些数学表示法会略微不同于 shell(bc
更类似于 C),大多数的用法基于我们目前所学过的,所以会相当熟悉。
总结
本章中,我们了解了许多可用于通过脚本完成「实际工作」的小事情。随着我们在脚本编写方面的经验的增长,有效地操作字符串和数字的能力将变得极为宝贵。loan-calc
脚本就演示了即便是简单的脚本也可以用来做一些实际有用的事务。
额外学分
loan-calc
脚本的基本功能已经完成了,不过其距离完成还很远。作为额外学分,尝试改进 loan-calc
脚本,使其具有如下功能:
完整的命令行参数的验证
用于实现「交互」模式的命令行选项,该模式将提示用户输入贷款的本金,利率和期限
一个更好的输出格式
扩展阅读
Bash Hackers Wiki 有关于参数扩展的讨论:http://wiki.bash-hackers.org/syntax/pe
Bash Reference Manual 也有这部分:http://www.gnu.org/software/bash/manual/bashref.html#Shell-Parameter-Expansion
维基百科上有一篇关于位运算的文章:http://en.wikipedia.org/wiki/Bit_operation
以及一篇关于三元运算符的文章:http://en.wikipedia.org/wiki/Ternary_operation
以及用于
loan-calc
脚本的对计算贷款还款额的公式的描述:http://en.wikipedia.org/wiki/Amortization_calculator
Last updated
Was this helpful?