<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>beyond feelings</title>
	<atom:link href="http://boke.name/beyondfeelings/feed/" rel="self" type="application/rss+xml" />
	<link>http://boke.name/beyondfeelings</link>
	<description>思考，分享，积累</description>
	<lastBuildDate>Sat, 07 Apr 2012 14:03:02 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.5.1</generator>
		<item>
		<title>火车运煤问题（上）</title>
		<link>http://boke.name/beyondfeelings/2012/04/07/train_carrying_coal_problem_1/</link>
		<comments>http://boke.name/beyondfeelings/2012/04/07/train_carrying_coal_problem_1/#comments</comments>
		<pubDate>Sat, 07 Apr 2012 07:58:07 +0000</pubDate>
		<dc:creator>Karl</dc:creator>
				<category><![CDATA[algorithem]]></category>
		<category><![CDATA[tech]]></category>

		<guid isPermaLink="false">http://boke.name/beyondfeelings/?p=22</guid>
		<description><![CDATA[酷壳上面有这样一道题： 你是山西的一个煤老板，你在矿区开采了有3000吨煤需要运送到市场上去卖，从你的矿区到市场有1000公里，你手里有一列烧煤的火车，这个火车最多只能装1000吨煤，且其能耗比较大——每一公里需要耗一吨煤。请问，作为一个懂编程的煤老板的你，你会怎么运送才能运最多的煤到集市？ 不少人回复了答案和简要解答过程，粗粗浏览一遍，正确解答不少，但是思路中到了关键步骤就有种天马行空的感觉，缺乏比较严谨的证明和思考过程。那么，对于这个问题，我们应该如何思考？  首先，从题目的描述，可以得到两个推断： 1、火车需要来回把煤运到若干个中间点后再出发的（如果一次过去不回头，到了终点煤量就是0了），运送过程中需要消耗燃料； 2、在中间点M时，其他地方不存在剩余的煤，否则不是最优解。 对于推论2，证明如下：假设其他地方有剩余的煤，并假设放着煤的地方在N点。若N点在M点的后面，那火车要么回头把那部分煤运到中间点，要么直接把那部分煤丢弃不管，这两种情况都会最终得到“N点没有煤”的结果；若N点在中间点前方m公里的位置，那么为了把煤从M点运到N点，需要消耗至少2m吨煤（因为火车是从中间点出发），那么，先把所有煤运到M点，再把部分煤运送到N点，这种情况下火车在M点可以满载出发，能运到N点的煤量必然比较多。因此这样得到的结果一定优于前者。所以其他地方无剩余的煤 因此，从上面的推断，会得到一个更具普遍性的问题：若起点有n吨煤，运输k公里后，最多还剩多少吨？不妨使用f(n,k)记录“起点有n吨煤，需要运送k公里，剩下煤量的最大值”。由于有若干中间点，那我们设下一个中间点离本次起点的距离为x。然后问题就转化成：对于给定的n和k，如何求x，使得本问题有最优解。 显然，这是一个递推的思路，看到“递推”和“最优问题”，很自然就会想，是否可以使用动态规划求解？这是本问题的第二种解法，咱下集再议。 要运到x公里以外的下一个中间点，火车需要若干来回才能把煤运过去。那么，火车需要多少次来回呢？我们先算一算，看看结果有啥规律： 首先，x是否可以无限大？当然不行，要跑到前方x公里处， 那火车剩下的煤量得足够返回才行。所以若火车需要返回，则x&#60;500。 1、若n&#60;=1000，显然火车不需要来回，直接带上所有的煤往前跑，跑到前方x公里时，能放下的煤量为min(1000-x, k-x)。若起点煤量n&#62;1000，则有部分煤被浪费。 2、若n比较大，火车可以通过一次返回得到更多的煤量积累，则跑到前方x公里处消耗的煤量为3x（一个来回再加上单程），由于火车从起点出发两次，累计最多拿2000吨煤，一共运2000吨煤，所以最后该点能放下的煤量为min(2000-3x, n-3x)。若起点煤量n&#62;2000，则有部分煤被浪费。 3、若n再大一点，火车可以两次来回跑到前方x公里处，那两次来回消耗的总煤量为5x，最后该点能放下的煤量为min(3000-5x, n-5x) = n-5x。因为起点最多就3000吨煤，所以n&#60;3000。 我们注意到，若决定了火车返回的次数，则途中消耗煤量与x为线性关系。也就是说，火车直接选前方x公里的N点作为下一个中间点，与火车在中途再选若干个中间点，两种走法消耗的煤量是一样的。因此，在n&#62;2000的时候，我们可以选取一个较小的x值，让火车返回2次把n-2000吨煤烧掉；n&#62;1000的时候，我们可以选取另外一个x值，让火车返回1次吧n-1000吨煤烧掉；n&#60;=1000的时候，带上所有家当欢快地往前奔吧！这个就是最优解。 画个函数图像可以更明确地看到这点，用横轴表示路程，纵轴表示煤量，图上的一点(k, n)表示在离终点k公里处剩下煤量。火车返回两次时位置和煤量的关系使用黑线表示、返回一次时位置和煤量的关系使用红线表示，返回零次时位置和煤量的关系使用蓝线表示，则黑线的起点只能在n=3000水平线以下，红线的起点在n=2000水平线以下，蓝线的起点在n=1000水平线以下。我们要做的事情，就是把这几条线连起来，得到一条贯穿左右的通路，右边终点的高度就表示最终剩余的煤量。 看着这个图，如何取得最优解，一目了然。]]></description>
				<content:encoded><![CDATA[<p><a href="http://coolshell.cn/articles/4429.html" target="_blank">酷壳</a>上面有这样一道题：</p>
<blockquote><p>你是山西的一个煤老板，你在矿区开采了有3000吨煤需要运送到市场上去卖，从你的矿区到市场有1000公里，你手里有一列烧煤的火车，这个火车最多只能装1000吨煤，且其能耗比较大——每一公里需要耗一吨煤。请问，作为一个懂编程的煤老板的你，你会怎么运送才能运最多的煤到集市？</p></blockquote>
<p>不少人回复了答案和简要解答过程，粗粗浏览一遍，正确解答不少，但是思路中到了关键步骤就有种天马行空的感觉，缺乏比较严谨的证明和思考过程。那么，对于这个问题，我们应该如何思考？<br />
<span id="more-22"></span> 首先，从题目的描述，可以得到两个推断：</p>
<p>1、火车需要来回把煤运到若干个中间点后再出发的（如果一次过去不回头，到了终点煤量就是0了），运送过程中需要消耗燃料；</p>
<p>2、在中间点M时，其他地方不存在剩余的煤，否则不是最优解。</p>
<p>对于推论2，证明如下：假设其他地方有剩余的煤，并假设放着煤的地方在N点。若N点在M点的后面，那火车要么回头把那部分煤运到中间点，要么直接把那部分煤丢弃不管，这两种情况都会最终得到“N点没有煤”的结果；若N点在中间点前方m公里的位置，那么为了把煤从M点运到N点，需要消耗至少2m吨煤（因为火车是从中间点出发），那么，先把所有煤运到M点，再把部分煤运送到N点，这种情况下火车在M点可以满载出发，能运到N点的煤量必然比较多。因此这样得到的结果一定优于前者。所以其他地方无剩余的煤</p>
<p>因此，从上面的推断，会得到一个更具普遍性的问题：<strong><em>若起点有n吨煤，运输k公里后，最多还剩多少吨？</em></strong>不妨使用f(n,k)记录“起点有n吨煤，需要运送k公里，剩下煤量的最大值”。由于有若干中间点，那我们设下一个中间点离本次起点的距离为x。然后问题就转化成：<em><strong>对于给定的n和k，如何求x，使得本问题有最优解</strong></em>。</p>
<p>显然，这是一个递推的思路，看到“递推”和“最优问题”，很自然就会想，是否可以使用动态规划求解？这是本问题的第二种解法，咱下集再议。</p>
<p>要运到x公里以外的下一个中间点，火车需要若干来回才能把煤运过去。那么，火车需要多少次来回呢？我们先算一算，看看结果有啥规律：</p>
<p>首先，x是否可以无限大？当然不行，要跑到前方x公里处， 那火车剩下的煤量得足够返回才行。所以若火车需要返回，则x&lt;500。</p>
<p>1、若n&lt;=1000，显然火车不需要来回，直接带上所有的煤往前跑，跑到前方x公里时，能放下的煤量为min(1000-x, k-x)。若起点煤量n&gt;1000，则有部分煤被浪费。</p>
<p>2、若n比较大，火车可以通过一次返回得到更多的煤量积累，则跑到前方x公里处消耗的煤量为3x（一个来回再加上单程），由于火车从起点出发两次，累计最多拿2000吨煤，一共运2000吨煤，所以最后该点能放下的煤量为min(2000-3x, n-3x)。若起点煤量n&gt;2000，则有部分煤被浪费。</p>
<p>3、若n再大一点，火车可以两次来回跑到前方x公里处，那两次来回消耗的总煤量为5x，最后该点能放下的煤量为min(3000-5x, n-5x) = n-5x。因为起点最多就3000吨煤，所以n&lt;3000。</p>
<p>我们注意到，若决定了火车返回的次数，则<em><strong>途中消耗煤量与x为线性关系</strong></em>。也就是说，火车直接选前方x公里的N点作为下一个中间点，与火车在中途再选若干个中间点，两种走法消耗的煤量是一样的。因此，在n&gt;2000的时候，我们可以选取一个较小的x值，让火车返回2次把n-2000吨煤烧掉；n&gt;1000的时候，我们可以选取另外一个x值，让火车返回1次吧n-1000吨煤烧掉；n&lt;=1000的时候，带上所有家当欢快地往前奔吧！这个就是最优解。</p>
<p>画个函数图像可以更明确地看到这点，用横轴表示路程，纵轴表示煤量，图上的一点(k, n)表示在离终点k公里处剩下煤量。火车返回两次时位置和煤量的关系使用黑线表示、返回一次时位置和煤量的关系使用红线表示，返回零次时位置和煤量的关系使用蓝线表示，则黑线的起点只能在n=3000水平线以下，红线的起点在n=2000水平线以下，蓝线的起点在n=1000水平线以下。我们要做的事情，就是把这几条线连起来，得到一条贯穿左右的通路，右边终点的高度就表示最终剩余的煤量。</p>
<p><a href="http://boke.name/beyondfeelings/files/2012/04/train1.jpg"><img class="alignnone size-medium wp-image-114" src="http://boke.name/beyondfeelings/files/2012/04/train1.jpg" alt="" width="600" height="234" /></a></p>
<p>看着这个图，如何取得最优解，一目了然。</p>
]]></content:encoded>
			<wfw:commentRss>http://boke.name/beyondfeelings/2012/04/07/train_carrying_coal_problem_1/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>在 solaris 下使用 pkgutil 安装软件包</title>
		<link>http://boke.name/beyondfeelings/2012/04/07/solaris_software_install_with_pkgutil/</link>
		<comments>http://boke.name/beyondfeelings/2012/04/07/solaris_software_install_with_pkgutil/#comments</comments>
		<pubDate>Sat, 07 Apr 2012 07:48:59 +0000</pubDate>
		<dc:creator>Karl</dc:creator>
				<category><![CDATA[solaris]]></category>
		<category><![CDATA[tech]]></category>

		<guid isPermaLink="false">http://boke.name/beyondfeelings/?p=20</guid>
		<description><![CDATA[想在 solaris 下使用 GNU 的软件，以前一直在 sunfreeware 里面找，还得手动解决依赖关系。今天终于发现了 pkgutil 这个好东东，可以实现类似 debian 下 apt-get 的效果，自动解决依赖关系，并能通过网络的软件源下载软件包。 简单的 pkgutil 的配置说明见这里，记录 solaris 10 下配置过程如下： 配置 DNS 因为 pkgutil 需要从网上下载软件包，通过域名会方便很多，因此需要先配置好 DNS。 在 /etc/resolv.conf 写入这两行： nameserver 8.8.8.8 nameserver 8.8.4.4 修改 /etc/nsswitch.conf，找到 hosts 所在的行，加入dns这个service： hosts: files dns 安装 pkgutil # pkgadd -d http://get.opencsw.org/now 配置 pkgutil pkgutil 使用的配置文件会放在两个地方：/etc/opt/csw/pkgutil.conf 和 /opt/csw/etc/pkgutil.conf，需要注意，前者优先级高于后者。 在 pkgutil.conf 中添加软件源，加入如下配置项： mirror=http://mirror.opencsw.org/opencsw/unstable [...]]]></description>
				<content:encoded><![CDATA[<p>想在 solaris 下使用 GNU 的软件，以前一直在 <a href="http://sunfreeware.com/" title="Sunfreeware" target="_blank">sunfreeware</a> 里面找，还得手动解决依赖关系。今天终于发现了 <a href="http://pkgutil.net/" title="pkgutil" target="_blank">pkgutil</a> 这个好东东，可以实现类似 debian 下 apt-get 的效果，自动解决依赖关系，并能通过网络的软件源下载软件包。<br />
<span id="more-20"></span><br />
简单的 pkgutil 的配置说明见<a href="http://www.opencsw.org/get-it/pkgutil/" target="_blank">这里</a>，记录 solaris 10 下配置过程如下：</p>
<ul>
<li>配置 DNS</li>
<p>因为 pkgutil 需要从网上下载软件包，通过域名会方便很多，因此需要先配置好 DNS。</p>
<p>在 /etc/resolv.conf 写入这两行：</p>
<pre>nameserver 8.8.8.8
nameserver 8.8.4.4
</pre>
<p>修改 /etc/nsswitch.conf，找到 hosts 所在的行，加入dns这个service：</p>
<pre>hosts:      files dns</pre>
<li>安装 pkgutil</li>
<pre># pkgadd -d http://get.opencsw.org/now</pre>
<li>配置 pkgutil</li>
<p>pkgutil 使用的配置文件会放在两个地方：/etc/opt/csw/pkgutil.conf 和 /opt/csw/etc/pkgutil.conf，需要注意，<strong><em>前者优先级高于后者</em></strong>。</p>
<p>在 pkgutil.conf 中添加软件源，加入如下配置项：</p>
<pre>mirror=http://mirror.opencsw.org/opencsw/unstable</pre>
<p>把 wget 所在目录加入 PATH：</p>
<pre>export PATH=$PATH:/usr/sfw/bin</pre>
<p>更新 pkgutil 软件包目录：</p>
<pre># pkgutil --catalog</pre>
<p>成功后即可通过 pkgutil 安装 opencsw 软件包。</p>
<li>常用命令</li>
<p>列出可用的软件包：</p>
<pre># pkgutil -a</pre>
<p>安装软件包：</p>
<pre># pkgutil -i bash</pre>
</ul>
]]></content:encoded>
			<wfw:commentRss>http://boke.name/beyondfeelings/2012/04/07/solaris_software_install_with_pkgutil/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用 python 通过 smtp 密码验证发送邮件</title>
		<link>http://boke.name/beyondfeelings/2012/04/07/send_mail_using_python/</link>
		<comments>http://boke.name/beyondfeelings/2012/04/07/send_mail_using_python/#comments</comments>
		<pubDate>Sat, 07 Apr 2012 07:46:03 +0000</pubDate>
		<dc:creator>Karl</dc:creator>
				<category><![CDATA[programming]]></category>
		<category><![CDATA[python]]></category>
		<category><![CDATA[tech]]></category>

		<guid isPermaLink="false">http://boke.name/beyondfeelings/?p=18</guid>
		<description><![CDATA[使用 python 通过 smtp 密码验证发送邮件的山寨代码如下，perl版本见这里。 def send_mail(receiver, ccer, msg): sender = "sender " user = "sender" passwd = "password" smtpsvr = "smtp.mail.com" msgall = r"""From: %s To: %s CC: %s MIME-Version: 1.0 Content-type: text/html Subject: subject %s""" % (sender, ", ".join(receiver), ", ".join(ccer), day, msg) try: s = smtplib.SMTP() s.connect(smtpsvr) s.login(user, passwd) s.sendmail(sender, receiver, msgall) [...]]]></description>
				<content:encoded><![CDATA[<p>使用 python 通过 smtp 密码验证发送邮件的山寨代码如下，perl版本见<a href="http://caspian.dotconf.net/menu/Software/SendEmail/" title="An Email Program for Sending SMTP Mail from a Command LineAn Email Program for Sending SMTP Mail from a Command Line" target="_blank">这里</a>。<br />
<span id="more-18"></span></p>
<pre>
def send_mail(receiver, ccer, msg):
    sender = "sender "
    user = "sender"
    passwd = "password"
    smtpsvr = "smtp.mail.com"

    msgall = r"""From: %s
To: %s
CC: %s
MIME-Version: 1.0
Content-type: text/html
Subject: subject

%s""" % (sender, ", ".join(receiver), ", ".join(ccer), day, msg)
    try:
        s = smtplib.SMTP()
        s.connect(smtpsvr)
        s.login(user, passwd)
        s.sendmail(sender, receiver, msgall)
        print "Successfully sent email"
    except Exception, e:
       print e
    s.close()

if __name__ == "__main__":
    send_mail(["recv1@mail1.com", "recv2@mail2.com"],
              ["ccer@mail1.com"],
              "mail content")
</pre>
]]></content:encoded>
			<wfw:commentRss>http://boke.name/beyondfeelings/2012/04/07/send_mail_using_python/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用 perl 获取一天之后的日期</title>
		<link>http://boke.name/beyondfeelings/2012/04/07/get_time_using_perl/</link>
		<comments>http://boke.name/beyondfeelings/2012/04/07/get_time_using_perl/#comments</comments>
		<pubDate>Sat, 07 Apr 2012 07:42:31 +0000</pubDate>
		<dc:creator>Karl</dc:creator>
				<category><![CDATA[perl]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[tech]]></category>

		<guid isPermaLink="false">http://boke.name/beyondfeelings/?p=14</guid>
		<description><![CDATA[GNU 的 date 命令有很强大的 -d 功能，比如 date -d &#8220;2 days&#8221; 可以获取两天之后的日期，但 solaris 没有。所以用 perl 实现一下，用于在脚本中使用。如： perl -MPOSIX -e 'print strftime "%Y-%m-%d %H:%M:%S\n", localtime(time() + 86400)']]></description>
				<content:encoded><![CDATA[<p>GNU 的 date 命令有很强大的 -d 功能，比如 date -d &#8220;2 days&#8221; 可以获取两天之后的日期，但 solaris 没有。所以用 perl 实现一下，用于在脚本中使用。如：</p>
<pre>
perl -MPOSIX -e 'print strftime "%Y-%m-%d %H:%M:%S\n", localtime(time() + 86400)'
</pre>
]]></content:encoded>
			<wfw:commentRss>http://boke.name/beyondfeelings/2012/04/07/get_time_using_perl/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>如何写出乐百氏的 bash 脚本</title>
		<link>http://boke.name/beyondfeelings/2012/04/07/writing_robust_bash_shell_scripts/</link>
		<comments>http://boke.name/beyondfeelings/2012/04/07/writing_robust_bash_shell_scripts/#comments</comments>
		<pubDate>Sat, 07 Apr 2012 07:34:30 +0000</pubDate>
		<dc:creator>Karl</dc:creator>
				<category><![CDATA[programming]]></category>
		<category><![CDATA[shell]]></category>
		<category><![CDATA[tech]]></category>
		<category><![CDATA[bash]]></category>
		<category><![CDATA[SA]]></category>

		<guid isPermaLink="false">http://boke.name/beyondfeelings/?p=4</guid>
		<description><![CDATA[本文是 Writing Robust Bash Shell Scripts 的半翻译半总结，并加入了工作中的一点经验。 使用set -u 我们的脚本中可能会有类似的代码： chroot=$1 rm -rf $chroot/usr/share/doc 这段代码的问题在于，如果 $chroot 变量未初始化，那么 $chroot/usr/share/doc 就是 /usr/share/doc，结果可想而知…… set -u 就是用于避免这种情况的，若程序引用了一个未初始化的变量，则脚本会报错退出。如： $ cat test.sh #!/bin/bash set -u echo "$no_such_var" echo "script cannot arrive here" $ ./test.sh ./test.sh: line 3: no_such_var: unbound variable 但需要注意的是，如果变量不是未初始化而是空值，则 set -u 无法检测这种情况。如： $ cat test.sh #!/bin/bash set -u empty="" [...]]]></description>
				<content:encoded><![CDATA[<p>本文是 <a title="Writing Robust Bash Shell Scripts" href="http://www.davidpashley.com/articles/writing-robust-shell-scripts.html" target="_blank">Writing Robust Bash Shell Scripts</a> 的半翻译半总结，并加入了工作中的一点经验。<br />
<span id="more-4"></span></p>
<h1>使用set -u</h1>
<p>我们的脚本中可能会有类似的代码：</p>
<pre>chroot=$1
rm -rf $chroot/usr/share/doc</pre>
<p>这段代码的问题在于，如果 $chroot 变量未初始化，那么 $chroot/usr/share/doc 就是 /usr/share/doc，结果可想而知…… set -u 就是用于避免这种情况的，若程序引用了一个未初始化的变量，则脚本会报错退出。如：</p>
<pre>$ cat test.sh
#!/bin/bash
set -u
echo "$no_such_var"
echo "script cannot arrive here"
$ ./test.sh
./test.sh: line 3: no_such_var: unbound variable</pre>
<p>但需要注意的是，如果变量不是未初始化而是空值，则 set -u 无法检测这种情况。如：</p>
<pre>$ cat test.sh
#!/bin/bash
set -u
empty=""
echo "$empty"
echo "script exits happily"
$ ./test.sh

script exits happily</pre>
<h1>使用set -e</h1>
<p>set -e用于在bash脚本中出现程序执行失败时退出，准确地说，当有程序执行得到非0返回值时，脚本会自动退出。使用set -e有利于在脚本中程序执行异常情况下中断。</p>
<p>需要注意的是，使用set -e可能会有两个副作用：</p>
<ul>
<ul>
<li>无法检测程序执行的返回值</li>
</ul>
</ul>
<p>我们在一个程序结束的时候，可以通过$?变量检测其返回值，但若在脚本中使用set -e的话，我们还没来得及检查$?变量，脚本就退出了。若需要检查程序的返回值，可以使用如下结构（不使用set -e）：</p>
<pre>command
if [ "$?"-ne 0 ]; then echo "command failed"; exit 1; fi</pre>
<p>简单的遇到错误退出可以这么写：</p>
<pre>command || { echo "command failed"; exit 1; }</pre>
<ul>
<ul>
<li>若程序返回非零值是我们所希望的，那也会导致脚本退出</li>
</ul>
</ul>
<p>比如，运行let &#8220;i=0&#8243;这样的语句，返回值为1，脚本便退出了。对于这类情况，可以有两种解决办法：</p>
<ul>
<ul>
<ul>
<li>使用 command || true 的写法；</li>
<li>若需要运行的此类命令较多，可暂时关闭set -e：</li>
</ul>
</ul>
</ul>
<pre>set +e
command1
command2
set -e</pre>
<p>另外有一点需要注意的是，如果脚本中使用了管道，bash会把管道的最后一个程序执行的结果作为返回值，如 false | true 的返回值为0，若需要把这种情况也作失败处理，可以使用 set -o pipefail。</p>
<h1>防御式编程</h1>
<p>在脚本运行的过程中，经常会有各种各样的异常情况，如果你使用了 set -e ，就更应该注意这些情况。当使用mkdir时，如果父目录可能不存在，则可以加上 -p 参数自动创建；rm 文件时，若文件不存在，则脚本可能会因为 set -e 的作用退出，对于这种情况可使用 rm -f。</p>
<h1>注意文件名中的空格和回车等特殊字符</h1>
<p>引用变量时，如果使用这样的语句</p>
<pre>if [ $filename = "foo" ];</pre>
<p>则当 $filename 变量包含空格或者为空时，脚本会报错，而且报错信息非常费解。因此应该在变量引用加上双引号：</p>
<pre>if [ "$filename" = "foo" ];</pre>
<p>使用 $@ 变量时，若传入的变量包含空格，则应该使用双引号引用（反正不管啥情况都用双引号应该就对了）：</p>
<pre>david% foo() { for i in $@; do echo $i; done }; foo bar "baz quux"
bar
baz
quux
david% foo() { for i in "$@"; do echo $i; done }; foo bar "baz quux"
bar
baz quux</pre>
<p>find 和 xargs 配合时，应该使用 find 的 -print0 和 xargs 的 -0 参数，否则文件名包含空格时会有问题：</p>
<pre>david% touch "foo bar"
david% find | xargs ls
ls: ./foo: No such file or directory
ls: bar: No such file or directory
david% find -print0 | xargs -0 ls
./foo bar</pre>
<h1>使用trap</h1>
<p>在脚本中如果需要退出时进行一些清理工作，比如把锁文件删除，更新文件要保留一致性等等，可以使用trap来捕获退出的信号，以确保不管是正常退出还是非正常退出都能成功进行清理：</p>
<pre>trap command signal [signal ...]</pre>
<p>使用 kill -l 可以得到信号列表，设置 command 之后，使用“-”作为 command 可以让相应的信号恢复捕获前的设置。对于退出的清理工作，只需要捕获下列三种：INT、TERM、EXIT。其中INT和TERM不介绍了，EXIT信号是个伪信号，当脚本运行到结束处、使用 exit 命令或者因为 set -e 导致退出，该信号就会触发。</p>
<p>不使用 trap 时，脚本可能会写成这样：</p>
<pre>if [ ! -e $lockfile ]; then
   touch $lockfile
   critical-section
   rm $lockfile
else
   echo "critical-section is already running"
fi</pre>
<p>这段代码的问题在于，如果在 critical-section 中途脚本被 kill 了，那么 critical-section 后面的清理语句就不会运行到。这个问题可以使用 trap 解决：</p>
<pre>if [ ! -e $lockfile ]; then
   trap "rm -f $lockfile; exit" INT TERM EXIT
   touch $lockfile
   critical-section
   rm $lockfile
   trap - INT TERM EXIT
else
   echo "critical-section is already running"
fi</pre>
<p>这里，还有两个需要注意的地方：</p>
<ul>
<ul>
<li>这种方法对于 kill -9 这种暴力行为还是没用的……因为 SIGKILL 也不能被捕获嘛～</li>
<li>在测试 $lockfile 是否存在，以及使用 touch 命令建立 $lockfile 这两个操作之间，存在一个时间窗口。</li>
</ul>
</ul>
<p>对于这个问题，可以使用 bash 的 noclobber 选项解决，代码如下：</p>
<pre>if ( set -o noclobber; echo "$$" &gt; "$lockfile") 2&gt; /dev/null;
then
   trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT

   critical-section

   rm -f "$lockfile"
   trap - INT TERM EXIT
else
   echo "Failed to acquire lockfile: $lockfile."
   echo "Held by $(cat $lockfile)"
fi</pre>
<p>一个更复杂的情况是，如果脚本中需要执行一系列的操作，而且我们期望这些操作要么全部完成，若中途脚本被 kill 则全部不做，那我们也可以通过 trap 实现，范例代码如下：</p>
<pre>rollback() {
   del_from_passwd $user
   if [ -e /home/$user ]; then
      rm -rf /home/$user
   fi
   exit
}

trap rollback INT TERM EXIT
add_to_passwd $user
cp -a /etc/skel /home/$user
chown $user /home/$user -R
trap - INT TERM EXIT</pre>
<p>注意，脚本最后需要把 trap 所做的修改还原回去，否则脚本正常退出的时候， rollback 函数也会执行一遍，脚本就白干了……</p>
<h1>注意原子操作</h1>
<p>有时候我们可能需要对若干文件进行修改，比如对网站的若干文件修改链接，把 www.example.net 修改成 www.example.com：</p>
<pre>for file in $(find /var/www -type f -name "*.html"); do
   perl -pi -e 's/www.example.net/www.example.com/' $file
done</pre>
<p>如果这个脚本需要执行比较长的时间，执行到一半的时候中断了，那就会出现一半链接修改了一半链接没修改的情况；或者在脚本执行过程中，我们的网站也会出现同样的问题。要解决这个问题，可以利用 unix 系统对目录的 mv 操作比较快的特点，先把文件备份出来，修改完之后把整个目录 mv 回去：</p>
<pre>cp -a /var/www /var/www-tmp
for file in $(find /var/www-tmp -type f -name "*.html"); do
   perl -pi -e 's/www.example.net/www.example.com/' $file
done
mv /var/www /var/www-old
mv /var/www-tmp /var/www</pre>
<p>不过需要注意，如果有进程一直打开着我们的目录中某些文件，那我们这样进行修改之后，那些进程读取到的还是旧内容，需要重启一下这样的进程。</p>
<h1>注意脚本中依赖的环境变量</h1>
<p>一般，我们脚本的运行环境都是我们 login 后的环境，有很多环境变量通过我们的 .bashrc 已经设置了。但如果我们需要通过 cron 定时运行脚本，则需要注意里面依赖的环境变量。假设我们有个程序是 /usr/local/specialdir/bin/prog，我们可能在 .bashrc 中已经执行了</p>
<pre>export PATH=$PATH:/usr/local/specialdir/bin</pre>
<p>那我们的脚本写成这样，在 login 后的环境执行是没有问题的：</p>
<pre>#!/bin/bash

/usr/local/specialdir/bin/prog arg</pre>
<p>但如果我们在 cron 里面运行，则可能会在 $PATH 中缺少路径 /usr/local/specialdir/bin 导致脚本执行失败。因此脚本应该在前面加入环境变量 $PATH 的设置：</p>
<pre>#!/bin/bash

export PATH=$PATH:/usr/local/specialdir/bin
/usr/local/specialdir/bin/prog arg</pre>
<p>类似地，对于如 LD_LIBRARY_PATH 等常用环境变量，在编写 cron 脚本时也需要特别留意。</p>
]]></content:encoded>
			<wfw:commentRss>http://boke.name/beyondfeelings/2012/04/07/writing_robust_bash_shell_scripts/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
