這是我寫BASH程式的招式。這裡本沒有什麼新的內容,但是從我的經驗來看,人們愛濫用BASH。他們忽略了電腦科學,而從他們的程式中創造的是“大泥球”(譯註:指架構不清晰的軟體系統)。
在此我告訴你方法,以保護你的程式免於障礙,並保持程式碼的整潔。
不可改變的全域性變數
- 儘量少用全域性變數
- 以大寫命名
- 只讀宣告
- 用全域性變數來代替隱晦的$0,$1等
-
在我的程式中常使用的全域性變數:
1 2 3 |
readonly PROGNAME=$(basename $0) readonly PROGDIR=$(readlink -m $(dirname $0)) readonly ARGS="$@" |
一切皆是區域性的
所有變數都應為區域性的。
1 2 3 4 5 6 7 |
change_owner_of_file() { local filename=$1 local user=$2 local group=$3 chown $user:$group $filename } |
1 2 3 4 5 6 7 8 9 10 11 |
change_owner_of_files() { local user=$1; shift local group=$1; shift local files=$@ local i for i in $files do chown $user:$group $i done } |
- 自注釋(self documenting)的引數
- 通常作為迴圈用的變數i,把它宣告為區域性變數是很重要的。
- 區域性變數不作用於全域性域。
1 2 |
kfir@goofy ~ $ local a bash: local: can only be used in a function |
main()
- 有助於保持所有變數的區域性性
- 直觀的函數語言程式設計
-
程式碼中唯一的全域性命令是:main
1 2 3 4 5 6 7 8 9 10 |
main() { local files="/tmp/a /tmp/b" local i for i in $files do change_owner_of_file kfir users $i done } main |
一切皆是函式
- 唯一全域性性執行的程式碼是:
– 不可變的全域性變數宣告
– main()函式
- 保持程式碼整潔
- 過程變得清晰
1 2 3 |
main() { local files=$(ls /tmp | grep pid | grep -v daemon) } |
1 2 3 4 5 6 7 8 9 10 11 |
temporary_files() { local dir=$1 ls $dir \ | grep pid \ | grep -v daemon } main() { local files=$(temporary_files /tmp) } |
-
第二個例子好得多。查詢檔案是temporary_files()的問題而非main()的。這段程式碼用temporary_files()的單元測試也是可測試的。
- 如果你一定要嘗試第一個例子,你會得到查詢臨時檔案以和main演算法的大雜燴。
1 2 3 4 5 6 7 8 9 10 11 12 |
test_temporary_files() { local dir=/tmp touch $dir/a-pid1232.tmp touch $dir/a-pid1232-daemon.tmp returns "$dir/a-pid1232.tmp" temporary_files $dir touch $dir/b-pid1534.tmp returns "$dir/a-pid1232.tmp $dir/b-pid1534.tmp" temporary_files $dir } |
如你所見,這個測試不關心main()。
除錯函式
- 帶-x標誌執行程式:
1 |
bash -x my_prog.sh |
只除錯一小段程式碼,使用set-x和set+x,會只對被set -x和set +x包含的當前程式碼列印除錯資訊。
1 2 3 4 5 6 7 8 9 |
temporary_files() { local dir=$1 set -x ls $dir \ | grep pid \ | grep -v daemon set +x } |
列印函式名和它的引數:
1 2 3 4 5 6 7 8 |
temporary_files() { echo $FUNCNAME $@ local dir=$1 ls $dir \ | grep pid \ | grep -v daemon } |
呼叫函式:
1 |
temporary_files /tmp |
會列印到標準輸出:
1 |
temporary_files /tmp |
程式碼的清晰度
這段程式碼做了什麼?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
main() { local dir=/tmp [[ -z $dir ]] \ && do_something... [[ -n $dir ]] \ && do_something... [[ -f $dir ]] \ && do_something... [[ -d $dir ]] \ && do_something... } main |
讓你的程式碼說話:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
is_empty() { local var=$1 [[ -z $var ]] } is_not_empty() { local var=$1 [[ -n $var ]] } is_file() { local file=$1 [[ -f $file ]] } is_dir() { local dir=$1 [[ -d $dir ]] } main() { local dir=/tmp is_empty $dir \ && do_something... is_not_empty $dir \ && do_something... is_file $dir \ && do_something... is_dir $dir \ && do_something... } main |
每一行只做一件事
-
用反斜槓\來作分隔符。例如:
1 2 3 4 5 |
temporary_files() { local dir=$1 ls $dir | grep pid | grep -v daemon } |
可以寫得簡潔得多:
1 2 3 4 5 6 7 |
temporary_files() { local dir=$1 ls $dir \ | grep pid \ | grep -v daemon } |
- 符號在縮排行的開始
符號在行末的壞例子:(譯註:原文在此例中用了temporary_files()程式碼段,疑似是貼錯了。結合上下文,應為print_dir_if_not_empty())
1 2 3 4 5 6 7 |
print_dir_if_not_empty() { local dir=$1 is_empty $dir && \ echo "dir is empty" || \ echo "dir=$dir" } |
好的例子:我們可以清晰看到行和連線符號之間的聯絡。
1 2 3 4 5 6 7 |
print_dir_if_not_empty() { local dir=$1 is_empty $dir \ && echo "dir is empty" \ || echo "dir=$dir" } |
列印用法
不要這樣做:
1 2 3 |
echo "this prog does:..." echo "flags:" echo "-h print help" |
它應該是個函式:
1 2 3 4 5 |
usage() { echo "this prog does:..." echo "flags:" echo "-h print help" } |
echo在每一行重複。因此我們得到了這個文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
usage() { cat <<- EOF usage: $PROGNAME options Program deletes files from filesystems to release space. It gets config file that define fileystem paths to work on, and whitelist rules to keep certain files. OPTIONS: -c --config configuration file containing the rules. use --help-config to see the syntax. -n --pretend do not really delete, just how what you are going to do. -t --test run unit test to check the program -v --verbose Verbose. You can specify more then one -v to have more verbose -x --debug debug -h --help show this help --help-config configuration help Examples: Run all tests: $PROGNAME --test all Run specific test: $PROGNAME --test test_string.sh Run: $PROGNAME --config /path/to/config/$PROGNAME.conf Just show what you are going to do: $PROGNAME -vn -c /path/to/config/$PROGNAME.conf EOF } |
注意在每一行的行首應該有一個真正的製表符‘\t’。
在vim裡,如果你的tab是4個空格,你可以用這個替換命令:
1 |
:s/^ /\t/ |
命令列引數
這裡是一個例子,完成了上面usage函式的用法。我從Kirk’s blog post – bash shell script to use getopts with gnu style long positional parameters得到這段程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
cmdline() { # got this idea from here: # http://kirk.webfinish.com/2009/10/bash-shell-script-to-use-getopts-with-gnu-style-long-positional-parameters/ local arg= for arg do local delim="" case "$arg" in #translate --gnu-long-options to -g (short options) --config) args="${args}-c ";; --pretend) args="${args}-n ";; --test) args="${args}-t ";; --help-config) usage_config && exit 0;; --help) args="${args}-h ";; --verbose) args="${args}-v ";; --debug) args="${args}-x ";; #pass through anything else *) [[ "${arg:0:1}" == "-" ]] || delim="\"" args="${args}${delim}${arg}${delim} ";; esac done #Reset the positional parameters to the short options eval set -- $args while getopts "nvhxt:c:" OPTION do case $OPTION in v) readonly VERBOSE=1 ;; h) usage exit 0 ;; x) readonly DEBUG='-x' set -x ;; t) RUN_TESTS=$OPTARG verbose VINFO "Running tests" ;; c) readonly CONFIG_FILE=$OPTARG ;; n) readonly PRETEND=1 ;; esac done if [[ $recursive_testing || -z $RUN_TESTS ]]; then [[ ! -f $CONFIG_FILE ]] \ && eexit "You must provide --config file" fi return 0 } |
你像這樣,使用我們在頭上定義的不可變的ARGS變數:
1 2 3 4 |
main() { cmdline $ARGS } main |
單元測試
- 在更高階的語言中很重要。
-
使用shunit2做單元測試
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
test_config_line_paths() { local s='partition cpm-all, 80-90,' returns "/a" "config_line_paths '$s /a, '" returns "/a /b/c" "config_line_paths '$s /a:/b/c, '" returns "/a /b /c" "config_line_paths '$s /a : /b : /c, '" } config_line_paths() { local partition_line="$@" echo $partition_line \ | csv_column 3 \ | delete_spaces \ | column 1 \ | colons_to_spaces } source /usr/bin/shunit2 |
這裡是另一個使用df命令的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
DF=df ? mock_df_with_eols() { ????cat <<- EOF ????Filesystem?????????? 1K-blocks??????Used Available Use% Mounted on ????/very/long/device/path ???????????????????????? 124628916??23063572 100299192??19% / ????EOF } ? test_disk_size() { ????returns 1000 "disk_size /dev/sda1" ? ????DF=mock_df_with_eols ????returns 124628916 "disk_size /very/long/device/path" } ? df_column() { ????local disk_device=$1 ????local column=$2 ? ????$DF $disk_device \ ????????| grep -v 'Use%' \ ????????| tr '\n' ' ' \ ????????| awk "{print \\)column}" } disk_size() { local disk_device=$1 df_column $disk_device 2 } |
這裡我有個例外,為了測試,我在全域性域中宣告瞭DF為非只讀。這是因為shunit2不允許改變全域性域函式。