摘自 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。