shell 脚本实战 四

摘自 shell脚本实战 第二版 第三章 创建实用工具

脚本22 提醒工具

像 Stickies 这样简单的实用工具多年来广受 Windows 和 Mac 用户的欢迎,你可以用它在屏幕 上保留一些小笔记并发出提醒。这种应用非常适合于记录电话号码或其他提醒事项。可惜在 Unix 命令行中并没有与此对应的命令可用,不过这个问题用两个脚本就能解决。

第一个脚本 remember(如代码清单 3-1 所示)可以让你轻松地将信息片段保存在用户主目录 下的文件 rememberfile 中。如果调用时不使用任何参数,那么该脚本会从标准输入中读取,直到 用户按下 CTRL-D,生成文件结束序列(^D)。如果加入参数,则将这些参数直接保存到数据文 件中。

另一个配套脚本 remindme(如代码清单 3-2 所示),可以显示整个 rememberfile 的内容(如 果没有指定参数)或者搜索结果(将参数作为搜索模式)。

代码 remember

#!/bin/bash

# remember -- 一个易用的命令行提醒工具

rememberfile="$HOME/.remember"

if [ $# -eq 0 ];then
        # 提醒用户输入并将输入信息追加到文件.remember中。
        echo "Enter note, end with ^D: "
        cat - >> $rememberfile # 1 cat 命令从stdin 中读取输入 (命令中的-是stdin或stdout的简写,具体表示那个,取决于上下文环境)
else
        # 将传入脚本的参数追到到文件.remember中。
        echo "$@" >> $rememberfile # 2 如果指定了脚本参数,那么所有参数都会被追加到rememberfile中
fi
exit 0

代码 remindme

#!/bin/bash

# remindme -- 查找数据文件中匹配的行,如果没有指定参数,则显示数据的全部内容

rememberfile="$HOME/.remember"

if [ !-f $remrmberfile ];then
        echo "$0: You don't seem to have a .remember file." >&2
        echo "To remedy this,please use 'remember' to add reminders" ?&2
        exit 1
fi

if [ $# -eq 0 ];then
        # 如果没有指定人格搜索条件,则显示整个数据文件
        more $rememberfile # 3 使用more命令为用户分页显示文件内容
else
        # 否则,搜索指定内容并整齐的显示结果
        grep -i -- "$@" $rememberfile | ${PAGER:-more} # 4 使用区分大小写的grep命令 搜索关键字,然后分页形式显示搜索结果
fi

exit 0

运行结果

$ remember Southwest Airlines: 800-IFLYSWA 
$ remember 
Enter note, end with ^D:
Find Dave's film reviews at http://www.DaveOnFilm.com/ ^D

# 如果几个月后,你想查看某条便笺
$ remindme film reviews 
Find Dave's film reviews at http://www.DaveOnFilm.com/

# 如果有个 800 的号码,你实在是记不起来了
$ remindme 800 
Southwest Airlines: 800-IFLYSWA

精益求精

尽管这肯定算不上 shell 编程的代表作品,但这些脚本很好地展现了 Unix 命令行的可扩展 性。如果你有新的想法,那么实现方法可能会非常简单。

改进这些脚本的方法有很多。例如,你可以引入记录的概念:把每个 remember 条目都加上 时间戳,多行输入可以保存成一条可供正则表达式搜索的记录。可以通过这种方法保存一组人的 电话号码,只需要记住其中某个人的名字就可以检索出整个组。如果你不仅仅满足于此,还可以 加入编辑和删除功能。另外,手动编辑~/.remember 文件也非常简单。

脚本23 交互式计算器

要是你还没忘记的话,scriptbc(脚本#9)允许我们以命令行参数的形式调用 bc 执行浮点 运算。接下来自然就是编写一个包装器,把这个脚本完全变成基于命令行的交互式计算器。最终 的包装器脚本(如代码清单 3-6 所示)非常短小!一定要确保 scriptbc 位于 PATH 之中,否则脚 本无法运行。

代码 calc

#!/bin/bash

# calc -- 一个命令行计算器,可用作bc的前段


scale=2

show_help(){
cat << EOF
        In addition to standard math functions, calc also supports:

        a % b           remainder of a/b
        a ^ b           exponential: a raised to the b power
        s(x)            sine of x, x in radians
        c(x)            cosine of x, x in radians
        a(x)            arctangent of x, in radians
        l(x)            natural log of x 
        e(x)            exponential log of raising e to the x
        j(n,x)          Bessel function of integer order n of x
        scale N         show N fractional digits (default = 2)
EOF
}

if [ $# -gt 0 ];then
        exec ./scriptbc "$@"

fi

echo "Calc -- a simple calculator. Enter 'help' for help, 'quit' to quit."

/bin/echo -n "Calc> "

while read command args;do # 1 创建一个无穷循环,不断地显示提示符calc>,直到用户输入quit或按下CTRL-D(^D)退出为止
        case $command
                in
                quit|exit )             exit 0                                  ;;
                help|\? )               show_help                               ;;
                scale )                 scale=$args                             ;;
                * )                     ./scriptbc -p $scale "$command" "$args" ;;
        esac

        /bin/echo -n "Calc> "
done

echo ""

exit 0

运行结果

$ ./calc 150/3.5
42.85
$ ./calc 
Calc -- a simple calculator. Enter 'help' for help, 'quit' to quit.
Calc> 3/4
.75
Calc> 3^5
243
Calc> quit

精益求精

你在命令行中用 bc 可以做到的事情,在脚本中一样能行,但要注意,calc.sh 没有跨行记忆 (line-to-line memory)或状态保留功能。这意味着如果你喜欢的话,可以在帮助系统中添加更多 的数学函数。例如,变量 obase 和 ibase 允许用户指定输入和输出的数字基数,但由于缺少跨行 记忆,你只能修改 scriptbc(脚本#9),或是学着在一行中输入所有的设置和等式。

脚本24 温度转换

代码 convertatemp

#!/bin/bash

# convertatemp -- 温度转换脚本。用户可以输入采用特定单位(华氏单位,摄氏单位或开尔文单位)
# 的温度,脚本会输出其对应于其他两种单位的温度

if [ $# -eq 0 ];then
        cat << EOF >&2
Usage: $0 temperature[F|C|K]
where the suffix:
        F       indicates input is in Fahrenheit (default)
        C       indicates input is in Calsius
        K       indicates input is in Kelvin
EOF
        exit 1
fi

unit="$(echo $1 |sed -e 's/[-[:digit:]]*//g' |tr '[:lower:]' '[:upper:]' )" # 1 匹配零个或多个"-" 以及紧接着的任意一组数组 并替换为空
temp="$(echo $1 |sed -e 's/[^-[:digit:]]*//g' )" # 2 删除所有非"-"以及数组字符

case ${unit:=F}
in
        F ) # 华氏温度转换为摄氏温度的公式: Tc = (F -32) / 1.8
                farn="$temp"
                cels="$(echo "scale=2;($farn -32)/1.8" |bc)" # 3 将该公式转换成一个可以传给bc的序列
                kelv="$(echo "scale=2;$cels + 273.15" |bc)"
                ;;
        C ) # 摄氏温度转化为华氏温度公式: Tf = (9 /5 )*Tc+32
                cels=$temp
                kelv="$(echo "scale=2;$cels + 273.15" |bc)"
                farn="$(echo "scale=2;(1.8 * $cels) + 32" | bc)" # 4 将摄氏转换为华氏的公式
                ;;

        K ) # 摄氏温度 = 开尔文温度 - 273.15,然后使用摄氏温度转换为华氏温度的公式 # 5 将设置转换为开尔文
                kelv=$temp
                cels="$(echo "scale=2;$kelv - 273.15" |bc)"
                farn="$(echo "scale=2;(1.8 * $cels) + 32" |bc)"
                ;;

        * )
                echo "Given temperature unit is not supported"
                exit 1
esac

echo "Fahrehit = $farn"
echo "Celsius  = $cels"
echo "Kelvin   = $kelv"

exit 0

运行结果

$ ./convertatemp 212
Fahrehit = 212
Celsius  = 100.00
Kelvin   = 373.15
$ ./convertatemp 100C
Fahrehit = 212.0
Celsius  = 100
Kelvin   = 373.15
$ ./convertatemp 100K
Fahrehit = -279.67
Celsius  = -173.15
Kelvin   = 100

精益求精

你可以加入几个输入选项,一次只生成一种单位转换结果的简洁输出。例如,convertatemp -c 100F 就只输出华氏 100°所对应的摄氏温度。这种方法也可以帮助你在别的脚本中转换数值。

脚本25 计算贷款

另一种用户要经常接触的计算大概就是贷款偿还金额了。代码清单 3-10 中的脚本也能帮你 回答“我能用这笔奖金做什么?”以及“我到底能买得起那台新款特斯拉吗?”这类相关问题。

虽然根据贷款金额、利率和贷款期限计算偿还金额的公式有点棘手,但恰当地利用 shell 变 量是可以驯服这匹数学猛兽的,而且能使其变得出奇地易懂。

代码loabcalc

#!/bin/bash

# loancalc -- 根据贷款金额、利率和贷款期限(年),计算每笔付金

# 公式为: M = P * ( J / ( 1 - (1 + J)) ^ - N ))
# 其中,P = 贷款金额,J = 月利率, N = 贷款期限 (以月为单位)
# 用户一般要输入P、I(年利率) 以及L(年数)

# . ../1/library.sh     # 引入脚本 1

if [ $# -ne 3 ];then
        echo "Usage: $0 principal interst loan-duration-years" >&2
        exit 1
fi

P=$1 I=$2 L=$3 # 2 把公式拆成多个间接部分
J="$(./scriptbc -p 8 $I / \(12 \* 100\))"
N="$(( $L * 12 ))"
M="$(./scriptbc -p 8 $P \* \($J / \(1-\(1+$J\)\^ -$N\)\))"

# 对金额略做美化处理:

dollars="$(echo $M | cut -d. -f1)" # 3 第二行代码获取到月支付金额小数点之后的部分,然后只保留两位数字
cents="$(echo $M |cut -d. -f2|cut -c1-2)"

cat << EOF

A $L-year loan at $I% interest with a principal amount of $(./nicenumber $P 1 ) 
results in a payment of \$$dollars.$cents each month for the duration of 
the loan ($N payments).

EOF

exit 0

运行结果

$ ./loancalc  44900 4.75 4

A 4-year loan at 4.75% interest with a principal amount of 44,900 
results in a payment of $1028.93 each month for the duration of 
the loan (48 payments).

$ ./loancalc  44900 4.75 5

A 5-year loan at 4.75% interest with a principal amount of 44,900 
results in a payment of $842.18 each month for the duration of 
the loan (60 payments).

精益求精

如果用户没有提供任何参数,那么脚本也可以采用逐项提示的方式处理。更实用的版本是让 用户指定 4 个参数(贷款金额、利率、支付次数和月支付金额)中的任意 3 个,然后自动得出第 四个值。这样的话,如果你知道自己只能承受每月 500 美元的支出,利率 6%的汽车贷款最长期 限是 5 年,那么就能确定可以贷到的最大金额。你可以实现相应的选项,让用户传入他们需要的 值来完成这种计算。

脚本26 跟踪事件

这个简单的日历程序实际上是由两个脚本配合实现的,类似于脚本#22 中的提醒工具。第一 个脚本 addagenda(如代码清单 3-12 所示)允许用户设立一个定期事件(对于周事件,指定星期 几;对于年事件,指定月份和天数)或一次性事件(指定日、月和年)。所有被验证过的日期会 连同一行事件描述信息被保存在用户主目录的.agenda 文件内。第二个脚本 agenda(如代码清单 3-13 所示)会检查所有已知的事件,显示出目前安排的是哪个事件。

这种工具对记住生日和纪念日特别有用。如果你记不住事情,那么这个方便的脚本可以帮你 减少很多痛苦。

代码 addagenda

#!/bin/bash

# addagenda -- 提示用户添加新事件

agendafile="$HOME/.agenda"

isDayName(){

        # 如果日期没有问题,返回0;否则,返回1
        case $(echo $1 |tr '[[:upper:]]' '[[:lower:]]') in
                sun*|mon*|tue*|wed*|thu*|fri*|sat* ) retval=0 ;;
                * ) retval=1 ;;
        esac
        return $retval
}

isMonthName(){

        case $(echo $1 |tr '[[:upper:]]' '[[:lower:]]') in
                jan*|feb*|apr*|may*|jun*) return 0 ;;
                jul*|aug*|sep*|oct*|dec*) return 0 ;;
                * ) return 1 ;;
        esac
}

normalize(){ # 1 规范 压缩字符

        # 返回首字母大写,接下来两个字母小写的字符串
        /bin/echo -n $1 |cut -c1|tr '[[:lower:]]' '[[:upper:]]'
        echo $1 |cut -c2-3|tr '[[:upper:]]' '[[:lower:]]'
}

if [ ! -w $HOME ];then # -w 文件是否存在且可写
        echo "$0: cannot write in your home directory ($HOME)" >&2
        exit 1
fi

echo "Agenda: The Unix Reminder Service"
read -p "Date of event (day mon,day month year,or dayname):" word1 word2 word3 junk

if isDayName $word1;then
        if [ ! -z "$word2" ];then
                echo "Bad dayname format:just specify the dayname by itself." >&2
                exit 1
        fi
        date="$(normalize $word1)"
else
        if [ ! -z "$(echo $word1 |sed 's/[[:digit:]]//g')" ];then
                echo "Bad ate format: please specify day first, by day number" >&2
                exit 1
        fi

        if [ "$word1" -lt 1 -o "$word1" -gt 31 ];then
                echo "Bad date formate: day number can only be in range 1-31" >&2
                exit 1
        fi

        word2="$(normalize $word2)"

        if [ -z "$word3" ];then
                date="$word1$word2"
        else
                if [ ! -z "$(echo $word3 |sed 's/[[:digit:]]//g')" ];then
                        echo "Bad date formate: third field should be year." >&2
                        exit 1
                elif [ $word3 -lt 2000 -o $word3 -gt 2500 ];then
                        echo "Bad date format: year value should be 2000 - 2500 " >&2
                        exit 1
                fi

                date="$word1$word2$word3"
        fi
fi
read -p "One-line description: " description

# 准备写入数据文件

echo "$(echo $date |sed 's/ //g')|$description " >> $agendafile # 2 将规范后的记录写入 隐藏文件

exit 0

代码 agenda

#!/bin/bash

# agenda -- 扫描用户的.agenda 文件,查找是否有安排砸当天或第二天的事情

agendafile="$HOME/.agenda"

checkDate(){

        # 创建匹配当天的默认值
        weekday=$1 day=$2 month=$3 year=$4
        format1="$weekday" format2="$day$month" format3="$day$month$year" # 3 为了检查事件,将当*期装换成三种可能的日期字符串格式
        echo $format1 $format2 $format3
        # 在数据文件中对比日期.....
        IFS="|" # 读入的内容自然在IFS 分割处 将 31Oct 与Hello World分割开 分别赋值给date description

        echo "On the agenda for today:"

        while read date description;do
                echo $date $description
                if [ "$date" = "$format1" -o "$date" = "$format2" -o "$date" = "$format3" ];then
                        echo "  $description"
                fi
        done < $agendafile

}

if [ ! -e $agendafile ];then
        echo "$0: You don't seem to have an .agenda file. " >&2
        echo "To remdy this ,please use 'addagenda' to add events" >&2
        exit i
fi

# 获得当天日期...

eval $(date '+weekday="%a" month="%b" day="%e" year="%G" ') # 4 将所需的4个日期值分配给对应的变量

day="$(echo $day|sed 's/ //g')" # 删除可能存在的前挡空格 # 5 

checkDate $weekday $day $month $year

exit 0

运行结果

$ ./addagenda 
Agenda: The Unix Reminder Service
Date of event (day mon,day month year,or dayname):31 Oct
One-line description: Hello World

$ cat ~/.agenda 
31Oct|Hello World 

# 此处博主环境为国内时间 未切换成英语 故打印不在预期
$ ./agenda 
二 1412月 1412月2021
On the agenda for today:
31Oct Hello World  

精益求精

像事件跟踪这种既复杂又有趣的话题,这个脚本只能说是仅仅触碰到了表面而已。如果它能 够查看之前几天的事件安排就更好了,这需要在脚本 agenda 中做一些日期匹配操作。如果你使 用的是 GNU date 命令,那么匹配日期不是什么难事。但如果不是的话,单是在 shell 中执行日期 计算就需要复杂的脚本才能实现。关于日期操作,随后会在书中详述,尤其见脚本#99、脚本#100 和脚本#101。

另一处(更简单的)改进是让 agenda 在当前没有事件安排的时候输出 Nothing scheduled for today,而不是只输出 On the agenda for today:就草草了事。

这个脚本也可以用在 Unix 主机中发送系统范围的事件提醒(例如日程安排备份、公司放假 和员工生日)。首先,让每个用户机器上的 agenda 脚本额外检查只读的共享文件.agenda。然后, 在每个用户的.login 或类似的登录文件中调用脚本 agenda。

上一篇:区间DP


下一篇:Agenda文档与源码学习