Apakah ada yang tahu sumber daya apa pun yang berbicara tentang praktik terbaik atau pola desain untuk skrip shell (sh, bash dll)?
Apakah ada yang tahu sumber daya apa pun yang berbicara tentang praktik terbaik atau pola desain untuk skrip shell (sh, bash dll)?
Jawaban:
Saya menulis skrip shell yang cukup kompleks dan saran pertama saya adalah "jangan". Alasannya adalah cukup mudah untuk membuat kesalahan kecil yang menghalangi skrip Anda, atau bahkan membuatnya berbahaya.
Yang mengatakan, saya tidak punya sumber daya lain untuk melewati Anda tetapi pengalaman pribadi saya. Inilah yang biasanya saya lakukan, yang berlebihan, tetapi cenderung solid, meskipun sangat bertele-tele.
Doa
buat skrip Anda menerima opsi panjang dan pendek. hati-hati karena ada dua perintah untuk mengurai opsi, getopt dan getopts. Gunakan getopt karena Anda menghadapi lebih sedikit masalah.
CommandLineOptions__config_file=""
CommandLineOptions__debug_level=""
getopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: -- "$@"`
if test $? != 0
then
echo "unrecognized option"
exit 1
fi
eval set -- "$getopt_results"
while true
do
case "$1" in
--config_file)
CommandLineOptions__config_file="$2";
shift 2;
;;
--debug_level)
CommandLineOptions__debug_level="$2";
shift 2;
;;
--)
shift
break
;;
*)
echo "$0: unparseable option $1"
EXCEPTION=$Main__ParameterException
EXCEPTION_MSG="unparseable option $1"
exit 1
;;
esac
done
if test "x$CommandLineOptions__config_file" == "x"
then
echo "$0: missing config_file parameter"
EXCEPTION=$Main__ParameterException
EXCEPTION_MSG="missing config_file parameter"
exit 1
fi
Poin penting lainnya adalah bahwa suatu program harus selalu mengembalikan nol jika selesai dengan sukses, bukan nol jika terjadi kesalahan.
Panggilan fungsi
Anda dapat memanggil fungsi dalam bash, hanya ingat untuk mendefinisikannya sebelum panggilan. Fungsinya seperti skrip, mereka hanya bisa mengembalikan nilai numerik. Ini berarti Anda harus menemukan strategi berbeda untuk mengembalikan nilai string. Strategi saya adalah menggunakan variabel yang disebut HASIL untuk menyimpan hasilnya, dan mengembalikan 0 jika fungsi selesai dengan bersih. Selain itu, Anda dapat meningkatkan pengecualian jika Anda mengembalikan nilai yang berbeda dari nol, dan kemudian menetapkan dua "variabel pengecualian" (milik saya: EXCEPTION dan EXCEPTION_MSG), yang pertama berisi jenis pengecualian dan yang kedua adalah pesan yang dapat dibaca manusia.
Ketika Anda memanggil suatu fungsi, parameter fungsi ditugaskan ke vars khusus $ 0, $ 1 dll. Saya sarankan Anda untuk memasukkannya ke nama yang lebih bermakna. mendeklarasikan variabel di dalam fungsi sebagai lokal:
function foo {
local bar="$0"
}
Situasi rawan kesalahan
Di bash, kecuali Anda menyatakan sebaliknya, variabel yang tidak disetel digunakan sebagai string kosong. Ini sangat berbahaya jika terjadi kesalahan ketik, karena variabel yang diketik dengan buruk tidak akan dilaporkan, dan akan dievaluasi sebagai kosong. menggunakan
set -o nounset
untuk mencegah hal ini terjadi. Berhati-hatilah, karena jika Anda melakukan ini, program akan dibatalkan setiap kali Anda mengevaluasi variabel yang tidak ditentukan. Untuk alasan ini, satu-satunya cara untuk memeriksa apakah suatu variabel tidak didefinisikan adalah sebagai berikut:
if test "x${foo:-notset}" == "xnotset"
then
echo "foo not set"
fi
Anda dapat mendeklarasikan variabel sebagai hanya baca:
readonly readonly_var="foo"
Modularisasi
Anda dapat mencapai modularisasi "python like" jika Anda menggunakan kode berikut:
set -o nounset
function getScriptAbsoluteDir {
# @description used to get the script path
# @param $1 the script $0 parameter
local script_invoke_path="$1"
local cwd=`pwd`
# absolute path ? if so, the first character is a /
if test "x${script_invoke_path:0:1}" = 'x/'
then
RESULT=`dirname "$script_invoke_path"`
else
RESULT=`dirname "$cwd/$script_invoke_path"`
fi
}
script_invoke_path="$0"
script_name=`basename "$0"`
getScriptAbsoluteDir "$script_invoke_path"
script_absolute_dir=$RESULT
function import() {
# @description importer routine to get external functionality.
# @description the first location searched is the script directory.
# @description if not found, search the module in the paths contained in $SHELL_LIBRARY_PATH environment variable
# @param $1 the .shinc file to import, without .shinc extension
module=$1
if test "x$module" == "x"
then
echo "$script_name : Unable to import unspecified module. Dying."
exit 1
fi
if test "x${script_absolute_dir:-notset}" == "xnotset"
then
echo "$script_name : Undefined script absolute dir. Did you remove getScriptAbsoluteDir? Dying."
exit 1
fi
if test "x$script_absolute_dir" == "x"
then
echo "$script_name : empty script path. Dying."
exit 1
fi
if test -e "$script_absolute_dir/$module.shinc"
then
# import from script directory
. "$script_absolute_dir/$module.shinc"
elif test "x${SHELL_LIBRARY_PATH:-notset}" != "xnotset"
then
# import from the shell script library path
# save the separator and use the ':' instead
local saved_IFS="$IFS"
IFS=':'
for path in $SHELL_LIBRARY_PATH
do
if test -e "$path/$module.shinc"
then
. "$path/$module.shinc"
return
fi
done
# restore the standard separator
IFS="$saved_IFS"
fi
echo "$script_name : Unable to find module $module."
exit 1
}
Anda kemudian dapat mengimpor file dengan ekstensi .shinc dengan sintaks berikut
impor "AModule / ModuleFile"
Yang akan dicari di SHELL_LIBRARY_PATH. Saat Anda selalu mengimpor di namespace global, ingatlah untuk awalan semua fungsi dan variabel Anda dengan awalan yang tepat, jika tidak, Anda berisiko bentrok nama. Saya menggunakan garis bawah ganda sebagai titik python.
Juga, letakkan ini sebagai hal pertama dalam modul Anda
# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
return 0
fi
BashInclude__imported=1
Pemrograman berorientasi objek
Dalam bash, Anda tidak dapat melakukan pemrograman berorientasi objek, kecuali jika Anda membangun sistem alokasi objek yang cukup kompleks (saya memikirkannya. Ini layak, tetapi gila). Namun dalam praktiknya, Anda dapat melakukan "Pemrograman Berorientasi Singleton": Anda memiliki satu instance dari setiap objek, dan hanya satu.
Apa yang saya lakukan adalah: saya mendefinisikan objek ke dalam modul (lihat entri modularisasi). Kemudian saya mendefinisikan vars kosong (analog dengan variabel anggota) fungsi init (konstruktor) dan fungsi anggota, seperti dalam kode contoh ini
# avoid double inclusion
if test "${Table__imported+defined}" == "defined"
then
return 0
fi
Table__imported=1
readonly Table__NoException=""
readonly Table__ParameterException="Table__ParameterException"
readonly Table__MySqlException="Table__MySqlException"
readonly Table__NotInitializedException="Table__NotInitializedException"
readonly Table__AlreadyInitializedException="Table__AlreadyInitializedException"
# an example for module enum constants, used in the mysql table, in this case
readonly Table__GENDER_MALE="GENDER_MALE"
readonly Table__GENDER_FEMALE="GENDER_FEMALE"
# private: prefixed with p_ (a bash variable cannot start with _)
p_Table__mysql_exec="" # will contain the executed mysql command
p_Table__initialized=0
function Table__init {
# @description init the module with the database parameters
# @param $1 the mysql config file
# @exception Table__NoException, Table__ParameterException
EXCEPTION=""
EXCEPTION_MSG=""
EXCEPTION_FUNC=""
RESULT=""
if test $p_Table__initialized -ne 0
then
EXCEPTION=$Table__AlreadyInitializedException
EXCEPTION_MSG="module already initialized"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
local config_file="$1"
# yes, I am aware that I could put default parameters and other niceties, but I am lazy today
if test "x$config_file" = "x"; then
EXCEPTION=$Table__ParameterException
EXCEPTION_MSG="missing parameter config file"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
p_Table__mysql_exec="mysql --defaults-file=$config_file --silent --skip-column-names -e "
# mark the module as initialized
p_Table__initialized=1
EXCEPTION=$Table__NoException
EXCEPTION_MSG=""
EXCEPTION_FUNC=""
return 0
}
function Table__getName() {
# @description gets the name of the person
# @param $1 the row identifier
# @result the name
EXCEPTION=""
EXCEPTION_MSG=""
EXCEPTION_FUNC=""
RESULT=""
if test $p_Table__initialized -eq 0
then
EXCEPTION=$Table__NotInitializedException
EXCEPTION_MSG="module not initialized"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
id=$1
if test "x$id" = "x"; then
EXCEPTION=$Table__ParameterException
EXCEPTION_MSG="missing parameter identifier"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
local name=`$p_Table__mysql_exec "SELECT name FROM table WHERE id = '$id'"`
if test $? != 0 ; then
EXCEPTION=$Table__MySqlException
EXCEPTION_MSG="unable to perform select"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
RESULT=$name
EXCEPTION=$Table__NoException
EXCEPTION_MSG=""
EXCEPTION_FUNC=""
return 0
}
Menjebak dan menangani sinyal
Saya menemukan ini berguna untuk menangkap dan menangani pengecualian.
function Main__interruptHandler() {
# @description signal handler for SIGINT
echo "SIGINT caught"
exit
}
function Main__terminationHandler() {
# @description signal handler for SIGTERM
echo "SIGTERM caught"
exit
}
function Main__exitHandler() {
# @description signal handler for end of the program (clean or unclean).
# probably redundant call, we already call the cleanup in main.
exit
}
trap Main__interruptHandler INT
trap Main__terminationHandler TERM
trap Main__exitHandler EXIT
function Main__main() {
# body
}
# catch signals and exit
trap exit INT TERM EXIT
Main__main "$@"
Petunjuk dan kiat
Jika sesuatu tidak berhasil karena suatu alasan, cobalah untuk menyusun ulang kode. Ketertiban itu penting dan tidak selalu intuitif.
bahkan tidak mempertimbangkan untuk bekerja dengan tcsh. itu tidak mendukung fungsi, dan itu mengerikan secara umum.
Semoga ini bisa membantu, walaupun harap diperhatikan. Jika Anda harus menggunakan hal-hal yang saya tulis di sini, itu berarti masalah Anda terlalu rumit untuk diselesaikan dengan shell. gunakan bahasa lain. Saya harus menggunakannya karena faktor manusia dan warisan.
getopt
vs getopts
? getopts
lebih portabel dan berfungsi di semua shell POSIX. Terutama karena pertanyaannya adalah praktik terbaik shell, bukan khusus praktik terbaik bash, saya akan mendukung kepatuhan POSIX untuk mendukung beberapa shell jika memungkinkan.
Lihatlah Panduan Bash-Scripting Lanjutan untuk banyak kebijaksanaan tentang skrip shell - bukan hanya Bash, baik.
Jangan dengarkan orang yang memberi tahu Anda untuk melihat bahasa lain yang bisa dibilang lebih rumit. Jika skrip shell memenuhi kebutuhan Anda, gunakan itu. Anda menginginkan fungsionalitas, bukan fantasi. Bahasa baru memberikan keterampilan baru yang berharga untuk resume Anda, tetapi itu tidak membantu jika Anda memiliki pekerjaan yang perlu dilakukan dan Anda sudah tahu shell.
Seperti yang dinyatakan, tidak ada banyak "praktik terbaik" atau "pola desain" untuk skrip shell. Penggunaan yang berbeda memiliki pedoman dan bias yang berbeda - seperti bahasa pemrograman lainnya.
skrip shell adalah bahasa yang dirancang untuk memanipulasi file dan proses. Meskipun bagus untuk itu, itu bukan bahasa tujuan umum, jadi selalu mencoba untuk merekatkan logika dari utilitas yang ada daripada menciptakan kembali logika baru dalam skrip shell.
Selain itu prinsip umum saya telah mengumpulkan beberapa kesalahan skrip shell umum .
Ada sesi hebat di OSCON tahun ini (2008) tentang topik ini: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
Tahu kapan harus menggunakannya. Untuk perintah perekatan yang cepat dan kotor tidak masalah. Jika Anda perlu membuat lebih dari beberapa keputusan non-sepele, loop, apa pun, gunakan Python, Perl, dan modularisasi .
Masalah terbesar dengan shell seringkali adalah hasil akhirnya hanya tampak seperti bola lumpur besar, 4000 baris bash dan terus bertambah ... dan Anda tidak dapat menyingkirkannya karena sekarang seluruh proyek Anda bergantung padanya. Tentu saja, itu dimulai pada 40 baris pesta indah.
Mudah: gunakan python bukan skrip shell. Anda mendapatkan peningkatan keterbacaan hampir 100 kali lipat, tanpa harus menyulitkan apa pun yang tidak Anda butuhkan, dan mempertahankan kemampuan untuk mengembangkan bagian skrip Anda menjadi fungsi, objek, objek persisten (zodb), objek terdistribusi (piro) hampir tanpa ada kode tambahan.
gunakan set -e agar Anda tidak terus maju setelah kesalahan. Cobalah membuatnya kompatibel tanpa mengandalkan bash jika Anda ingin dijalankan di bukan-linux.
Untuk menemukan "praktik terbaik", lihat bagaimana distro Linux (misalnya Debian) menulis skrip init mereka (biasanya ditemukan di /etc/init.d)
Sebagian besar dari mereka tanpa "bash-isme" dan memiliki pemisahan yang baik dari pengaturan konfigurasi, file perpustakaan dan pemformatan sumber.
Gaya pribadi saya adalah menulis naskah-master yang mendefinisikan beberapa variabel default, dan kemudian mencoba memuat ("sumber") file konfigurasi yang mungkin berisi nilai-nilai baru.
Saya mencoba untuk menghindari fungsi karena cenderung membuat skrip lebih rumit. (Perl diciptakan untuk tujuan itu.)
Untuk memastikan skripnya portabel, uji tidak hanya dengan #! / Bin / sh, tetapi juga gunakan #! / Bin / ash, #! / Bin / dash, dll. Anda akan segera menemukan kode spesifik Bash.