2017年6月8日 星期四

有效率的大海撈針

筆者的工作中常常碰到一種需求:給定一字串,找出這個字串定義或存放在目錄(含子目錄)下哪個檔案中。


如果確定是 source code(如 C/C++)的一部分,這只是一碟小蛋糕:
grep -Prn --color --include=\*.{c,cpp,h} "pattern"
尷尬的是有時根本不確定是存放在哪種檔案中(甚至可能不是文字檔),這時候就只好開地圖砲了:
find . -name "*" -exec sh -c "strings -f {} | grep 'pattern'" \; 
strings 指令會過濾出檔案中的 ASCII 可見字元,此時再用 pipe 餵給 grep 就安全了。
(雖然 grep 有個 --text 選項,但實際測試結果很不理想)

strings 的參數 -f 是顯示檔名,因此假如您想要再串 grep -v 過濾多餘的資料,比方說註解,以下方式是錯的:
find . -name "*" -exec sh -c "strings -f {} | grep 'pattern' | grep -P '^\s*(//|/\*)'" \; 
請改成:
find . -name "*" -exec sh -c "strings -f {} | grep 'pattern' | grep -P 'filename:^\s*(//|/\*)'" \; 
因為 strings 在輸出過濾後的檔案時也把檔名一併輸出給第一個 grep 了。

這個小技巧有兩個不完美的地方:

  • 如果原本就是文字檔,丟給 strings 多此一舉,浪費效能
  • 行號丟失:不過如果字串放在非文字檔裡,就沒有行號這種問題。
如果要做更的精細一點,可以用 file 指令預先判斷檔案的型態再決定是否要套用 strings,不過這就不是一行就能解決了,範例如下:
#!/bin/bash
#fireinholde.sh
IFS=$'\n'
for filename in $(find . -name "*")
do
    #is ASCII text?
    isAscii=$(file $filename | grep -P 'ASCII text$')
    if [ -n "$isAscii" ]; then
        grep -PHn $1 $filename
    else        
    #no, you need strings it!
        result=$(strings $filename | grep -P $1)
        if [ -n "$result" ]; then
            for matched in $result
            do
                echo "$filename:$matched"
            done
        fi
    fi
done
不過在 SSD 越來越普及的現在,這一點效能損失應該是可以被忍受的,希望這個小技巧對大家有幫助 :)

1 則留言: