Work the Shell - 函数返回代码和日光计算
上个月,我探讨了退出代码,以及如何在您的 shell 脚本中进行适当的错误纠正,始终应包括在每个有意义的命令之后测试 $? 的值。编写万无一失的 shell 脚本也需要大量使用 test 命令,一个典型的序列如下所示
if [ ! -d $DIRECTORY ] ; then echo "Error: Directory $DIRECTORY doesn't exist." exit 1 fi date > $DIRECTORY/$file if [ $? -ne 0 ] ; then echo "Error: failed with error $? trying to create $file" exit 1 fi
这提醒了我,我谈到了 test 命令,但您并没有看到我在上面实际使用它。实际上,您看到了。事实证明,为了代码清晰起见,您的文件系统中有一个名为 [ 的文件,它是 test 命令的链接(硬链接)
$ ls -l /bin/{[,test} -r-xr-xr-x 2 root wheel 63184 May 18 2009 /bin/[ -r-xr-xr-x 2 root wheel 63184 May 18 2009 /bin/test
老派脚本作者使用if test <条件>,您有时会看到它出现,但现在很少见了。
本月,我想完成这个讨论,探讨 shell 脚本中的 return 命令如何允许您从脚本自身内的函数中发回信息。
假设您正忙于编写某种游戏,并且发现您想要能够确定程序运行时是白天还是晚上。也许您有一个图形背景会发生变化,就像 Gmail 的某些主题会根据您当地的天气而变化一样。
“啊哈!” 我已经听到您在说,“弄清楚白天比您想象的要棘手,Dave!” 您说得对,当然,但我会在第二阶段解决这个问题。在这个函数的第一个阶段,让我们创建一个存根,它会愚蠢地假设上午 8:00 到下午 6:00 是白天,而一天中的其余时间是晚上。
这可以很容易地实现
hour=$(date +%H) if [ $hour -ge 8 -a $hour -le 18 ] ; then yes, it's daytime else no, it's nighttime fi
这很好,但是如何在不让整个脚本都存在于一些庞大、丑陋的 if-then-else 语句中的情况下,将其与脚本的其余部分进行通信呢?更重要的是,当您想要使测试更加复杂时,例如从互联网上获取当前日期和位置的日出/日落时间,该怎么办?
让我们编写一个函数,如果现在是白天则返回 true,否则返回 false。 像这样
function isdaytime { hour=$(date +%H) if [ $hour -ge 8 -a $hour -le 18 ] ; then return 1 else return 0 fi }
您可以在脚本的 if 语句中引用它,如下所示
if isdaytime ; then
这样做的一个小问题是,您需要使用违反直觉的返回代码,1 表示失败,0 表示成功。 这类似于使用 exit 命令:您以 0 退出表示成功,以任何其他值退出表示失败。您可能从上个月回忆起的另一个小问题是,如果您要测试返回代码,那么如果在调用和测试之间有任何其他命令(包括友好的调试 echo 语句),您很容易搞砸,因为 $? 将是最近调用的函数或程序的退出代码。
假设您想要保存返回代码以供以后使用,您可以像这样调用该函数
isdaytime ; daytime=$?
此时您可能会想,“等等,为什么不这样做呢?”
function isdaytime { hour=$(date +%H) if [ $hour -ge 8 -a $hour -le 18 ] ; then daytime=1 else daytime=0 fi }
任何认真的程序员都会知道答案。让子例程或函数设置或更改全局变量是不好的形式。为什么?因为当变量在脚本中的任何位置被设置或更改时,调试变得不可能。
让我们力求使我们的脚本编写得相当优雅,因为:a) 这是好的形式,并且 b) 它使脚本更容易理解,这才是本专栏的重点,对吧?所以,认真起来,只需更改函数,使其在成功时返回 0,在失败时返回 1。
现在您有了一个函数,您可以在以后扩展它,并且有一种方法可以将 true/false 值返回给调用脚本,那么它可能如何被利用呢?
这是一种方法
if isdaytime ; then echo it is daytime else echo it is nighttime fi
非常简单,但是有了这个基本框架,让我们再看一下函数本身。
别担心,我不会突然唱起《屋顶上的提琴手》中的歌曲,但是日出和日落时间不仅非常依赖于一年中的时间,而且还依赖于您的位置。
经过一番挖掘,似乎 Almanac.com 是最容易使用的网站之一,所以我会使用它。对 Almanac.com 的日出/日落查询最终会得到如下形式的 URL:http://www.almanac.com/astronomy/rise/zipcode/80302/2010-08-01。
您将必须使用date以正确的格式计算当前日期,并将本地邮政编码硬编码到函数中。
与大多数网站一样,Almanac.com 结果生成的 HTML 不易于解析,所以我不得不挖掘一段时间才能弄清楚如何进行。这是我想出的
yourzip="80302" # set this to your local zip code request="http://www.almanac.com/astronomy/rise/zipcode" thedate="$(date +%Y-%m-%d)" url="$request/$yourzip/$thedate" curl --silent "$url" | grep rise_nextprev | \ cut -d\< -f28-30
您可以看到邮政编码确实是硬编码的,并注意到我如何使用 $() 表示法以 YYYY-MM-DD 格式获取日期。Curl 为您提供生成的 HTML 页面,grep 查找您感兴趣的行,然后 cut 剪切出以下代码段
td> 5:38 A.M.</td><td> 8:34 P.M.
还有一些步骤需要完成,因此您可以分别提取日出和日落的小时和分钟(因为您必须以这种方式进行测试)。这是我想出的代码
raw="$(/usr/bin/curl --silent "$url" | \ grep rise_nextprev | cut -d\<-f28-30)" sunrise="$(echo $raw | cut -d\ -f2)" sunset="$(echo $raw | cut -d\ -f4)" srh=$(echo $sunrise | cut -d: -f1) srm=$(echo $sunrise | cut -d: -f2) ssh=$(echo $sunset | cut -d: -f1) ssm=$(echo $sunset | cut -d: -f2)
通过避免日出和日落的中间计算,您可以使其更快一些,但是在现代 Linux 系统上,这应该是毫秒级的事情,所以就让它保持这样吧。
还有一个重要的调整:日落小时 (ssh) 需要采用 24 小时制,因为这是您从前面显示的 date 调用中获得的内容。事实证明,您可以将 cut 子 shell 调用放入计算中
ssh=$(( $(echo $sunset | cut -d: -f1) + 12 ))
是的,它可以工作。看起来我要进入 LISP 领域了,但幸运的是没有!
为了正常工作,脚本需要执行三个测试
是否是日出小时且大于日出分钟。
是否大于日出小时但小于日落小时。
是否是日落小时但小于日落分钟。
以下是脚本中的样子
if [ $hour -eq $srh -a $min -ge $srm ] ; then return 0 # special case of sunrise hour fi if [ $hour -gt $srh -a $hour -lt $ssh ] ; then return 0 # easy: after sunrise, before sunset fi if [ $hour -eq $ssh -a $min -le $ssm ] ; then return 0 # special case: sunset hour fi
瞧! 如果我自己说的话,还真不错。
我在 Linux Journal FTP 服务器上提供了 isdaytime 的完整实现,地址为 ftp.linuxjournal.com/pub/lj/listings/issue198/10860.tgz。
Dave Taylor 长期以来一直在编写 shell 脚本,已有 30 年。他是流行的 Wicked Cool Shell Scripts 的作者,可以在 Twitter 上通过 @DaveTaylor 找到他,更普遍地可以通过 www.DaveTaylorOnline.com 找到他。