Una funzione bash che prende argomento come altre lingue?


17

Ho una funzione bash per impostare in $PATHquesto modo -

assign-path()
{
    str=$1
    # if the $PATH is empty, assign it directly.
    if [ -z $PATH ]; then
        PATH=$str;
    # if the $PATH does not contain the substring, append it with ':'.
    elif [[ $PATH != *$str* ]]; then
        PATH=$PATH:$str;
    fi
}

Ma il problema è che devo scrivere funzioni diverse per variabili diverse (ad esempio un'altra funzione $CLASSPATHcome like, assign-classpath()ecc.). Non sono riuscito a trovare un modo per passare l'argomento alla funzione bash in modo che possa accedervi come riferimento.

Sarebbe meglio se avessi qualcosa di simile -

assign( bigstr, substr )
{
    if [ -z bigstr ]; then
        bigstr=substr;
    elif [[ bigstr != *str* ]]; then
        bigstr=bigstr:substr;
    fi
}

Qualche idea, come ottenere qualcosa come sopra in bash?


Quali "altre lingue"?
Choroba,

bene, ho provato a dire se bash permette "passa per riferimento" come in c / java ecc.
ramgorur

1
assign-path /abcnon accodare /abca PATH se $ PATH contiene già /abc/def, /abcd, /def/abcecc Soprattutto non è possibile aggiungere /binse PATH contiene già /usr/bin.
miracle173

@ miracle173 - questo è vero, quello che dovete fare è diviso $PATHe la prova negate contro i tuoi argomenti come: add=/bin dir=/usr/bin ; [ -z "${dir%"$add"}" ] || dir="${dir}:${add}". Nella mia risposta lo faccio in questo modo con tutti gli argomenti che vuoi solo usare IFS=:.
Mikeserv,

In relazione al problema specifico (istanza) dell'aggiunta di valori agli elenchi separati da due punti: Come posso aggiungere in modo pulito  $PATH?  e Aggiungi directory a $PATHse non è già presente (su Super User ).
Scott,

Risposte:


17

In bashpuoi usare ${!varname}per espandere la variabile a cui fa riferimento il contenuto di un altro. Per esempio:

$ var=hello
$ foo () { echo "${!1}"; }
$ foo var
hello

Dalla pagina man:

${!prefix*}
${!prefix@}
       Names matching prefix.  Expands to the names of variables whose names
       begin with prefix, separated by the first character of the IFS special
       variable.  When @ is used  and the expansion appears within double quotes,
       each variable name expands to a separate word.

Inoltre, per impostare una variabile a cui fa riferimento il contenuto (senza i pericoli di eval), è possibile utilizzare declare. Per esempio:

$ var=target
$ declare "$var=hello"
$ echo "$target"
hello

Quindi, potresti scrivere la tua funzione in questo modo (fai attenzione perché se usi declareuna funzione, devi dare -go la variabile sarà locale):

shopt -s extglob

assign()
{
  target=$1
  bigstr=${!1}
  substr=$2

  if [ -z "$bigstr" ]; then
    declare -g -- "$target=$substr"
  elif [[ $bigstr != @(|*:)$substr@(|:*) ]]; then
    declare -g -- "$target=$bigstr:$substr"
  fi
}

E usalo come:

assign PATH /path/to/binaries

Si noti che ho anche corretto un bug in cui se substrè già una sottostringa di uno dei membri separati dai due punti bigstr, ma non il suo membro, non sarebbe stato aggiunto. Ad esempio, ciò consentirebbe l'aggiunta /bina una PATHvariabile già contenente /usr/bin. Usa gli extglobinsiemi per abbinare l'inizio / fine della stringa o i due punti, quindi qualsiasi altra cosa. Senza extglob, l'alternativa sarebbe:

[[ $bigstr != $substr && $bigstr != *:$substr &&
   $bigstr != $substr:* && $bigstr != *:$substr:* ]]

-gin declarenon è disponibile nella versione precedente di bash, esiste un modo per renderlo compatibile con le versioni precedenti?
Ramgorur,

2
@ramgorur, potresti usarlo exportper metterlo nel tuo ambiente (a rischio di sovrascrivere qualcosa di importante) o eval(vari problemi tra cui la sicurezza se non stai attento). Se si utilizza evalsi dovrebbe essere ok, se lo fate come eval "$target=\$substr". Se si dimentica il \ pensiero, eseguirà potenzialmente un comando se c'è uno spazio nel contenuto di substr.
Graeme,

9

Nuovo in bash 4.3, è l' -nopzione per declare& local:

func() {
    local -n ref="$1"
    ref="hello, world"
}

var='goodbye world'
func var
echo "$var"

Che stampa hello, world.


L'unico problema con namerefs in Bash è che non puoi avere un nameref (in una funzione per esempio) che fa riferimento a una variabile (al di fuori della funzione) con lo stesso nome del nameref stesso. Questo potrebbe essere risolto per la versione 4.5.
Kusalananda

2

È possibile utilizzare evalper impostare un parametro. Una descrizione di questo comando è disponibile qui . Il seguente utilizzo di evalè errato:

sbagliato(){
  valutazione $ 1 = $ 2
}

Per quanto riguarda la valutazione aggiuntiva evaldovresti usare

assegnare(){
  eval $ 1 = '$ 2'
}

Controlla i risultati dell'utilizzo di queste funzioni:

$ X1 = '$ X2'
$ X2 = '$ X3'
$ X3 = 'xxx'
$ 
$ echo: $ X1:
: $ X2:
$ echo: $ X2:
: $ X3:
$ echo: $ X3:
: Xxx:
$ 
$ errato Y $ X1
$ echo: $ Y:
: $ X3:
$ 
$ assegna Y $ X1
$ echo: $ Y:
: $ X2:
$ 
$ assegnare Y "ciao mondo"
$ echo: $ Y:
: ciao mondo:
$ # quanto segue potrebbe essere inaspettato
$ assegna Z $ Y
$ echo ": $ Z:"
: Ciao:
$ # quindi devi citare il secondo argomento se è una variabile
$ assegna Z "$ Y"
$ echo ": $ Z:"
: ciao mondo:

Ma puoi raggiungere il tuo obiettivo senza l'utilizzo di eval. Preferisco questo modo che è più semplice.

La seguente funzione effettua la sostituzione nel modo giusto (spero)

aumentare(){
  CORRENTE locale = $ 1
  AUGMENT locale = $ 2
  NOVITÀ locale
  if [[-z $ CURRENT]]; poi
    NEW = $ AUGMENT
  elif [[! (($ CURRENT = $ AUGMENT) || ($ CURRENT = $ AUGMENT: *) || \
    ($ CURRENT = *: $ AUGMENT) || ($ CURRENT = *: $ AUGMENT: *))]]; poi
    NEW = $ CORRENTE: $ AUGMENT
  altro
    NUOVO = $ CORRENTE
    fi
  echo "$ NUOVO"
}

Controllare il seguente output

augment / usr / bin / bin
/ Usr / bin: bin /

augment / usr / bin: / bin / bin
/ Usr / bin: bin /

augment / usr / bin: / bin: / usr / local / bin / bin
/ Usr / bin: / bin: / usr / local / bin

augment / bin: / usr / bin / bin
/ Bin: / usr / bin

augment / bin / bin
/bidone


augment / usr / bin: / bin
/ Usr / bin :: / bin

augment / usr / bin: / bin: / bin
/ Usr / bin: bin /:

augment / usr / bin: / bin: / usr / local / bin: / bin
/ Usr / bin: / bin: / usr / local / bin:

augment / bin: / usr / bin: / bin
/ Bin: / usr / bin:

augment / bin: / bin
/bidone:


aumento: / bin
::/bidone


aumenta "/ usr lib" "/ usr bin"
/ usr lib: / usr bin

augment "/ usr lib: / usr bin" "/ usr bin"
/ usr lib: / usr bin

Ora puoi utilizzare la augmentfunzione nel modo seguente per impostare una variabile:

PATH = `aumenta PATH / bin`
CLASSPATH = `aumenta CLASSPATH / bin`
LD_LIBRARY_PATH = `aumenta LD_LIBRARY_PATH / usr / lib`

Anche la tua affermazione di valutazione è sbagliata. Questo, ad esempio: v='echo "OHNO!" ; var' ; l=val ; eval $v='$l' - farebbe eco a "OHNO!" Prima di assegnare var. Potresti "$ {v ## * [;" $ IFS "]} = '$ l'" per assicurarti che la stringa non possa espandersi in qualcosa che non verrà valutato con =.
mikeserv

@mikeserv grazie per il tuo commento ma penso che non sia un esempio valido. Il primo argomento dello script di assegnazione dovrebbe essere un nome di variabile o una variabile che contiene un nome di variabile utilizzato sul lato sinistro =dell'istruzione di assegnazione. Puoi obiettare che il mio script non controlla l'argomento. Questo è vero. Non controllo nemmeno se ci sono argomenti o se il numero di argomenti è valido. Ma questo era per intenzione. L'OP può aggiungere tali controlli se lo desidera.
miracle173,

@mikeserv: Penso che la tua proposta di trasformare silenziosamente il primo argomento in un nome di variabile valido non sia una buona idea: 1) viene impostata / sovrascritta una variabile che non è stata progettata dall'utente. 2) l'errore è nascosto all'utente della funzione. Non è mai una buona idea. Uno dovrebbe semplicemente generare un errore se ciò accade.
miracle173,

@mikeserv: è interessante quando si vuole usare la propria variabile v(meglio il suo valore) come secondo argomento della funzione di assegnazione. Quindi il suo valore dovrebbe essere sul lato destro di un'assegnazione. È necessario citare l'argomento della funzione assign. Ho aggiunto questa sottigliezza al mio post.
miracle173,

Probabilmente vero - e in realtà non stai usando eval nel tuo ultimo esempio - il che è saggio - quindi non importa davvero. Ma quello che sto dicendo è che qualsiasi codice che utilizza eval e accetta l'input dell'utente è intrinsecamente rischioso - e se hai usato il tuo esempio, potrei fare la funzione progettata per cambiare il mio percorso per cambiare il mio percorso con poco sforzo.
Mikeserv,

2

Con alcuni trucchi puoi effettivamente passare parametri nominati alle funzioni, insieme ad array (testati in bash 3 e 4).

Il metodo che ho sviluppato ti consente di accedere ai parametri passati a 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 puoi chiamare i tuoi parametri con i loro nomi (che compensano un nucleo più leggibile), ma puoi anche passare array (e riferimenti a variabili - questa funzionalità funziona solo in bash 4.3 però)! Inoltre, le variabili mappate sono tutte nell'ambito locale, proprio come $ 1 (e altre).

Il codice che rende questo lavoro abbastanza leggero e funziona sia in bash 3 che 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 semplice, puoi dare un'occhiata al mio Bash Infinity Framework , il codice qui sotto è stato sviluppato a tale 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\" \"\$@\"; [[ \$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'

1
assign () 
{ 
    if [ -z ${!1} ]; then
        eval $1=$2
    else
        if [[ ${!1} != *$2* ]]; then
            eval $1=${!1}:$2
        fi
    fi
}

$ echo =$x=
==
$ assign x y
$ echo =$x=
=y=
$ assign x y
$ echo =$x=
=y=
$ assign x z
$ echo =$x=
=y:z=

Questo va bene?


ciao, ho provato a farlo come il tuo, ma non funziona, scusami per essere un principiante. Potresti dirmi cosa c'è che non va in questo script ?
Ramgorur,

1
L'utilizzo di evalè suscettibile all'esecuzione arbitraria di comandi.
Chris Down,

Hai un'idea per rendere le evallinee più sicure? Normalmente, quando penso di aver bisogno di eval, decido di non usare * sh e passare invece a una lingua diversa. D'altra parte, usando questo negli script per aggiungere voci ad alcune variabili tipo PATH, verrà eseguito con costanti di stringa e non input dell'utente ...

1
puoi fare in evalsicurezza - ma ci vuole molto pensiero. Se stai solo cercando di fare riferimento a un parametro, vorresti fare qualcosa del genere: in eval "$1=\"\$2\""questo modo al eval'sprimo passaggio valuta solo $ 1 e al secondo valore = "$ 2". Ma devi fare qualcos'altro - questo non è necessario qui.
Mikeserv,

In realtà, anche il mio commento sopra è sbagliato. Devi farlo "${1##*[;"$IFS"]}=\"\$2\""- e anche questo non ha garanzia. Or eval "$(set -- $1 ; shift $(($#-1)) ; echo $1)=\"\$2\"". Non è facile.
Mikeserv,

1

Gli argomenti nominati non sono semplicemente il modo in cui è stata progettata la sintassi di Bash. Bash è stato progettato per essere un miglioramento iterativo sulla shell Bourne. Come tale, deve garantire che certe cose funzionino tra le due shell il più possibile. Quindi non è pensato per essere più facile da scrivere nel complesso, è solo per essere migliore di Bourne, garantendo allo stesso tempo che se si prende uno script da un ambiente Bourne al bashpiù semplice possibile. Ciò non è banale poiché molte conchiglie trattano ancora Bourne come uno standard di fatto. Dal momento che le persone scrivono i loro copioni per essere compatibili con Bourne (per questa portabilità) la necessità rimane in vigore ed è improbabile che cambi mai.

Probabilmente stai meglio guardando uno script di shell diverso (come pythono qualcosa del genere) se è del tutto fattibile. Se stai affrontando i limiti di una lingua, devi iniziare a utilizzare una nuova lingua.


Forse all'inizio della bashvita questo era vero. Ma ora sono previste disposizioni specifiche. Le variabili di riferimento complete sono ora disponibili in bash 4.3- vedi la risposta di derobert .
Graeme,

E se guardi qui, vedrai che puoi fare questo tipo di cose davvero facilmente anche con solo il codice portatile POSIX: unix.stackexchange.com/a/120531/52934
mikeserv

1

Con la shsintassi standard (funzionerebbe bashe non solo in bash), potresti fare:

assign() {
  eval '
    case :${'"$1"'}: in
      (::) '"$1"'=$2;;   # was empty, copy
      (*:"$2":*) ;;      # already there, do nothing
      (*) '"$1"'=$1:$2;; # otherwise, append with a :
    esac'
}

Come per le soluzioni utilizzando bashs' declare, e' al sicuro fintanto che $1contiene un nome di variabile valido.


0

SU ARG NOMATE:

Questo è fatto in modo molto semplice e bashnon è affatto necessario: questo è il comportamento di base specificato dall'assegnazione POSIX tramite espansione dei parametri:

: ${PATH:=this is only assigned to \$PATH if \$PATH is null or unset}

Per provare in modo simile a @Graeme, ma in modo portatile:

_fn() { echo "$1 ${2:-"$1"} $str" ; }

% str= ; _fn "${str:=hello}"
> hello hello hello

E lì faccio solo str=per assicurarmi che abbia un valore nullo, perché l'espansione dei parametri ha la protezione integrata contro la riassegnazione dell'ambiente di shell in linea se è già impostata.

SOLUZIONE:

Per il tuo problema specifico, non credo che gli argomenti con nome siano necessari, sebbene siano certamente possibili. Usa $IFSinvece:

assign() { oFS=$IFS ; IFS=: ; add=$* 
    set -- $PATH ; for p in $add ; do { 
        for d ; do [ -z "${d%"$p"}" ] && break 
        done ; } || set -- $* $p ; done
    PATH= ; echo "${PATH:="$*"}" ; IFS=$oFS
}

Ecco cosa ottengo quando lo eseguo:

% PATH=/usr/bin:/usr/yes/bin
% assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/nope/bin \
    /usr/bin \
    /nope/usr/bin \
    /usr/nope/bin

> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

% echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

% dir="/some crazy/dir"
% p=`assign /usr/bin /usr/bin/new "$dir"`
% echo "$p" ; echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin:/some crazy/dir:/usr/bin/new
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin:/some crazy/dir:/usr/bin/new

Notate che ha aggiunto solo gli argomenti che non erano già presenti $PATHo che venivano prima? O anche che ci sono voluti più di un argomento? $IFSè utile.


ciao, non sono riuscito a seguirlo, quindi potresti per favore elaborarlo un po 'di più? Grazie.
Ramgorur,

Lo sto già facendo ... Ancora qualche momento, per favore ...
Mikeserv,

@ramgorur Qualcosa di meglio? Mi dispiace, ma la vita reale si è intromessa e mi ci è voluto un po 'più di quanto mi aspettassi per finire la scrittura.
Mikeserv,

lo stesso anche qui, ceduto alla vita reale. Sembra che ci siano molti approcci diversi per codificare questa cosa, permettimi di darmi un momento per accontentarmi di quello migliore.
Ramgorur,

@ramgorur - certo, volevo solo assicurarmi di non lasciarti sospeso. A proposito di questo: scegli quello che vuoi, amico. Dirò che nessuna delle altre risposte che vedo è una soluzione concisa, portatile o robusta come assignqui. Se hai domande su come funziona, sarei felice di rispondere. E a proposito, se vuoi davvero argomenti con nome, potresti voler guardare questa mia altra risposta in cui mostro come dichiarare una funzione chiamata per argomenti di un'altra funzione: unix.stackexchange.com/a/120531/52934
mikeserv

-2

Non riesco a trovare niente come rubino, pitone, ecc. Ma questo mi sembra più vicino

foo() {
  BAR="$1"; BAZ="$2"; QUUX="$3"; CORGE="$4"
  ...
}

La leggibilità è migliore secondo me, 4 righe sono eccessive per dichiarare i nomi dei parametri. Sembra anche più vicino alle lingue moderne.


(1) La domanda riguarda le funzioni della shell. La tua risposta presenta una funzione di shell scheletro. Oltre a ciò, la tua risposta non ha nulla a che fare con la domanda. (2) Pensi che aumenti la leggibilità per prendere righe separate e concatenarle in una riga, con punti e virgola come separatori? Credo che tu l'abbia indietro; che il tuo stile a una riga è  meno leggibile rispetto allo stile a più righe.
Scott,

Se il punto è ridurre le parti non necessarie, allora perché le virgolette e i punti e virgola? a=$1 b=$2 ...funziona altrettanto bene.
ilkkachu,
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.