BASH的保護性編程技巧
這是我寫BASH程序的招式。這里本沒有什么新的內容,但是從我的經驗來看,人們愛濫用BASH。他們忽略了計算機科學,而從他們的程序中創造的是“大泥球”(譯注:指架構不清晰的軟件系統)。
不可改變的全局變量
- 盡量少用全局變量
- 以大寫命名
- 只讀聲明
- 用全局變量來代替隱晦的$0,$1等
-
在我的程序中常使用的全局變量:</li> </ul>
readonly PROGNAME=$(basename $0) readonly PROGDIR=$(readlink -m $(dirname $0)) readonly ARGS="$@"
一切皆是局部的
所有變量都應為局部的。
change_owner_of_file() { local filename=$1 local user=$2 local group=$3
chown $user:$group $filename
}</pre>
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
}</pre>
- 自注釋(self documenting)的參數
- 通常作為循環用的變量i,把它聲明為局部變量是很重要的。
- 局部變量不作用于全局域。 </ul>
kfir@goofy ~ $ local a bash: local: can only be used in a function
main()
</div>- 有助于保持所有變量的局部性
- 直觀的函數式編程
-
代碼中唯一的全局命令是:main</li> </ul>
main() { local files="/tmp/a /tmp/b" local i
for i in $files do change_owner_of_file kfir users $i done
} main</pre>
一切皆是函數
- 唯一全局性運行的代碼是: </ul>
- 保持代碼整潔
- 過程變得清晰 </ul>
- 不可變的全局變量聲明
- main()函數
main() { local files=$(ls /tmp | grep pid | grep -v daemon) }
temporary_files() { local dir=$1
ls $dir \ | grep pid \ | grep -v daemon
}
main() { local files=$(temporary_files /tmp) }</pre>
-
第二個例子好得多。查找文件是temporary_files()的問題而非main()的。這段代碼用temporary_files()的單元測試也是可測試的。</li>
- 如果你一定要嘗試第一個例子,你會得到查找臨時文件以和main算法的大雜燴。 </ul>
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
}</pre>
如你所見,這個測試不關心main()。調試函數
- 帶-x標志運行程序: </ul>
bash -x my_prog.sh
只調試一小段代碼,使用set-x和set+x,會只對被set -x和set +x包含的當前代碼打印調試信息。temporary_files() { local dir=$1
set -x ls $dir \ | grep pid \ | grep -v daemon set +x
}</pre></div>
打印函數名和它的參數:temporary_files() { echo $FUNCNAME $@ local dir=$1
ls $dir \ | grep pid \ | grep -v daemon
}</pre></div> </div>
調用函數:
temporary_files /tmp
會打印到標準輸出:
temporary_files /tmp
代碼的清晰度
</div>這段代碼做了什么?
main() { local dir=/tmp
[[ -z $dir ]] \ && do_something... [[ -n $dir ]] \ && do_something... [[ -f $dir ]] \ && do_something... [[ -d $dir ]] \ && do_something...
} main</pre>
讓你的代碼說話:
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</pre>
每一行只做一件事
-
用反斜杠\來作分隔符。例如:</li> </ul>
temporary_files() { local dir=$1
ls $dir | grep pid | grep -v daemon
}</pre>
可以寫得簡潔得多:
temporary_files() { local dir=$1
ls $dir \ | grep pid \ | grep -v daemon
}</pre>
- 符號在縮進行的開始 </ul>
符號在行末的壞例子:(譯注:原文在此例中用了temporary_files()代碼段,疑似是貼錯了。結合上下文,應為print_dir_if_not_empty())
</div>print_dir_if_not_empty() { local dir=$1
is_empty $dir && \ echo "dir is empty" || \ echo "dir=$dir"
}</pre>
好的例子:我們可以清晰看到行和連接符號之間的聯系。
print_dir_if_not_empty() { local dir=$1
is_empty $dir \ && echo "dir is empty" \ || echo "dir=$dir"
}</pre>
打印用法
不要這樣做:
echo "this prog does:..." echo "flags:" echo "-h print help"
它應該是個函數:
usage() { echo "this prog does:..." echo "flags:" echo "-h print help" }
echo在每一行重復。因此我們得到了這個文檔:
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
}</pre>
注意在每一行的行首應該有一個真正的制表符‘\t’。
在vim里,如果你的tab是4個空格,你可以用這個替換命令:
:s/^ /\t/
命令行參數
這里是一個例子,完成了上面usage函數的用法。我從Kirk’s blog post – bash shell script to use getopts with gnu style long positional parameters得到這段代碼
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
}</pre>
你像這樣,使用我們在頭上定義的不可變的ARGS變量:
main() { cmdline $ARGS } main
單元測試
- 在更高級的語言中很重要。
-
使用shunit2做單元測試</li> </ul>
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</pre>
這里是另一個使用df命令的例子:
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
}</pre>
這里我有個例外,為了測試,我在全局域中聲明了DF為非只讀。這是因為shunit2不允許改變全局域函數。
原文鏈接: Kfir Lavi 翻譯: 伯樂在線 - cjpan
譯文鏈接: http://blog.jobbole.com/73257/
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
sesese色