第 26 章 一些小有意思的脚本

本章内容

  • 发送消息
  • 获取灵感
  • 发送文本

学习编写shell脚本的主要原因在于能够创建自己的Linux系统实用工具。明白如何编写有实用价值的脚本工具很重要。但有时候寓教于乐也是不错的选择。本章中出现的脚本
未必实用,但都充满了趣味!这同时也有助于巩固你的脚本编写知识。

26.1 发送消息

无论是在办公室还是在家里,发送消息的方法有很多:短信、电子邮件,甚至打电话。有种不常用的方法是将消息直接发送到同伴系统的用户终端上。因为这种方法并不广为人知,所以用它和别人来交流一定很好玩。
这个shell脚本工具能够帮你简单快速地向你的Linux系统登录用户发送消息。这个脚本简单至极,也乐趣满满。

26.1.1 功能分析

对于这种简单的脚本,需要的功能不多。涉及的一些命令很常见,本书也讲过。不过有几个命令我们只接触过皮毛,你可能还不太熟悉。本节会讲解编写这个简单有趣的脚本所需的命令。

  1. 确定系统中都有谁26

要用到的第一个工具就是who命令。该命令可以告诉你当前系统中所有的登录用户。

$ who
christine tty2 2015-09-10 11:43
timothy tty3 2015-09-10 11:46
[...]
$

发送消息所需要的所有信息都可以在这部分输出的信息列表中找到。who命令默认给出的是

可用信息的简略版本。这些信息包括:

  • 用户名
  • 用户所在终端
  • 用户登入系统的时间

如果要发送消息,只需使用前两项信息。用户名和用户当前终端是必须要用到的。

  1. 启用消息功能

用户可以禁止他人使用mesg工具向自己发送消息。因此你在打算发送消息前,最好先检查一下是否允许发送消息。这只需要输入命令mesg就行了。

$ mesg
is n
$

结果中显示的is n表明消息发送功能被关闭了。如果结果是y,表明允许发送消息。

窍门 有些发行版(如Ubuntu)默认关闭了消息发送功能。而对于其他发行版(如CentOS),消息发送功能默认是开启的。因此在发送消息前,你需要检查一下所使用发行版的具体设置以及其他用户的消息状态。

要查看别人的消息状态,还可以使用who命令。记住,这只检查当前已登入用户的消息状态。使用who命令的-T选项:

$ who -T
christine - tty2 2015-09-10 12:56
timothy - tty3 2015-09-10 11:46
[...]
$

用户名后面的破折号(-)表示这些用户的消息功能已经关闭。如果启用的话,你看到的会是加号(+)。
如果要接收消息,你需要使用mesg命令的y选项。

$ whoami
christine
$
$ mesg y
$
$ mesg
is y
$

当发出mesg y命令后,用户christine的消息功能就启用了。可以使用mesg命令来检查用户的消息状态。毫无疑问,命令的结果是is y,这说明该用户已经可以接收消息。
其他用户使用who命令可以看到用户christine已经改变了她的消息状态。现在消息状态已经变成了加号,表明她可以接收他人的消息了。

$ who -T
christine + tty2 2015-09-10 12:56
timothy - tty3 2015-09-10 11:46
[...]
$

要想进行双向通信,其他用户也必须启用消息功能。在这个例子中,用户timothy也启用了他的消息功能。

$ who -T
christine + tty2 2015-09-10 12:56
timothy + tty3 2015-09-10 11:46
[...]
$

现在,消息功能至少在两名用户之间启用了,你可以试试用命令发送消息。不过who命令还用得上,因为它能够提供消息发送的必需信息。

  1. 向其他用户发送消息

我们的脚本用到的主要工具是write命令。只要消息功能启用,就可以使用write命令通过其他登录用户的用户名和当前终端向其发送消息。

说明 你只能使用write命令向登录到虚拟控制台终端(参见第2章)的用户成功发送消息。登入图形化环境的用户是无法接收到消息的。

在下面的例子中,用户christine向登录在终端tty3上的用户timothy发送了一条消息。在christine的终端上,会话过程看起来如下。

$ who
christine tty2 2015-09-10 13:54
timothy tty3 2015-09-10 11:46
[...]
$
$ write timothy tty3
Hello Tim!
$

消息的接收方会看到如下信息。

Message from christine@server01 on tty2 at 14:11 ...
Hello Tim!
EOF

接收方可以看到消息是由哪个用户在哪个终端上发送的。也可以给消息加上一个时间戳。注意,消息的末尾出现了EOF,表示文件结束,这可以让接收方知道消息已经全部显示出来了。

窍门 接收到消息之后,接收方经常需要按回车键来重新获得命令行提示符。

现在,你可以发送消息了!接下来要使用这些命令创建脚本。

26.1.2 创建脚本

使用脚本发送消息有助于解决一些潜在的问题。首先,如果系统中有很多用户,要找出你想发送消息的那个用户可是个苦差事!你还得确定这个用户是否启用了消息功能。另外,脚本还能够提高效率,可以让你一步就把消息快速发送给特定的用户。

  1. 检查用户是否登录

第一个问题就是得让脚本知道要给谁发送消息。这一点很容易实现,只需要在执行脚本是加上一个参数就行了。对于确定特定用户是否登录的问题,可以利用who命令,脚本代码如下。

# Determine if user is logged on:
#
logged_on=$(who | grep -i -m 1 $1 | gawk '{print $1}')
#

在上面的代码中,who命令的结果被管接入grep命令(参见第4章)。grep命令使用选项-i 来忽略大小写,用户名使用大小写字母都可以。grep命令中还包含了选项-m 1,这是为了防止用户多次登入系统。grep命令要么什么都不输出(如果用户还没有登录),要么生成用户首次登录的信息。输出的信息被传给gawk命令(参见第19章)。gawk命令只返回第一个字段,要么为空, 要么是用户名。该命令最终的输出结果被保存在变量logged_on中。

窍门 在有些Linux发行版中(例如Ubuntu),可能并没有默认安装gawk。可以输入apt-get install gawk进行安装。还可以在第9章中找到更多有关软件包安装的信息。

变量logged_on中可能什么都没有(如果用户没有登录),也可能包含用户名,可以对变量内容进行测试,并根据测试结果进行相应的处理。

#
if [ -z $logged_on ]
then
  echo "$1 is not logged on."
  echo "Exiting script..."
  exit
fi
#

利用if语句和test命令来测试变量logged_on是否为空。如果变量为空,通过echo命令提 醒脚本用户指定的用户尚未登录系统,然后使用exit命令退出脚本。如果指定用户已经登入系 统,则变量logged_on中包含了该用户的用户名,脚本继续执行。
在下面的例子中,用户Charlie被作为参数传给shell脚本。这个用户尚未登入系统。

$ ./mu.sh Charlie
Charlie is not logged on.
Exiting script...
$

代码工作良好!现在你不用埋头在who命令的输出中翻看某个用户是否登录系统,用这个脚本就可以帮你搞定。

  1. 检查用户是否接受消息

下一个重要事项是确定登录用户是否接受消息。这部分脚本的工作方法和确定用户是否登录的那部分脚本非常像。

# Determine if user allows messaging:
#
allowed=$(who -T | grep -i -m 1 $1 | gawk '{print $2}')
#
if [ $allowed != "+" ]
then
  echo "$1 does not allowing messaging."
  echo "Exiting script..."
  exit
fi
#

注意,这次我们不仅使用了who命令,还加上了-T选项。如果允许接收消息的话,这会在用户名后显示+,否则会显示一个-。who命令的结果会被管接入grep和gawk,只提取出消息接收人,并将其存储在变量allowed中。最后使用if语句测试消息接收人是否被设置了+。如果没有设置+,则提示脚本用户并退出脚本。如果消息接收人能够接收消息,脚本继续向下执行。
要检验这部分脚本,需要一个已登录且不接受消息的用户参与测试。用户Samantha目前关闭接收消息功能。

$ ./mu.sh Samantha
Samantha does not allowing messaging.
Exiting script...
$

测试结果和预期的一样。有了这部分脚本,就再也不需要手动检查消息功能是否启用了。

  1. 检查是否包含要发送的消息

待发送的消息会被作为脚本参数。因此,还要检查mu.sh脚本是否将消息作为了参数。要测试这个消息参数,和之前一样,需要在脚本代码中加入if语句。

# Determine if a message was included:
#
if [ -z $2 ]
then
  echo "No message parameter included."
  echo "Exiting script..."
  exit
fi
#

我们使用一个已登录且启用了消息功能的用户来测试这部分脚本,不过在测试中并没有加入要发送的消息。

$ ./mu.sh Timothy
No message parameter included.
Exiting script...
$

这正是我们需要的!现在脚本已经完成了这些前期检查工作,可以开始执行它的主要任务了: 发送消息。

  1. 发送简单的消息

在发送消息前,必须识别并将用户当前终端保存在变量中。who、grep和gawk再次出马。

# Send message to user:
#
uterminal=$(who | grep -i -m 1 $1 | gawk '{print $2}')
#

要发送消息,需要使用echo和write。

#
echo $2 | write $logged_on $uterminal
#

因为write是一个交互式命令,所以它必须从管道中接收消息,这样脚本才能正常工作。echo 命令用来将保存在$2中的消息发送到STDOUT,然后再通过管道传给write命令。logged_on变量保存了用户名,uterminal变量保存了用户当前的终端。
现在来测试一下,通过脚本向指定用户发送一条简单的消息。

$ ./mu.sh Timothy test
$

用户Timothy在自己的终端上接收到了以下消息。

Message from christine@server01 on tty2 at 10:23 ...
test
EOF

搞定!现在可以通过脚本向系统中的其他用户发送一个单词的消息了。

  1. 发送长消息

你通常可不会愿意只向其他用户发送一个单词的消息。让我们来试试用当前的脚本发送更多内容的消息。

$ ./mu.sh Timothy Boss is coming. Look busy.
$

用户Timothy在自己的终端上接收到了以下消息。

Message from christine@server01 on tty2 at 10:24 ...
Boss
EOF

看来不行。只有消息中第一个单词Boss被成功发送了。这是因为脚本使用了参数(参见第14 章)。bash shell使用空格来区分不同的参数。因为消息中有空格,所以消息中的每个单词都被视为一个不同的参数。必须修改脚本来解决这个问题。

对此,shift命令(参见第14章)和while循环(参见第13章)可助其一臂之力。

# Determine if there is more to the message:
#
shift
#
while [ -n "$1" ]
do
  whole_message=$whole_message' '$1
  shift
done
#

shift命令允许你在不知道参数总数的情况下处理各种脚本参数。该命令会将下一个参数移 动到$1。一开始必须在while循环前使用一次shift,因为消息是从$2参数开始的,而非$1。
进入while循环后,它接着获取消息中的每个单词,并将单词添加到变量whole_message 中,然后使用shift命令移动到下一个参数。处理完最后一个参数后,while循环退出,完整的消息就被保存在了变量whole_message中。
还要对脚本进行另一处修改。脚本需要将变量whole_message发送给write,而不是仅仅发送参数$2。

# Send message to user:
#
uterminal=$(who | grep -i -m 1 $1 | gawk '{print $2}')
#
echo $whole_message | write $logged_on $uterminal
#

现在再试试发送一条警告消息,告诉Timothy,老板正走向他。

$ ./mu.sh Timothy Boss is coming
Usage: grep [OPTION]... PATTERN [FILE]...
Try 'grep --help' for more information.
$

还是不行。这是因为在脚本中使用shift命令时,参数$1中的内容被删除了。因此当脚本试图在grep命令中使用$1时,就产生了错误。要解决这个问题,需要使用一个变量muser来保存参数$1的内容。

# Save the username parameter
#
muser=$1
#

现在变量muser中保存了用户名。grep和echo命令中涉及使用参数$1的地方都可以使用
muser来替换。

# Determine if user is logged on:
#
logged_on=$(who | grep -i -m 1 $muser | gawk '{print $1}')
[...]
echo "$muser is not logged on."
[...]
# Determine if user allows messaging:
#
allowed=$(who -T | grep -i -m 1 $muser | gawk '{print $2}')
[...]
echo "$muser does not allowing messaging."
[...]
# Send message to user:
#
uterminal=$(who | grep -i -m 1 $muser | gawk '{print $2}')
[...]

可以再发送一次长消息来测试一下修改后的脚本。另外我们还在消息中加入了几个惊叹号。

$ ./mu.sh Timothy The boss is coming! Look busy!
$

用户Timothy在自己的终端上接收到下面的消息。

Message from christine@server01 on tty2 at 10:30 ...
The boss is coming! Look busy!
EOF

没问题啦!现在可以使用这个脚本快速向系统中的其他用户发送消息。最终的脚本代码如下。

#!/bin/bash
#
#mu.sh - Send a Message to a particular user
#############################################
#
# Save the username parameter
#
muser=$1
#
# Determine if user is logged on:
#
logged_on=$(who | grep -i -m 1 $muser | gawk '{print $1}')
#
if [ -z $logged_on ]; then
    echo "$muser is not logged on."
    echo "Exiting script..."
    exit
fi
#
# Determine if user allows messaging:
#
allowed=$(who -T | grep -i -m 1 $muser | gawk '{print $2}')
#
if [ $allowed != "+" ]; then
    echo "$muser does not allowing messaging."
    echo "Exiting script..."
    exit
fi
#
# Determine if a message was included:
#
if [ -z $2 ]; then
    echo "No message parameter included."
    echo "Exiting script..."
    exit
fi
#
# Determine if there is more to the message:
#
shift
#
while [ -n "$1" ]; do
    whole_message=$whole_message' '$1
    shift
done
#
# Send message to user:
#
uterminal=$(who | grep -i -m 1 $muser | gawk '{print $2}')
#
echo $whole_message | write $logged_on $uterminal
#
exit

既然你已经读到了本书的最后一章,自然也就应该准备好了应对脚本编写中出现的挑战。下面是对于这个消息发送脚本的一些改进意见,可以试着加入这些功能。

  • 选择使用选项(参见第14章),不把用户名和消息作为参数传递。
  • 如果用户登入多个终端,允许将消息发往这些终端(提示:使用多个write命令)。
  • 如果消息的接收方目前只登入了GUI环境,提示脚本用户并退出脚本(write命令只能向虚拟控制台终端写入信息)。
  • 允许将保存在文件中的长消息发送给终端(提示:使用管道将cat命令的输出传入write 命令,不要使用echo命令)。

要想巩固学到的脚本编写知识,不仅要通读脚本,还得修改脚本。加入一些自己的点子。找点小乐子吧!这有助于你的学习。

26.2 获取格言

26
励志格言常见于商业环境中。你的办公室墙上可能现在就有那么几句。这个有趣的小脚本可以帮助你每天获得一句格言以供使用。
本节将介绍如何创建这样的脚本。其中包括一个功能丰富但至今尚未讲过的工具,另外还会用了一些我们已经熟悉的工具,例如sed和gawk。

26.2.1 功能分析

有一些不错的网站可以获得每日格言。打开你惯用的搜索引擎,可以找到很多这类网站。找到之后,你需要使用工具来下载这些格言。对于这种用途的脚本,正是wget工具发挥用途之处。

  1. 学习wget

wget是一款非常灵活的工具,它能够将Web页面下载到本地Linux系统中。你可以从这些页面中收集每日格言。

说明 wget命令功能极其丰富。本章中仅使用了很小一部分功能。可以查看wget的手册页获得更多的相关信息。

要通过wget下载Web页面,只需要使用wget命令和网站的地址就行了。

$ wget www.quotationspage.com/qotd.html
--2015-09-23 09:14:28-- http://www.quotationspage.com/qotd.html
Resolving www.quotationspage.com... 67.228.101.64
Connecting to www.quotationspage.com|67.228.101.64|:80. connected
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: "qotd.html"
[ <=> ] 13,806 --.-K/s in 0.1s
2015-09-23 09:14:28 (118 KB/s) - "qotd.html" saved [13806]
$

网站的信息被存储在与Web页面同名的文件中。在这个例子中,文件名就是qotd.html。你大概已经猜到了,这个文件中都是HTML代码。

$ cat qotd.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html xmlns:fb="http://ogp.me/ns/fb#">
<head>
<title>Quotes of the Day - The Quotations Page</title>
[...]

这里只列出了部分HTML代码。脚本可以使用sed和gawk工具提取出需要的格言。不过在使用脚本之前,你需要对wget工具的输入和输出施加一点控制。
可以使用一个变量来保存页面地址(URL)。然后把这个变量作为参数传递给wget就行了。记住,别忘了在变量名前加上$。

$ url=www.quotationspage.com/qotd.html
$
$ wget $url
--2015-09-23 09:24:21-- http://www.quotationspage.com/qotd.html
Resolving www.quotationspage.com... 67.228.101.64
Connecting to www.quotationspage.com|67.228.101.64|:80 connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: "qotd.html.3"
[ <=> ] 13,806 --.-K/s in 0.1s
2015-09-23 09:24:21 (98.6 KB/s) - "qotd.html.3" saved [13806]
$

每日格言脚本最终会通过cron(参见第16章)或其他的脚本自动化工具设置成每天执行一次。所以让wget命令的会话输出出现在STDOUT是不合适的。可以使用-o选项将会话输出保存在日志文件中,随后再浏览。

$ url=www.quotationspage.com/qotd.html
$
$ wget -o quote.log $url
$
$ cat quote.log
--2015-09-23 09:41:46-- http://www.quotationspage.com/qotd.html
Resolving www.quotationspage.com... 67.228.101.64
Connecting to www.quotationspage.com|67.228.101.64|:80 connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: "qotd.html.1"
0K .......... ... 81.7K=0.2s
2015-09-23 09:41:46 (81.7 KB/s) - "qotd.html.1" saved [13806]
$

现在,当wget检索到Web页面信息时,它会将会话输出保存在日志文件中。如果需要,你可以像上面代码中那样使用cat命令浏览会话日志。

说明 出于各种原因,你可能不希望wget生成日志文件或显示会话输出。如果是这样的话,可以使用-q选项,wget命令会安安静静地完成你下达给它的任务。

要控制Web页面信息保存的位置,可以使用wget命令的-O选项。这样你就可以自己指定文件名,而不是非得使用Web页面的名字作为文件名。

$ url=www.quotationspage.com/qotd.html
$
$ wget -o quote.log -O Daily_Quote.html $url
$
$ cat Daily_Quote.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html xmlns:fb="http://ogp.me/ns/fb#">
<head>
[...]
$

-O选项允许将Web页面数据保存在指定的文件Daily_Quote.html中。现在我们已经能够控制
wget工具的输出了,下一个需要的功能是核查Web地址的有效性。

  1. 测试Web地址

Web地址会发生变化。这些地址有时候似乎每天都在变。所以在脚本中测试地址的有效性就非常重要。可以使用wget工具的—spider选项完成这项任务。

$ url=www.quotationspage.com/qotd.html
$
$ wget --spider $url
Spider mode enabled. Check if remote file exists.
--2015-09-23 12:45:41-- http://www.quotationspage.com/qotd.html
Resolving www.quotationspage.com... 67.228.101.64
Connecting to www.quotationspage.com|67.228.101.64|:80 connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Remote file exists and could contain further links,
but recursion is disabled -- not retrieving.
$

命令输出表明指定的URL是有效的,但就是输出的内容太多了。可以加上-nv(代表
non-verbose)选项来精简输出信息。

$ wget -nv --spider $url
2015-09-23 12:49:13
URL: http://www.quotationspage.com/qotd.html 200 OK
$

-nv选项只显示出Web地址的状态,这种输出要容易理解得多。不过和你认为的恰恰相反, 行尾的OK并不是说Web地址是有效的,而是表明返回的Web地址和发送的地址是一样的。这个概 念有点让人迷惑,等你看到无效的Web地址是什么样的时候就能明白了。
将URL变量的内容修改成一个错误的Web地址,看看wget是如何显示的。使用错误地址重新发出wget命令。

$ url=www.quotationspage.com/BAD_URL.html
$
$ wget -nv --spider $url
2015-09-23 12:54:33
URL: http://www.quotationspage.com/error404.html 200 OK
$

注意,输出的最后仍然是OK。但是Web地址的结尾是error404.html。这才表示Web地址是无效的。
使用必要的wget命令抓取励志格言的Web页面,并能够测试页面地址的有效性,现在可以来动手编写脚本了。你的每日励志格言正在等着你呢。

26.2.2 创建脚本

要在脚本编写过程中进行测试,需要将一个包含网站URL的参数传递给脚本。在脚本中,变量qutoe_url包含了传入参数的值。

#
quote_url=$1
#
  1. 检查所传递的URL

在脚本中多做检查总是没错的。要检查的第一件事就是确保每日励志格言脚本所使用的网站
URL是有效的。
和你想的一样,脚本仍旧使用wget和—spider选项来检查Web地址的有效性。但是结果必须保存到变量中,以便随后使用if语句进行检查。使用wget命令实现这一点稍微有些麻烦。
要保存输出结果,需要在命令上使用标准的$()语法。除此之外,还得重定向STDERR和STDOUT。这可以通过在wget命令后使用2>&1来实现。

#
check_url=$(wget -nv --spider $quote_url 2>&1)
#

现在网站URL的状态消息被保存在了变量check_url中。要从变量中找出错误指示error404,需要使用参数扩展和echo命令。

#
bad_url=$(echo ${check_url/*error404*/error404})
#

在这个例子中,字符串参数扩展(string parameter expansion)允许对保存在check_url中的字符串进行搜索。可以把字符串参数扩展视为sed的另一种简单快速的替代形式。在搜索关键词周围加上通配符(error404),这样可以搜索整个字符串。如果找到了,echo命令会使得字符串error404被保存在bad_url变量中。要是没有找到,bad_url变量中包含的就是check_url变量中的内容。
现在可以使用if语句(参见第12章)检查bad_url变量中的字符串了。如果从中找到了error404,则显示一条消息,然后退出脚本。

#
if [ "$bad_url" = "error404" ]
then
  echo "Bad web address"
  echo "$quote_url invalid"
  echo "Exiting script..."
  exit
fi
#

还有一种更简洁易行的方法。这种方法完全不需要使用字符串参数扩展和bad_url变量。if 语句的双方括号可以对变量check_url进行搜索。

if [[ $check_url == *error404* ]]
then
  echo "Bad web address"
  echo "$quote_url invalid"
  echo "Exiting script..."
  exit
fi

if结构中的test语句搜索变量check_url中的字符串。如果从中找到了子串error404,则显示提示信息并退出脚本。要是没有发现错误,脚本继续执行。这条语句可谓省时省力,不需要使用任何的字符串参数扩展,甚至连bad_url变量都用不着。
现在检查工作已经就绪了,可以用一个无效的Web地址来测试一下脚本。将url变量设置成一个错误的URL,作为参数传给get_quote.sh脚本。

$ url=www.quotationspage.com/BAD_URL.html
$
$ ./get_quote.sh $url
Bad web address
www.quotationspage.com/BAD_URL.html invalid
Exiting script...
$

看起来没问题。为了确保万无一失,再试试有效的Web地址。

$ url=www.quotationspage.com/qotd.html
$
$ ./get_quote.sh $url
$

没有出现错误。到目前为止一切顺利!目前只是做了必要的检查,下一个需要加入脚本的功能是获取Web页面的数据。

  1. 获取Web页面信息

抓取每日励志格言的页面数据很简单。可以在脚本中使用本章先前讲过的wget命令。唯一需要的改变就是将日志文件和包含页面信息的HTML文件保存在/tmp目录中。

#
wget -o /tmp/quote.log -O /tmp/quote.html $quote_url
#

在编写脚本的其余部分之前,需要使用一个有效的Web地址测试这部分代码。

$ url=www.quotationspage.com/qotd.html
$
$ ./get_quote.sh $url
$
$ ls /tmp/quote.*
/tmp/quote.log /tmp/quote.html
$
$ cat /tmp/quote.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html xmlns:fb="http://ogp.me/ns/fb#">
<head>
[...]
</body>
</html>
$

脚本运行一切正常!日志文件/tmp/quote.log和HTML文件/tmp/quote.html也都创建好了。

窍门 如果在获取网站信息时不需要cookie,可以加入wget命令的—no-cookies选项。默认情况下是不会存储cookie的。

下一个任务是从下载好的Web页面文件的HTML代码中找出每日励志格言。这需要借助sed工具和gawk工具。

  1. 解析出需要的信息

为了找出实际的励志格言,需要做一些处理。这部分脚本将使用sed和gawk来解析出需要的信息。

说明 当根据自己的需要修改这个脚本时,这部分需要作出的变动最大。sed和gawk工具用来搜索针对特定格言网站数据的关键字。可能需要使用不同的关键字以及不同的sed/gawk命令来提取需要的数据。

脚本首先从保存着Web页面信息的/tmp/quote.html文件中删除所有的HTML标签。sed工具能够完成这项任务。

#
sed 's/<[^>]*//g' /tmp/quote.html
#

上面的代码看起来非常眼熟,我们在21.7.6节中讲过。删除掉HTML标签后,输出信息变成了下面的样子。

$ url=www.quotationspage.com/qotd.html
$
$ ./get_quote.sh $url
[...]
>Quotes of the Day - The Quotations Page>
>
[...]
>>Selected from Michael Moncur's Collection of Quotations
- September 23, 2015>>
>>>Horse sense is the thing a horse has which keeps
[...]
>
$

从这段经过删节后的输出信息可以看出,文件中还有太多无用的数据,因此还需要进一步解析。幸运的是,我们需要的格言正好位于当前日期的右边。因此脚本可以使用当前日期作为搜索关键字!
这里需要用到grep命令、$()以及date命令。sed命令的输出通过管道传入grep命令。grep 命令经过格式化的当前日期来匹配格言页面中的日期。找到日期文本之后,使用-A2选项提取出另外两行文本。

#
sed 's/<[^>]*//g' /tmp/quote.html |
grep "$(date +%B' '%-d,' '%Y)" -A2
#

现在,脚本的输出如下。

$ ./get_quote.sh $url
>>Selected from Michael Moncur's Collection of Quotations
- September 23, 2015>>
>>>Horse sense is the thing a horse has which keeps it from
betting on people.> >>>>>>>>>>>>>>>>>>W. C. Fields> (1880 -
1946)> &nbsp; >>>
>>Newspapermen learn to call a murderer 'an alleged murderer'
and the King of England 'the alleged King of England' to
avoid libel suits.> >>>>>>>>>>>>>>>>>>Stephen Leacock> (1869
- 1944)> &nbsp; >>> - More quotations on: [>Journalism>] >
$

窍门 如果Linux系统的日期设置和格言页面上的日期不一样,你只能得到一个空行。上面的grep命令假定你的系统日期和Web页面上的日期是相同的。

尽管输出的信息量已经大为降低,但是文本仍然太杂乱。多余的>符号可以很轻松的使用sed 工具删除掉。在脚本中,grep命令的输出被管接到sed工具中,后者用来移除>符号。

#
sed 's/<[^>]*//g' /tmp/quote.html |
grep "$(date +%B' '%-d,' '%Y)" -A2 |
sed 's/>//g'
#

加入了新的脚本代码后,输出信息看起来清晰了一些。

$ ./get_quote.sh $url
Selected from Michael Moncur's Collection of Quotations
- September 23, 2015
Horse sense is the thing a horse has which keeps it from
betting on people. W. C. Fields (1880 - 1946) &nbsp;
Newspapermen learn to call a murderer 'an alleged murderer'
and the King of England 'the alleged King of England' to
avoid libel suits. Stephen Leacock (1869 - 1944) &nbsp; -
More quotations on: [Journalism]
$

现在我们总算小有收获了!不过还要继续删除剩下那些杂乱的文本。
你可能已经注意到了,在输出中有不止一条格言(出现了两条)。在我们选用的这个网站上, 这种情况偶有发生:有时是一条,有时是两条。所以脚本需要找到一种方法只提取前一条格言。
sed工具又有用武之地了。使用它的next命令和delete命令(参见第21章),先定位字符串&nbsp,找到之后,移动并删除下一行文本。

#
sed 's/<[^>]*//g' /tmp/quote.html |
grep "$(date +%B' '%-d,' '%Y)" -A2 |
sed 's/>//g' |
sed '/&nbsp;/{n ; d}'
#

可以测试一下脚本看看新加入的sed命令是否能够解决多条格言的问题。

$ ./get_quote.sh $url
Selected from Michael Moncur's Collection of Quotations
- September 23, 2015
Horse sense is the thing a horse has which keeps it from
betting on people. W. C. Fields (1880 - 1946) &nbsp;
$

多余的格言被删掉啦!留下来的那条还需要继续清理。在格言的末尾仍然有一个字符串 。脚本可以使用另一条sed命令来解决这个麻烦,不过出于多样性的考虑,我们这次使用gawk命令。

#
sed 's/<[^>]*//g' /tmp/quote.html |
grep "$(date +%B' '%-d,' '%Y)" -A2 |
sed 's/>//g' |
sed '/&nbsp;/{n ; d}' |
gawk 'BEGIN{FS="&nbsp;"} {print $1}'
#

在上面的代码中,gawk命令使用了输入字段分隔符FS(参见第22章)。这个字段分隔符被设置成字符串 ,这样会使得gawk从输出中把它丢弃掉。

$ ./get_quote.sh $url
Selected from Michael Moncur's Collection of Quotations
- September 23, 2015
Horse sense is the thing a horse has which keeps it from
betting on people. W. C. Fields (1880 - 1946)
$

脚本要做的最后一步是将格言保存到文件中。这里该tee命令(参见第15章)登场了。目前, 整个格言提取过程如下。

#
sed 's/<[^>]*//g' /tmp/quote.html |
grep "$(date +%B' '%-d,' '%Y)" -A2 |
sed 's/>//g' |
sed '/&nbsp;/{n ; d}' |
gawk 'BEGIN{FS="&nbsp;"} {print $1}' |
tee /tmp/daily_quote.txt > /dev/null
#

提取出的格言被保存在/tmp/daily_quote.txt中,gawk命令生成的所有输出被重定向到/dev/null 中(参见第15章)。要想让这个脚本更自主一点的话,可以将URL硬编码到脚本中。

#
quote_url=www.quotationspage.com/qotd.html
#

现在来测试一下新加入的这两处改变。

$ ./get_quote.sh
$
$ cat /tmp/daily_quote.txt
Selected from Michael Moncur's Collection of Quotations
- September 23, 2015
Horse sense is the thing a horse has which keeps it from
betting on people. W. C. Fields (1880 - 1946)
$

棒极了!我们成功从站点数据中提取出了每日励志格言,并将其保存在了一个文本文件中。你可能注意到了,这则格言不太像传统的励志格言,倒更像是一句幽默语录。不过有些人就是能够从幽默中得到激励!
为了便于审看,下面是最终的每日励志格言脚本。

#!/bin/bash
#
# Get a Daily Inspirational Quote
#####################################
#
# Script Variables ####
#
quote_url=www.quotationspage.com/qotd.html
#
# Check url validity ###
#
check_url=$(wget -nv --spider $quote_url 2>&1)
#
if [[ $check_url == *error404* ]]; then
    echo "Bad web address"
    echo "$quote_url invalid"
    echo "Exiting script..."
    exit
fi
#
# Download Web Site's Information
#
wget -o /tmp/quote.log -O /tmp/quote.html $quote_url
#
# Extract the Desired Data
#
sed 's/<[^>]*//g' /tmp/quote.html |
    grep "$(date +%B' '%-d,' '%Y)" -A2 |
    sed 's/>//g' |
    sed '/&nbsp;/{n ; d}' |
    gawk 'BEGIN{FS="&nbsp;"} {print $1}' |
    tee /tmp/daily_quote.txt >/dev/null
#
exit

这个脚本提供了一个极好的机会,可以让你试试新学到的脚本编程以及命令行技巧。下面是对每日励志格言脚本提出的几个改进意见,可以试着加入下列功能。

  • 把网站修改成你喜欢的格言或谚语网站,并对格言提取命令作出必要的修改。
  • 尝试使用不同的sed和gawk命令来提取每日格言。
  • 通过cron(参见第16章)将该脚本设置成每天自动运行。
  • 加入可以在特定时刻(比如每天第一次登录的时候)显示格言文件内容的命令。

阅读每日格言能够激励你自己,不过也许只是鼓励你逃避接下来的商务会议。下一节就会教你怎么编写一个远离会议的脚本。

26.3 编造借口

永无休止的员工会议充斥着无关紧要的信息。你对此绝对深有体会。与其在那里开会,不如回到办公桌前和有趣的bash shell脚本项目打交道。这里有一个有意思的小脚本,你可以用它逃离下一次员工大会。
短信服务(SMS)允许在手机之间发送文本消息。不过你也能够直接在电子邮件或命令行中使用SMS发送短信。可以使用本节中的脚本编写短信,然后在特定时间把这条短信发送到你的手机上。收到来自你的Linux系统的“重要”信息可算得上是提前离会的绝佳理由。

26.3.1 功能分析

在命令行中发送短信的方法有好几种。其中一种方法是通过系统的电子邮件使用手机运营商的SMS服务。另一种方法是使用curl工具。
26

  1. 学习curl

和wget类似,curl工具允许你从特定的Web服务器中接收数据。与wget不同之处在于,你 还可以用它向Web服务器发送数据。而这一点正是我们需要的。

窍门 有些Linux发行版(例如Ubuntu)默认没有安装curl命令。可以输入apt-get install curl进行安装。你可以在第9章中找到更多关于安装软件包的相关信息。

除了curl工具,你还需要一个能够提供免费SMS消息发送服务的网站。在本节脚本中用到的是http://textbelt.com/text。这个网站允许你每天免费发送最多75条短信。只需要用它发送一条就够了,所以完全没有问题。

窍门 如果你的公司已经有了SMS供应商,例如http://sendhub.comhttp://eztexting.com,那你可以在脚本中使用这些站点。注意,要根据SMS供应商的要求修改语法。

$ curl http://textbelt.com/text \
-d number=YourPhoneNumber \
-d "message=Your Text Message"

-d选项告诉curl向网站发送指定的数据。在这里,网站需要特定的数据来发送短信。这些数据包括YourPhoneNumber,即你的手机号码;还包括Your Text Message,即你要发送的短信。

说明 curl能做的远不止向Web服务器发送数据(或从Web服务器接收数据)。它无需用户干预就能够处理很多其他的网络协议,例如FTP。可以阅读curl的手册页来了解它的强大功能。

发送消息后,如果没有什么问题,网站会给出一条表示发送成功的消息:”success”: true。

$ curl http://textbelt.com/text \
> -d number=3173334444 \
> -d "message=Test from curl"
{
"success": true
}$
$

如果数据(例如手机号)不正确的话,会产生一条错误消息:”success”: false。

$ curl http://textbelt.com/text \
-d number=317AAABBBB \
-d "message=Test from curl"
{
"success": false,
"message": "Invalid phone number."
}$
$

说明 如果你的手机运营商不在美国,http://textbelt.com/text可能没法工作。要是手机运营商在加拿大的话,你不妨试试http://textbelt.com/Canada。假如是在其他地区的话,可以换用http://textbell.com/intl看看。更多的帮助,请访问http://textbelt.com

表明发送成功或失败的消息非常有用,不过对脚本来说就没必要了。要删除这些消息,只需将STDOUT重定向到/dev/null(参见第15章)就行了。遗憾的是,curl现在的输出结果无法令人满意。

$ curl http://textbelt.com/text \
> -d number=3173334444 \
> -d "message=Test from curl" > /dev/null
% Total % Received % Xferd Average Speed...
Dload Upload...
0 21 0 21 0 45 27 58 ...
$

上面这段经过节选的输出显示了各种统计数据,如果使用curl进行错误排查的话,这些信息将很有用。但是对脚本而言,它们必须被屏蔽掉。好在curl命令有一个-s选项能够满足我们这个需求。

$ curl -s http://textbelt.com/text \
> -d number=3173334444 \
> -d "message=Test from curl" > /dev/null

这就好多了。可以把curl命令放入脚本中了。不过在查看脚本代码之前,有个话题还得讨论一下:通过电子邮件发送短信。

  1. 使用电子邮件发送短信

如果不打算使用http://textbelt.com/text提供的短信中继服务,或是出于某些原因,这些服务没法使用,你可以转而使用电子邮件来发送短信。本节简要讲述了如何实现这种方法。

警告 如果你的手机运营商不在美国,这项网络服务可能没法使用。除此之外,你的手机运营商也许会屏蔽发送自该网站的SMS消息。在这种情况下,你只能尝试使用电子邮件发送。

是否能够使用电子邮件作为替代方案要取决于你的手机运营商。如果运营商使用了SMS网关,那算你运气好。联系你的手机运营商,拿到网关的名字。网关名通常类似于txt.att.net或vtext.com。

窍门 你通常可以使用因特网找出手机运营商的SMS 网关。有一个很棒的网站, http://martinfitzpatrick.name/list-of-email-to-sms-gateways/,上面列出了各种SMS网关以及使用技巧。如果在上面没有找到你的运营商,那就使用搜索引擎搜索吧。

通过电子邮件发送短信的基本语法如下。
26

mail -s "your text message" your_phone_number@your_sms_gateway

说明 如果mail命令在你的Linux系统上无法使用,就需要安装mailutils包。请阅读本书第9章查看如何安装软件包。

不幸的是,当你按照语法输入完命令之后,必须输入要发送的短信并按下Ctrl+D才能够发送。这类似于发送普通的电子邮件(参见第24章)。在脚本中显然不适合这样做。可以将电子邮件内容保存在文件中,然后用这个文件来发送短信,具体的做法如下。

$ echo "This is a test" > message.txt
$ mail -s "Test from email" \
3173334444@vtext.com < message.txt

现在,发送电子邮件的语法就更适用于脚本了。不过要注意的是,这种方法还存在不少问题。首先,你的系统中必须运行一个邮件服务器(参见第24章)。其次,你的手机服务提供商可能会屏蔽通过电子邮件发送的SMS消息。如果你打算在家里用这个法子的话,这种事经常会发生。

窍门 如果你的手机服务提供商屏蔽了来自系统的SMS消息,可以使用基于云的电子邮件服务提供商作为SMS中继。使用你惯用的浏览器搜索关键字SMS relay yourfavorite cloud_email,查看搜索到的网站。

尽管使用电子邮件发送短信可以作为一种备选方案,但这种方法还是问题多多。如果可以的话,免费的SMS中继网站和curl工具要来得容易。在下一节的脚本中,我们使用curl向你的手机发送短信。

26.3.2 创建脚本

实现了相应的功能之后,创建脚本来发送短信就非常简单了。你需要的只是几个变量和curl 命令。
脚本中要用到3个变量。如果信息发生了变化,将特定的数据项设置成变量更易于对脚本作出修改,这些变量如下。

#
phone="3173334444"
SMSrelay_url=http://textbelt.com/text
text_message="System Code Red"
#

另外需要用到的就是curl工具了。完整的短信发送脚本代码如下。

#!/bin/bash
#
# Send a Text Message
################################
#
# Script Variables ####
#
phone="3173334444"
SMSrelay_url=http://textbelt.com/text
text_message="System Code Red"
#
# Send text ###########
#
curl -s $SMSrelay_url -d \
    number=$phone \
    -d "message=$text_message" >/dev/null
#
exit

如果你觉得这个脚本简单易用,那就对了!更重要的是,这意味着你的shell脚本编程功力已增进不小。就算是简单的脚本也需要测试,在继续之前,先确保使用你的手机号测试了脚本。

窍门 在测试脚本时,要注意网站http://textbelt.com/text不允许你在3分钟之内向同一个手机号码发送三条以上的短信。

要想定时发送短信,必须使用at命令。如果不太记得这个命令的用法,请参见第16章。
首先,可以使用这个新脚本测试一下at命令。在本例中,使用at命令的-f选项以及脚本文 件名send_text.sh来运行脚本。如果需要立刻运行的话,使用Now选项。

$ at -f send_text.sh Now
job 22 at 2015-09-24 10:22
$

脚本立刻就开始运行了。不过在你手机接收到短信之前可能需要等待1~2分钟。
要想让脚本在别的时间运行,使用其他的at命令选项(参见第16章)就可以了。在下面的例子中,脚本会在当前时间的25分钟之后运行。

$ at -f send_text.sh Now + 25 minutes
job 23 at 2015-09-24 10:48
$

注意,在提交了脚本之后,at命令给出了一条提示信息。信息中给出了日期和时间,指明脚本何时会运行。

真有意思!现在你拥有了一件脚本工具,可以在需要借口离开员工会议的时候助你一臂之力。更妙的是,你还可以修改脚本,让它发送真正需要解决的真正严重的系统故障信息。

26.4 小结

本章展示了如何综合运用本书所讲授的shell脚本编程知识来创建一些有乐趣的shell脚本。每个脚本都巩固了我们先前学到的知识,另外还引入了一些新的命令和思路。
26
首先演示了如何向Linux系统中的其他用户发送消息。脚本检查了用户是否已经登入系统以及是否允许消息功能。检查完之后,使用write命令发送指定的消息。除此之外,我们还给出了一些脚本的修改建议,这些建议有助于提高你的脚本编写水平。
接下来一节介绍了如何使用wget工具获取网站信息。本节所创建的脚本可以从Web页面中提取格言。检索完毕后,脚本利用一些工具找出实际的格言文本。这些工具包括熟悉的sed、grep、

gawk和tee命令。对于这个脚本,我们同样给出了一些修改建议,值得你用心思考,以巩固和提高自己的技能。
本章最后介绍了简单有趣的可以给自己发送短信的脚本。在这一节中我们认识了curl工具的用法以及SMS的概念。尽管这只是个趣味性脚本,但你也可以对其进行修改,用于更严肃的目的。
感谢你加入这场Linux命令与shell脚本编程之旅。希望你能够享受这段旅程,学会如何使用命令行,如何创建shell脚本,提高工作效率。但不要就此停下学习命令行的脚步。在开源世界中, 总有一些新东西正在孕育,可能是新的命令行实用工具,也可能是一个全新的shell。不要丢下Linux命令行,也别忘了紧随新的发展和功能。