shell 脚本实战 五

摘自 shell脚本实战 第二版 第四章 Unix调校

脚本27 显示带有行号的文件

在显示文件时添加行号有很多种方法,其中一些实现起来相当简洁。例如,下面是 awk 的 做法:

awk '{print NR ":" $0} < inputfile

在有些 Unix 实现中,cat 命令有一个-n 选项;在另一些实现中,分页程序 more(less 或 pg) 可以用选项指定为输出的每一行加上行号。但两者皆无的 Unix 版本也不是没有,在这种情况下, 用代码清单 4-1 中的简单脚本就能搞定

代码 numberlines

#!/bin/bash

# numberlines -- cat -n 等具备类似功能命令的一个简单的替代品

for filename in "$@";do
        linecount="1" 
        while IFS="\n" read line;do # 1 以此获取文件中的数据
                echo "${linecount}: $line"
                linecount="$(( $linecount + 1 ))" # 2 内部的while循环每次读入一行,将其保存在变量line中,然后将行号作为前缀,输出该行,并增加变量linecount
        done < $filename # 3 从 filename 文件中获取
done

exit 

运行结果

$ ./numberlines calc 
1: #!/bin/bash
2: 
3: # calc -- 一个命令行计算器,可用作bc的前段
4: 
5: 
6: scale=2
7: 
8: show_help(){
9: cat << EOF
10:     In addition to standard math functions, calc also supports:
11: 
12:     a % b           remainder of a/b
13:     a ^ b           exponential: a raised to the b power
14:     s(x)            sine of x, x in radians
15:     c(x)            cosine of x, x in radians
16:     a(x)            arctangent of x, in radians
17:     l(x)            natural log of x 
18:     e(x)            exponential log of raising e to the x
19:     j(n,x)          Bessel function of integer order n of x
20:     scale N         show N fractional digits (default = 2)
21: EOF
22: }
23: 
24: if [ $# -gt 0 ];the
25:     exec ./scriptbc "$@"
26: 
27: fi
28: 
29: echo "Calc -- a simple calculator. Enter 'help' for help, 'quit' to quit."
30: 
31: /bin/echo -n "Calc> "
32: 
33: while read command args;do # 1 创建一个无穷循环,不断地显示提示符calc>,直到用户输入quit或按下CTRL-D(^D)退出为止
34:     case $command
35:             i
36:             quit|exit )             exit 0                                  ;;
37:             help|? )                show_help                               ;;
38:             scale )                 scale=$args                             ;;
39:             * )                     ./scriptbc -p $scale "$command" "$args" ;;
40:     esac
41: 
42:     /bin/echo -n "Calc> "
43: done
44: 
45: echo ""
46: 
47: exit 0

精益求精

给文件加上行号之后,可以将文件中所有的行颠倒过来:

cat -n filename | sort -rn |cut -c8-

脚本28 仅折行过长的行

fmt 命令及其等效的 shell 脚本(脚本#14)所存在的一个局限是它们会折行和填充所遇到的 每一行文本,不管这么做是否有意义。这种行为会把电子邮件(折行.signature 可不好)和那些 重视换行的输入文件格式搞乱。

如果你只是想折行文档中过长的那些行,其他内容保持不变,该怎么办?考虑 Unix 用户可 用的默认命令,只有一种方法能实现这种要求:在编辑器中逐行排查,把过长的行单独传给 fmt。 (在 vi 中将光标移动到需要调整的行并使用!$fmt。)

代码清单 4-3 中的脚本可以自动完成这项任务,它利用了 shell 的变量替换功能${#varname}, 该功能可以返回变量 varname 中内容的长度。

代码 toolong

#!/bin/bash

# toolong -- 只将输入流中超出指定长度的行传给fmt命令

width=72

if [ ! -r "$1" ];then # 如果文件存在且可读
        echo "Cannot read file $1" >&2
        echo "Usage: $0 filename" >&2
        exit 1
fi

while read input;do # 1 使用read input 将文件内容逐行读取到变量input中,分析每一行
        if [ ${#input} -gt $width ];then
                echo "$input" | fmt
        else
                echo "$input"
        fi
done < $1 # 2 结果通过一个简单的 < $1 传入循环

运行结果

$ toolong ragged.txt 
So she sat on, with closed eyes, and half believed herself in 
Wonderland, though she knew she had but to open them again, and 
all would change to dull reality--the grass would be only rustling 
in the wind, and the pool rippling to the waving of the reeds--the 
rattling teacups would change to tinkling sheep-bells, and the 
Queen's shrill cries to the voice of the shepherd boy--and the 
sneeze of the baby, the shriek of the Gryphon, and all the other 
queer noises, would change (she knew) to the confused clamour of the busy 
farm-yard--while the lowing of the cattle in the distance would 
take the place of the Mock Turtle's heavy sobs.

脚本29 显示文件及其附加信息

许多最为常见的 Unix 和 Linux 命令起初是针对缓慢的、几乎不存在交互的输出环境而设计 的(记不记得我们曾经说过 Unix 是一个古老的操作系统?),因此提供的输出和交互性也是最少 的。cat 就是一个例子:在浏览短小的文件时,它并不会给出太多有用的输出。要是能显示出有 关文件的更多信息就好了。所以,我们还是自己动手,丰衣足食吧!代码清单 4-5 给出了 cat 命 令之外的另一种选择:showfile 命令。

showfile

#!/bin/bash

# showfile -- 显示包括附加信息在内的文件内容

width=72
for input ;do
        echo $input
        lines="$(wc -l < $input |sed 's/ //g')"
        chars="$(wc -c < $input |sed 's/ //g')"
        owner="$(ls -ld $input | awk '{print $3}')"

        echo "------------------------------------------------------------------"
        echo "File $input ($line lines, $chars characters, owned by $owner):"
        echo "------------------------------------------------------------------"
        while read line;do
                if [ ${#line} -gt $width ];then
                        echo "$line" | fmt | sed -e '1s/^/ /' -e '2,$s/^/+ /' # 第一行用双空格缩进,之后的行前面添加一个+号和一个空格
                else
                        echo "  $line"
                fi
        done < $input # 1 为了逐行读取输入的同时添加头部和尾部信息,脚本中用了一个很方便的技巧:在接近末尾的地方使用done < $input 将输入重定向入 while 循环

        echo "------------------------------------------------------------------"
done | ${PAGER:more}

exit 0

# 示例 
$ echo -e "coco is handsome\ncoco\ncoco" | sed -e '1s/^/ /' -e '2,$s/^/+ /'
 coco is handsome
+ coco
+ coco

运行结果

$ showfile ragged.txt
----------------------------------------------------------------
File ragged.txt (7 lines, 639 characters, owned by taylor):
----------------------------------------------------------------
  So she sat on, with closed eyes, and half believed herself in Wonderland, though she knew she had but to open them again, and all would change to dull reality--the grass would be only rustling 
+ in the wind, and the pool rippling to the waving of the reeds--the rattling teacups would change to tinkling sheep-bells, and the Queen's shrill cries to the voice of the shepherd boy--and the sneeze of the baby, the shriek of the Gryphon, and all the other queer + noises, would change (she knew) to the confused clamour of the busy + farm-yard--while the lowing of the cattle in the distance would + take the place of the Mock Turtle's heavy sobs.
  So she sat on, with closed eyes, and half believed herself in Wonderland, though she knew she had but to open them again, and all would change to dull reality--the grass would be only rustling + in the wind, and the pool rippling to the waving of the reeds--the rattling teacups would change to tinkling sheep-bells, and the Queen's shrill cries to the voice of the shepherd boy--and the sneeze of the baby, the shriek of the Gryphon, and all the other queer 
+ noises, would change (she knew) to the confused clamour of the busy 
+ farm-yard--while the lowing of the cattle in the distance would 
+ take the place of the Mock Turtle's heavy sobs.

脚本30 用quota模拟GNU风格选项

各种 Unix 和 Linux 系统命令选项的不一致性可谓是一个永久性的问题,这给那些需要在主 要版本,尤其是商业 Unix 系统(SunOS/Solaris、HP-UX 等)和开源 Linux 系统之间切换的用户 带来了不少苦恼。quota 就反映出了这个问题,该命令在有些 Unix 系统中支持全字选项① ,但在 另一些系统中只支持单字母选项。

代码清单 4-7 中给出了一个简洁的 shell 脚本,它通过将全字选项映射到等效的单字母选项来 解决这个烦心事。

代码 newquota

#!/bin/bash

# newquota -- 可以处理全字选项的quota前端

# quota有3中选项:-g、-v和-q,该脚本也允许使用圈子选项:--group、--verbose和--quiet、

flags=""
realquota="$(which quota)"

while [$# -gt 0];do
        case $1
                in
                --help    )     echo "Usage: $0 [--group --verbose --quiet -gvq]" >&2
                                exit 1  ;;
                --group   )     flags="$flags -g";      shift   ;;
                --verbose )     flags="$flags -v";      shift   ;;
                --quiet   )     flags="$flags -q";      shift   ;;
                --        )     shift ;                 break   ;;
                *         )     break ;         # 跳出while循环!
        esac
done

exec $realquota $flags "$@" # 1 该脚本实际上可归结为一个while语句,该语句逐个检查传入的参数,标识出匹配的全字选项,然后将之与相应的单字母选项加入到flags变量中。处理完参数后,只需要调用原始的quota程序并根据需要添加用户指定的选项即可

运行结果

$ newquota --verbose 
Disk quotas for user dtint (uid 24810):

	Filesystem  usage  quota  limit  grace files  quota  limit grace 
		  /usr 338262 614400 675840        10703 120000 126000

$ newquota --quiet

脚本31 让sftp用起来像ftp

ftp(file transfer protocol)程序的安全版本是 Secure Shell 软件包 ssh 的组成部分,但对于从 古老的 ftp 客户端转战而来的用户来说,其界面有点让人摸不着头脑。根本问题在于 ftp 的调用 形式为 ftp remotehost,然后会提醒用户输入用户名和密码。相比之下,sftp 要求在命令行中指 定账户和远程主机,如果只指定主机的话,就没法正常(或如预期那样)工作。

代码清单 4-9 中的包装器脚本就可以解决这个问题,用户可以像使用 ftp 程序那样使用 mysftp,脚本会逐一提示用户输入必要的信息。

代码 mysftp

#!/bin/bash

# mysftp -- 让sftp用起来更像ftp

read -p "User account: " account

if [ -z $account ];then
        exit 0; # 用户大概是改主意了。
fi

if [ -z "$1" ];then
        read -p "Remote host: "
        if [ -z $host ];then
                exit 0
        fi
else
        host=$1
fi

# 最后以切换到sftp作结尾。-C选项在此处启用压缩功能

exec sftp -C $account$host # 1 这里调用了exec 这样做会使得当前运行的shell被指定的程序所替换

运行结果

# 运行 sftp 时如果不指定参数,产生的输出晦涩难懂
$ sftp
usage: sftp [-46aCfNpqrv] [-B buffer_size] [-b batchfile] [-c cipher]
          [-D sftp_server_path] [-F ssh_config] [-i identity_file]
          [-J destination] [-l limit] [-o ssh_option] [-P port]
          [-R num_requests] [-S program] [-s subsystem | sftp_server]
          destination

# 不指定参数时的 mysftp 脚本要清晰得多
$ mysftp 
User account: taylor 
Remote host: intuitive.com 
Connecting to intuitive.com... 
taylor@intuitive.com's password: 
sftp> quit

精益求精

当你有了像这样的脚本时,总是要考虑的一件事是能否将其作为自动化备份或同步工具的基 础,而 mysftp 就颇为适合。所以说,我们可以做出的一处重大改进是在系统中指定一个目录, 然后编写一个可以为重要文件创建 ZIP 归档的包装器,使用 mysftp 将归档复制到服务器或云存 储系统。实际上,脚本#72 中正是这么做的。

脚本32 改进grep

有些版本的 grep 功能特别多,其中包括可以显示匹配行的上下文(匹配行前后的一到两行), 这一点特别实用。除此之外,还能将文本行中匹配指定模式的部分高亮显示。你可能已经用上了 这种 grep。当然,也可能还没有。

好在这两种特性都能用 shell 脚本模拟,所以就算你所用的商业 Unix 系统比较陈旧,其自带 的 grep 命令相对原始,依然不会妨碍你享受新功能。为了指定匹配行的前后行数量,我们在匹 配模式之后使用-c value。该脚本(如代码清单 4-13 所示)还借用了脚本#11 中的 ANSI 着色功 能来高亮匹配区域。

代码 cgrep

#!/bin/bash

# cgrep -- 能够显示上下文并高亮匹配模式的grep

context=0
esc="^["
boldon="${esc}[1m" boldoff="${esc}[22m"
sedscript="/tmp/cfrep.sed.$$"
tempout="/tmp/cgrep.$$"

function showMatches
{

        matches=0
        echo "s/$pattern/${boldon}$pattern${boldoff}/g" > $sedscript # 1 将匹配到的模式用ANSI序列bold-on和bold-off包装起来,以粗体显示
        
        for lineno in $(grep -n "$pattern" $1 |cut -d: -f1);do # 2 使用grep -n获得文件匹配到的行数
                if [ $context -gt 0 ];then 
                        prev="$(( $lineno - $context ))" # 3 计算出要显示的起止行
                        if [ $prev -lt 1 ];then
                                # 这会导致 "invalid usage of line address 0."
                                prev="1"
                        fi
                        next="$(( $lineo + $context ))" # 4 计算出要显示的起止行

                        if [ $matches -gt 0 ];then
                                echo "${prev}i\\" >> $sedscript
                                echo "---" >> $sedscript
                        fi
                        echo "${prev},${next}p" >> $sedscript
                else
                        echo "${lineo}p" >> $sedscript
                fi
                matches="$(( $matches + 1 ))"
        done

        if [ $matches -gt 0 ];then
                sed -n -f $sedscript $1 | uniq | more # 命令用于检查及删除文本文件中重复出现的行列,一般与 sort 命令结合使用
        fi

}

trap "$(which rm) -f $tempout $sedscript" EXIT # 5 信号捕捉 当脚本退出是删除 两个临时文件

if [ -z "$1" ];then
        echo "Usage: $0 [-c X] pattern {filename}" >&2
        exit 0
fi

if [ "$1" = "-c" ];then
        context="$2"
        shift;shift
elif [ "$(echo $1|cut -c1-2)" = "-c" ];then
        context="$(echo $1| cut -c3-)"
        shift
fi

pattern="$1";shift

if [ $# -gt 0 ];then
        for filename ;do
                echo "----- $filename -----"
                showMatches $filename
        done
else
        cat - > $tempout # 将输入保存到临时文件中
        showMatches $tempout
fi

exit 0

运行结果

$ ./cgrep -c 1 coco data 
----- data -----
^[[1mcoco^[[22m is handsome

精益求精

该脚本中一处有用的改进就是返回匹配的行及行号。

脚本33 处理压缩文件

在 Unix 多年的发展过程中,很少有哪个程序能像 compress 这样被多次重新设计。在大多数 Linux 系统中,有 3 个截然不同的压缩程序可用:compress、gzip 和 bzip2。它们各自使用不同的 文件名后缀(分别是.z、.gz 和.bz2),其压缩程度根据每个文件内数据的布局也不尽相同。

抛开压缩级别,也不管你安装了哪些压缩程序,在很多 Unix 系统中处理压缩文件时都得手 动解压缩,完成要求的任务,然后再重新压缩文件。实在是枯燥无味。这正是 shell 脚本大显身 手的绝佳舞台!代码清单 4-15 中的脚本可以作为一个方便的压缩/解压缩包装器,它能够实现 3 种常见的压缩文件操作:cat、more 和 grep。

代码zcat/zmore/zgrep

#!/bin/bash

# zcat, zmore, and zgrep--This script should be either symbolically
#   linked or hard linked to all three names. It allows users to work with
#   compressed files transparently.

 Z="compress";  unZ="uncompress"  ;  Zlist=""
gz="gzip"    ; ungz="gunzip"      ; gzlist=""
bz="bzip2"   ; unbz="bunzip2"     ; bzlist=""

# First step is to try to isolate the filenames in the command line.
# We'll do this lazily by stepping through each argument, testing to 
# see if it's a filename or not. If it is, and it has a compression
# suffix, we'll decompress the file, rewrite the filename, and proceed.
# When done, we'll recompress everything that was decompressed.

for arg
do 
  if [ -f "$arg" ] ; then
    case "$arg" in
       *.Z) $unZ "$arg"
            arg="$(echo $arg | sed 's/\.Z$//')"
            Zlist="$Zlist \"$arg\""
            ;;

      *.gz) $ungz "$arg"
            arg="$(echo $arg | sed 's/\.gz$//')"
            gzlist="$gzlist \"$arg\""
            ;;

     *.bz2) $unbz "$arg"
            arg="$(echo $arg | sed 's/\.bz2$//')"
            bzlist="$bzlist \"$arg\""
            ;;

    esac
  fi
  newargs="${newargs:-""} \"$arg\""
done

case $0 in
  *zcat*  ) eval  cat $newargs                  ;;
  *zmore* ) eval more $newargs                  ;;
  *zgrep* ) eval grep $newargs                  ;;
      *   ) echo "$0: unknown base name. Can't proceed." >&2; exit 1
esac

# now recompress everything

if [ ! -z "$Zlist"  ] ; then
  eval $Z $Zlist
fi
if [ ! -z "$gzlist"] ; then
  eval $gz $gzlist
fi
if [ ! -z "$bzlist" ] ; then
  eval $bz $bzlist 
fi

# and done

exit 0


运行结果

$ gzip data
$ zcat data.gz 
coco is handsome

精益求精

也许这个脚本最大的弱点是如果它在中途被取消,则无法保证文件会被重新压缩。一个很好 的改进方案是巧妙地利用 trap 命令并在重新压缩的时候检查错误

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值