【Shell】Shell编程中的数值运算 – Blog


shell数值运算

  • 整数运算
    • 对某个数加1
    • 从 1 加到某个数
    • 求模
    • 求幂
    • 进制转换
  • 浮点运算
    • 求1除以13保留3位有效数字
    • 求人均收入最高家庭
  • 随机数
    • 获取一组随机数
    • 随机产生一个0-255的数字
  • 其他运算
    • 获取一系列数值
    • 统计字符串中各单词出现的个数
    • 指定单词统计个数
  • 小结

整数运算

对某个数加1

```$ i=0;
$ 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 个数。

使用type命令查看各个命令的类型如下:

$ 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`命令的用法: `man test`

下面,逐一演示用其他方法计算变量加一,即把 ((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 特有的语法。

### 求模

$ expr 5 % 2 1 $ let i=5%2 $ echo $i 1 $ echo 5 % 2 | bc 1 $ ((i=5%2)) $ echo $i 1

### 求幂

$ let i=52 $ echo $i 25 $ ((i=52)) $ 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 分别指定进制源和进制转换目标。

### 浮点运算

`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

### 有一组数据,求人均月收入最高家庭

在这里先随机生成一组测试数据,文件名为`income.txt`。

!/bin/bash

genrandomdata.sh

for i in $(seq 1 10) do echo $i $(($RANDOM/8192+3)) $((RANDOM/10+3000)) >> /income.txt done $ cat 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` 命令, `man 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

### 随机数

环境变量 `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 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 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 1 1 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 w get -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 https://www.xganfeni.top $ cat index.html | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | uniq -c

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

$ cat index.html | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | uniq -c | sort -n - k 1 -r | head -10 388 u 170 li 156 a 137 class 106 span 92 https 90 xganfeni 88 top 86 www 85 mejs

说明:

- `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 的用法 


演示:

./statistic.sh index.html top xganfeni www mysql 88 top 90 xganfeni 86 www 19 mysql

实际上,如果使用 grep 的 -E 选项,我们无须引入循环,而用一条命令就可以搞定

$ cat index.html | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | grep -E "^xganfeni$| ^top$" | uniq -c 88 top 90 xganfeni

或者:

$ cat index.html | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | egrep "^xganfeni$| ^top$" | uniq -c 88 top 90 xganfeni

```

小结

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

  • Shell 编程中的整数运算、浮点运算、随机数的产生、数列的产生

  • Shell 的内置命令、外部命令的区别,以及如何查看他们的类型和帮助

  • Shell 脚本的几种执行办法

  • 几个常用的 Shell 外部命令: sedawkgrepuniqsort

  • 几个范例:数字递增;求月均收入;自动获取 IP 地址;统计单词个数