# 数值运算

**关注作者公众号**： \
&#x20;![](/files/-MRfZZWty4bKGjJ7aqBQ) <br>

## 数值运算

* [前言](/shellbook/01-chapter2.md#toc_22467_18587_1)
* [整数运算](/shellbook/01-chapter2.md#toc_22467_18587_2)
  * [范例：对某个数加 1](/shellbook/01-chapter2.md#toc_22467_18587_3)
  * [范例：从 1 加到某个数](/shellbook/01-chapter2.md#toc_22467_18587_4)
  * [范例：求模](/shellbook/01-chapter2.md#toc_22467_18587_5)
  * [范例：求幂](/shellbook/01-chapter2.md#toc_22467_18587_6)
  * [范例：进制转换](/shellbook/01-chapter2.md#toc_22467_18587_7)
  * [范例：ascii 字符编码](/shellbook/01-chapter2.md#toc_22467_18587_8)
* [浮点运算](/shellbook/01-chapter2.md#toc_22467_18587_9)
  * [范例：求 1 除以 13，保留 3 位有效数字](/shellbook/01-chapter2.md#toc_22467_18587_10)
  * [范例：余弦值转角度](/shellbook/01-chapter2.md#toc_22467_18587_11)
  * [范例：有一组数据，求人均月收入最高家庭](/shellbook/01-chapter2.md#toc_22467_18587_12)
* [随机数](/shellbook/01-chapter2.md#toc_22467_18587_13)
  * [范例：获取一个随机数](/shellbook/01-chapter2.md#toc_22467_18587_14)
  * [范例：随机产生一个从 0 到 255 之间的数字](/shellbook/01-chapter2.md#toc_22467_18587_15)
* [其他运算](/shellbook/01-chapter2.md#toc_22467_18587_16)
  * [范例：获取一系列数](/shellbook/01-chapter2.md#toc_22467_18587_17)
  * [范例：统计字符串中各单词出现次数](/shellbook/01-chapter2.md#toc_22467_18587_18)
  * [范例：统计指定单词出现次数](/shellbook/01-chapter2.md#toc_22467_18587_19)
* [小结](/shellbook/01-chapter2.md#toc_22467_18587_20)
* [资料](/shellbook/01-chapter2.md#toc_22467_18587_21)
* [后记](/shellbook/01-chapter2.md#toc_22467_18587_22)

### 前言

从本文开始，打算结合平时积累和进一步实践，通过一些范例来介绍Shell编程。因为范例往往能够给人以学有所用的感觉，而且给人以动手实践的机会，从而激发人的学习热情。

考虑到易读性，这些范例将非常简单，但是实用，希望它们能够成为我们解决日常问题的参照物或者是“茶余饭后”的小点心，当然这些“点心”肯定还有值得探讨、优化的地方。

更复杂有趣的例子请参考 [Advanced Bash-Scripting Guide](http://www.tldp.org/LDP/abs/html/) (一本深入学习 Shell 脚本艺术的书籍)。

该系列概要：

* 目的：享受用 Shell 解决问题的乐趣；和朋友们一起交流和探讨。
* 计划：先零散地写些东西，之后再不断补充，最后整理成册。
* 读者：熟悉 Linux 基本知识，如文件系统结构、常用命令行工具、Shell 编程基础等。
* 建议：看范例时，可参考[《Shell基础十二篇》](http://bbs.chinaunix.net/forum.php?mod=viewthread\&tid=2198159)和[《Shell十三问》](http://bbs.chinaunix.net/thread-218853-1-1.html)。
* 环境：如没特别说明，该系列使用的 Shell 将特指 Bash，版本在 3.1.17 以上。
* 说明：该系列不是依据 Shell 语法组织，而是面向某些潜在的操作对象和操作本身，它们反应了现实应用。当然，在这个过程中肯定会涉及到 Shell 的语法。

这一篇打算讨论一下 Shell 编程中的基本数值运算，这类运算包括：

* 数值（包括整数和浮点数）间的加、减、乘、除、求幂、求模等
* 产生指定范围的随机数
* 产生指定范围的数列

Shell 本身可以做整数运算，复杂一些的运算要通过外部命令实现，比如 `expr`，`bc`，`awk` 等。另外，可通过 `RANDOM` 环境变量产生一个从 0 到 32767 的随机数，一些外部工具，比如 `awk` 可以通过 `rand()` 函数产生随机数。而 `seq` 命令可以用来产生一个数列。下面对它们分别进行介绍。

### 整数运算

#### 范例：对某个数加 1

```
$ i=0;
$ ((i++))
$ echo $i
1

$ let i++
$ echo $i
2

$ expr $i + 1
3
$ echo $i
2

$ echo $i 1 | awk '{printf $1+$2}'
3
```

说明： `expr` 之后的 `$i`，`+`，1 之间有空格分开。如果进行乘法运算，需要对运算符进行转义，否则 Shell 会把乘号解释为通配符，导致语法错误； `awk` 后面的 `$1` 和 `$2` 分别指 `$i` 和 1，即从左往右的第 1 个和第 2 个数。

用 Shell 的内置命令查看各个命令的类型如下：

```
$ type type
type is a shell builtin
$ type let
let is a shell builtin
$ type expr
expr is hashed (/usr/bin/expr)
$ type bc
bc is hashed (/usr/bin/bc)
$ type awk
awk is /usr/bin/awk
```

从上述演示可看出： `let` 是 Shell 内置命令，其他几个是外部命令，都在 `/usr/bin` 目录下。而 `expr` 和 `bc` 因为刚用过，已经加载在内存的 `hash` 表中。这将有利于我们理解在上一章介绍的脚本多种执行方法背后的原理。

说明：如果要查看不同命令的帮助，对于 `let` 和 `type` 等 Shell 内置命令，可以通过 Shell 的一个内置命令 `help` 来查看相关帮助，而一些外部命令可以通过 Shell 的一个外部命令 `man` 来查看帮助，用法诸如 `help let`，`man expr` 等。

#### 范例：从 1 加到某个数

```
#!/bin/bash
# calc.sh

i=0;
while [ $i -lt 10000 ]
do
    ((i++))
done
echo $i
```

说明：这里通过 `while [ 条件表达式 ]; do .... done` 循环来实现。`-lt` 是小于号 `<`，具体见 `test` 命令的用法：`help test`。

如何执行该脚本？

办法一：直接把脚本文件当成子 Shell （Bash）的一个参数传入

```
$ bash calc.sh
$ type bash
bash is hashed (/bin/bash)
```

办法二：是通过 `bash` 的内置命令 `.` 或 `source` 执行

```
$ . ./calc.sh
```

或

```
$ source ./calc.sh
$ type .
. is a shell builtin
$ type source
source is a shell builtin
```

办法三：是修改文件为可执行，直接在当前 Shell 下执行

```
$ chmod ./calc.sh
$ ./calc.sh
```

下面，逐一演示用其他方法计算变量加一，即把 `((i++))` 行替换成下面的某一个：

```
let i++;

i=$(expr $i + 1)

i=$(echo $i+1|bc)

i=$(echo "$i 1" | awk '{printf $1+$2;}')
```

比较计算时间如下：

```
$ time calc.sh
10000

real    0m1.319s
user    0m1.056s
sys     0m0.036s
$ time calc_let.sh
10000

real    0m1.426s
user    0m1.176s
sys     0m0.032s
$  time calc_expr.sh
1000

real    0m27.425s
user    0m5.060s
sys     0m14.177s
$ time calc_bc.sh
1000

real    0m56.576s
user    0m9.353s
sys     0m24.618s
$ time ./calc_awk.sh
100

real    0m11.672s
user    0m2.604s
sys     0m2.660s
```

说明： `time` 命令可以用来统计命令执行时间，这部分时间包括总的运行时间，用户空间执行时间，内核空间执行时间，它通过 `ptrace` 系统调用实现。

通过上述比较可以发现 `(())` 的运算效率最高。而 `let` 作为 Shell 内置命令，效率也很高，但是 `expr`，`bc`，`awk` 的计算效率就比较低。所以，在 Shell 本身能够完成相关工作的情况下，建议优先使用 Shell 本身提供的功能。但是 Shell 本身无法完成的功能，比如浮点运算，所以就需要外部命令的帮助。另外，考虑到 Shell 脚本的可移植性，在性能不是很关键的情况下，不要使用某些 Shell 特有的语法。

`let`，`expr`，`bc` 都可以用来求模，运算符都是 `%`，而 `let` 和 `bc` 可以用来求幂，运算符不一样，前者是 `**`，后者是 `^` 。例如：

#### 范例：求模

```
$ expr 5 % 2
1

$ let i=5%2
$ echo $i
1

$ echo 5 % 2 | bc
1

$ ((i=5%2))
$ echo $i
1
```

#### 范例：求幂

```
$ let i=5**2
$ echo $i
25

$ ((i=5**2))
$ echo $i

25
$ echo "5^2" | bc
25
```

#### 范例：进制转换

进制转换也是比较常用的操作，可以用 `Bash` 的内置支持也可以用 `bc` 来完成，例如把 8 进制的 11 转换为 10 进制，则可以：

```
$ echo "obase=10;ibase=8;11" | bc -l
9

$ echo $((8#11))
9
```

上面都是把某个进制的数转换为 10 进制的，如果要进行任意进制之间的转换还是 `bc` 比较灵活，因为它可以直接用 `ibase` 和 `obase` 分别指定进制源和进制转换目标。

#### 范例：ascii 字符编码

如果要把某些字符串以特定的进制表示，可以用 `od` 命令，例如默认的分隔符 `IFS` 包括空格、 `TAB` 以及换行，可以用 `man ascii` 佐证。

```
$ echo -n "$IFS" | od -c
0000000      t  n
0000003
$ echo -n "$IFS" | od -b
0000000 040 011 012
0000003
```

### 浮点运算

`let` 和 `expr` 都无法进行浮点运算，但是 `bc` 和 `awk` 可以。

#### 范例：求 1 除以 13，保留 3 位有效数字

```
$ echo "scale=3; 1/13"  | bc
.076

$ echo "1 13" | awk '{printf("%0.3f\n",$1/$2)}'
0.077
```

说明： `bc` 在进行浮点运算时需指定精度，否则默认为 0，即进行浮点运算时，默认结果只保留整数。而 `awk` 在控制小数位数时非常灵活，仅仅通过 `printf` 的格式控制就可以实现。

补充：在用 `bc` 进行运算时，如果不用 `scale` 指定精度，而在 `bc` 后加上 `-l` 选项，也可以进行浮点运算，只不过这时的默认精度是 20 位。例如：

```
$ echo 1/13100 | bc -l
.00007633587786259541
```

#### 范例：余弦值转角度

用 `bc -l` 计算，可以获得高精度：

```
$ export cos=0.996293; echo "scale=100; a(sqrt(1-$cos^2)/$cos)*180/(a(1)*4)" | bc -l
4.934954755411383632719834036931840605159706398655243875372764917732
5495504159766011527078286004072131
```

当然也可以用 `awk` 来计算：

```
$ echo 0.996293 | awk '{ printf("%s\n", atan2(sqrt(1-$1^2),$1)*180/3.1415926535);}'
4.93495
```

#### 范例：有一组数据，求人均月收入最高家庭

在这里随机产生了一组测试数据，文件名为 `income.txt`。

```
1 3 4490
2 5 3896
3 4 3112
4 4 4716
5 4 4578
6 6 5399
7 3 5089
8 6 3029
9 4 6195
10 5 5145
```

说明：上面的三列数据分别是家庭编号、家庭人数、家庭月总收入。

分析：为了求月均收入最高家庭，需要对后面两列数进行除法运算，即求出每个家庭的月均收入，然后按照月均收入排序，找出收入最高家庭。

实现：

```
#!/bin/bash
# gettopfamily.sh

[ $# -lt 1 ] && echo "please input the income file" && exit -1
[ ! -f $1 ] && echo "$1 is not a file" && exit -1

income=$1
awk '{
    printf("%d %0.2f\n", $1, $3/$2);
}' $income | sort -k 2 -n -r
```

说明：

* `[ $# -lt 1 ]`：要求至少输入一个参数，`$#` 是 Shell 中传入参数的个数
* `[ ! -f $1 ]`：要求输入参数是一个文件，`-f` 的用法见 `test` 命令，`help test`
* `income=$1`：把输入参数赋给 income 变量，再作为 `awk` 的参数，即需处理的文件
* `awk`：用文件第三列除以第二列，求出月均收入，考虑到精确性，保留了两位精度
* `sort -k 2 -n -r`：这里对结果的 `awk` 结果的第二列 `-k 2`，即月均收入进行排序，按照数字排序 `-n`，并按照递减的顺序排序 `-r`。

演示：

```
$ ./gettopfamily.sh income.txt
7 1696.33
9 1548.75
1 1496.67
4 1179.00
5 1144.50
10 1029.00
6 899.83
2 779.20
3 778.00
8 504.83
```

补充：之前的 `income.txt` 数据是随机产生的。在做一些实验时，往往需要随机产生一些数据，在下一小节，我们将详细介绍它。这里是产生 `income.txt` 数据的脚本：

```
#!/bin/bash
# genrandomdata.sh

for i in $(seq 1 10)
do
    echo $i $(($RANDOM/8192+3)) $((RANDOM/10+3000))
done
```

说明：上述脚本中还用到`seq`命令产生从1到10的一列数，这个命令的详细用法在该篇最后一节也会进一步介绍。

### 随机数

环境变量 `RANDOM` 产生从 0 到 32767 的随机数，而 `awk` 的 `rand()` 函数可以产生 0 到 1 之间的随机数。

#### 范例：获取一个随机数

```
$ echo $RANDOM
81

$ echo "" | awk '{srand(); printf("%f", rand());}'
0.237788
```

说明： `srand()` 在无参数时，采用当前时间作为 `rand()` 随机数产生器的一个 `seed` 。

#### 范例：随机产生一个从 0 到 255 之间的数字

可以通过 `RANDOM` 变量的缩放和 `awk` 中 `rand()` 的放大来实现。

```
$ expr $RANDOM / 128

$ echo "" | awk '{srand(); printf("%d\n", rand()*255);}'
```

思考：如果要随机产生某个 IP 段的 IP 地址，该如何做呢？看例子：友善地获取一个可用的 IP 地址。

```
#!/bin/bash
# getip.sh -- get an usable ipaddress automatically
# author: falcon &lt;zhangjinw@gmail.com>
# update: Tue Oct 30 23:46:17 CST 2007

# set your own network, default gateway, and the time out of "ping" command
net="192.168.1"
default_gateway="192.168.1.1"
over_time=2

# check the current ipaddress
ping -c 1 $default_gateway -W $over_time
[ $? -eq 0 ] && echo "the current ipaddress is okey!" && exit -1;

while :; do
    # clear the current configuration
    ifconfig eth0 down
    # configure the ip address of the eth0
    ifconfig eth0 \
        $net.$(($RANDOM /130 +2)) \
        up
    # configure the default gateway
    route add default gw $default_gateway
    # check the new configuration
    ping -c 1 $default_gateway -W $over_time
    # if work, finish
    [ $? -eq 0 ] && break
done
```

说明：如果你的默认网关地址不是 `192.168.1.1`，请自行配置 `default_gateway`（可以用 `route -n` 命令查看），因为用 `ifconfig` 配置地址时不能配置为网关地址，否则你的IP地址将和网关一样，导致整个网络不能正常工作。

### 其他运算

其实通过一个循环就可以产生一系列数，但是有相关工具为什么不用呢！`seq` 就是这么一个小工具，它可以产生一系列数，你可以指定数的递增间隔，也可以指定相邻两个数之间的分割符。

#### 范例：获取一系列数

```
$ seq 5
1
2
3
4
5
$ seq 1 5
1
2
3
4
5
$ seq 1 2 5
1
3
5
$ seq -s: 1 2 5
1:3:5
$ seq 1 2 14
1
3
5
7
9
11
13
$ seq -w 1 2 14
01
03
05
07
09
11
13
$ seq -s: -w 1 2 14
01:03:05:07:09:11:13
$ seq -f "0x%g" 1 5
0x1
0x2
0x3
0x4
0x5
```

一个比较典型的使用 `seq` 的例子，构造一些特定格式的链接，然后用 `wget` 下载这些内容：

```
$ for i in `seq -f"http://thns.tsinghua.edu.cn/thnsebooks/ebook73/%02g.pdf" 1 21`;do wget -c $i; done
```

或者

```
$ for i in `seq -w 1 21`;do wget -c "http://thns.tsinghua.edu.cn/thnsebooks/ebook73/$i"; done
```

补充：在 `Bash` 版本 3 以上，在 `for` 循环的 `in` 后面，可以直接通过 `{1..5}` 更简洁地产生自 1 到 5 的数字（注意，1 和 5 之间只有两个点），例如：

```
$ for i in {1..5}; do echo -n "$i "; done
1 2 3 4 5
```

#### 范例：统计字符串中各单词出现次数

我们先给单词一个定义：由字母组成的单个或者多个字符系列。

首先，统计每个单词出现的次数：

```
$ wget -c http://tinylab.org
$ cat index.html | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | uniq -c
```

接着，统计出现频率最高的前10个单词：

```
$ wget -c http://tinylab.org
$ cat index.html | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | uniq -c | sort -n -k 1 -r | head -10
    524 a
    238 tag
    205 href
    201 class
    193 http
    189 org
    175 tinylab
    174 www
    146 div
    128 title
```

说明：

* `cat index.html`: 输出 index.html 文件里的内容
* `sed -e "s/[^a-zA-Z]/\n/g"`: 把非字母字符替换成空格，只保留字母字符
* `grep -v ^$`: 去掉空行
* `sort`: 排序
* `uniq -c`：统计相同行的个数，即每个单词的个数
* `sort -n -k 1 -r`：按照第一列 `-k 1` 的数字 `-n` 逆序 `-r` 排序
* `head -10`：取出前十行

#### 范例：统计指定单词出现次数

可以考虑采取两种办法：

* 只统计那些需要统计的单词
* 用上面的算法把所有单词的个数都统计出来，然后再返回那些需要统计的单词给用户

不过，这两种办法都可以通过下面的结构来实现。先看办法一：

```
#!/bin/bash
# statistic_words.sh

if [ $# -lt 1 ]; then
    echo "Usage: basename $0 FILE WORDS ...."
    exit -1
fi

FILE=$1
((WORDS_NUM=$#-1))

for n in $(seq $WORDS_NUM)
do
    shift
    cat $FILE | sed -e "s/[^a-zA-Z]/\n/g" \
        | grep -v ^$ | sort | grep ^$1$ | uniq -c
done
```

说明：

* `if 条件部分`：要求至少两个参数，第一个单词文件，之后参数为要统计的单词
* `FILE=$1`: 获取文件名，即脚本之后的第一个字符串
* `((WORDS_NUM=$#-1))`：获取单词个数，即总的参数个数 `$#` 减去文件名参数（1个）
* `for 循环部分`：首先通过 `seq` 产生需要统计的单词个数系列，`shift` 是 Shell 内置变量（请通过 `help shift` 获取帮助)，它把用户从命令行中传入的参数依次往后移动位置，并把当前参数作为第一个参数即 `$1`，这样通过 `$1`就可以遍历用户所有输入的单词（仔细一想，这里貌似有数组下标的味道）。你可以考虑把 `shift` 之后的那句替换成 `echo $1` 测试 `shift` 的用法

演示：

```
$ chmod +x statistic_words.sh
$ ./statistic_words.sh index.html tinylab linux python
    175 tinylab
     43 linux
      3 python
```

再看办法二，我们只需要修改 `shift` 之后的那句即可：

```
#!/bin/bash
# statistic_words.sh

if [ $# -lt 1 ]; then
    echo "ERROR: you should input 2 words at least";
    echo "Usage: basename $0 FILE WORDS ...."
    exit -1
fi

FILE=$1
((WORDS_NUM=$#-1))

for n in $(seq $WORDS_NUM)
do
    shift
    cat $FILE | sed -e "s/[^a-zA-Z]/\n/g" \
        | grep -v ^$ | sort | uniq -c | grep " $1$"
done
```

演示：

```
$ ./statistic_words.sh index.html tinylab linux python
    175 tinylab
     43 linux
      3 python
```

说明：很明显，办法一的效率要高很多，因为它提前找出了需要统计的单词，然后再统计，而后者则不然。实际上，如果使用 `grep` 的 `-E` 选项，我们无须引入循环，而用一条命令就可以搞定：

```
$ cat index.html | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | grep -E "^tinylab$|^linux$" | uniq -c
     43 linux
    175 tinylab
```

或者

```
$ cat index.html | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | egrep  "^tinylab$|^linux$" | uniq -c
     43 linux
    175 tinylab
```

说明：需要注意到 `sed` 命令可以直接处理文件，而无需通过 `cat` 命令输出以后再通过管道传递，这样可以减少一个不必要的管道操作，所以上述命令可以简化为：

```
$ sed -e "s/[^a-zA-Z]/\n/g" index.html | grep -v ^$ | sort | egrep  "^tinylab$|^linux$" | uniq -c
     43 linux
    175 tinylab
```

所以，可见这些命令 `sed`，`grep`，`uniq`，`sort` 是多么有用，它们本身虽然只完成简单的功能，但是通过一定的组合，就可以实现各种五花八门的事情啦。对了，统计单词还有个非常有用的命令 `wc -w`，需要用到的时候也可以用它。

补充：在 [Advanced Bash-Scripting Guide](http://www.tldp.org/LDP/abs/html/) 一书中还提到 `jot` 命令和 `factor` 命令，由于机器上没有，所以没有测试，`factor` 命令可以产生某个数的所有素数。如：

```
$ factor 100
100: 2 2 5 5
```

### 小结

到这里，Shell 编程范例之数值计算就结束啦。该篇主要介绍了：

* Shell 编程中的整数运算、浮点运算、随机数的产生、数列的产生
* Shell 的内置命令、外部命令的区别，以及如何查看他们的类型和帮助
* Shell 脚本的几种执行办法
* 几个常用的 Shell 外部命令： `sed`，`awk`，`grep`，`uniq`，`sort` 等
* 范例：数字递增；求月均收入；自动获取 `IP` 地址；统计单词个数
* 其他：相关用法如命令列表，条件测试等在上述范例中都已涉及，请认真阅读之

如果您有时间，请温习之。

### 资料

* [Advanced Bash-Scripting Guide](http://www.tldp.org/LDP/abs/html/)
* [shell 十三问](http://bbs.chinaunix.net/thread-218853-1-1.html)
* [shell 基础十二篇](http://bbs.chinaunix.net/forum.php?mod=viewthread\&tid=2198159)
* SED 手册
* AWK 使用手册
* 几个 Shell 讨论区
  * [LinuxSir.org](http://www.linuxsir.org/bbs/forumdisplay.php?f=60)
  * [ChinaUnix.net](http://bbs.chinaunix.net/forum-24-1.html)

### 后记

大概花了 3 个多小时才写完，目前是 23:33，该回宿舍睡觉啦，明天起来修改错别字和补充一些内容，朋友们晚安！

10 月 31 号，修改部分措辞，增加一篇统计家庭月均收入的范例，添加总结和参考资料，并用附录所有代码。

Shell 编程是一件非常有趣的事情，如果您想一想：上面计算家庭月均收入的例子，然后和用 `M$ Excel` 来做这个工作比较，你会发现前者是那么简单和省事，而且给您以运用自如的感觉。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://tinylab-1.gitbook.io/shellbook/01-chapter2.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
