Odpowiedzi:
Napisałem dość złożone skrypty powłoki i moja pierwsza sugestia brzmi „nie rób tego”. Powodem jest to, że dość łatwo jest popełnić mały błąd, który utrudnia skrypt, a nawet czyni go niebezpiecznym.
To powiedziawszy, nie mam innych zasobów, aby ci przekazać, ale moje osobiste doświadczenie. Oto, co zwykle robię, co jest przesadą, ale zwykle jest solidne, chociaż bardzo rozwlekłe.
Wezwanie
spraw, aby twój skrypt akceptował długie i krótkie opcje. bądź ostrożny, ponieważ istnieją dwa polecenia do analizowania opcji, getopt i getopts. Użyj getopt, gdy będziesz mieć mniej problemów.
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
Inną ważną kwestią jest to, że program powinien zawsze zwracać zero, jeśli zakończy się pomyślnie, niezerowe, jeśli coś poszło nie tak.
Wywołania funkcji
Możesz wywoływać funkcje w bashu, pamiętaj tylko, aby zdefiniować je przed wywołaniem. Funkcje są jak skrypty, mogą zwracać tylko wartości liczbowe. Oznacza to, że musisz wymyślić inną strategię zwracania wartości ciągów. Moja strategia polega na użyciu zmiennej o nazwie RESULT do przechowywania wyniku i zwracaniu 0, jeśli funkcja zakończyła się poprawnie. Możesz także zgłosić wyjątki, jeśli zwracasz wartość różną od zera, a następnie ustawić dwie „zmienne wyjątków” (moje: EXCEPTION i EXCEPTION_MSG), pierwsza zawiera typ wyjątku, a druga czytelną dla człowieka wiadomość.
Kiedy wywołujesz funkcję, parametry funkcji są przypisywane do specjalnych zmiennych $ 0, $ 1 itd. Proponuję nadać im bardziej znaczące nazwy. zadeklaruj zmienne wewnątrz funkcji jako lokalne:
function foo {
local bar="$0"
}
Sytuacje podatne na błędy
W bash, chyba że zadeklarujesz inaczej, nieustawiona zmienna jest używana jako pusty łańcuch. Jest to bardzo niebezpieczne w przypadku literówki, ponieważ źle wpisana zmienna nie zostanie zgłoszona i zostanie oceniona jako pusta. posługiwać się
set -o nounset
aby temu zapobiec. Uważaj jednak, ponieważ jeśli to zrobisz, program przerwie działanie za każdym razem, gdy oceniasz niezdefiniowaną zmienną. Z tego powodu jedynym sposobem sprawdzenia, czy zmienna nie jest zdefiniowana, jest:
if test "x${foo:-notset}" == "xnotset"
then
echo "foo not set"
fi
Możesz zadeklarować zmienne jako tylko do odczytu:
readonly readonly_var="foo"
Modularyzacja
Możesz osiągnąć modularyzację "podobną do Pythona", używając następującego kodu:
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
}
możesz następnie importować pliki z rozszerzeniem .shinc z następującą składnią
import „AModule / ModuleFile”
Które będą przeszukiwane w SHELL_LIBRARY_PATH. Ponieważ zawsze importujesz w globalnej przestrzeni nazw, pamiętaj, aby poprzedzać wszystkie funkcje i zmienne odpowiednim prefiksem, w przeciwnym razie ryzykujesz konfliktami nazw. Używam podwójnego podkreślenia jako kropki Pythona.
Ponadto umieść to jako pierwszą rzecz w swoim module
# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
return 0
fi
BashInclude__imported=1
Programowanie obiektowe
W bashu nie możesz robić programowania obiektowego, chyba że zbudujesz dość złożony system alokacji obiektów (myślałem o tym. Jest to wykonalne, ale szalone). W praktyce możesz jednak wykonać „programowanie zorientowane na singleton”: masz jedną instancję każdego obiektu i tylko jedną.
Co robię, to: definiuję obiekt w module (patrz wpis modularyzacji). Następnie definiuję puste zmienne (analogiczne do zmiennych składowych), funkcję init (konstruktor) i funkcje składowe, jak w tym przykładowym kodzie
# 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
}
Wychwytywanie i obsługa sygnałów
Uważam, że jest to przydatne do wychwytywania i obsługi wyjątków.
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 "$@"
Porady i wskazówki
Jeśli z jakiegoś powodu coś nie działa, spróbuj ponownie zamówić kod. Porządek jest ważny i nie zawsze intuicyjny.
nawet nie myśl o pracy z tcsh. nie obsługuje funkcji i ogólnie jest okropny.
Mam nadzieję, że to pomoże, chociaż pamiętaj. Jeśli musisz użyć rzeczy, które tutaj napisałem, oznacza to, że twój problem jest zbyt złożony, aby można go było rozwiązać za pomocą powłoki. używać innego języka. Musiałem go użyć ze względu na czynniki ludzkie i dziedzictwo.
getopt
vs getopts
? getopts
jest bardziej przenośny i działa w każdej powłoce POSIX. Zwłaszcza, że pytanie dotyczy najlepszych praktyk powłoki, a nie tylko najlepszych praktyk basha, poparłbym zgodność z POSIX, aby obsługiwać wiele powłok, jeśli to możliwe.
Zapoznaj się z Przewodnikiem po zaawansowanych skryptach Bash aby uzyskać wiele informacji na temat skryptów powłoki - nie tylko Bash.
Nie słuchaj, jak ludzie mówią ci, żebyś spojrzał na inne, prawdopodobnie bardziej złożone języki. Jeśli skrypt powłoki spełnia Twoje potrzeby, użyj tego. Chcesz funkcjonalności, a nie elegancji. Nowe języki zapewniają cenne nowe umiejętności w Twoim CV, ale to nie pomaga, jeśli masz pracę do wykonania i znasz już powłokę Shell.
Jak już wspomniano, nie ma wielu „najlepszych praktyk” ani „wzorców projektowych” dotyczących skryptów powłoki. Różne zastosowania mają różne wytyczne i odchylenia - jak każdy inny język programowania.
skrypt powłoki to język przeznaczony do manipulowania plikami i procesami. Chociaż jest do tego świetny, nie jest to język ogólnego przeznaczenia, więc zawsze staraj się sklejać logikę z istniejących narzędzi, zamiast odtwarzać nową logikę w skrypcie powłoki.
Oprócz tej ogólnej zasady zebrałem kilka typowych błędów w skryptach powłoki .
W tym roku (2008) na OSCON odbyła się świetna sesja poświęcona tylko temu tematowi: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
Wiedz, kiedy go używać. W przypadku szybkich i brudnych poleceń sklejania jest w porządku. Jeśli potrzebujesz więcej niż kilka nietrywialnych decyzji, pętli, czegokolwiek, wybierz Python, Perl i modularyzację .
Największym problemem związanym z powłoką jest często to, że efekt końcowy wygląda jak wielka kula błota, 4000 linii uderzenia i rośnie ... i nie możesz się jej pozbyć, ponieważ teraz cały twój projekt od tego zależy. Oczywiście zaczęło się od 40 linii pięknego basha.
Łatwe: używaj Pythona zamiast skryptów powłoki. Otrzymujesz prawie 100-krotny wzrost czytelności, bez konieczności komplikowania czegokolwiek, czego nie potrzebujesz, i zachowując możliwość przekształcania części twojego skryptu w funkcje, obiekty, trwałe obiekty (zodb), rozproszone obiekty (pyro) prawie bez żadnych dodatkowy kod.
Aby znaleźć "najlepsze praktyki", spójrz, jak dystrybucje Linuksa (np. Debian) piszą swoje skrypty inicjujące (zwykle znajdują się w /etc/init.d)
Większość z nich nie zawiera "bash-izmów" i ma dobrą separację ustawień konfiguracyjnych, plików bibliotek i formatowania źródła.
Moim osobistym stylem jest napisanie głównego skryptu powłoki, który definiuje niektóre domyślne zmienne, a następnie próbuje załadować („źródło”) plik konfiguracyjny, który może zawierać nowe wartości.
Staram się unikać funkcji, ponieważ powodują one, że skrypt jest bardziej skomplikowany. (Perl został stworzony w tym celu.)
Aby upewnić się, że skrypt jest przenośny, przetestuj nie tylko z #! / Bin / sh, ale także użyj #! / Bin / ash, #! / Bin / dash itp. Wkrótce zauważysz specyficzny kod Bash.