Progettare modelli e best practice per gli script di shell [chiuso]
-
09-06-2019 - |
Domanda
Qualcuno sa di eventuali risorse che parlare di buone pratiche o di design pattern per gli script di shell (sh, bash, etc.)?
Soluzione
Ho scritto abbastanza complesso script di shell e il mio primo consiglio è "non".Il motivo è che è abbastanza facile da fare un piccolo errore che ostacola il vostro script, o addirittura pericoloso.
Detto questo, non ho altre risorse per passare, ma la mia esperienza personale.Qui è quello che faccio di solito, che è eccessivo, ma tende ad essere solida, anche se molto verbose.
Invocazione
rendere il vostro script di accettare lungo e corto di opzioni.attenzione perché ci sono due comandi per analizzare le opzioni, getopt e getopts.Utilizzare getopt come si faccia meno problemi.
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
Un altro punto importante è che un programma deve sempre restituire zero se completato con successo, camere non-zero se qualcosa è andato storto.
Chiamate di funzione
È possibile chiamare funzioni in bash, basta ricordarsi di definire prima della chiamata.Le funzioni sono come gli script, si può restituire solo valori numerici.Questo significa che si devono inventare una diversa strategia per restituire i valori di stringa.La mia strategia è quella di utilizzare una variabile denominata RISULTATO per memorizzare il risultato, e restituisce 0 se la funzione è stata completata correttamente.Inoltre, si può sollevare eccezioni se si restituisce un valore diverso da zero, e quindi impostare due di eccezione "variabili" (la mia:ECCEZIONE e EXCEPTION_MSG), il primo contenente il tipo di eccezione e la seconda, leggibile messaggio.
Quando si chiama una funzione, i parametri della funzione sono assegnate a speciali var $0, $1, etc.Ti consiglio di metterli in nomi più significativi.dichiarare le variabili all'interno della funzione locale:
function foo {
local bar="$0"
}
Di errori di situazioni
In bash, a meno che non si dichiari altrimenti, una variabile è utilizzata come una stringa vuota.Questo è molto pericoloso in caso di errore di battitura, come il mal digitato variabile non essere segnalato, e sarà valutato come vuoto.utilizzare
set -o nounset
per evitare che questo accada.Attenzione però, perché se si esegue questa operazione, il programma verrà interrotta ogni volta che si valuta una variabile non definita.Per questo motivo, l'unico modo per verificare se una variabile non è definita è la seguente:
if test "x${foo:-notset}" == "xnotset"
then
echo "foo not set"
fi
È possibile dichiarare variabili di sola lettura:
readonly readonly_var="foo"
La modularizzazione
È possibile raggiungere il "pitone" modularizzazione se si utilizza il seguente codice:
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
}
è quindi possibile importare i file con l'estensione .shinc con la seguente sintassi
importare "AModule/ModuleFile"
Che viene ricercato nel SHELL_LIBRARY_PATH.Come sempre importare il namespace globale, ricordate di prefisso tutti le funzioni e le variabili con un proprio prefisso, altrimenti si rischia di nome scontri.Io uso doppia sottolineatura, come il pitone dot.
Inoltre, mettere questo come prima cosa nel tuo modulo
# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
return 0
fi
BashInclude__imported=1
La programmazione orientata agli oggetti
In bash, non si può fare programmazione orientata agli oggetti, a meno di non costruire un bel complesso sistema di allocazione di oggetti (ho pensato che.è fattibile, ma folle).In pratica, tuttavia, è possibile fare "Singleton programmazione orientata":è un'istanza di ogni oggetto, e solo uno.
Quello che faccio io è:definire un oggetto in un modulo (vedere la modularizzazione voce).Poi mi definiscono vuoto vars (analogamente alle variabili membro) una funzione init (costruttore) e le funzioni membro, come in questo esempio di codice
# 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
}
Cattura e la gestione dei segnali
Ho trovato questo utile per intercettare e gestire le eccezioni.
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 "$@"
Suggerimenti e consigli
Se qualcosa non funziona per qualche motivo, il tentativo di riordinare il codice.L'ordine è importante e non sempre facile e intuitivo.
non prendono nemmeno in considerazione di lavoro con tcsh.non supporta le funzioni, ed è orribile in generale.
Speranza che aiuta, anche se si prega di notare.Se è necessario utilizzare il tipo di cose che ho scritto qui, significa che il problema è troppo complesso per essere risolto con la shell.usare un'altra lingua.Ho dovuto utilizzare a causa di fattori umani e legacy.
Altri suggerimenti
Date un'occhiata al Advanced Bash-Scripting Guide per un sacco di saggezza su shell scripting - non solo Bash, sia.
Non ascoltare la gente che ti dice di guardare le altre, sicuramente più complesso lingue.Se la shell scripting soddisfa le vostre esigenze, usare quella.Si desidera che la funzionalità, non fanciness.Nuovi linguaggi di fornire preziose nuove abilità per il tuo curriculum, ma che non aiuta se si dispone di un lavoro che deve essere fatto e sai già shell.
Come detto, non ci sono un sacco di "buone pratiche" o "design patterns" per gli script di shell.Usi diversi hanno diversi orientamenti e pregiudizi - come qualsiasi altro linguaggio di programmazione.
shell script è un linguaggio progettato per manipolare file e processi.Mentre è grande per questo, non è un uso generale della lingua, quindi, cerca sempre di colla logica di utilità esistenti, piuttosto che ricreare nuovi logica in script di shell.
Più che altro principio generale ho raccolto alcuni comune di shell script errori.
C'era una grande sessione di OSCON quest'anno (2008) proprio su questo argomento: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
Facile:utilizzare python invece di script di shell.Si ottiene un quasi 100 volte maggiore nel readablility, senza dover complicare tutto quello che non serve, e conservando la capacità di evolvere parti di script, funzioni, oggetti, oggetti persistenti (zodb), oggetti distribuiti (piro) quasi senza alcun codice aggiuntivo.
utilizzare set-e quindi non arare in avanti, dopo gli errori.Provare a fare sh senza fare affidamento su bash se si desidera eseguire su linux.
Sapere quando usarlo. Per un rapido e sporco incollaggio comandi insieme va bene.Se avete bisogno di fare più di qualche non banale decisioni, loop, qualsiasi cosa, andare per Python, Perl, e modularizzare.
Il problema più grande con la shell è spesso che il risultato finale appare come una grande palla di fango, di 4000 righe di bash e in crescita...e non è possibile sbarazzarsi di esso, perché ora tutto il vostro progetto dipende da esso.Naturalmente, e ' iniziato al 40 righe di bella bash.
Per trovare alcune "best practices", guardate come le distro Linux (per esempioDebian) scrivere le loro init-script (di solito si trova in /etc/init.d)
La maggior parte di loro sono senza "bash-ismi" e di avere una buona separazione delle impostazioni di configurazione, biblioteca-i file di origine e di formattazione.
Il mio stile personale è quello di scrivere un master-script shell che definisce alcune variabili predefinite, e quindi si tenta di caricare ("sorgente") un file di configurazione che possono contenere nuovi valori.
Cerco di evitare di funzioni che tendono a rendere lo script più complicato.(Perl è stato creato per questo scopo.)
Per assicurarsi che lo script è portatile, prova non solo con #!/bin/sh, ma anche utilizzare #!/bin/ash, #!/bin/dash, etc.Ti posto il Bash codice specifico abbastanza presto.
O il vecchio preventivo simile a quello che Joao ha detto:
"L'uso di perl.Si vuole sapere bash, ma non lo uso."
Purtroppo ho dimenticato chi l'ha detto che.
E sì, in questi giorni mi sento di raccomandare di python su perl.