對(duì)話 UNIX: 更多 shell 腳本技術(shù)
盡管在近兩年使用過 Unix 的一些人可能嘗試過 shell 腳本編程,但是他們很可能只是研究操作系統(tǒng)的細(xì)節(jié),并不精通 shell 腳本編程。本文針對(duì)那些希望進(jìn)一步了解 shell 腳本,并開始編寫更高級(jí)腳本的讀者。本文提供腳本編程的基礎(chǔ)知識(shí),包括如何簡(jiǎn)化腳本、如何盡可能保持腳本的靈活性、如何編寫干凈的腳本、在腳本內(nèi)編寫注釋以及調(diào)試腳本。
保持簡(jiǎn)單
在人們學(xué)習(xí)如何編寫 shell 腳本時(shí),常常遇到的一個(gè)問題是,重復(fù)他們?cè)诹硪粋€(gè)腳本中已經(jīng)做過的工作。他們其實(shí)不需要復(fù)制原來的腳本并修改幾個(gè)硬編碼值,只需創(chuàng)建一個(gè)函數(shù)來處理兩個(gè)腳本的重復(fù)部分。創(chuàng)建集中的函數(shù)還可以促進(jìn)標(biāo)準(zhǔn)化,幫助創(chuàng)建統(tǒng)一的腳本。如果一個(gè)函數(shù)在腳本的一個(gè)部分工作正常,那么它在腳本中的其他地方也會(huì)正常工作。
例如,清單 1 所示的腳本應(yīng)該濃縮和簡(jiǎn)化為更簡(jiǎn)單、更干凈的程序。
清單 1. 可以簡(jiǎn)化的腳本示例
#!/usr/bin/kshif [[ $# -lt 2 ]]then echo "Usage: ${0##*/} <file name #1> <file name #2> exit 0fiif [[ ! -f "${1}" ]]then echo "Unable to find file '${1}'" exit 1fiif [[ ! -r "${1}" ]]then echo "Unable to read file '${1}'" exit 2figzip ${1}ls -l ${1}.gzif [[ ! -f "${2}" ]]then echo "Unable to find file '${2}'" exit 1fiif [[ ! -r "${2}" ]]then echo "Unable to read file '${2}'" exit 2figzip ${2}ls -l ${2}.gz
這個(gè)腳本看起來很糟糕!(謝天謝地,它只是一個(gè)示例)。這個(gè)腳本應(yīng)該盡可能進(jìn)行濃縮。從便于閱讀的角度來看,清單 2 提供的版本更干凈。
清單 2. 對(duì)清單 1 腳本進(jìn)行濃縮的版本
#!/usr/bin/kshexit_msg() { [[ $# -gt 1 ]] && echo "${0##*/} (${1}) - ${2}" exit ${1:-0}}[[ $# -lt 2 ]] && exit_msg 0 "Usage: ${0##*/} <file name #1> <file name #2>for _FNAME in $@do [[ ! -f "${_FNAME}" ]] && exit_msg 1 "Unable to find file '${_FNAME}'" [[ ! -r "${_FNAME}" ]] && exit_msg 2 "Unable to read file '${_FNAME}'" gzip ${_FNAME} ls -l ${_FNAME}.gzdone
注意到這兩者的差異了嗎?這個(gè)腳本增加了一個(gè)簡(jiǎn)單的函數(shù)來顯示一個(gè)消息并帶適當(dāng)?shù)姆祷卮a退出,還把所有操作轉(zhuǎn)移到一個(gè) for 循環(huán)中,這使這個(gè)腳本看起來更干凈、更容易理解了。
保持靈活性
編程和 shell 腳本編程的新手常常犯的另一個(gè)錯(cuò)誤是,在程序或 shell 腳本中對(duì)靜態(tài)值進(jìn)行硬編碼。這會(huì)限制腳本的靈活性,是一種糟糕的編程習(xí)慣。這迫使管理員或開發(fā)人員不得不經(jīng)常修改腳本以使用其他值;為了避免這個(gè)問題,應(yīng)該使用變量并為腳本或函數(shù)提供參數(shù)。
例如,清單 3 是一個(gè)編寫得很差的不靈活的示例腳本。
清單 3. 不靈活的示例腳本
#!/bin/bashif [[ -f /home/cormany/FileA ]]then echo "Found file '/home/cormany/FileA'"elif [[ -f /home/cormany/DirA/FileA ]]then echo "Found file '/home/cormany/DirA/FileA'"else echo "Unable to find file FileA"fi
這個(gè)腳本可以正常工作,但是它只能在兩個(gè)位置搜索一個(gè)文件。
清單 4 提供相同的功能,但是允許用戶在任何位置搜索任何文件。
清單 4. 使腳本更靈活
#!/bin/bashexit_msg() { [[ $# -gt 1 ]] && echo "${0##*/} (${1}) - ${2}" exit ${1:-0}}[[ $# -lt 2 ]] && exit_msg 1 "Usage: ${0##*/} <file name> <location>"_FNAME="${1}"_DNAME="${2}"[[ ! -d "${_DNAME}" ]] && exit_msg 2 "Unable to read or find Directory '${_DNAME}'"if [[ -f "${_DNAME}/${_FNAME}" ]]then exit_msg 0 "Found file '${_DNAME}/${_FNAME}'"else exit_msg 3 "Unable to find file '${_DNAME}/${_FNAME}'"fi
這個(gè)腳本更靈活,因?yàn)樗试S用戶指定要搜索的任何文件和任何搜索目錄。
提供選項(xiàng)
在編寫一個(gè) shell 腳本時(shí),一些用戶可能會(huì)說,“它真不錯(cuò)! 或者 “我喜歡使用它;而同時(shí),其他用戶可能不同意這個(gè)評(píng)價(jià),他們可能不希望執(zhí)行相同的操作。人們喜歡有選擇,為什么不給他們提供選項(xiàng)呢??jī)?nèi)置的 shell 命令 getopt 可以完成這個(gè)任務(wù)。
清單 5 提供一個(gè)在 AIX 中使用 getopt 的基本示例。
清單 5. getopt 示例
#!/usr/bin/ksh_ARGS=`getopt -o x --long xxxxx -n ${0##*/} -- "$@"`while [[ $# -gt 0 ]]do case "${1}" in-x|--xxxxx) echo "Arg x hit!"shift;;--) shift; break;; *) echo "Invalid Option: ${1}"break;; esacdone
在執(zhí)行包含 getopt(稱為 opttest)的腳本時(shí),如果在 -x 或 --xxxxx 中使用有效的參數(shù),getopt 會(huì)識(shí)別出開關(guān)并執(zhí)行 case 開關(guān)中的代碼:
# ./hm -xArg x hit!
下面是使用無效開關(guān)或選項(xiàng)時(shí)的結(jié)果:
# ./hm -aInvalid Option: -a
文檔,文檔,文檔
我們?cè)诼殬I(yè)生涯中早晚會(huì)受到這個(gè)問題的困擾。老板要求您看看一個(gè) 10 年前編寫的腳本,它的作者已經(jīng)不再為公司工作了。您會(huì)說 “沒問題 嗎?通常情況下,可能沒問題;但是,如果這個(gè)腳本很復(fù)雜,執(zhí)行了您不習(xí)慣使用的命令,采用的編寫風(fēng)格與您的風(fēng)格不一樣,或者干脆就不能正常工作,您就遇到大麻煩了。在這種情況下,一些反映作者當(dāng)初編寫這個(gè)腳本時(shí)的想法的提示會(huì)有很大的幫助。有時(shí)候,您開發(fā)了一個(gè)自認(rèn)為只使用一次的腳本,但是以后卻發(fā)現(xiàn)還需要修改它。或者,您用幾星期時(shí)間編寫了一個(gè)巨大的腳本,您了解這個(gè)腳本的所有細(xì)節(jié),但是如果別人閱讀它,卻不知所云。這幾種情況說明,文檔之于開發(fā)人員就像腳本之于用戶,都非常重要。
看看清單 6 所示的函數(shù)。
清單 6. 沒有注釋的腳本示例
confirm_and_exit() { [[ ${_DEBUG_LEVEL} -ge 3 ]] && set -x while [[ -z ${_EXIT_ANS} ]] docup_echo "Are you sure you want to exit? [Y/N] c" ${_PROMPT_ERR_ROW} ${_PROMPT_ERR_COL}${_TPUT_CMD} cnormread ${_NO_EOL_FLAG:+${_READ_FLAG:-'-n'}} ${_NO_EOL_FLAG} _EXIT_ANS${_TPUT_CMD} civis done case ${_EXIT_ANS} in[Nn]) unset _EXIT_ANS; return 0;;[Yy]) exit_msg 0 1 "Exiting Script";; *) invalid_selection ${_EXIT_ANS}; unset _EXIT_ANS;; esac return 0}
如果您有比較豐富的 shell 腳本編程經(jīng)驗(yàn),可能能夠讀懂這個(gè)腳本。但是,腳本編程的初學(xué)者很難理解這個(gè)函數(shù)的作用。如果花上幾分鐘在這個(gè)腳本中添加注釋,情況就大不一樣了。清單 7 給出包含注釋的同一個(gè)函數(shù)。
清單 7. 包含注釋的腳本示例
########################################## function confirm_and_exit#########################################confirm_and_exit() { # if the debug level is set to 3 or higher, send every evaluated line to stdout [[ ${_DEBUG_LEVEL} -ge 3 ]] && set –x # Continue to prompt the user until they provide a valid answer while [[ -z ${_EXIT_ANS} ]] do# prompt user if they want to exit the script# cup_echo function calls tput cup <x> <y># syntax:# cup_echo <string to display> <row on stdout to display><column on stdout to display>cup_echo "Are you sure you want to exit? [Y/N] c" ${_PROMPT_ERR_ROW} ${_PROMPT_ERR_COL}# change cursor to normal via tput${_TPUT_CMD} cnorm# read value entered by user# if _NO_EOL_FLAG is supplIEd, use value of _READ_FLAG or “-n# if _NO_EOL_FLAG is supplied, use value as characters aloud on read# assign value entered by user to variable _EXIT_ANSread ${_NO_EOL_FLAG:+${_READ_FLAG:-'-n'}} ${_NO_EOL_FLAG} _EXIT_ANS# change cursor to invisible via tput${_TPUT_CMD} civis done # if user entered “n, return to previous block of code with return code 0 # if user entered “y, exit the script # if user entered anything else, execute function invalid_selection case ${_EXIT_ANS} in[Nn]) unset _EXIT_ANS; return 0;;[Yy]) exit_msg 0 1 "Exiting Script";; *) invalid_selection ${_EXIT_ANS}; unset _EXIT_ANS;; esac # exit function with return code 0 return 0}
對(duì)于這么小的函數(shù),這似乎太麻煩了,甚至有點(diǎn)過分,但是對(duì)于 shell 腳本編程新手和閱讀這個(gè)函數(shù)的人員而言,注釋是非常有價(jià)值的。
在 shell 腳本中,注釋的另一個(gè)極其有幫助的用途是,解釋變量的有效值以及解釋返回碼的含義。
清單 8 中的示例取自一個(gè) shell 腳本的開頭。
清單 8. 未加注釋的變量示例
#!/usr/bin/bashtrap 'exit_msg 1 0 "Signal Caught. Exiting..."' HUP INT QUIT KILL ABRTtrap 'window_size_changed' WINCH_MSG_SLEEP_TIME=3_RETNUM_SIZE=6_DEBUG_LEVEL=0_TMPDIR="/tmp"_SP_LOG="${0##*/}.log"_SP_REQUESTS="${HOME}/sp_requests"_MENU_ITEMS=15LESS="-P LINE: %l"export _SP_REQUESTS _TMPDIR _SP_LOG _DB_BACKUP_DIRexport _DEBUG_LEVEL _NEW_RMSYNC _RMTOTS_OFFSET_COL
同樣,很難理解 trap 語句的作用以及每個(gè)變量可以是哪些值。除非把整個(gè)腳本都讀一遍,否則不可能看出這些變量的意義。另外,這里沒有提到這個(gè)腳本中使用的任何返回碼。這會(huì)大大增加解決 shell 腳本問題的難度。向 清單 8 的代碼行中添加一些注釋和一個(gè)專門描述返回碼的注釋塊,這樣就可以顯著降低理解難度。看看下面的清單 9。
清單 9. 帶注釋的變量示例
#!/usr/bin/bash########################################################################## traps########################################################################## trap when a user is attempting to leave the scripttrap 'exit_msg 1 0 "Signal Caught. Exiting..."' HUP INT QUIT KILL ABRTtrap 'window_size_changed' WINCH# trap when a user has resized the window################################################################################################################################################### defined/exported variables#########################################################################_MSG_SLEEP_TIME=3 # seconds to sleep for all messages # (if not defined, default will is 1 second)_CUSTNUM_SIZE=6 # length of a customer number in this location # (if not defined, default is 6)_DEBUG_LEVEL=0 # log debug messages. log level is accumulative # (i.e. 1 = 1, 2 = 1 & 2, 3 = 1, 2, & 3) # (if not defined, default is 0) # Log levels: # 0 = No messages # 1 = brIEf messages (start script, errors, etc) # 2 = environment setup (set / env) # 3 = set -x (A LOT of spam)_TMPDIR="/tmp" # Directory to put work/tmp files # (if not defined, default is /tmp)_SP_LOG="${0##*/}.log" # log of script events_SP_REQUESTS="${HOME}/sp_requests"# file to customer record requests, # also read at startup_MENU_ITEMS=15# default number of items to display per page # (it not defined, default is 10)LESS="-P LINE: %l"# format 'less' prompt. MAN less if more info# export the variables defined aboveexport _MSG_SLEEP_TIME _CUSTNUM_SIZE _DEBUG_LEVEL _TMPDIR_SP_LOG _SP_REQUESTS _MENU_ITEMS#########################################################################
看起來好多了,不是嗎?所有東西都組織有序,并且有詳細(xì)的描述,初次閱讀這個(gè)腳本的人更容易理解它的作用。
調(diào)試
編寫完一個(gè)腳本之后,就要第一次運(yùn)行它了。但是,如果在執(zhí)行腳本時(shí)顯示某些意外的錯(cuò)誤,應(yīng)該怎么辦呢?沒有人是完美的,而且從頭編寫腳本并保持沒有錯(cuò)誤需要大量時(shí)間和豐富的經(jīng)驗(yàn);大多數(shù)時(shí)候,開發(fā)人員很容易漏掉一個(gè)字母或者顛倒了兩個(gè)字母的順序,這幾乎是不可避免的。不必?fù)?dān)心:AIX、其他風(fēng)格的 Unix 和 Linux 中的 shell 已經(jīng)考慮到了這個(gè)問題,可以幫助您進(jìn)行調(diào)試。
例如,清單 10 中的 shell 腳本(名為 make_errors)已經(jīng)編寫好等待執(zhí)行。
清單 10. 包含錯(cuò)誤的腳本示例
#!/bin/bash_X=1while [[ ${_X} -le 10 ]]do [[ ${_X} -lt 5 ]] && echo "X is less than 5! _Y=`expr ${_X) + 1` if [[ ${_Y} -eq 6 ]]echo "Y is now equal to ${_Y}" fi _X=${_Y}done
但是,初次執(zhí)行這個(gè)腳本時(shí),顯示以下錯(cuò)誤:
# ./make_errors./make_errors: line 11: unexpected EOF while looking for matching `"'./make_errors: line 16: syntax error: unexpected end of file
Vim 是一種出色的調(diào)試工具,您可能使用過它,但不一定了解它的真正價(jià)值。Vim 是一種強(qiáng)大的文本編輯器,但是它對(duì)調(diào)試也很有幫助。如果通過設(shè)置 .exrc 或 .vimrc 文件指定用不同的顏色顯示某些錯(cuò)誤,Vim 就會(huì)替您完成大部分調(diào)試工作,見圖 1。
圖 1. 用 Vim 進(jìn)行調(diào)試
第一個(gè)錯(cuò)誤消息(line 11: unexpected EOF while looking for matching `"')指出在第 11 行上有錯(cuò)誤,但是看過這一行之后,并沒有發(fā)現(xiàn)任何錯(cuò)誤。再看看第 9 行。echo 后面的字符串的末尾缺少一個(gè)雙引號(hào)(")。這個(gè)示例很好地說明了在進(jìn)行調(diào)試時(shí)為什么必須查看整個(gè)腳本。錯(cuò)誤消息中顯示的行號(hào)不一定是出現(xiàn)錯(cuò)誤的實(shí)際位置。報(bào)告第 11 行有錯(cuò)誤是因?yàn)榈?9 行用雙引號(hào)標(biāo)出一個(gè)字符串的開頭,但是這個(gè)字符串直到第 11 行還沒有結(jié)束。要想糾正這個(gè)錯(cuò)誤,應(yīng)該在第 9 行末尾添加雙引號(hào)。
其他一些問題也會(huì)顯示為錯(cuò)誤。在第 11 行上,變量值 _X 后面是一個(gè)用紅色突出顯示的后圓括號(hào)())。這是 Vim 替您做出的判斷,它指出這里有錯(cuò)誤。這里用一個(gè)前花括號(hào)({)標(biāo)出了變量值 _X 的開頭,但是沒有用后花括號(hào)(})結(jié)束。只需把 ) 改為 },就能夠糾正這個(gè)錯(cuò)誤。
到目前為止,已經(jīng)糾正了兩個(gè)錯(cuò)誤。再次運(yùn)行這個(gè)腳本,看看會(huì)發(fā)生什么:
./make_errors: line 12: syntax error near unexpected token `fi'./make_errors: line 12: ` fi'
還有另一個(gè)錯(cuò)誤。錯(cuò)誤消息指出問題出現(xiàn)在第 12 行上,但是這一行只有一個(gè)用來結(jié)束 if 語句的 fi。這有什么錯(cuò)呢?請(qǐng)牢記前一個(gè)錯(cuò)誤的情況。并非所有錯(cuò)誤都源自 shell 所報(bào)告的行上。shell 僅僅報(bào)告發(fā)生錯(cuò)誤的位置,但是錯(cuò)誤的根源可能出現(xiàn)在這個(gè)位置之前。對(duì)于這個(gè)小腳本,可以很有把握地猜測(cè)錯(cuò)誤可能出現(xiàn)在實(shí)際的 if 語句中。回憶一下基本的腳本編程邏輯:if 語句由 if、then 和 fi 組成。看看這個(gè)條件語句,可以看出缺少了 then。只需在腳本中添加 then。完成之后,這個(gè)腳本應(yīng)該類似于清單 11。
清單 11. 糾正清單 10 中的錯(cuò)誤之后的腳本
#!/bin/bash_X=1while [[ ${_X} -le 10 ]]do [[ ${_X} -lt 5 ]] && echo "X is less than 5!" _Y=`expr ${_X} + 1` if [[ ${_Y} -eq 6 ]] thenecho "Y is now equal to ${_Y}" fi _X=${_Y}done
再次運(yùn)行這個(gè)腳本:
# ./make_errorsX is less than 5!X is less than 5!X is less than 5!X is less than 5!Y is now equal to 6
恭喜!這個(gè)腳本現(xiàn)在正常工作了!
set -x 選項(xiàng)
有時(shí)候,對(duì) shell 腳本執(zhí)行基本的錯(cuò)誤排除步驟并不像前一個(gè)示例那么容易。如果所有努力都失敗了,并且想不出腳本的錯(cuò)誤之處在哪里,那么最后一招就是動(dòng)用 “殺手锏!Ksh、Bash 和其他現(xiàn)代 shell 都支持在 set 命令中使用 -x 開關(guān)。如果使用 set –x 選項(xiàng),執(zhí)行的每個(gè)命令都顯示在 stdout 中。為了突出顯示執(zhí)行的代碼,set –x 把 PS4 變量的值加在顯示的每行代碼前面。請(qǐng)記住,這種做法會(huì)產(chǎn)生大量文本,所以在查看輸出時(shí)要有耐心。
減小前一個(gè)示例中的循環(huán)計(jì)數(shù)值,在腳本的開頭添加 set -x 和一個(gè)注釋,見清單 12。
清單 12. set -x 示例
#!/bin/bashset -x# loop through and display some test statements_X=1while [[ ${_X} -le 4 ]]do [[ ${_X} -lt 2 ]] && echo "X is less than 2! _Y=`expr ${_X} + 1` if [[ ${_Y} -eq 3 ]] thenecho "Y is now equal to ${_Y}" fi _X=${_Y}done
在執(zhí)行這個(gè)腳本之前,把 PS4 變量改為某個(gè)看起來醒目的字符串:
# export PS4="DEBUG => "
接下來,執(zhí)行這個(gè)腳本,就會(huì)看到可能非常有價(jià)值的信息,見清單 13。
清單 13. set -x 的輸出
# ./make_errorsDEBUG => _X=1DEBUG => [[ 1 -le 4 ]]DEBUG => [[ 1 -lt 2 ]]DEBUG => echo 'X is less than 2!'X is less than 2!DDEBUG => expr 1 + 1DEBUG => _Y=2DEBUG => [[ 2 -eq 3 ]]DEBUG => _X=2DEBUG => [[ 2 -le 4 ]]DEBUG => [[ 2 -lt 2 ]]DDEBUG => expr 2 + 1DEBUG => _Y=3DEBUG => [[ 3 -eq 3 ]]DEBUG => echo 'Y is now equal to 3'Y is now equal to 3DEBUG => _X=3DEBUG => [[ 3 -le 4 ]]DEBUG => [[ 3 -lt 2 ]]DDEBUG => expr 3 + 1DEBUG => _Y=4DEBUG => [[ 4 -eq 3 ]]DEBUG => _X=4DEBUG => [[ 4 -le 4 ]]DEBUG => [[ 4 -lt 2 ]]DDEBUG => expr 4 + 1DEBUG => _Y=5DEBUG => [[ 5 -eq 3 ]]DEBUG => _X=5DEBUG => [[ 5 -le 4 ]]
可以看到這里有大量信息:處理并執(zhí)行的每個(gè)命令都顯示出來了。還要注意,在調(diào)試信息中沒有顯示 shell 腳本中的注釋。這是因?yàn)?shell 在讀取注釋之后并不執(zhí)行它。還好,在完成前面的修改之后,這個(gè)腳本沒有錯(cuò)誤了!
在使用 set -x 時(shí)還要記住一點(diǎn):如果腳本有內(nèi)部函數(shù),而且 set -x 放在代碼的主體部分,那么它的輸出會(huì)包含子函數(shù)的運(yùn)算過程。但是,如果 set -x 只放在內(nèi)部函數(shù)中,那么 debug 選項(xiàng)的影響范圍只包含這個(gè)內(nèi)部函數(shù)中的代碼和在其中調(diào)用的子函數(shù);shell 腳本的主體并不包含在內(nèi),這是因?yàn)樗恢浪膬?nèi)部函數(shù)會(huì)調(diào)用這個(gè)例程。
結(jié)束語
無論是使用 shell 腳本、C、Java™ 語言或其他語言,我們都在不斷地改進(jìn)編程方法。堅(jiān)持簡(jiǎn)單化的基本規(guī)則,保持代碼簡(jiǎn)潔靈活,給代碼加上適當(dāng)?shù)淖⑨專俳柚{(diào)試工具的幫助,您很快就能編寫出出色的 shell 腳本。祝您好運(yùn)!
