背景
使用哪一種Shell
Bash是唯一被允許執(zhí)行的shell腳本語言墨榄。
可執(zhí)行文件必須以 #!/bin/bash 和最小數(shù)量的標志開始。請使用 set 來設(shè)置shell的選項疼约,使得用 bash <script_name> 調(diào)用你的腳本時不會破壞其功能。
限制所有的可執(zhí)行shell腳本為bash使得我們安裝在所有計算機中的shell語言保持一致性。
無論你是為什么而編碼,對此唯一例外的是當你被迫時可以不這么做的似舵。其中一個例子是Solaris SVR4包,編寫任何腳本都需要用純Bourne shell葱峡。
什么時候使用Shell
Shell應(yīng)該僅僅被用于小功能或者簡單的包裝腳本砚哗。
盡管Shell腳本不是一種開發(fā)語言,但在整個谷歌它被用于編寫多種實用工具的腳本族沃。這個風(fēng)格指南更多的是認同它的使用频祝,而不是一個建議泌参,即它可被用于廣泛部署脆淹。
以下是一些準則:
如果你主要是在調(diào)用其他的工具并且做一些相對很小數(shù)據(jù)量的操作,那么使用shell來完成任務(wù)是一種可接受的選擇沽一。
如果你在乎性能盖溺,那么請選擇其他工具,而不是使用shell铣缠。
如果你發(fā)現(xiàn)你需要使用數(shù)據(jù)而不是變量賦值(如 ${PHPESTATUS} )烘嘱,那么你應(yīng)該使用Python腳本昆禽。
如果你將要編寫的腳本會超過100行,那么你可能應(yīng)該使用Python來編寫蝇庭,而不是Shell醉鳖。請記住,當腳本行數(shù)增加哮内,盡早使用另外一種語言重寫你的腳本盗棵,以避免之后花更多的時間來重寫。
Shell文件和解釋器調(diào)用
文件擴展名
可執(zhí)行文件應(yīng)該沒有擴展名(強烈建議)或者使用.sh擴展名北发。庫文件必須使用.sh作為擴展名纹因,而且應(yīng)該是不可執(zhí)行的。
當執(zhí)行一個程序時琳拨,并不需要知道它是用什么語言編寫的瞭恰。而且shell腳本也不要求有擴展名。所以我們更喜歡可執(zhí)行文件沒有擴展名狱庇。
然而惊畏,對于庫文件,知道其用什么語言編寫的是很重要的密任,有時候會需要使用不同語言編寫的相似的庫文件陕截。使用.sh這樣特定語言后綴作為擴展名,就使得用不同語言編寫的具有相同功能的庫文件可以采用一樣的名稱批什。
SUID / SGID
SUID(Set User ID)和SGID(Set Group ID)在shell腳本中是被禁止的农曲。
shell存在太多的安全問題,以致于如果允許SUID/SGID會使得shell幾乎不可能足夠安全驻债。雖然bash使得運行SUID非常困難乳规,但在某些平臺上仍然有可能運行,這就是為什么我們明確提出要禁止它合呐。
如果你需要較高權(quán)限的訪問請使用 sudo 暮的。
環(huán)境
STDOUT vs STDERR
所有的錯誤信息都應(yīng)該被導(dǎo)向STDERR。
這使得從實際問題中分離出正常狀態(tài)變得更容易淌实。
推薦使用類似如下函數(shù)冻辩,將錯誤信息和其他狀態(tài)信息一起打印出來。
err() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2
}
if ! do_something; then
err "Unable to do_something"
exit "${E_DID_NOTHING}"
fi
注釋
文件頭
每個文件的開頭是其文件內(nèi)容的描述拆祈。
每個文件必須包含一個頂層注釋恨闪,對其內(nèi)容進行簡要概述。版權(quán)聲明和作者信息是可選的放坏。
例如:
#!/bin/bash
#
# Perform hot backups of Oracle databases.
功能注釋
任何不是既明顯又短的函數(shù)都必須被注釋咙咽。任何庫函數(shù)無論其長短和復(fù)雜性都必須被注釋。
其他人通過閱讀注釋(和幫助信息淤年,如果有的話)就能夠?qū)W會如何使用你的程序或庫函數(shù)钧敞,而不需要閱讀代碼蜡豹。
所有的函數(shù)注釋應(yīng)該包含:
函數(shù)的描述
全局變量的使用和修改
使用的參數(shù)說明
返回值,而不是上一條命令運行后默認的退出狀態(tài)
例如:
#!/bin/bash
#
# Perform hot backups of Oracle databases.
export PATH='/usr/xpg4/bin:/usr/bin:/opt/csw/bin:/opt/goog/bin'
#######################################
# Cleanup files from the backup dir
# Globals:
# BACKUP_DIR
# ORACLE_SID
# Arguments:
# None
# Returns:
# None
#######################################
cleanup() {
...
}
實現(xiàn)部分的注釋
注釋你代碼中含有技巧溉苛、不明顯镜廉、有趣的或者重要的部分。
這部分遵循谷歌代碼注釋的通用做法愚战。不要注釋所有代碼桨吊。如果有一個復(fù)雜的算法或者你正在做一些與眾不同的,放一個簡單的注釋凤巨。
TODO注釋
使用TODO注釋臨時的视乐、短期解決方案的、或者足夠好但不夠完美的代碼敢茁。
這與C++指南中的約定相一致佑淀。
TODOs應(yīng)該包含全部大寫的字符串TODO,接著是括號中你的用戶名彰檬。冒號是可選的伸刃。最好在TODO條目之后加上 bug或者ticket 的序號。
例如:
# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)
格式
縮進
縮進兩個空格逢倍,沒有制表符捧颅。
在代碼塊之間請使用空行以提升可讀性〗系瘢縮進為兩個空格碉哑。無論你做什么,請不要使用制表符亮蒋。對于已有文件扣典,保持已有的縮進格式。
行的長度和長字符串
行的最大長度為80個字符慎玖。
如果你必須寫長度超過80個字符的字符串贮尖,如果可能的話,盡量使用here document或者嵌入的換行符趁怔。長度超過80個字符的文字串且不能被合理地分割湿硝,這是正常的。但強烈建議找到一個方法使其變短润努。
# DO use 'here document's
cat <<END;
I am an exceptionally long
string.
END
# Embedded newlines are ok too
long_string="I am an exceptionally
long string."
管道
如果一行容不下整個管道操作关斜,那么請將整個管道操作分割成每行一個管段。
如果一行容得下整個管道操作任连,那么請將整個管道操作寫在同一行蚤吹。
否則例诀,應(yīng)該將整個管道操作分割成每行一個管段随抠,管道操作的下一部分應(yīng)該將管道符放在新行并且縮進2個空格裁着。這適用于使用管道符’|’的合并命令鏈以及使用’||’和’&&’的邏輯運算鏈。
# All fits on one line
command1 | command2
# Long commands
command1 \
| command2 \
| command3 \
| command4
循環(huán)
請將 ; do , ; then 和 while , for , if 放在同一行拱她。
shell中的循環(huán)略有不同二驰,但是我們遵循跟聲明函數(shù)時的大括號相同的原則。也就是說秉沼, ; do , ; then 應(yīng)該和 if/for/while 放在同一行桶雀。 else 應(yīng)該單獨一行,結(jié)束語句應(yīng)該單獨一行并且跟開始語句垂直對齊唬复。
例如:
for dir in ${dirs_to_cleanup}; do
if [[ -d "${dir}/${ORACLE_SID}" ]]; then
log_date "Cleaning up old files in ${dir}/${ORACLE_SID}"
rm "${dir}/${ORACLE_SID}/"*
if [[ "$?" -ne 0 ]]; then
error_message
fi
else
mkdir -p "${dir}/${ORACLE_SID}"
if [[ "$?" -ne 0 ]]; then
error_message
fi
fi
done
case語句
通過2個空格縮進可選項矗积。
在同一行可選項的模式右圓括號之后和結(jié)束符 ;; 之前各需要一個空格。
長可選項或者多命令可選項應(yīng)該被拆分成多行敞咧,模式棘捣、操作和結(jié)束符 ;; 在不同的行。
匹配表達式比 case 和 esac 縮進一級休建。多行操作要再縮進一級乍恐。一般情況下,不需要引用匹配表達式测砂。模式表達式前面不應(yīng)該出現(xiàn)左括號茵烈。避免使用 ;& 和 ;;& 符號。
case "${expression}" in
a)
variable="..."
some_command "${variable}" "${other_expr}" ...
;;
absolute)
actions="relative"
another_command "${actions}" "${other_expr}" ...
;;
*)
error "Unexpected expression '${expression}'"
;;
esac
只要整個表達式可讀砌些,簡單的命令可以跟模式和 ;; 寫在同一行呜投。這通常適用于單字母選項的處理。當單行容不下操作時存璃,請將模式單獨放一行宙彪,然后是操作,最后結(jié)束符 ;; 也單獨一行有巧。當操作在同一行時释漆,模式的右括號之后和結(jié)束符 ;; 之前請使用一個空格分隔。
verbose='false'
aflag=''
bflag=''
files=''
while getopts 'abf:v' flag; do
case "${flag}" in
a) aflag='true' ;;
b) bflag='true' ;;
f) files="${OPTARG}" ;;
v) verbose='true' ;;
*) error "Unexpected option ${flag}" ;;
esac
done
變量擴展
按優(yōu)先級順序:保持跟你所發(fā)現(xiàn)的一致篮迎;引用你的變量男图;推薦用 var ,詳細解釋如下甜橱。
這些僅僅是指南逊笆,因為作為強制規(guī)定似乎飽受爭議。
以下按照優(yōu)先順序列出岂傲。
與現(xiàn)存代碼中你所發(fā)現(xiàn)的保持一致难裆。
引用變量參閱下面一節(jié),引用。
除非絕對必要或者為了避免深深的困惑乃戈,否則不要用大括號將單個字符的shell特殊變量或定位變量括起來褂痰。推薦將其他所有變量用大括號括起來。
# Section of recommended cases.
# Preferred style for 'special' variables:
echo "Positional: $1" "$5" "$3"
echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..."
# Braces necessary:
echo "many parameters: ${10}"
# Braces avoiding confusion:
# Output is "a0b0c0"
set -- a b c
echo "${1}0${2}0${3}0"
# Preferred style for other variables:
echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
while read f; do
echo "file=${f}"
done < <(ls -l /tmp)
# Section of discouraged cases
# Unquoted vars, unbraced vars, brace-quoted single letter
# shell specials.
echo a=$avar "b=$bvar" "PID=${$}" "${1}"
# Confusing use: this is expanded as "${1}0${2}0${3}0",
# not "${10}${20}${30}
set -- a b c
echo "$10$20$30"
引用
除非需要小心不帶引用的擴展症虑,否則總是引用包含變量缩歪、命令替換符、空格或shell元字符的字符串谍憔。
推薦引用是單詞的字符串(而不是命令選項或者路徑名)匪蝙。
千萬不要引用整數(shù)。
注意 [[ 中模式匹配的引用規(guī)則习贫。
請使用 * 逛球。
# 'Single' quotes indicate that no substitution is desired.
# "Double" quotes indicate that substitution is required/tolerated.
# Simple examples
# "quote command substitutions"
flag="$(some_command and its args "$@" 'quoted separately')"
# "quote variables"
echo "${flag}"
# "never quote literal integers"
value=32
# "quote command substitutions", even when you expect integers
number="$(generate_number)"
# "prefer quoting words", not compulsory
readonly USE_INTEGER='true'
# "quote shell meta characters"
echo 'Hello stranger, and well met. Earn lots of $$$'
echo "Process $$: Done making \$\$\$."
# "command options or path names"
# ($1 is assumed to contain a value here)
grep -li Hugo /dev/null "$1"
# Less simple examples
# "quote variables, unless proven false": ccs might be empty
git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"}
# Positional parameter precautions: $1 might be unset
# Single quotes leave regex as-is.
grep -cP '([Ss]pecial|\|?characters*)$' ${1:+"$1"}
# For passing on arguments,
# "$@" is right almost everytime, and
# $* is wrong almost everytime:
#
# * $* and $@ will split on spaces, clobbering up arguments
# that contain spaces and dropping empty strings;
# * "$@" will retain arguments as-is, so no args
# provided will result in no args being passed on;
# This is in most cases what you want to use for passing
# on arguments.
# * "$*" expands to one argument, with all args joined
# by (usually) spaces,
# so no args provided will result in one empty string
# being passed on.
# (Consult 'man bash' for the nit-grits ;-)
set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$*"; echo "$#, $@")
set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$@"; echo "$#, $@")
特性及錯誤
命令替換
使用 $(command) 而不是反引號。
嵌套的反引號要求用反斜杠轉(zhuǎn)義內(nèi)部的反引號苫昌。而 $(command) 形式嵌套時不需要改變需忿,而且更易于閱讀。
例如:
# This is preferred:
var="$(command "$(command1)")"
# This is not:
var="`command \`command1\``"
test蜡歹,[和[[
推薦使用 [[ ... ]] 屋厘,而不是 [ , test , 和 /usr/bin/ [ 。
因為在 [[ 和 ]] 之間不會有路徑名稱擴展或單詞分割發(fā)生月而,所以使用 [[ ... ]] 能夠減少錯誤汗洒。而且 [[ ... ]] 允許正則表達式匹配,而 [ ... ] 不允許父款。
# This ensures the string on the left is made up of characters in the
# alnum character class followed by the string name.
# Note that the RHS should not be quoted here.
# For the gory details, see
# E14 at http://tiswww.case.edu/php/chet/bash/FAQ
if [[ "filename" =~ ^[[:alnum:]]+name ]]; then
echo "Match"
fi
# This matches the exact pattern "f*" (Does not match in this case)
if [[ "filename" == "f*" ]]; then
echo "Match"
fi
# This gives a "too many arguments" error as f* is expanded to the
# contents of the current directory
if [ "filename" == f* ]; then
echo "Match"
fi
測試字符串
盡可能使用引用溢谤,而不是過濾字符串。
Bash足以在測試中處理空字符串憨攒。所以世杀,請使用空(非空)字符串測試,而不是過濾字符肝集,使得代碼更易于閱讀恳守。
# Do this:
if [[ "${my_var}" = "some_string" ]]; then
do_something
fi
# -z (string length is zero) and -n (string length is not zero) are
# preferred over testing for an empty string
if [[ -z "${my_var}" ]]; then
do_something
fi
# This is OK (ensure quotes on the empty side), but not preferred:
if [[ "${my_var}" = "" ]]; then
do_something
fi
# Not this:
if [[ "${my_var}X" = "some_stringX" ]]; then
do_something
fi
為了避免對你測試的目的產(chǎn)生困惑秉扑,請明確使用-z
或者-n
# Use this
if [[ -n "${my_var}" ]]; then
do_something
fi
# Instead of this as errors can occur if ${my_var} expands to a test
# flag
if [[ "${my_var}" ]]; then
do_something
fi```
**文件名的通配符擴展**
當進行文件名的通配符擴展時派敷,請使用明確的路徑嫡霞。
因為文件名可能以 - 開頭浮创,所以使用擴展通配符 ./* 比 * 來得安全得多斩披。
Here's the contents of the directory:
-f -r somedir somefile
This deletes almost everything in the directory by force
psa@bilby$ rm -v *
removed directory: somedir' removed
somefile'
As opposed to:
psa@bilby$ rm -v ./*
removed ./-f' removed
./-r'
rm: cannot remove ./somedir': Is a directory removed
./somefile'
Eval
應(yīng)該避免使用eval。
當用于給變量賦值時煌抒,Eval解析輸入摧玫,并且能夠設(shè)置變量绑青,但無法檢查這些變量是什么。
# What does this set?
# Did it succeed? In part or whole?
eval $(set_my_variables)
# What happens if one of the returned values has a space in it?
variable="$(eval some_function)"
管道導(dǎo)向while循環(huán)
請使用過程替換或者for循環(huán)邪乍,而不是管道導(dǎo)向while循環(huán)庇楞。在while循環(huán)中被修改的變量是不能傳遞給父shell的吕晌,因為循環(huán)命令是在一個子shell中運行的睛驳。
管道導(dǎo)向while循環(huán)中的隱式子shell使得追蹤bug變得很困難膜廊。
last_line='NULL'
your_command | while read line; do
last_line="${line}"
done
# This will output 'NULL'
echo "${last_line}"
如果你確定輸入中不包含空格或者特殊符號(通常意味著不是用戶輸入的)蹬跃,那么可以使用一個for循環(huán)炬转。
total=0
# Only do this if there are no spaces in return values.
for value in $(command); do
total+="${value}"
done
使用過程替換允許重定向輸出扼劈,但是請將命令放入一個顯式的子shell中荐吵,而不是bash為while循環(huán)創(chuàng)建的隱式子shell先煎。
total=0
last_file=
while read count filename; do
total+="${count}"
last_file="${filename}"
done < <(your_command | uniq -c)
# This will output the second field of the last line of output from
# the command.
echo "Total = ${total}"
echo "Last one = ${last_file}"
當不需要傳遞復(fù)雜的結(jié)果給父shell時可以使用while循環(huán)遥倦。這通常需要一些更復(fù)雜的“解析”袒哥。請注意簡單的例子使用如awk這類工具可能更容易完成堡称。當你特別不希望改變父shell的范圍變量時這可能也是有用的却紧。
# Trivial implementation of awk expression:
# awk '$3 == "nfs" { print $2 " maps to " $1 }' /proc/mounts
cat /proc/mounts | while read src dest type opts rest; do
if [[ ${type} == "nfs" ]]; then
echo "NFS ${dest} maps to ${src}"
fi
done
命名約定
函數(shù)名
使用小寫字母晓殊,并用下劃線分隔單詞巫俺。使用雙冒號 :: 分隔庫识藤。函數(shù)名之后必須有圓括號次伶。關(guān)鍵詞 function 是可選的冠王,但必須在一個項目中保持一致。
如果你正在寫單個函數(shù)豪娜,請用小寫字母來命名瘤载,并用下劃線分隔單詞鸣奔。如果你正在寫一個包挎狸,使用雙冒號 :: 來分隔包名锨匆。大括號必須和函數(shù)名位于同一行(就像在Google的其他語言一樣)茅主,并且函數(shù)名和圓括號之間沒有空格暗膜。
# Single function
my_func() {
...
}
# Part of a package
mypackage::my_func() {
...
}
當函數(shù)名后存在 () 時鞭衩,關(guān)鍵詞 function 是多余的。但是其促進了函數(shù)的快速辨識坯台。
變量名
如函數(shù)名蜒蕾。
循環(huán)的變量名應(yīng)該和循環(huán)的任何變量同樣命名咪啡。
for zone in ${zones}; do
something_with "${zone}"
done
常量和環(huán)境變量名
全部大寫撤摸,用下劃線分隔,聲明在文件的頂部莺掠。
常量和任何導(dǎo)出到環(huán)境中的都應(yīng)該大寫彻秆。
# Constant
readonly PATH_TO_FILES='/some/path'
# Both constant and environment
declare -xr ORACLE_SID='PROD'
第一次設(shè)置時有一些就變成了常量(例如,通過getopts)膀估。因此察纯,可以在getopts中或基于條件來設(shè)定常量针肥,但之后應(yīng)該立即設(shè)置其為只讀饼记。值得注意的是,在函數(shù)中 declare 不會對全局變量進行操作慰枕。所以推薦使用 readonly 和 export 來代替具则。
VERBOSE='false'
while getopts 'v' flag; do
case "${flag}" in
v) VERBOSE='true' ;;
esac
done
readonly VERBOSE
源文件名
小寫,如果需要的話使用下劃線分隔單詞具帮。
這是為了和在Google中的其他代碼風(fēng)格保持一致: maketemplate 或者 make_template 博肋,而不是 make-template 。
只讀變量
使用 readonly 或者 declare -r 來確保變量只讀蜂厅。
因為全局變量在shell中廣泛使用匪凡,所以在使用它們的過程中捕獲錯誤是很重要的掘猿。當你聲明了一個變量,希望其只讀唧龄,那么請明確指出。
zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)"
if [[ -z "${zip_version}" ]]; then
error_message
else
readonly zip_version
fi
使用本地變量
使用 local 聲明特定功能的變量胖烛。聲明和賦值應(yīng)該在不同行。
使用 local 來聲明局部變量以確保其只在函數(shù)內(nèi)部和子函數(shù)中可見。這避免了污染全局命名空間和不經(jīng)意間設(shè)置可能具有函數(shù)之外重要性的變量绍弟。
當賦值的值由命令替換提供時年碘,聲明和賦值必須分開涤久。因為內(nèi)建的 local 不會從命令替換中傳遞退出碼蔗彤。
my_func2() {
local name="$1"
# Separate lines for declaration and assignment:
local my_var
my_var="$(my_func)" || return
# DO NOT do this: $? contains the exit code of 'local', not my_func
local my_var="$(my_func)"
[[ $? -eq 0 ]] || return
...
}
**函數(shù)位置**
將文件中所有的函數(shù)一起放在常量下面。不要在函數(shù)之間隱藏可執(zhí)行代碼秧倾。
如果你有函數(shù)售淡,請將他們一起放在文件頭部鹤啡。只有includes, set 聲明和常量設(shè)置可能在函數(shù)聲明之前完成。不要在函數(shù)之間隱藏可執(zhí)行代碼运杭。如果那樣做熊榛,會使得代碼在調(diào)試時難以跟蹤并出現(xiàn)意想不到的討厭結(jié)果云挟。
**主函數(shù)main**
對于包含至少一個其他函數(shù)的足夠長的腳本沸枯,需要稱為 main 的函數(shù)。
為了方便查找程序的開始赂弓,將主程序放入一個稱為 main 的函數(shù)绑榴,作為最下面的函數(shù)杨耙。這使其和代碼庫的其余部分保持一致性堪遂,同時允許你定義更多變量為局部變量(如果主代碼不是一個函數(shù)就不能這么做)。文件中最后的非注釋行應(yīng)該是對 main 函數(shù)的調(diào)用柬祠。
main "$@"
顯然漫蛔,對于僅僅是線性流的短腳本嗜愈, main 是矯枉過正,因此是不需要的莽龟。
# 調(diào)用命令
**檢查返回值**
總是檢查返回值蠕嫁,并給出信息返回值。
對于非管道命令毯盈,使用 $? 或直接通過一個 if 語句來檢查以保持其簡潔拌阴。
例如:
if ! mv "{dest_dir}/" ; then
echo "Unable to move {dest_dir}" >&2
exit "${E_BAD_MOVE}"
fi
Or
mv "{dest_dir}/"
if [[ "{file_list} to
{E_BAD_MOVE}"
fi
Bash也有 PIPESTATUS 變量,允許檢查從管道所有部分返回的代碼奶镶。如果僅僅需要檢查整個管道是成功還是失敗迟赃,以下的方法是可以接受的:
tar -cf - ./* | ( cd "{PIPESTATUS[0]}" -ne 0 || "
{dir}" >&2
fi
可是,只要你運行任何其他命令厂镇, PIPESTATUS 將會被覆蓋纤壁。如果你需要基于管道中發(fā)生的錯誤執(zhí)行不同的操作,那么你需要在運行命令后立即將 PIPESTATUS 賦值給另一個變量(別忘了 [ 是一個會將 PIPESTATUS 擦除的命令)捺信。
tar -cf - ./* | ( cd "{PIPESTATUS[*]})
if [[ "{return_codes[1]}" -ne 0 ]]; then
do_something_else
fi
**內(nèi)建命令和外部命令**
可以在調(diào)用shell內(nèi)建命令和調(diào)用另外的程序之間選擇酌媒,請選擇內(nèi)建命令。
我們更喜歡使用內(nèi)建命令迄靠,如在 bash(1) 中參數(shù)擴展函數(shù)秒咨。因為它更強健和便攜(尤其是跟像 sed這樣的命令比較)
例如:
Prefer this:
addition={X} +
{string/#foo/bar}"
Instead of this:
addition="{X} +
(echo "${string}" | sed -e 's/^foo/bar/')"
# 結(jié)論
使用常識并保持一致。希望讀到這的您能轉(zhuǎn)發(fā)分享和關(guān)注一下我掌挚,以后還會更新技術(shù)干貨雨席,謝謝您的支持!