第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 不为空,则扩展的结果就是 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。另外,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 不为空,则扩展的结果就是 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 未设置或为空,则该扩展的结果为空。如果 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 字符开始,一直到字符串的末尾,除非指定了 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 定义的前半部分。pattern 是一个通配符模式,就像那些用在路径名扩展中的那样。上面两种形式的差别在于 # 只删除最短匹配,而 ## 则删除最长匹配。

[me@linuxbox ~]$ foo=file.txt.zip
[me@linuxbox ~]$ echo ${foo#*.}
txt.zip
[me@linuxbox ~]$ echo ${foo##*.}
zip

${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 中的内容替换。在普通形式中,只替换第一次发现的 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

参数扩展是值得去理解的。字符操作扩展可以用来替换其它命令,如 sedcut。扩展可以避免使用外部程序,从而提高脚本的效率。作为一个示例,我们将修改上一章中讨论过的 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 创建了两个变量,upperlower。我们将第一个命令行参数(位置参数 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 脚本,使其具有如下功能:

  • 完整的命令行参数的验证

  • 用于实现「交互」模式的命令行选项,该模式将提示用户输入贷款的本金,利率和期限

  • 一个更好的输出格式

扩展阅读

Last updated

Was this helpful?