Linux命令:shell如何操作字符串和数字?

计算机程序其实就是处理数据。前面的章节主要从文件层面讲解了数据的处理。然而,很多编程问题需要用到更小的数据单元,例如字符串和数字,来解决。

本章将学习几个用于操纵字符串和数字的shell脚本特性。Shell提供了多种字符串操作的参数扩展。除了算术扩展(在第7章讲到),还有一个常见的名为bc的命令行程序,它能执行更高层次的数学运算。

34.1 参数扩展(Parameter Expansion)

虽然参数扩展在第7章就已出现,但是因为大部分参数扩展使用在脚本文件,而非命令行中,所以我们未加详细解释,在这之前已经使用了某些形式的参数扩展,例如shell变量。Shell提供了多种参数的扩展形式。

34.1.1 基本参数

参数扩展的最简单形式体现在平常对变量的使用中。举例来说,$a扩展后成为变量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}。

34.1.2 空变量扩展的管理

有的参数扩展用于处理不存在的变量和空变量。这些参数扩展在处理缺失的位置参数和给参数赋默认值时很有用处。这种参数扩展形式如下。

${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

34.1.3 返回变量名的扩展

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

34.1.4 字符串操作

对字符串的操作,存在着大量的扩展集合。其中一些扩展尤其适用于对路径名的操作。扩展式

${#parameter}

扩展为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(长度)的话,length不能小于0。

如果参数是“@”,扩展的结果则是从offset开始,length为位置参数。

[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo ${foo: -5}
long.
[me@linuxbox ~]$ echo ${foo: -5:2}
lo
${parameter%pattern}
${parameter%%pattern}

根据pattern定义,这些扩展去除了包含在parameter中的字符串的主要部分。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

参数扩展是一个比较重要的功能。进行字符串操作的扩展可以替代其他常用的命令,例如set和cut命令。扩展通过取代外部程序,改善了脚本的执行效率。比如,我们将改动前面章节中讨论过的longest-word程序,运用参数扩展${#j}代替在subshell中输出结果的$(echo $j | wc -c),如下所示。

#!/bin/bash 

# longest-word3 : find longest string in a file 

for i; do 
     if [[ -r $i ]]; then 
          max_word= 
          max_len= 
          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 
     shift 
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秒来扫描text文件,而使用参数扩展的新版本只花费了0.06秒——这是一个比较大的改进。

34.2 算术计算和扩展

第7章我们学习了算术扩展,用来对整数进行算术运算。它的基本形式如下所示:

$((expression))

其中expression是一个有效的算术表达式。

这和用于算法计算(真实性测试)的复合命令“(( ))”有关,我们曾在第27章中遇到过。

通过前面章节的学习,我们已经了解了表达式和运算符的常见类型。下面,我们将更为完整地学习它们。

34.2.1 数字进制

回顾第9章,我们已经学习了八进制(octal)和十六进制(hexadecimal)的数字。在算术表达式中,shell支持任何进制表示的整数。表34-1列出了基本数字进制的描述。

表34-1 不同的数字进制

Linux命令:shell如何操作字符串和数字?

看一些例子,如下所示。

[me@linuxbox ~]$ echo $((0xff))
255
[me@linuxbox ~]$ echo $((2#11111111))
255

这些例子中,我们输出了十六进制数字ff(最大的两位数字)的值以及最大的八位数(二进制)。

34.2.2 一元运算符

有两种一元运算符:+和-。它们分别被用来指示一个数字是正或是负。

34.2.3 简单算术

表34-2列出了普通算术运算符。

表34-2 算术操作符

Linux命令:shell如何操作字符串和数字?

这里大部分操作符具有自描述性,但是整数除法和取模需要更深入的讨论。

由于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>

34.2.4 赋值

尽管算术表达式并非立等可见,但是它们可以用来进行赋值。通过前面不同的场景,我们已经进行了多次赋值。每当赋给变量一个值时,就是赋值操作。算术表达式可以这样使用。

[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))为条件的if语句。整个过程有两件有趣的事。(1)它给变量foo赋值5。(2)因为赋值是成功的,所以条件为true。

注意:

记住上式中“=”的确切含义很重要。单个的“=”执行赋值,即foo = 5意味着“让foo等于5”。两个“==”用来判断是否相等,即foo == 5意味着“foo是否等于5?”这可能十分令人费解,因为test命令认为单个的“=”判断字符串是否相等。但这也正是另一个使用新式的“[[ ]]”和“(( ))”混合命令代替test的理由。

此外,除了“=”之外,shell还提供了一些相当有用的赋值语句,如表34-3所示。

表34-3 赋值操作符

Linux命令:shell如何操作字符串和数字?

这些赋值操作为很多常见算术任务提供了一种快捷方式。增量(++)和减量(--)运算特别有意义,它们以1为间隔增加或减少参数的值。这种风格的表示法是从C编程语言衍生而来,并且已经被其他几种编程语言采用,其中包括bash。

[me@linuxbox ~]$ foo=1
[me@linuxbox ~]$ echo $((foo++))
1
[me@linuxbox ~]$ echo $foo
2

这些操作既可能在参数前部也可能在参数尾部出现。虽然它们都以1为间隔增加或减少参数的值,两者的位置安排有一个微妙的区别。如果在参数前部,参数在返回前增加(或减少)。如果在参数尾部,该操作在参数返回后执行。这十分奇怪,但却是故意为之。下面是一个示例。

[me@linuxbox ~]$ foo=1
[me@linuxbox ~]$ echo $((++foo))
2
[me@linuxbox ~]$ echo $foo
2

如果给变量foo赋值1,然后用“++”操作增加它的值,“++”位置在参数名后,那么foo返回值为1。然而,如果再次查看变量的值,会发现该值已经增加1。如果“++”位置在参数名前,将得到比较期望的结果,如下所示。

对于大部分shell应用,前置运算操作则是最常用的。

“++”和“--”操作符经常和循环联合使用。下面我们将对modulo脚本做些改善,使它变得更为紧凑。

#!/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"

34.2.5 位操作

有一种操作符以一种非同寻常的方式巧妙地进行数字运算,这些操作符在位层面执行运算。它们被用于特定的低层任务中,常用来位标志的设置和读取。表34-4列出了位操作符。

表34-4 位操作

Linux命令:shell如何操作字符串和数字?

注意,除了按位取反之外,也存在相应的赋值操作(例如“<<=”)。

这里将示范使用逐位左移操作,产生2的次方的一串数字。

[me@linuxbox ~]$ for ((i=0;i do echo $((1<<i)); done
1
24
8
16
32
64
128

34.2.6 逻辑操作

正如在第7章中所述,“(( ))”复合命令支持多种比较操作。有另外几种可以被用于判断逻辑是否成立的操作。表34-5列出了完整的清单。

表34-5 Comparison Operators

Linux命令:shell如何操作字符串和数字?

当使用逻辑操作时,表达式遵循算术逻辑的规则。这就是说,值为零的表达式为false,而非零表达式为true。如下所示,“(( ))”复合命令将结果映射到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

最奇怪的逻辑操作是三元操作。这个操作(该操作模仿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,或从1变为0。

请注意在表达式内的赋值操作并不能简单使用。当试图这样做时,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循环。最初,该变量被设为0(算术假),循环继续,直到变量成为非零值。在循环体内,计算计数变量a的平方和立方。在循环最后,判断计数变量的值。如果它小于10(最大迭代次数),就加1,否则给变量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

34.3 bc:一种任意精度计算语言

我们已经了解了shell可以处理所有种类的整数运算,但是如果需要执行更高级的数学运算,或者甚至使用浮点数怎么办呢?答案是无法实现,至少无法用shell直接实现。为了达到这个目的,我们需要使用一个外部程序。这里有几种方法可以使用,如嵌入Perl或AWK是一个可能的解决方案。但不幸的是,这已超出本书范围。

另一种方法是使用一个专门的计算器程序,大多数Linux系统都支持这种程序bc。

bc程序读取一个使用类C语言编写的程序文件,并执行它。bc脚本可以是一个单独的文件,也可以从标准输入中读取。bc语言支持很多功能,包括变量、循环以及由程序员自定义的函数。在这里我们不会完整地涵盖bc的知识点,而只是抛砖引玉。bc的man手册已有充分详细的说明。

以一个浅显易懂的例子开始,我们下面将写一个2加2的bc脚本。

/* A very simple bc script */

2 + 2

脚本的第一行是一个注释。bc使用和C编程语言同样的注释语法。注释可以跨越多行,以/“*”开始,以“*/”结束。

34.3.1 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 <</strong> foo.bc
4

既然支持标准输入,那么意味着可以使用嵌入文档、嵌入字符串和管道传递脚本。下面是一个嵌入字符串的例子。

[me@linuxbox ~]$ bc <<<</strong> "2+2"
4

34.3.2 脚本例子

作为一个实际的例子,我们将构建一个常见运算的脚本——按月偿还贷款。以下脚本使用嵌入文档传递脚本到bc。

#!/bin/bash

# loan-calc : script to calculate monthly loan payments

PROGNAME=$(basename $0)

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贷款的月付款数,180个月(15年)的年度百分率为7.75%。注意答案的精度,它是由赋给bc脚本中scale特定变量的值决定的。bc的man手册提供了bc脚本编程语言的完整描述。虽然它的数学表示法与shell的稍微不同(bc更接近于C语言),但是就本书目前涵盖的知识而言,大部分内容都是相似的。

34.4 本间结尾语

本章我们学习了脚本中许多“实用的”小技巧。随着脚本编程经验的增长,有效而巧妙地操纵字符串和数字将会是极其珍贵的。本书中的loan-calc脚本,说明了哪怕是最简单的脚本也可以做一些真正有用的事。

34.5 附加项

虽然loan-calc脚本的基本功能已经实现,但是这个脚本远非完善。读者可以试着改善loan-calc脚本,使其包括下面的功能。

  •  对命令行参数的充分验证。
  •  用来实现“交互”模式的命令行选项,该模式提示用户输入贷款的本金、利率和偿还期。
  •  更好的输出格式。

本文摘自:《Linux命令行大全》

Linux命令:shell如何操作字符串和数字?

本书分为四部分。第一部分开始了对命令行基本语言的学习之旅,包括命令结构、文件系统的导引、命令行的编辑以及关于命令的帮助系统和使用手册。第二部分主要讲述配置文件的编辑,用于计算机操作的命令行控制。第三部分讲述了从命令行开始执行的常规任务。类UNIX操作系统,比如Linux,包含了很多“经典的”命令行程序,这些程序可以高效地对数据进行操作。第四部分介绍了shell编程,这是一个公认的初级技术,并且容易学习,它可以使很多常见的系统任务自动运行。通过学习shell编程,读者也可以熟悉其他编程语言的使用。

本书适合从其他平台过渡到Linux的新用户和初级Linux服务器管理员阅读。没有任何Linux基础和Linux编程经验的读者,也可以通过本书掌握Linux命令行的使用方法。

猜你喜欢

转载自blog.csdn.net/epubit17/article/details/112798849