2012-08-26 7 views
35

Devo scrivere una funzione in bash. La funzione richiederà circa 7 argomenti. So che posso chiamare una funzione come questa:C'è un modo per evitare gli argomenti posizionali in bash?

per chiamare una funzione con parametri:

function_name $arg1 $arg2 

E posso fare riferimento i miei parametri come questo all'interno della funzione:

function_name() { 
    echo "Parameter #1 is $1" 
} 

La mia domanda è, c'è un modo migliore per fare riferimento ai parametri all'interno della funzione? Posso evitare $ 1, $ 2, $ 3, .... e usare semplicemente $ arg1, $ arg2, ...?

C'è un metodo corretto per questo o devo riassegnare questi parametri ad alcune altre variabili all'interno della funzione? Es .:

function_name() { 
    $ARG1=$1 
    echo "Parameter #1 is $ARG1" 
} 

Qualsiasi esempio sarebbe molto apprezzato.

+1

Qual è la differenza tra l'utilizzo di '$ 1' vs' $ ARG1'? –

+0

@JonLin: posso usare un nome più intuitivo, invece di $ 1, $ 2. I numeri sono confusi. – Bhushan

+2

Hai davvero bisogno di sette parametri obbligatori? Un approccio comune sarebbe quello di trasformare il maggior numero possibile in opzioni e fornire un valore predefinito dove ha senso. Rende il tuo script molto più facile da usare se non si deve memorizzare un particolare ordine posizionale per un numero così grande di argomenti, anche se la maggior parte degli utenti sovrascrive i valori predefiniti per la maggior parte del tempo. – tripleee

risposta

45

Il modo più comune di fare che sta assegnando gli argomenti di variabili locali nella funzione, vale a dire:

copy() { 
    local from=${1} 
    local to=${2} 

    # ... 
} 

Un'altra soluzione può essere getopt in stile parsing opzione.

copy() { 
    local arg from to 
    while getopts 'f:t:' arg 
    do 
     case ${arg} in 
      f) from=${OPTARG};; 
      t) to=${OPTARG};; 
      *) return 1 # illegal option 
     esac 
    done 
} 

copy -f /tmp/a -t /tmp/b 

Purtroppo, bash non può gestire le opzioni lunghe che sarebbe essere più leggibile, vale a dire:

copy --from /tmp/a --to /tmp/b 

Per questo, vi sia bisogno di usare il programma esterno getopt (che Penso che abbia un lungo supporto opzionale solo sui sistemi GNU) o implementare il parser di opzioni lunghe a mano, ovvero:

copy() { 
    local from to 

    while [[ ${1} ]]; do 
     case "${1}" in 
      --from) 
       from=${2} 
       shift 
       ;; 
      --to) 
       to=${2} 
       shift 
       ;; 
      *) 
       echo "Unknown parameter: ${1}" >&2 
       return 1 
     esac 

     if ! shift; then 
      echo 'Missing parameter argument.' >&2 
      return 1 
     fi 
    done 
} 

copy --from /tmp/a --to /tmp/b 
.210

Vedi anche: using getopts in bash shell script to get long and short command line options


Si può anche essere pigro, e solo passare i 'variabili' come argomenti alla funzione, vale a dire:

copy() { 
    local "${@}" 

    # ... 
} 

copy from=/tmp/a to=/tmp/b 

e avrete ${from} e ${to} nella funzione come variabili locali.

Basta notare che si applica lo stesso problema di seguito: se una determinata variabile non viene passata, sarà ereditata dall'ambiente padre. Si consiglia di aggiungere una 'linea di sicurezza' come:

copy() { 
    local from to # reset first 
    local "${@}" 

    # ... 
} 

per garantire che ${from} e ${to} sarà impostata quando non è passato.


E se qualcosa molto male è di tuo interesse, si potrebbe anche assegnare gli argomenti come variabili globali quando si richiama la funzione, vale a dire:

from=/tmp/a to=/tmp/b copy 

Allora si potrebbe utilizzare ${from} e ${to} all'interno della funzione copy(). Basta notare che dovresti quindi sempre passare passare tutti i parametri. Altrimenti, una variabile casuale potrebbe infiltrarsi nella funzione.

from= to=/tmp/b copy # safe 
to=/tmp/b copy   # unsafe: ${from} may be declared elsewhere 

Se si dispone di bash 4.1 (credo), si può anche provare a utilizzare array associativi. Ti permetterà di passare argomenti con nome ma è essere brutto. Qualcosa di simile:

args=([from]=/tmp/a [to]=/tmp/b) 
copy args 

E poi nel copy(), avresti bisogno di grab the array.

+0

Il metodo "locale": copia da =/tmp/a a =/tmp/b; sembra molto simile a come si potrebbe chiamare una copia di destinazione e passare due nuove variabili di ambiente (da e verso). Mi piacciono molto le similitudini della sintassi. –

+0

@claytontstanley: sì, quello era l'intento;). –

+0

Uso l'approccio di variabile globale per gli argomenti facoltativi, ma denominare detta variabile dopo la funzione e cancellarla all'interno della funzione dopo che è stata verificata. Per esempio, se ho un prompt facoltativo per 'get_input()', allora userei 'gi_prompt =" blah blah blah "', e nella funzione, 'unset gi_prompt'. Con il namespace, tutto ciò che riguarda la funzione, impedisce perdite e conflitti di denominazione pur consentendo una certa flessibilità. –

0

Gli argomenti vengono inviati alle funzioni come una tupla di singoli elementi, quindi non hanno nomi in quanto tali, solo posizioni. questo permette alcune possibilità interessanti come sotto, ma significa che sei bloccato con $ 1. $ 2, ecc. Per decidere se associarli a nomi migliori, la domanda si riduce a quanto è grande la funzione, e quanto più chiara sarà la lettura del codice. se il suo complesso, quindi mappare i nomi significativi ($ BatchID, $ FirstName, $ SourceFilePath) è una buona idea. per cose semplici però, probabilmente non è necessario. Certianly non mi preoccuperebbe se usi nomi come $ arg1.

ora, se si vuole solo fare eco indietro i parametri, è possibile scorrere su di loro:

for $arg in "[email protected]" 
do 
    echo "$arg" 
done 

solo un fatto di divertimento; a meno che non si sta elaborando una lista, si sono probabilmente interessati a somthing più utile

3

Le funzioni di shell hanno pieno accesso a qualsiasi variabile disponibile nel loro ambito di chiamata, eccetto per quei nomi di variabili che vengono utilizzati come variabili locali all'interno della funzione stessa. Inoltre, qualsiasi variabile non locale impostata all'interno di una funzione è disponibile all'esterno dopo il richiamo della funzione. Si consideri il seguente esempio:

A=aaa 
B=bbb 

echo "A=$A B=$B C=$C" 

example() { 
    echo "example(): A=$A B=$B C=$C" 

    A=AAA 
    local B=BBB 
    C=CCC 

    echo "example(): A=$A B=$B C=$C" 
} 

example 

echo "A=$A B=$B C=$C" 

Questo frammento ha la seguente output:

A=aaa B=bbb C= 
example(): A=aaa B=bbb C= 
example(): A=AAA B=BBB C=CCC 
A=AAA B=bbb C=CCC 

Lo svantaggio evidente di questo approccio è che le funzioni non sono autonomi più e che l'impostazione di una variabile fuori da una funzione potrebbe avere effetti collaterali indesiderati Inoltre renderebbe le cose più difficili se si volesse passare dati a una funzione senza assegnarla prima a una variabile, poiché questa funzione non utilizza più i parametri posizionali.

Il modo più comune per gestire questo è utilizzare variabili locali per argomenti e tutte le variabili temporanee all'interno di una funzione:

example() { 
    local A="$1" B="$2" C="$3" TMP="/tmp" 

    ... 
} 

Questo evita inquinare il namespace shell con variabili di funzione locale.

7

Si può sempre passare le cose attraverso l'ambiente:

#!/bin/sh 
foo() { 
    echo arg1 = $arg1 
    echo arg2 = $arg2 
} 

arg1=banana arg2=apple foo 
0

questo è un argomento vecchio, ma ancora mi piacerebbe condividere la seguente funzione (richiede bash 4). Analizza gli argomenti con nome e imposta le variabili nell'ambiente degli script. Assicurati solo di avere valori predefiniti per tutti i parametri necessari. La dichiarazione di esportazione alla fine potrebbe anche essere solo una valutazione. È fantastico in combinazione con lo spostamento per estendere gli script esistenti che hanno già alcuni parametri posizionali e non si desidera modificare la sintassi, ma aggiungere ancora una certa flessibilità.

parseOptions() 
{ 
    args=("[email protected]") 
    for opt in "${args[@]}"; do 
    if [[ ! "${opt}" =~ .*=.* ]]; then 
     echo "badly formatted option \"${opt}\" should be: option=value, stopping..." 
     return 1 
    fi 
    local var="${opt%%=*}" 
    local value="${opt#*=}" 
    export ${var}="${value}" 
    done 
    return 0 
} 
1

Penso di avere una soluzione per voi. Con alcuni trucchi puoi effettivamente passare i parametri con nome alle funzioni, insieme agli array.

Il metodo che ho sviluppato permette di accedere parametri passati ad una funzione come questa:

testPassingParams() { 

    @var hello 
    l=4 @array anArrayWithFourElements 
    l=2 @array anotherArrayWithTwo 
    @var anotherSingle 
    @reference table # references only work in bash >=4.3 
    @params anArrayOfVariedSize 

    test "$hello" = "$1" && echo correct 
    # 
    test "${anArrayWithFourElements[0]}" = "$2" && echo correct 
    test "${anArrayWithFourElements[1]}" = "$3" && echo correct 
    test "${anArrayWithFourElements[2]}" = "$4" && echo correct 
    # etc... 
    # 
    test "${anotherArrayWithTwo[0]}" = "$6" && echo correct 
    test "${anotherArrayWithTwo[1]}" = "$7" && echo correct 
    # 
    test "$anotherSingle" = "$8" && echo correct 
    # 
    test "${table[test]}" = "works" 
    table[inside]="adding a new value" 
    # 
    # I'm using * just in this example: 
    test "${anArrayOfVariedSize[*]}" = "${*:10}" && echo correct 
} 

fourElements=(a1 a2 "a3 with spaces" a4) 
twoElements=(b1 b2) 
declare -A assocArray 
assocArray[test]="works" 

testPassingParams "first" "${fourElements[@]}" "${twoElements[@]}" "single with spaces" assocArray "and more... " "even more..." 

test "${assocArray[inside]}" = "adding a new value" 

In altre parole, non solo è possibile chiamare i parametri con i loro nomi (che costituisce per un nucleo più leggibile), puoi effettivamente passare array (e riferimenti a variabili - questa caratteristica funziona solo in bash 4.3)! Inoltre, le variabili mappate sono tutte nell'ambito locale, proprio come $ 1 (e altri).

Il codice che rende questo lavoro è piuttosto leggero e funziona sia in bash 3 che in bash 4 (queste sono le uniche versioni con cui l'ho provato). Se sei interessato a più trucchi come questo che rendono lo sviluppo con bash molto più bello e più facile, puoi dare un'occhiata al mio Bash Infinity Framework, il codice sotto è stato sviluppato per quello scopo.

Function.AssignParamLocally() { 
    local commandWithArgs=($1) 
    local command="${commandWithArgs[0]}" 

    shift 

    if [[ "$command" == "trap" || "$command" == "l="* || "$command" == "_type="* ]] 
    then 
     paramNo+=-1 
     return 0 
    fi 

    if [[ "$command" != "local" ]] 
    then 
     assignNormalCodeStarted=true 
    fi 

    local varDeclaration="${commandWithArgs[1]}" 
    if [[ $varDeclaration == '-n' ]] 
    then 
     varDeclaration="${commandWithArgs[2]}" 
    fi 
    local varName="${varDeclaration%%=*}" 

    # var value is only important if making an object later on from it 
    local varValue="${varDeclaration#*=}" 

    if [[ ! -z $assignVarType ]] 
    then 
     local previousParamNo=$(expr $paramNo - 1) 

     if [[ "$assignVarType" == "array" ]] 
     then 
      # passing array: 
      execute="$assignVarName=(\"\${@:$previousParamNo:$assignArrLength}\")" 
      eval "$execute" 
      paramNo+=$(expr $assignArrLength - 1) 

      unset assignArrLength 
     elif [[ "$assignVarType" == "params" ]] 
     then 
      execute="$assignVarName=(\"\${@:$previousParamNo}\")" 
      eval "$execute" 
     elif [[ "$assignVarType" == "reference" ]] 
     then 
      execute="$assignVarName=\"\$$previousParamNo\"" 
      eval "$execute" 
     elif [[ ! -z "${!previousParamNo}" ]] 
     then 
      execute="$assignVarName=\"\$$previousParamNo\"" 
      eval "$execute" 
     fi 
    fi 

    assignVarType="$__capture_type" 
    assignVarName="$varName" 
    assignArrLength="$__capture_arrLength" 
} 

Function.CaptureParams() { 
    __capture_type="$_type" 
    __capture_arrLength="$l" 
} 

alias @trapAssign='Function.CaptureParams; trap "declare -i \"paramNo+=1\"; Function.AssignParamLocally \"\$BASH_COMMAND\" \"\[email protected]\"; [[ \$assignNormalCodeStarted = true ]] && trap - DEBUG && unset assignVarType && unset assignVarName && unset assignNormalCodeStarted && unset paramNo" DEBUG; ' 
alias @param='@trapAssign local' 
alias @reference='_type=reference @trapAssign local -n' 
alias @var='_type=var @param' 
alias @params='_type=params @param' 
alias @array='_type=array @param' 
+0

Grazie. Lo proverò. – Bhushan

+0

@Bhushan Ho appena aggiornato il codice con un'altra aggiunta: i riferimenti di passaggio, introdotti in bash 4.3. Tra le altre cose rende possibile il passaggio di array associativi in ​​funzioni. – niieani

1

Sono stato personalmente sperando di vedere una sorta di sintassi come

func(a b){ 
    echo $a 
    echo $b 
} 

Ma dal momento che non è una cosa, e vedo un bel paio di riferimenti alle variabili globali (non senza l'avvertenza di scoping e conflitti di denominazione), condividerò il mio approccio.

Utilizzando la funzione copy da Michal's answer:

copy(){ 
    cp $from $to 
} 
from=/tmp/a 
to=/tmp/b 
copy 

Questo è male, perché from e to sono tali parole di massima che qualsiasi numero di funzioni potrebbe usare questo. Potresti finire rapidamente con un conflitto di denominazione o una "perdita" tra le tue mani.

letter(){ 
    echo "From: $from" 
    echo "To: $to" 
    echo 
    echo "$1" 
} 

to=Emily 
letter "Hello Emily, you're fired for missing two days of work." 

# Result: 
# From: /tmp/a 
# To: Emily 

# Hello Emily, you're fired for missing two days of work. 

Quindi il mio approccio è quello di "namespace". Diamo un nome alla variabile dopo la funzione e la cancello dopo che la funzione è stata eseguita con esso. Certo, lo uso solo per valori opzionali che hanno valori predefiniti. Altrimenti, uso solo argomenti posizionali.

copy(){ 
    if [[ $copy_from ]] && [[ $copy_to ]]; then 
     cp $copy_from $copy_to 
     unset copy_from copy_to 
    fi 
} 
copy_from=/tmp/a 
copy_to=/tmp/b 
copy # Copies /tmp/a to /tmp/b 
copy # Does nothing, as it ought to 
letter "Emily, you're 'not' re-hired for the 'not' bribe ;)" 
# From: (no /tmp/a here!) 
# To: 

# Emily, you're 'not' re-hired for the 'not' bribe ;) 

vorrei fare un terribile boss ...


In pratica, i miei nomi delle funzioni sono più elaborati di "copia" o "lettera".

L'esempio più recente nella mia memoria è get_input(), che ha gi_no_sort e gi_prompt.

  • gi_no_sort è un valore vero/falso che determina se i suggerimenti di completamento sono ordinati o meno. L'impostazione predefinita è true
  • gi_prompt è una stringa che è ... beh, questo è auto-esplicativo. Il valore predefinito è "".

Gli argomenti effettivi funzione prende sono la fonte dei citati 'suggerimenti completamento' per la richiesta di immissione, e come detto elenco è preso dal [email protected] nella funzione, i "args denominati" sono opzionali [1], e non esiste un modo ovvio per distinguere tra una stringa intesa come completamento e un booleano/prompt-messaggio, o in realtà qualcosa di separato da spazi in bash, per quello [2]; la soluzione di cui sopra ha finito per salvarmi un sacco di problemi.

note:

  1. Quindi, un hard-coded shift e $1, $2, ecc, sono fuori discussione.

  2. E.g. è "0 Enter a command: {1..9} $(ls)" un valore di 0, "Enter a command:" e un set di 1 2 3 4 5 6 7 8 9 <directory contents>? Oppure sono "0", "Enter", "a" e "command:" parte di questo set? Bash assumerà quest'ultimo se ti piace o no.

0

Tutto quello che devi fare è assegnare un nome alle variabili durante il richiamo della funzione.

function test() { 
    echo $a 
} 

a='hello world' test 
#prove variable didnt leak 
echo $a . 

enter image description here

Questo non è solo una caratteristica di funzioni, si potrebbe avere questa funzione in un suo copione e chiamare a='hello world' test.sh e sarebbe lavorare lo stesso


Come un po 'di divertimento in più, puoi combinare questo metodo con argomenti posizionali (ad esempio stai facendo uno script e alcuni utenti potrebbero non conoscere i nomi delle variabili).
Heck, perché non lasciare che abbia anche i valori predefiniti per questi argomenti? Bene, certo, facile!

function test2() { 
    [[ -n "$1" ]] && local a="$1"; [[ -z "$a" ]] && local a='hi' 
    [[ -n "$2" ]] && local b="$2"; [[ -z "$b" ]] && local b='bye' 
    echo $a $b 
} 

#see the defaults 
test2 

#use positional as usual 
test2 '' there 
#use named parameter 
a=well test2 
#mix it up 
b=one test2 nice 

#prove variables didnt leak 
echo $a $b . 

enter image description here

Nota che se test fosse una propria scrittura, non è necessario utilizzare la parola chiave local.