【转】小心bash的管道

Monday, July 26, 2010

管道是Shell中非常常用的东西,*nix的神奇,一大部分要归功于各式各样全能的小工具和管道。
简而言之,管道就是把若干个程序连接起来,一个管道符号的前一个程序的输出作为后一个程序的输入,比如,统计hello.c里面有多少行带有双斜杠可以这样写:

cat hello.c | grep '//' | wc -l

这篇日志不是普及管道的,而是因为今天自己写Shell脚本时遇到了一个问题,记在这里作为提醒。
Shell中可以打开另外一个Shell,新打开的Shell就是Subshell,对于这个Subshell,打开它的Shell叫做Parent
Shell。这两个Shell的最大区别在于环境变量,Parent
Shell中所有被export过的环境变量会被Subshell继承,而Subshell对环境变量做的任何修改都不会影响Parent
Shell中的环境变量。 在圆括号括起来的命令会在Subshell中执行:

export a=1; ( echo 'Sub 1: '$a; a=2; echo 'Sub 2: '$a; unset a; ); echo 'Parent: '$a

会得到这样的结果 Sub 1: 1 Sub 2: 2 Parent: 1
今天,我写了一个脚本对一个ZOJ比赛的Runs页面进行不停地监视,一旦有新的状态产生,就用libnotify把它显示出来,这个脚本大致是这样的:

#!/bin/bash

c=0
while true; do
    wget --load-cookies cookies.txt "http://xxxxxx" -qO a.html
    cat a.html | grep 'runId">[0-9]' -A  13 | sed 's/<[^>]*>//g;s/  //g' | uniq | while read i; do
        [ -n "$i" ] && i=`printf '%s' "$i" | tr -d '\r'`
        if [ "$i" = '--' ]; then
            c=0
        elif [ -n "$i" ]; then
            (( c++ ))
            case "$c" in
            ('1')
                id=$i;;
            ('2')
                time="$i";;
            ('3')
                result="$i";;
            ('4')
                prob="$i";;
            ('5')
                lang="$i";;
            ('6')
                during="$i";;
            ('7')
                mem="$i";;
            ('8')
                user="$i";
                if [ -z "${a[$id]}" ] && echo $result | grep -v 'ing' &>/dev/null; then
                    a[$id]=1
                    notify-send "$prob - $user" "$result, ${during}ms ${mem}KB"
                fi
                ;;
            esac
        fi
    done
    sleep 10;
done

这个脚本运行起来会反复地显示获得的内容,也就是说if [ -z "${a[$id]}” ]检测被无视了。
为什么会这样子呢?经过调试,我发现bash遇到管道会创建一个Subshell,而zsh不会。 考虑一个简单的脚本,统计输入的行数:

c=0
while read; do
    (( c++ ));
done
echo 'Line count: '$c

这是可以工作的,稍微修改一下:

c=0
cat | while read; do
    (( c++ ));
done
echo 'Line count: '$c

在bash下就不能正常工作了,其中的c++会在Subshell中执行,导致最终结果是0,如果用zsh执行这段脚本仍然可以得到期待的结果。
那如何解决这个问题呢?把管道拆成两个重定向就可以啦 :-) 对于上面这段

cat | while read; do
    (( c++ ));
done

的写法,可以改成:

cat > tempfile
while read; do
    (( c++ ));
done < tempfile

第一个脚本也可以类似地修改。
虽然直接使用zsh可以解决问题,但是由于zsh并不是每个地方都有的,而zsh也不是便于携带的,安装起来需要管理员权限往往自己没有,在这种情况下,对于类似这样的“有歧义”的写法,还是使用比较有“兼容性”的做法比较好。
当然,一味地追求“兼容性”也不是好事情。比如目前的Ubuntu/Debian发行版中,/bin/sh是链接到dash的。dash是一个极其轻量的Shell,一般zsh或者bash需要占用1到3MB的内存,而dash往往只需要几十KB的内存就可以工作了,没有自动补全,适合脚本使用。但是dash有严重问题,即便用它执行很简单的脚本。比如对于中文文件名,dash可能会在参数传递时出现编码问题,一个具体表现是使用for
filename in *遍历时,把$filename当作参数传入cp这样的程序后,会说文件找不到 -.-b。
不追求“兼容性”,也不要迷信某一个程序会很“标准”,或者周围的人都在按照“标准”做事情。Ubuntu的Wiki上面说第一行是#!/bin
/sh的脚本都应该兼容dash,这是个符合POSIX标准的Shell,但是实际情况绝对不是这样的。我使用Archlinux,把/bin/sh改成
dash后,升级系统就发现系统无法启动了,折腾了好长时间才发现是这个原因导致的,Archlinux的kernel软件包的安装后执行脚本不兼容
dash。 《三国演义》中第一句话就是“话说天下大势,分久必合,合久必分。”,期待一下zsh把Shell天下“合”的那一天 :-P

This entry was tagged Linux

comments powered by Disqus

© 2009-2013 lxneng.com. All rights reserved. Powered by Pyramid

go to Top