shell編程的若干實用技巧

37 人贊了文章

不少人感興趣, 想到會繼續補充.

也可在評論區留言, 筆者選擇比較典型的做補充.

=====

筆者工作中, 發現很多實用的shell編程技巧, 使用極少. 所以, 略作整理.

很多熟悉python的朋友, 本來在用shell就可以搞定問題的地方, 意外地使用python. 有一種說法, shell容易出錯. 其實shell也可以寫得很健壯, 健壯在於人而不在語言.

skill#0: 不可逆操作引用環境變數時用${var:?"undefined var"}

比如:

rm -fr ${dir}/ # 如果dir未定義, 則刪除根目錄.rm -fr ${dir:?"undefined dir"} # 如果dir未定義, 報錯.

skill#1: 腳本出錯即停使用 set -e -o pipefail

腳本開頭第一句話, 寫:#!/bin/bashset -e -o pipefail

如果出現單行或者單行管道命令出現錯誤, 腳本會停止執行並且報錯.

skill#2: 獲取basedir並且發起一個非util命令時使用絕對路徑

script=$(basename ${BASH_SOURCE:-$0})basedir=$(cd $(dirname ${BASH_SOURCE:-$0});pwd)${basedir}/bin/execuable

用絕對路徑的好處是用命令ps -C execuable可直接獲得可執行程序的絕對路徑. 不然的話, 需要查看/proc/${pid}/cwd 確定路徑, 比較麻煩. 所以, 能省事就省事.

skill#3: 檢查後臺進程是否成功啟動

set -e -o pipefailsleep 1000 &pid=$!kill -0 ${pid} # kill -0 檢查進程是否存活

先要獲取後臺進程的pid, 然後用kill -9檢查是否存活.

skill#4: 優雅退出時清理用trap

mkdir /tmp/some.dirfinally(){ local last_status=$? #最後一條命令的執行結果 trap "" EXIT #避免執行finally嵌套調用死循環 rm -fr /tmp/some.dir #清理工作 exit ${last_status} #真正退出}trap finally EXIT...

用trap給EXIT事件註冊一個處理函數finally, 進入finally首先要獲取最後一條命令的執行結果, 然後重置EXIT事件的處理函數, 否則可能會發生嵌套調用死循環.

注意: normal/abnormal exit都會調用EXIT的處理函數, 此不同於on_exit.

skill#5: 參數傳遞使用shift

script=$(basename ${BASH_SOURCE:-$0})usage="FORMAT: ${script} <srcdir> <dstdir>"srcdir=${1:?"undefined srcdir, $usage"};shiftdstdir=${1:?"undefined dstdir, $usage"};shift

skill#6: 通配符中含有$((表達式))時使用eval

num=100for i in $(eval "echo {0..$(($num-1))}");do echo $i; done

用eval可以構造特別複雜的序列

skill#7: eval可建立soft reference, 動態修改referenced對象

name=10name_ref=nameecho $(eval "echo $$name_ref")

eval類似perl/python的eval, 能夠在運行時, 把一段字元串, 當成代碼執行, 並且可以讀取和修改當前環境中綁定的變數. 具有元編程的能力, 可用於構造比較複雜的代碼.

skill#8: 使用perl正則

shell不夠, perl來湊; perl的sysadmin功能遠強於python. 推薦使用perl.

perl正則簡約統一, onelinar足以替換sed/awk/grep.

推薦perl的另外一個原因是, perl提供了非常豐富的括弧類型, 便於寫腳本.

sed/awk/grep的正則, 屬於不同的dialect, 需要查看或者記憶三套規則, 容易混淆. 複雜的正則表達式, 拼正確往往需要化費一定時間. 簡單場景, 比如搜索一個完整的單詞, 用sed/awk/grep無妨, 複雜的正則表示, 建議用perl re.

sed和perl的對應關係

# E0表示最後一行sed 1,$!d dat.txtperl -lne print if /1..E0/# re addr rangesed /begin/,/end/!d dat.txtperl -lne print if /begin/../end/# substitutionsed /begin/,/end/s/foobar/Foobar/g dat.txtperl -lpe s/foobar/Foobar/g if/begin/../end/ dat.txt# in-place substitution sed -i /begin/,/end/s/foobar/Foobar/g dat.txtperl -i -lpe s/foobar/Foobar/g if/begin/../end/ dat.txt

awk和perl的對應關係

awk {print $1} dat.txtperl -aF -lne print $F[0] dat.txtawk -F: {print $1} dat.txtperl -aF: -lne print $F[0] dat.txtperl -aF[;,s]+ print $F[0] dat.txt #用正則/[;,s]+/分割字元串

grep和perl的對應關係

grep word dat.txtperl -lne print if /word/ dat.txtgrep -Rin foobar *find -type f |xargs -i perl -lne print if /foobar/i {}

perl的其他舉例

提取email地址perl -lne print $1 if /(S+@w+(.w+)*)/ foobar.html批量修改文件名find -type f |perl -lne chomp;rename $_=>"$_.bak"find -name "*.bak" |perl -lne chomp;rename $_=>$1 if /^(.*).bak$/批量替換字元串find -name "*.cpp" -type f |xargs -i perl -i.bak -lpe s/0xdeadbeef/0XDEADBEEF/g {}

skill#9: 獲取含特定關鍵字的java進程pid的數組

# DataNode相關java進程pid存入positional variablesset -- $(ps h -C java -o pid,cmd | perl -ne print $1 if /^s+(d+).*DataNode/)for p in $*;do echo $pdone

用positional variables捕獲數組, 使用$* $@ $# $n shift操作數組, 比較方便.

雖然declare -a也可定義數組, 但難以記憶, 容易出錯.

skill#10: 統計日誌中某些詞出現的頻率

# 假設日誌中包含包含"2018-10-08 12:00:00.345 [INFO/WARN/FATAL]..."信息, 統計INFO, WARN, FATAL的出現次數.perl -lne $h{$1}++ if /^d{4}-d{2}-d{2}s+d{2}:d{2}:d{2}.d{3}s+[(w+)/}{print join "
", map{"$_:$h{$_}"} keys %h
log

skill#11: here doc

不允許{backslash, $variable, $(cmd), $((expr))} interpolation, 邊界詞DONE用單引號

cat <<DONE....DONE

允許{backslash, $variable, $(cmd), $((expr))} interpolation, 邊界詞DONE用雙引號或者裸詞.

cat <<"DONE"....DONEcat <<DONE....DONE

trim每一行前置的空白符用<<-

cat <<-DONE one two threeDONE

skill#12: 寫函數其實很方便

return 0/1表示執行成功/失敗, 函數用標準輸出流返回結果, 使用$()提取返回值.

函數 abs_path

# 函數 abs_pathabs_path(){ #函數名字為abs_path usage="abs_path <path>" # 參數傳遞用positional variables local p=${1:?"undefined path: $usage"};shift #用local避免污染全局環境變數 if [ -f $p ];then p=$(cd $(dirname $p);pwd)/$(basename $p) elif [ -d $p ];then p=$(cd $p;pwd) else # 錯誤返回1, 輸出到標準錯誤流 echo "error: $p is missing or is not a file/directory" >&2 return 1 fi # 成功返回0, 輸出掉標準輸出流 echo $p return 0}# 調用函數abs_path, 返回結果保存在cwd中.cwd=$(abs_path .)

過程 add_bridge和del_bridge

add_bridge(){ local usage="add_bridge <bridge-name> <subnet>" local bridge=${1:?"undefined <bridge-name>: $usage"};shift local subnet=${1:?"undefined <subnet>: $usage"};shift del_bridge $bridge ip link add $bridge type bridge ip link set dev $bridge up return 0}del_bridge(){ local usage="del_bridge <bridge-name>" local bridge=${1:?"undefined <bridge-name>:$usage"};shift if ip link list | grep "<$bridge>" >/dev/null 2>&1;then ip link set dev $bridge down ip link delete dev $bridge fi return 0}

skill#13: 死循環用colon(:)

while : ;do t=$(($RANDOM%10+1)); echo sleep $t secs; sleep $t;done

skill#14: 字元串比較

# 判斷字元串是否為空[ -z $s ] #錯誤[ -z "$s" ] #正確# 判斷字元串是否為不空[ -n $s ] #錯誤[ -n "$s" ] #正確# 判斷字元串是否相等[ $s = "OK" ] #錯誤[ x$sx = "xOKx" ] #錯誤[ "x${s}x = "xOKx" ] #正確

skill#15: 判斷字元串是否為合法IP地址

ip="192.168.1.1"ill_formed=$(echo $ip|perl -lne print "ill-formed" unless /^d{1,3}(.d{1,3}){3}$/if [ -z "${ill_formed}" ];then echo "match"else echo "not match"fi

skill#16: 判對一組token是否包含某一個詞

#!/bin/bashcontains(){ local usage="Usage: contains <w> <elm0> <elm1> ..." local w=${1:?"undefined w, ${usage} "};shift if [ "$#" -eq 0 ];then echo "Error: missing arguments, ${usage}" >&2 return 1 fi perl -e "@h{qw/$*/}=(1)x$#;print $h{qq/$w/}" return 0}contains $*

(未完待續...)


推薦閱讀:
查看原文 >>
相關文章