前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SHELL(bash)脚本编程八:技巧

SHELL(bash)脚本编程八:技巧

作者头像
用户5030870
发布2019-04-11 10:56:30
6030
发布2019-04-11 10:56:30
举报
文章被收录于专栏:self_studyself_study

至此,我们介绍了linux系统中常用命令的使用方法,简述了bash程序的使用方法和工作流程。在使用bash编写脚本程序时,熟练掌握这些工具的用法,往往能够达到事半功倍的效果。

本文将通过讲述一些实例,试着探讨bash脚本编程的技巧。需要说明的是,这里的技巧是多角度寻求解决方案的思路,是建立在对各种命令和bash编程技法深刻理解的基础之上的。

1、笔试题

先来看某公司的两个笔试题:

1、写脚本实现,可以用shell、perl等。在目录/tmp下找到100个以abc开头的文件,然后把这些文件的第一行保存到文件new中。

分析:寻找名字符合某个模式的文件可以用find,但find不能控制寻找到的文件数量,也许可以用for循环控制一下,查看文件的第一行有许多方法,可以用headsed等。

根据以上思路写出脚本:

代码语言:javascript
复制
#!/bin/bash
for name in `find /tmp -type f ! -empty -name 'abc*'`
do
    head -1 $name >> new && ((i++))
    [[ $i -eq 100 ]] && break
done

脚本中每次成功写入文件new中一行内容就令变量i自增,当i增长到100时,立即结束循环。

另一种方案:

代码语言:javascript
复制
#!/bin/bash
sed 100q <(head -qn1 $(find /tmp -type f -name 'abc*')) >new

此方案只有一条命令,也不难理解:$(find ...)部分获得文件名列表,<(head ...)部分获取每个文件的第一行(<(...)的用法请看这里),最后sed 100q ... >new取前100行写入文件new。

2、写脚本实现,可以用shell、perl等。把文件b中有的,但是文件a中没有的所有行,保存为文件c,并统计c的行数。

问题没什么可分析的,直接的解决方案:

代码语言:javascript
复制
#!/bin/bash
cat b|while read line
do
    if ! grep -xq $line a;then
        echo $line >>c
    fi
done
wc -l c

脚本通过循环读取文件b中的每一行,判断该行,如果该行不属于文件a,则输出该行内容到文件c中,循环结束后用wc统计文件c的行数。

另一种方案:

代码语言:javascript
复制
#!/bin/bash
grep -vxf a b|tee c|wc -l

此方案利用grep-f选项将文件a中的每行最为匹配模式匹配文件b的内容,-v表示不匹配,然后通过管道交给命令tee写入文件c中,然后在通过管道将标准输出交给wc命令统计行数。

2、清空日志

在使用linux服务器的过程当中,随着服务的长时间运行,有时会有删除服务日志的需求。由于日志文件正在被该服务所使用,并不能直接进行删除(准确说是:即使直接删除了,空间也没有得到释放,需要将服务重启),比较好的做法是利用重定向清空该文件(如:>some.log),既释放了空间,也不用重启服务。

但当需要清空的文件较多时,手动一个一个清空文件也有许多不方便,不如将需求写成脚本。

方案1:

代码语言:javascript
复制
#!/bin/bash
for log in `find /logs -name 'access_*.log'`
do
    >$i
done

脚本很容易理解,也可以用命令find /logs -name 'access_*.log' -exec cp /dev/null {} \;

但仍是一个文件执行一次,能不能一次性执行完呢?

方案2:

代码语言:javascript
复制
#!/bin/bash
find /logs -name 'access_*.log'|xargs tee

此方案巧妙的利用了命令xargstee将find找到的文件一次性清空。

3、分发

假设要对一个较大文件分别给不同的程序处理,并收集处理结果。 通常的处理的办法可能是串行的处理该文件,但如果各个程序需要较长的处理时间,串行处理将不能有效的利用机器的性能,如果不同的处理程序在后台并发运行,类似这样:cat file|command1 &cat file|command2 &cat file|command3 &...

这样处理能充分发挥服务器性能,但它的一个问题是,如果文件较大,对内存的消耗也会很大。

一种解决方案是:

代码语言:javascript
复制
#!/bin/bash
cat file|tee >(command1) >(command2) ... > >(commandN)|cat

此种方案的一个问题是,多个处理结果是随机的,如果需要处理结果是有序的(比如按命令的顺序输出),则不能满足需求。 另外,tee命令分发的速率是恒定的,所以只能按处理命令中最慢的速率分发,它们的输出将争用同一个管道,一定条件下,有可能造成死锁。

另一种解决方案:

代码语言:javascript
复制
#!/bin/bash
#定义处理函数
f() {
  mkfifo p{i,o}{1,2,3}
  command1 < pi1 > po1 &
  command2 < pi2 > po2 &
  command3 < pi3 > po3 &

  tee pi{1,2} > pi3 &
  cat po{1,2,3}
  rm -f p{i,o}{1,2,3}
}
cat file | f

此方案利用命名管道(见这里)处理分发及汇总各命令的输出,然后通过cat依次读取处理后的结果。

4、并发

我们在描述重定向与管道的文章中讲述过一种并发方式,下面介绍另一种。

我们说过,命令替换的问题是命令的立即执行然后等待结果,此时shell无法传入输入。bash使用一个称为进程替换的功能来弥补这些不足,进程替换实际上是命令替换管道的组合,和命令替换类似,bash运行一个命令,但令其运行于后台而不再等待其完成。关键在于Bash为这条命令打开了一个用于读和写的管道,并且绑定到一个文件名,最后展开为结果。

利用进程替换的这一特性,可以想到另外一种并发的方式:

代码语言:javascript
复制
#!/bin/bash
#处理函数,假设该函数的处理结果有且只有一个值
sth_todo() {
    #需要对第一个参数处理的命令
    some_command $1
}
#文件数组,也可以是其他待处理数据
file_list=('<(sth_todo '{1..10}.log')') #展开形如:<(sth_todo 1.log) <(sth_todo 2.log) <(sth_todo 3.log) ...
#收集结果并赋值给数组
read -a result <<<$(eval cat "${file_list[@]}")
#输出
echo "${result[@]}"

脚本中需要注意的地方在于数组的赋值和eval的运用(见这里)。

当然处理函数并不一定只有唯一结果,如有其他结果,只需将收集结果部分做相应更改即可。

5、数组交、并、差集

假定有需要取两个数组的交集(或并集、差集),简单的做法无非是两个循环对比两个数组中的每个值,取得相同的部分:

代码语言:javascript
复制
#!/bin/bash
list_1=(...)
list_2=(...)
for i in ${list_1[@]}
do
    for j in ${list_2[@]}
    do
        [[ "$i" == "$j" ]] && echo $i
    done
done

再看另一种方案:

代码语言:javascript
复制
list_1=(...)
list_2=(...)
#重置IFS值
IFS=$'\n'
#交集
grep -xf <(echo "${list_1[*]}") <<<"${list_2[*]}"
#并集
sort -u <<EOF
${ip[*]}
${ip_[*]}
EOF
#差集之一
grep -vxf <(echo "${list_1[*]}") <<<"${list_2[*]}"
#还原IFS
IFS=$' \t\n'

bash的一些特性和常用命令结合使用,使原本需要许多循环代码解决的问题变得“轻而易举”。但本例中,需要重点理解的是:IFS在数组扩展中的特性,命令grepsort的运用,以及进程替换的使用。

6、大量数据处理

假如需要对大量小文件进行简单的文本替换,而文件量已达到不可一次性处理的程度(比如几百万个)。 此时如果采用一般的处理办法,例如

代码语言:javascript
复制
find . -name '*.html' -exec sed -i 's/xxxx/oooo/g' {} \;

或类似的命令,显然,这样一个文件接着一个文件串行处理将花费巨大的时间成本。

对于此类问题,需要在服务器性能和时间成本上做取舍,先给出处理方案:

代码语言:javascript
复制
#!/bin/bash
#取得待处理文件数组
A=($(find . -name '*.html'))
#每10000个文件后台循环处理
for((i=0;i<${#A[@]};i+=10000))
do
    sed -i 's@xxxx@oooo@g' ${A[@]:$i:10000} &
done
#等待所有进程退出
wait

此方案假定服务器资源可以随意使用,只为达到时间效率最大化。如果需要控制服务器资源消耗(主要是IO性能),可以结合这一篇,控制并发的进程数量。

关于bash的文章,至此就告一段落了。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017-02-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、笔试题
  • 2、清空日志
  • 3、分发
  • 4、并发
  • 5、数组交、并、差集
  • 6、大量数据处理
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档