Modelli di progettazione o migliori pratiche per gli script di shell [chiuso]


167

Qualcuno sa di eventuali risorse che parlano di migliori pratiche o modelli di progettazione per gli script di shell (sh, bash ecc.)?


2
Ho appena scritto un piccolo articolo sul modello di modello in BASH ieri sera. Vedi cosa ne pensi.
cambio rapido il

Risposte:


222

Ho scritto script di shell piuttosto complessi e il mio primo suggerimento è "non". Il motivo è che è abbastanza facile fare un piccolo errore che impedisce la sceneggiatura o addirittura la rende pericolosa.

Detto questo, non ho altre risorse per passarti ma la mia esperienza personale. Ecco cosa faccio normalmente, che è eccessivo, ma tende ad essere solido, anche se molto dettagliato.

Invocazione

fai in modo che il tuo script accetti opzioni lunghe e brevi. fai attenzione perché ci sono due comandi per analizzare le opzioni, getopt e getopts. Usa getopt mentre affronti 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 dovrebbe sempre restituire zero se completato correttamente, diverso da zero se qualcosa è andato storto.

Chiamate di funzione

Puoi chiamare le funzioni in bash, ricorda di definirle prima della chiamata. Le funzioni sono come gli script, possono solo restituire valori numerici. Ciò significa che devi inventare una strategia diversa per restituire i valori di stringa. La mia strategia è quella di utilizzare una variabile denominata RISULTATO per memorizzare il risultato e restituire 0 se la funzione è stata completata correttamente. Inoltre, puoi aumentare le eccezioni se stai restituendo un valore diverso da zero e quindi impostare due "variabili di eccezione" (mia: EXCEPTION ed EXCEPTION_MSG), la prima contenente il tipo di eccezione e la seconda un messaggio leggibile.

Quando si chiama una funzione, i parametri della funzione sono assegnati agli speciali $ 0, $ 1 ecc. Ti suggerisco di inserirli in nomi più significativi. dichiarare le variabili all'interno della funzione come locali:

function foo {
   local bar="$0"
}

Situazioni soggette a errori

In bash, salvo diversa indicazione, una variabile non impostata viene utilizzata come stringa vuota. Questo è molto pericoloso in caso di errore di battitura, poiché la variabile mal digitata non verrà segnalata e verrà valutata come vuota. uso

set -o nounset

per evitare che ciò accada. Fai attenzione, perché se lo fai, il programma si interromperà ogni volta che valuti una variabile non definita. Per questo motivo, l'unico modo per verificare se una variabile non è definita è il seguente:

if test "x${foo:-notset}" == "xnotset"
then
    echo "foo not set"
fi

Puoi dichiarare le variabili come di sola lettura:

readonly readonly_var="foo"

La modularizzazione

Puoi ottenere la modularizzazione "simile a Python" se usi 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 file con estensione .shinc con la seguente sintassi

import "AModule / ModuleFile"

Che verrà cercato in SHELL_LIBRARY_PATH. Poiché esegui sempre l'importazione nello spazio dei nomi globale, ricorda di aggiungere un prefisso a tutte le tue funzioni e variabili con un prefisso appropriato, altrimenti rischi di scontrarsi con i nomi. Uso il doppio trattino basso come punto di pitone.

Inoltre, inseriscilo come prima cosa nel tuo modulo

# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
    return 0
fi
BashInclude__imported=1

Programmazione orientata agli oggetti

In bash, non puoi fare una programmazione orientata agli oggetti, a meno che tu non costruisca un sistema abbastanza complesso di allocazione degli oggetti (ci ho pensato. È fattibile, ma folle). In pratica, puoi comunque fare "programmazione orientata a Singleton": hai un'istanza di ciascun oggetto e solo una.

Quello che faccio è: definisco un oggetto in un modulo (vedi la voce di modularizzazione). Quindi definisco var vuoti (analogo alle variabili membro) una funzione init (costruttore) e funzioni membro, come in questo codice di esempio

# 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
}

Trapping e gestione dei segnali

L'ho trovato utile per rilevare 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, prova a riordinare il codice. L'ordine è importante e non sempre intuitivo.

non considerare nemmeno di lavorare con tcsh. non supporta le funzioni ed è orribile in generale.

Spero che sia d'aiuto, anche se per favore nota. Se devi usare il tipo di cose che ho scritto qui, significa che il tuo problema è troppo complesso per essere risolto con shell. usa un'altra lingua. Ho dovuto usarlo a causa di fattori umani e eredità.


7
Caspita, e pensavo che stavo andando per un eccesso di potenza in bash ... Tendo ad usare funzioni isolate e abusare di sottotitoli (quindi soffro quando la velocità è in qualche modo rilevante). Nessuna variabile globale mai, né dentro né fuori (per preservare resti di sanità mentale). Tutto ritorna tramite output stdout o file. set -u / set -e (troppo male set -e diventa inutile non appena se, e la maggior parte del mio codice è spesso presente). Argomenti della funzione presi con [local qualcosa = "$ 1"; shift] (consente un facile riordino durante il refactoring). Dopo uno script bash di 3000 righe tendo a scrivere anche i più piccoli script in questo modo ...
Eugene,

piccole correzioni per la modularizzazione: 1 è necessario un ritorno dopo. "$ script_absolute_dir / $ module.shinc" per evitare avvisi mancanti. 2 devi impostare IFS = "$ saved_IFS" prima del tuo ritorno alla ricerca del modulo in $ SHELL_LIBRARY_PATH
Duff

i "fattori umani" sono i peggiori. Le macchine non ti combattono quando dai loro qualcosa di meglio.
jeremyjjbrown,

1
Perché getoptvs getopts? getoptsè più portatile e funziona in qualsiasi shell POSIX. Soprattutto dal momento che la domanda riguarda le migliori pratiche della shell invece di basicamente le migliori pratiche, supporto la conformità POSIX per supportare più shell quando possibile.
Wimateeka,

1
grazie per aver offerto tutti i consigli per lo scripting della shell anche se sei onesto: "Spero che sia d'aiuto, anche se per favore nota. Se devi usare il tipo di cose che ho scritto qui, significa che il tuo problema è troppo complesso per essere risolto shell. usa un'altra lingua. Ho dovuto usarla a causa di fattori umani e eredità ".
dieHellste,

25

Dai un'occhiata alla Guida avanzata di script di Bash per molta saggezza sugli script di shell, non solo su Bash.

Non ascoltare le persone che ti dicono di guardare altre lingue, probabilmente più complesse. Se lo scripting della shell soddisfa le tue esigenze, utilizzalo. Vuoi funzionalità, non fantasia. Le nuove lingue forniscono nuove preziose competenze per il tuo curriculum, ma ciò non aiuta se hai del lavoro da svolgere e conosci già la shell.

Come detto, non ci sono molte "buone pratiche" o "modelli di progettazione" per gli script di shell. Usi diversi hanno linee guida e pregiudizi diversi, come qualsiasi altro linguaggio di programmazione.


9
Nota che per gli script anche di lieve complessità, questa NON è una buona pratica. La codifica non si tratta solo di far funzionare qualcosa. Si tratta di costruirlo rapidamente, facilmente ed è affidabile, riutilizzabile, facile da leggere e mantenere (specialmente per gli altri). Gli script della shell non si adattano bene a nessun livello. Linguaggi più robusti sono molto più semplici per i progetti con qualsiasi logica.
drifter

20

shell script è un linguaggio progettato per manipolare file e processi. Anche se è ottimo per questo, non è un linguaggio generico, quindi cerca sempre di incollare la logica dalle utility esistenti piuttosto che ricreare la nuova logica nello script della shell.

A parte questo principio generale, ho raccolto alcuni errori di script shell comuni .



11

Sapere quando usarlo. Per comandi di incollaggio rapidi e sporchi insieme va bene. Se devi prendere più di poche decisioni, cicli, qualsiasi cosa non banale, scegli Python, Perl e modularizza .

Il problema più grande con la shell è spesso che il risultato finale sembra solo una grande palla di fango, 4000 linee di bash e in crescita ... e non puoi liberartene perché ora l'intero progetto dipende da esso. Certo, è iniziato a 40 linee di bella base.


9

Facile: usa python invece degli script di shell. Ottieni un aumento di quasi 100 volte della leggibilità, senza dover complicare tutto ciò di cui non hai bisogno e preservando la capacità di trasformare parti della tua sceneggiatura in funzioni, oggetti, oggetti persistenti (zodb), oggetti distribuiti (piro) quasi senza alcun codice extra.


7
ti contraddici dicendo "senza dover complicare" e quindi elencando le varie complessità che ritieni aggiungano valore, mentre nella maggior parte dei casi vengono abusate di mostri brutti invece che usati per semplificare i problemi e l'implementazione.
Evgeny,

3
questo implica un grande svantaggio, i tuoi script non saranno portatili su sistemi in cui Python non è presente
astropanico

1
Mi rendo conto che a questa risposta fu data risposta nel '08 (ora è due giorni prima del '12); tuttavia, per coloro che lo guardano anni dopo, consiglierei a chiunque di non voltare le spalle a lingue come Python o Ruby in quanto è più probabile che sia disponibile e, in caso contrario, è un comando (o un paio di clic) lontano dall'installazione . Se hai bisogno di ulteriore portabilità, pensa a scrivere il tuo programma in Java poiché ti verrà difficile trovare una macchina che non ha una JVM disponibile.
Wil Moore III,

@astropanic praticamente tutte le porte Linux con Python al giorno d'oggi
Pithikos

@Pithikos, certo, e giocherellare con la seccatura di python2 vs python3. Oggi scrivo tutti i miei strumenti con go e non posso essere più felice.
astropanico

9

usa set -e in modo da non arare in avanti dopo gli errori. Prova a renderlo sh compatibile senza fare affidamento su bash se vuoi che funzioni su non-linux.


7

Per trovare alcune "best practice", guarda come la distro di Linux (es. Debian) scrive i loro script di init (di solito si trova in /etc/init.d)

Molti di questi sono senza "bash-isms" e hanno una buona separazione delle impostazioni di configurazione, dei file di libreria e della formattazione del sorgente.

Il mio stile personale è quello di scrivere un master-shellscript che definisce alcune variabili predefinite, e quindi tenta di caricare ("sorgente") un file di configurazione che può contenere nuovi valori.

Cerco di evitare le funzioni poiché tendono a rendere lo script più complicato. (Perl è stato creato a tale scopo.)

Per assicurarti che lo script sia portatile, prova non solo con #! / Bin / sh, ma usa anche #! / Bin / ash, #! / Bin / dash, ecc. Individuerai il codice specifico di Bash abbastanza presto.


-1

O la citazione più vecchia simile a quella che diceva Joao:

"Usa perl. Ti consigliamo di conoscere bash ma non di usarlo."

Purtroppo ho dimenticato chi l'ha detto.

E sì in questi giorni consiglierei Python su Perl.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.