第三十四章:字符串和数字

计算机程序都是关于处理数据的。在前面的章节中,我们重点介绍了在文件级别处理数据。然而,许多编程问题需要使用较小的数据单元(如字符串和数字)来解决。

在本章中,我们将介绍用于操纵字符串和数字的几个shell特性。shell提供了各种执行字符串操作的参数扩展。除了算术展开(我们在【第7章.以Shell的眼光看世界】中提到过),还有一个著名的命令行程序 bc ,它执行更高级的数学运算。

第三十四章:字符串和数字参数扩展基本参数管理空变量的扩展返回变量名的扩展字符串操作大小写转换算术评估与扩展数字进制单一运算符简单算术任务位操作逻辑bc —— 任意精度计算器语言使用 bc示例脚本总结额外信用

参数扩展

尽管【第7章】中提到了参数扩展,但我们没有详细介绍它,因为大多数参数扩展都是在脚本中使用的,而不是在命令行上使用的。我们已经使用了一些形式的参数展开,例如shell变量。shell提供了更多。

注意:除非有特殊原因,否则最好将参数扩展括在双引号中,以防止不必要的分词(word splitting)。在处理文件名时尤其如此,因为它们通常会包含嵌入式空格和其他各种脏东西(assorted nastiness)。

基本参数

最简单的参数展开形式反映在变量的普通使用中。这里有一个例子:

$a

展开后,这将成为变量 a 包含的任何内容。简单参数也可以用大括号括起来:

${a}

这对扩展没有影响,但如果变量与其他文本相邻,则是必需的,这可能会混淆shell。在这个例子中,我们试图通过将字符串 _file 附加到变量 a 的内容来创建文件名。

如果我们执行这一系列命令,结果将是啥都没有,因为shell将尝试展开一个名为 a_file 而不是 a 的变量。这个问题可以通过在“真实”变量名周围添加大括号来解决。

我们还看到,通过将数字括在大括号中,可以访问大于9的位置参数。例如,要访问第十一个位置参数,我们可以这样做:

${11}

管理空变量的扩展

几个参数扩展旨在处理不存在的空变量。这些扩展对于处理丢失的位置参数和为参数分配默认值非常方便。

注意:位置和其他特殊参数不能以这种方式分配。

返回变量名的扩展

shell能够返回变量的名称。这在一些相当奇特的情况下使用。

${!prefix*} ${!prefix@}

此扩展返回以 prefix 开头的现有变量的名称。根据bash文档,这两种形式的扩展执行方式相同。在这里,我们列出了环境中所有名称以BASH开头的变量:

字符串操作

有大量的扩展对字符串进行操作。其中许多扩展特别适合对路径名进行操作。

了解参数扩展是一件好事。字符串操作扩展可以用作其他常见命令(如 sedcut )的替代品。扩展可以通过消除外部程序的使用来提高脚本的效率。例如,我们将修改上一章讨论的 longest-word 程序,使用参数展开 ${#j} 代替命令替换 $(echo -n $j | wc -c) 及其生成的子shell,如下所示:

接下来,我们将使用 time 命令比较两个版本的效率。

原始版本的脚本需要3.618秒来扫描文本文件,而新版本使用参数扩展只需要0.06秒 - 这是一个显著的改进。

大小写转换

bash有四个参数扩展和两个 declare (声明)命令选项来支持字符串的大写/小写转换。

那么,大小写转换有什么好处呢?除了显而易见的审美价值之外,它在编程中也起着重要的作用。让我们来看看数据库搜索的情况。想象一下,用户在我们想要在数据库中查找的数据输入字段中输入了一个字符串。用户可能会输入所有大写或小写字母或两者的组合的值。我们当然不想用所有可能的大小写拼写排列来填充我们的数据库。怎么办?

解决这个问题的一个常见方法是将用户输入规范化(normalize)。也就是说,在尝试数据库查找之前,将其转换为标准化形式。我们可以通过将用户输入中的所有字符转换为小写或大写来做到这一点,并确保数据库条目以相同的方式正常化。

declare 命令可用于将字符串规范化为大写或小写。使用 declare ,我们可以强制一个变量始终包含所需的格式,无论分配给它的是什么。

在上面的脚本中,我们使用 declare 创建两个变量,upperlower 。我们将第一个命令行参数(位置参数1)的值分配给每个变量,然后在屏幕上显示它们。

正如我们所看到的,命令行参数(aBc)已经正常化。

除了 declare ,还有四个参数扩展执行大写/小写转换,如下表所述:

格式结果
${parameter,,pattern}parameter 的值扩展为所有小写。
pattern 是一个可选的shell模式(例如,[A-F]),它将限制转换哪些字符。请参阅 bash man 页面,了解模式的完整描述。
${parameter,pattern}扩展 parameter 值,只将第一个字符更改为小写。
${parameter^^pattern}parameter 的值扩展到所有大写字母。
${parameter^pattern}扩展 parameter 值,只将第一个字符更改为大写。

以下是演示这些扩展的脚本:

下面是脚本运行的效果:

我们再次处理第一个命令行参数并输出参数扩展支持的四个变化。虽然此脚本使用第一个位置参数,但 parameter 可以是任何字符串,变量或字符串表达式。

算术评估与扩展

我们在【第7章】中看了算术扩展。它用于对整数执行各种算术运算。其基本形式如下:

$((expression))

expression 是有效的算术表达式(arithmetic expression)。

这与我们在【第27章】中遇到的用于算术评估(truth tests)的复合命令 (( )) 有关。

在前面的章节中,我们看到了一些常见的表达式和运算符类型。在这里,我们将看到一个更完整的列表。

数字进制

在【第9章】中,我们看了八进制(以8为基础)和十六进制(以16为基础)数。在算术表达式中,shell支持任何基数中的整数常量。下表列出了用于指定基数的符号。

符号描述
number默认情况下,没有符号的数字被视为十进制(以10为基础)整数。
0number在算术表达式中,带零的数字被认为是八进制的。
0xnumber十六进制。
base#numberbase进制数。

以下是一些例子:

在上面的例子中,我们打印十六进制数 ff (最大的两位数)和最大的八位二进制(基数2)数的值。

单一运算符

有两个单数运算符, +- ,分别用于表示一个数是正数还是负数。一个例子是-5。

简单算术

普通算术运算符列于下表:

操作符说明
+加法(addition)
-减法(subtraction)
*乘法(multiplication)
/整数除法(integer division)
**指数(exponentiation)
%模(modulo、remainder,余)

其中大多数是可以解释的,但整数除(integer division)和模(modulo)需要进一步讨论。

由于shell的算术只在整数上运行,所以除法的结果总是整数。

这使得在除法操作中确定余数变得更加重要。

通过使用除法和模运算符,我们可以确定5除以2的结果为2,剩余为1。

计算剩余在循环中很有用。它允许在循环执行过程中在指定的时间间隔内执行操作。在下面的示例中,我们显示一行数字,突出显示 5 的每个倍数:

执行时,结果看起来像这样:

任务

虽然其用途可能不立即明显,但算术表达式可以执行分配。我们多次执行任务,尽管在不同的背景下。每当我们给一个变量一个值时,我们都在执行分配。我们也可以在算术表达式中做到这一点。

在上面的例子中,我们首先为变量 foo 指定一个空值,并验证它是否确实为空。接下来,我们使用复合命令 (( foo = 5 )) 执行 if 。这个过程做了两件有趣的事情:它为变量 foo 分配 5 的值,并且它评估为 true ,因为 foo 被分配了一个非零值。

注意:重要的是要记住前面表达式中 = 的确切含义。单独的 = 执行任务。foo = 5 说“使 foo 等于 5”,而 == 则评估等效性。foo == 5 说 “foo 等于 5 吗?” 这是许多编程语言的常见特征。在shell中,这可能有点混乱,因为 test 命令接受单个 = 作为字符串等效。这是使用更现代的 [[ ]](( )) 复合命令代替 test 的另一个原因。

除了 = 符号外,shell还提供了执行一些非常有用的任务的符号,如下表所示:

符号描述
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。

运算符可以出现在参数的前面或末尾。虽然它们都会增加或减少参数,但两个位置都有微妙的差异。如果放在参数的前面,则在返回参数之前,该参数会递增(或递减)。如果放置后,则在返回参数后执行操作。这是相当奇怪的,但这是预期的行为。这里有一个示范:

如果我们将 one 的值分配给变量 foo ,然后在参数名后放置 ++ 运算符,则 foo 将返回 one 的值。但是,如果我们第二次查看变量的值,我们会看到增量值。如果我们把 ++ 运算符放在参数前面,我们就会得到更预期的行为。

对于大多数shell应用程序,运算符前缀将是最有用的。

++-- 运算符通常与循环一起使用。我们将对我们的模块脚本进行一些改进,以收紧它一点。

位操作

一类运算符以不寻常的方式操纵数字。这些运算符在位级别上工作。它们用于某些类型的低级任务,通常涉及设置或读取位标志。位运算符列于下表:

操作符说明
~按位(bitwise)否定。否定一个数字中的所有位。
<<向左位移 。将数字中的所有位移动到左边。
>>右位移。将数字中的所有位移动到右边。
&按位与。对两个数字中的所有位执行 AND 操作。
|按位或。对两个数字中的所有位执行 OR 操作。
^按位异或。对两个数字中的所有位执行异或操作。

请注意,除按位否定(~)外,还有相应的分配运算符(例如<<=)。

在这里,我们将演示使用左边位移运算符生成 2 的功率列表:

逻辑

正如我们在【第27章】中发现的那样,复合命令 (( )) 支持各种比较运算符。还有一些可以用来评估逻辑。下表提供了完整清单:

操作符说明
<=小于等于
>=大于等于
<小于
>大于
==等于
!=不等于
&&逻辑与
||逻辑或
expr1?expr2:expr3比较(三元)运算符。如果表达式 expr1 评估为非零(算术真),则 expr2 ;其他 expr3

当用于逻辑运算时,表达式遵循算术逻辑的规则;也就是说,评估为零的表达式被认为是假的,而非零的表达式被认为是真的。 (( )) 复合命令将结果映射到 shell 的正常退出代码中。

最奇怪的逻辑运算符是三元运算符(ternary operator)。这个运算符(根据C编程语言中的运算符建模)执行一个独立的逻辑测试。它可以用作 if/then/else 语句。它对三个算术表达式(字符串不行)起作用,如果第一个表达式为true(或非零),则执行第二个表达式。否则,执行第三个表达式。我们可以在命令行上尝试:

在这里,我们看到一个三元运算符在运行。这个例子实施了一个切换(toggle)。每次运算符执行时,变量 a 的值从 0 切换到 1,反之亦然。

请注意,在表达式中执行分配并不简单。当尝试时,bash将声明一个错误。

这个问题可以通过用括号围绕分配表达式来缓解。

下面是一个更完整的例子,在生成简单数字表的脚本中使用算术运算符。

在这个脚本中,我们根据 finished 变量的值实现了一个 until 循环。最初,变量设置为零(算术假),我们继续循环直到变量变为非零。在循环中,我们计算计数器变量a的平方和立方。在循环结束时,计数器变量的值被评估。如果它小于 10(最大迭代次数),则增加 1,否则变量 finished 将被赋予 1 的值,使 finished 在算术上为真,从而终止循环。运行脚本会产生以下结果:

bc —— 任意精度计算器语言

我们已经看到shell如何处理整数算术,但如果我们需要执行更高的数学甚至只是使用浮点数呢?答案是,我们不能。至少直接用shell不能。要做到这一点,我们需要使用外部程序。我们可以采取几种方法。嵌入Perl或AWK程序是一个可能的解决方案,但不幸的是,它超出了本书的范围。

另一种方法是使用专门的计算器程序。在许多Linux系统上发现的一个程序被称为 bc

bc 程序读取用自己的C类语言编写的文件并执行它。 bc 脚本可以是一个单独的文件,也可以从标准输入中读取。 bc 语言支持很多功能,包括变量,循环和程序员定义的函数。我们不会在这里完全覆盖 bc ,只是足以获得味道。 bc 在其手册页上有很好的记录。

让我们从一个简单的例子开始。我们将编写一个 bc 脚本来添加2+2。

脚本的第一行是一个评论。 bc 使用与C编程语言相同的注释语法。注释可以跨越多行,以 /* 开头,以 */ 结尾。

使用 bc

如果我们将以前的 bc 脚本保存为 foo.bc ,我们可以这样运行它:

如果我们仔细观察,我们可以看到最底部的结果,在版权消息之后。此消息可以通过 -q ( quiet )选项来抑制。

bc 也可以互动使用:

在交互式使用 bc 时,我们只需输入我们想要执行的计算,结果将立即显示。bc quit 命令结束交互式会话。

也可以通过标准输入将脚本传递给bc

接受标准输入的能力意味着我们可以在这里使用文档,在这里使用字符串和管道来传递脚本。这是一个字符串示例:

示例脚本

作为一个现实世界的例子,我们将构建一个脚本,执行一个常见的计算,每月贷款付款。在下面的脚本中,我们使用 here 文档将脚本传递给 bc

执行时,结果看起来像这样:

此示例计算了一笔 $135,000 美元贷款的月付款,年利率为 7.75%,为期 180 个月(15 年)。注意答案的准确性。这取决于 bc 脚本中给予特殊比例变量的值。bc 脚本语言的完整描述由 bc 手册页提供。虽然它的数学符号与shell略有不同(bc 与C更为相似),但根据我们迄今所学到的知识,它的大部分内容都很熟悉。

总结

在本章中,我们了解了许多可以用来在脚本中完成“真正工作”的小东西。随着我们在脚本方面的经验的增长,有效操纵字符串和数字的能力将被证明是非常有价值的。我们的 loan-calc 脚本表明,即使是简单的脚本也可以创建来做一些非常有用的事情。

额外信用

虽然 loan-calc 脚本的基本功能已经到位,但脚本还远未完成。要获得额外的信用,请尝试使用以下功能改进 loan-calc 脚本: