数值运算

前言

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

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

Shell 本身可以做整数运算,复杂一些的运算要通过外部命令实现,比如 exprbcawk 等。另外,可通过 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 目录下。而 exprbc 因为刚用过,已经加载在内存的 hash 表中。这将有利于我们理解在上一章介绍的脚本多种执行方法背后的原理。

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

范例:从 1 加到某个数

#!/bin/bash
# calc.sh

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

注意:

特点:

1、在双括号结构中,所有表达式可以像c语言一样,如:a++,b--等。

2、在双括号结构中,所有变量可以不加入:“$”符号前缀。

3、双括号可以进行逻辑运算,四则运算

4、双括号结构 扩展了for,while,if条件测试运算

5、支持多个表达式运算,各个表达式之间用“,”分开

下面,逐一演示用其他方法计算变量加一,即把 ((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 内置命令,效率也很高,但是 exprbcawk 的计算效率就比较低。所以,在 Shell 本身能够完成相关工作的情况下,建议优先使用 Shell 本身提供的功能。但是 Shell 本身无法完成的功能,比如浮点运算,所以就需要外部命令的帮助。另外,考虑到 Shell 脚本的可移植性,在性能不是很关键的情况下,不要使用某些 Shell 特有的语法。

letexprbc 都可以用来求模,运算符都是 %,而 letbc 可以用来求幂,运算符不一样,前者是 **,后者是 ^ 。例如:

范例:求模

$ 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 比较灵活,因为它可以直接用 ibaseobase 分别指定进制源和进制转换目标。

范例: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

浮点运算

letexpr 都无法进行浮点运算,但是 bcawk 可以。

范例:求 1 除以 13,保留 3 位有效数字

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

$ echo "1 13" | awk '{printf("%0.3fn",$1/$2)}'
0.077

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

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

$ echo 1/13100 | bc -l
.00007633587786259541

随机数

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

范例:获取一个随机数

$ echo $RANDOM
81

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

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

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

可以通过 RANDOM 变量的缩放和 awkrand() 的放大来实现。

$ 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 <[email protected]>
# 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

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

补充:在 [Advanced Bash-Scripting Guide][2] 一书中还提到 jot 命令和 factor 命令,由于机器上没有,所以没有测试,factor 命令可以产生某个数的所有素数。如:

$ factor 100
100: 2 2 5 5

使用Shell进行数学计算

无论那一种编程语言都少不了,算术操作.在这则攻略中.我们将会研究在Shell中进行算术运算的各种方法. 在BASH Shell环境中,可以利用let (())和[]执行基本的算术操作.而在进行高级操作时,expr和bc这两个工具也会非常有用.

let命令

#!/bin/bash
no1=4
no2=5
let result=no1+no2
echo "no1=$no1"
echo "no2=$no2"
echo "no1+no2的结果是:$result"

#!/bin/bash
no1=4
no2=5
let result=no1+no2
echo "no1=$no1"
echo "no2=$no2"
echo "no1+no2的结果是:$result"

#自加自减操作符
let no1++
echo no1
let no1--
echo no1
let no1+=6
echo no1
let no1-=6
echo no1

#!/bin/bash
no1=4
no2=5
let result=no1+no2
echo "no1=$no1"
echo "no2=$no2"
echo "no1+no2的结果是:$result"

#自加自减操作符
let no1++
echo $no1
let no1--
echo $no1
let no1+=6
echo $no1
let no1-=6
echo $no1

#其他方法
#操作符[]的使用方法和let命令类似
result=$[ no1 + no2 ]
echo $result
#在[]中也可以使用$前缀
result=$[ $no1 +5 ]
echo $result
#也可以使用(()),但使用(())时,变量名之前需要加上$
result=$(( no1 + 50 ))
echo $result
#expr同样可以用于基本算术操作
result='expr 3+4'
result=$(expr $no1 + 5)
echo $result

小结

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

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

results matching ""

    No results matching ""