SHELL(bash)腳本編程八:技巧

至此,咱們介紹了linux系統中經常使用命令的使用方法,簡述了bash程序的使用方法和工做流程。在使用bash編寫腳本程序時,熟練掌握這些工具的用法,每每可以達到事半功倍的效果。html

本文將經過講述一些實例,試着探討bash腳本編程的技巧。須要說明的是,這裏的技巧是多角度尋求解決方案的思路,是創建在對各類命令和bash編程技法深入理解的基礎之上的。linux

一、筆試題

先來看某公司的兩個筆試題:shell

一、寫腳本實現,能夠用shell、perl等。在目錄/tmp下找到100個以abc開頭的文件,而後把這些文件的第一行保存到文件new中。編程

分析:尋找名字符合某個模式的文件能夠用find,但find不能控制尋找到的文件數量,也許能夠用for循環控制一下,查看文件的第一行有許多方法,能夠用headsed等。segmentfault

根據以上思路寫出腳本:數組

#!/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時,當即結束循環。bash

另外一種方案:服務器

#!/bin/bash
sed 100q <(head -qn1 $(find /tmp -type f -name 'abc*')) >new

此方案只有一條命令,也不難理解:$(find ...)部分得到文件名列表,<(head ...)部分獲取每一個文件的第一行(<(...)的用法請看這裏),最後sed 100q ... >new取前100行寫入文件new。併發

二、寫腳本實現,能夠用shell、perl等。把文件b中有的,可是文件a中沒有的全部行,保存爲文件c,並統計c的行數。函數

問題沒什麼可分析的,直接的解決方案:

#!/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的行數。

另外一種方案:

#!/bin/bash
grep -vxf a b|tee c|wc -l

此方案利用grep-f選項將文件a中的每行最爲匹配模式匹配文件b的內容,-v表示不匹配,而後經過管道交給命令tee寫入文件c中,而後在經過管道將標準輸出交給wc命令統計行數。

二、清空日誌

在使用linux服務器的過程中,隨着服務的長時間運行,有時會有刪除服務日誌的需求。因爲日誌文件正在被該服務所使用,並不能直接進行刪除(準確說是:即便直接刪除了,空間也沒有獲得釋放,須要將服務重啓),比較好的作法是利用重定向清空該文件(如:>some.log),既釋放了空間,也不用重啓服務。

但當須要清空的文件較多時,手動一個一個清空文件也有許多不方便,不如將需求寫成腳本。

方案1:

#!/bin/bash
for log in `find /logs -name 'access_*.log'`
do
    >$i
done

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

但還是一個文件執行一次,能不能一次性執行完呢?

方案2:

#!/bin/bash
find /logs -name 'access_*.log'|xargs tee

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

三、分發

假設要對一個較大文件分別給不一樣的程序處理,並收集處理結果。
一般的處理的辦法多是串行的處理該文件,但若是各個程序須要較長的處理時間,串行處理將不能有效的利用機器的性能,若是不一樣的處理程序在後臺併發運行,相似這樣:cat file|command1 &cat file|command2 &cat file|command3 &...

這樣處理能充分發揮服務器性能,但它的一個問題是,若是文件較大,對內存的消耗也會很大。

一種解決方案是:

#!/bin/bash
cat file|tee >(command1) >(command2) ... > >(commandN)|cat

此種方案的一個問題是,多個處理結果是隨機的,若是須要處理結果是有序的(好比按命令的順序輸出),則不能知足需求。
另外,tee命令分發的速率是恆定的,因此只能按處理命令中最慢的速率分發,它們的輸出將爭用同一個管道,必定條件下,有可能形成死鎖。

另外一種解決方案:

#!/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依次讀取處理後的結果。

四、併發

咱們在描述重定向與管道的文章中講述過一種併發方式,下面介紹另外一種。

咱們說過,命令替換的問題是命令的當即執行而後等待結果,此時shell沒法傳入輸入。bash使用一個稱爲進程替換的功能來彌補這些不足,進程替換其實是命令替換管道的組合,和命令替換相似,bash運行一個命令,但令其運行於後臺而再也不等待其完成。關鍵在於Bash爲這條命令打開了一個用於讀和寫的管道,而且綁定到一個文件名,最後展開爲結果。

利用進程替換的這一特性,能夠想到另一種併發的方式:

#!/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的運用(見這裏)。

固然處理函數並不必定只有惟一結果,若有其餘結果,只需將收集結果部分作相應更改便可。

五、數組交、並、差集

假定有須要取兩個數組的交集(或並集、差集),簡單的作法無非是兩個循環對比兩個數組中的每一個值,取得相同的部分:

#!/bin/bash
list_1=(...)
list_2=(...)
for i in ${list_1[@]}
do
    for j in ${list_2[@]}
    do
        [[ "$i" == "$j" ]] && echo $i
    done
done

再看另外一種方案:

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的運用,以及進程替換的使用。

六、大量數據處理

假如須要對大量小文件進行簡單的文本替換,而文件量已達到不可一次性處理的程度(好比幾百萬個)。
此時若是採用通常的處理辦法,例如

find . -name '*.html' -exec sed -i 's/xxxx/oooo/g' {} \;

或相似的命令,顯然,這樣一個文件接着一個文件串行處理將花費巨大的時間成本。

對於此類問題,須要在服務器性能和時間成本上作取捨,先給出處理方案:

#!/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的文章,至此就告一段落了。

個人博客即將同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/dev...

相關文章
相關標籤/搜索