Domanda

Qualcuno sa di eventuali risorse che parlare di buone pratiche o di design pattern per gli script di shell (sh, bash, etc.)?

È stato utile?

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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top