Come ottenere l'ultimo argomento in una funzione / bin / sh


11

Qual è un modo migliore per implementare print_last_arg?

#!/bin/sh

print_last_arg () {
    eval "echo \${$#}"  # this hurts
}

print_last_arg foo bar baz
# baz

(Se questo fosse, diciamo, #!/usr/bin/zshinvece di #!/bin/shsapere cosa fare. Il mio problema è trovare un modo ragionevole per implementarlo #!/bin/sh.)

EDIT: quanto sopra è solo un esempio sciocco. Il mio obiettivo non è quello di stampare l'ultimo argomento, ma piuttosto di avere un modo per fare riferimento all'ultimo argomento all'interno di una funzione shell.


EDIT2: mi scuso per una domanda così poco chiara. Spero di farlo bene questa volta.

Se questo fosse /bin/zshal posto di /bin/sh, potrei scrivere qualcosa di simile

#!/bin/zsh

print_last_arg () {
    local last_arg=$argv[$#]
    echo $last_arg
}

L'espressione $argv[$#]è un esempio di ciò che ho descritto nel mio primo EDIT come un modo per fare riferimento all'ultimo argomento all'interno di una funzione shell .

Pertanto, avrei davvero dovuto scrivere il mio esempio originale in questo modo:

print_last_arg () {
    local last_arg=$(eval "echo \${$#}")   # but this hurts even more
    echo $last_arg
}

... per chiarire che quello che sto cercando è una cosa meno terribile da mettere a destra dell'incarico.

Si noti, tuttavia, che in tutti gli esempi, si accede all'ultimo argomento in modo non distruttivo . IOW, l'accesso all'ultimo argomento lascia inalterati gli argomenti posizionali.


unix.stackexchange.com/q/145522/117549 sottolinea le varie possibilità di #! / bin / sh - puoi restringerlo?
Jeff Schaller

mi piace la modifica, ma forse potresti anche aver notato che c'è una risposta qui che offre un mezzo non distruttivo per fare riferimento all'ultimo argomento ...? Non si deve equiparare var=$( eval echo \${$#})a eval var=\${$#}- i due sono niente uguali.
Mikeserv,

1
Non sono sicuro di ricevere la tua ultima nota, ma quasi tutte le risposte fornite finora non sono distruttive nel senso che conservano gli argomenti dello script in esecuzione. Solo le soluzioni basate shifte set -- ...potrebbero essere distruttive se non utilizzate in funzioni in cui sono anche innocue.
jlliagre,

@jlliagre - ma nel complesso sono ancora distruttivi - richiedono la creazione di contesti usa e getta in modo che possano distruggere per scoprire. ma ... se ottieni comunque un secondo contesto - perché non ottenere quello che ti consente di indicizzare? c'è qualcosa di sbagliato nell'usare lo strumento destinato al lavoro? interpretare le espansioni della shell come input espandibili è il lavoro di eval. e non c'è nulla di significativamente diverso nel fare eval "var=\${$#}"rispetto a var=${arr[evaled index]}tranne che $#è un valore sicuro garantito. perché copiare l'intero set e poi distruggerlo quando potresti semplicemente indicizzarlo direttamente?
Mikeserv,

1
@mikeserv Un ciclo for eseguito nella parte principale della shell lascia invariati tutti gli argomenti. Sono d'accordo sul fatto che il looping di tutti gli argomenti sia molto non ottimizzato, specialmente se migliaia di essi vengono passati alla shell e sono anche d'accordo che l'accesso diretto all'ultimo argomento con l'indice corretto sia la risposta migliore (e non capisco perché sia ​​stato sottoposto a downgrade ) ma oltre a ciò, non c'è nulla di veramente distruttivo e nessun contesto aggiuntivo creato.
jlliagre,

Risposte:


1
eval printf %s${1+"'\n' \"the last arg is \${$#"\}\"}

... o stamperà la stringa the last arg isseguita da uno <spazio> , il valore dell'ultimo argomento e un <newline> finale se c'è almeno 1 argomento, oppure, per zero argomenti, non stamperà nulla.

Se avete fatto:

eval ${1+"lastarg=\${$#"\}}

... allora o assegnereste il valore dell'ultimo argomento alla variabile shell $lastargse c'è almeno 1 argomento, o non fareste nulla. Ad ogni modo, lo faresti in modo sicuro, e dovrebbe essere portatile anche per te, conchiglia Olde Bourne, credo.

Ecco un altro che funzionerebbe in modo simile, anche se richiede di copiare l'intero array arg due volte (e richiede un printfin $PATHper la shell Bourne) :

if   [ "${1+:}" ]
then set "$#" "$@" "$@"
     shift    "$1"
     printf %s\\n "$1"
     shift
fi

I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
terdon

2
Cordiali saluti, il tuo primo suggerimento fallisce sotto la legenda bourne shell con un errore "sostituzione errata" se non sono presenti argomenti. Entrambi i restanti funzionano come previsto.
jlliagre,

@jillagre - grazie. non ero così sicuro di quello in alto - ma ero abbastanza sicuro degli altri due. voleva solo essere dimostrativo di come si potesse accedere agli argomenti per riferimento in linea. preferibilmente una funzione si aprirebbe con qualcosa di simile ${1+":"} returnimmediatamente - perché chi lo vuole fare qualcosa o rischiare effetti collaterali di qualsiasi tipo se non fosse chiamato a nulla? La matematica è abbastanza per lo stesso - se puoi essere sicuro di poter espandere un numero intero positivo puoi farlo evaltutto il giorno. $OPTINDè ottimo per questo.
Mikeserv,

3

Ecco un modo semplicistico:

print_last_arg () {
  if [ "$#" -gt 0 ]
  then
    s=$(( $# - 1 ))
  else
    s=0
  fi
  shift "$s"
  echo "$1"
}

(aggiornato in base al punto di @ cuonglm secondo cui l'originale falliva quando non venivano passati argomenti; questo ora echeggia una riga vuota - elsese lo si desidera modificare tale comportamento nella clausola)


Ciò ha richiesto l'uso di / usr / xpg4 / bin / sh su Solaris; fatemi sapere se Solaris #! / bin / sh è un requisito.
Jeff Schaller

Questa domanda non riguarda uno script specifico che sto cercando di scrivere, ma piuttosto un how-to generale. Sto cercando una buona ricetta per questo. Certo, più portatile, meglio è.
kjo,

la matematica $ (()) non riesce in Solaris / bin / sh; usa qualcosa come: shift `expr $ # - 1` se è richiesto.
Jeff Schaller

in alternativa per Solaris / bin / sh,echo "$# - 1" | bc
Jeff Schaller

1
questo è esattamente quello che volevo scrivere, ma non è riuscito sulla seconda shell che ho provato - NetBSD, che a quanto pare è una shell Almquist che non la supporta :(
Jeff Schaller

3

Dato l'esempio del post iniziale (argomenti posizionali senza spazi):

print_last_arg foo bar baz

Per impostazione predefinita IFS=' \t\n', che ne dici di:

args="$*" && printf '%s\n' "${args##* }"

Per un'espansione più sicura di "$*", impostare IFS (per @ StéphaneChazelas):

( IFS=' ' args="$*" && printf '%s\n' "${args##* }" )

Ma quanto sopra fallirà se i tuoi argomenti posizionali possono contenere spazi. In tal caso, utilizzare questo invece:

for a in "$@"; do : ; done && printf '%s\n' "$a"

Si noti che queste tecniche evitano l'uso evale non hanno effetti collaterali.

Testato su shellcheck.net


1
Il primo fallisce se l'ultimo argomento contiene spazio.
cuonglm,

1
Si noti che anche il primo non funzionava nella shell pre-POSIX
cuonglm

@cuonglm Ben individuato, la tua osservazione corretta è ora incorporata.
AsymLabs

Presuppone anche che il primo carattere di $IFSsia uno spazio.
Stéphane Chazelas,

1
Sarà se non ci sono $ IFS nell'ambiente, altrimenti non specificato. Ma poiché è necessario impostare IFS praticamente ogni volta che si utilizza l'operatore split + glob (lasciare un'espansione non quotata), un approccio con la gestione di IFS è di impostarlo ogni volta che è necessario. Non sarebbe male avere IFS=' 'qui solo per chiarire che viene utilizzato per l'espansione di "$*".
Stéphane Chazelas,

3

Sebbene questa domanda abbia poco più di 2 anni, ho pensato di condividere un'opzione piuttosto compatta.

print_last_arg () {
    echo "${@:${#@}:${#@}}"
}

Eseguiamolo

print_last_arg foo bar baz
baz

Espansione dei parametri della shell Bash .

modificare

Ancora più conciso: echo "${@: -1}"

(Mente lo spazio)

fonte

Testato su macOS 10.12.6 ma dovrebbe anche restituire l'ultimo argomento sulla maggior parte dei gusti * nix disponibili ...

Fa molto meno male ¯\_(ツ)_/¯


Questa dovrebbe essere la risposta accettata. Ancora meglio sarebbe: di echo "${*: -1}"cui shellchecknon si lamentano.
Tom Hale,

4
Questo non funziona con un semplice POSIX sh, ${array:n:m:}è un'estensione. (la domanda menzionava esplicitamente /bin/sh)
ilkkachu,

2

POSIXly:

while [ "$#" -gt 1 ]; do
  shift
done

printf '%s\n' "$1"

(Questo approccio funziona anche nella vecchia shell Bourne)

Con altri strumenti standard:

awk 'BEGIN{print ARGV[ARGC-1]}' "$@"

(Questo non funzionerà con il vecchio awk, che non aveva ARGV)


Dove c'è ancora una shell Bourne, awkpotrebbe anche essere il vecchio awk che non aveva ARGV.
Stéphane Chazelas,

@ StéphaneChazelas: Accetto. Almeno ha funzionato con la versione di Brian Kernighan. Hai qualche ideale?
cuonglm,

Questo cancella gli argomenti. Come tenerli ?.

@BinaryZebra: usa l' awkapproccio, oppure usa altri forloop semplici come altri, oppure passali a una funzione.
cuonglm,

2

Questo dovrebbe funzionare con qualsiasi shell conforme a POSIX e funzionerà anche con la shell Solaris Bourne precedente a POSIX:

do=;for do do :;done;printf "%s\n" "$do"

e qui è una funzione basata sullo stesso approccio:

print_last_arg()
  if [ "$*" ]; then
    for do do :;done
    printf "%s\n" "$do"
  else
    echo
  fi

PS: non dirmi che ho dimenticato le parentesi graffe attorno al corpo della funzione ;-)


2
Hai dimenticato le parentesi graffe attorno al corpo della funzione ;-).

@BinaryZebra Ti avevo avvertito ;-) Non li avevo dimenticati. Le parentesi graffe sono sorprendentemente opzionali qui.
jlliagre,

1
@jlliagre, in effetti, sono stato avvertito ;-): P ...... E: certamente!

Quale parte delle specifiche della sintassi lo consente?
Tom Hale,

@TomHale Le regole grammaticali della shell consentono sia una parentesi mancante in ...in un forciclo che una parentesi graffa attorno a ifun'istruzione utilizzata come corpo della funzione. Vedi for_clausee compound_commandin pubs.opengroup.org/onlinepubs/9699919799 .
jlliagre,

2

Da "Unix - Domande frequenti"

(1)

unset last
if    [ $# -gt 0 ]
then  eval last=\${$#}
fi
echo  "$last"

Se il numero di argomenti potrebbe essere zero, $0verrà assegnato l' argomento zero (di solito il nome dello script) $last. Questa è la ragione del se.

(2)

unset last
for   last
do    :
done
echo  "$last"

(3)

for     i
do
        third_last=$second_last
        second_last=$last
        last=$i
done
echo    "$last"

Per evitare di stampare una riga vuota quando non ci sono argomenti, sostituire il echo "$last"per:

${last+false} || echo "${last}"

Un conteggio argomenti zero è evitato da if [ $# -gt 0 ].

Questa non è una copia esatta di ciò che è nel link nella pagina, sono stati aggiunti alcuni miglioramenti.


0

Questo dovrebbe funzionare su tutti i sistemi con perlinstallato (quindi la maggior parte degli UNICES):

print_last_arg () {
    printf '%s\0' "$@" | perl -0ne 's/\0//;$l=$_;}{print "$l\n"'
}

Il trucco è usare printfper aggiungere un \0argomento dopo ogni shell e quindi perll' -0opzione che imposta il separatore dei record su NULL. Quindi iteriamo sull'input, rimuoviamo \0e salvando ogni riga terminata NULL come $l. Il ENDblocco (che è quello che }{è) verrà eseguito dopo aver letto tutti gli input, quindi stamperà l'ultima "riga": l'ultimo argomento della shell.


@mikeserv ah, vero, non avevo provato con una nuova riga in nessuno tranne l'ultimo argomento. La versione modificata dovrebbe funzionare praticamente per tutto ma probabilmente è troppo contorta per un compito così semplice.
terdon

sì, chiamare un eseguibile esterno (se non fa già parte della logica) è, per me, un po 'pesante. ma se il punto è passare tale argomento a sed, awko perlallora potrebbe essere comunque informazioni preziose. puoi comunque fare lo stesso con sed -zversioni GNU aggiornate.
Mikeserv,

perché sei stato votato verso il basso? questo era buono ... ho pensato?
Mikeserv,

0

Ecco una versione che utilizza la ricorsione. Non ho idea di quanto POSIX sia conforme ...

print_last_arg()
{
    if [ $# -gt 1 ] ; then
        shift
        echo $( print_last_arg "$@" )
    else
        echo "$1"
    fi
}

0

Un concetto semplice, nessuna aritmetica, nessun ciclo, nessuna valutazione , solo funzioni.
Ricorda che la shell Bourne non aveva aritmetica (necessaria all'esterno expr). Se vuoi ottenere un'aritmetica libera, evallibera scelta, questa è un'opzione. La necessità di funzioni significa SVR3 o superiore (nessuna sovrascrittura dei parametri).
Cerca di seguito una versione più robusta con printf.

printlast(){
    shift "$1"
    echo "$1"
}

printlast "$#" "$@"          ### you may use ${1+"$@"} here to allow
                             ### a Bourne empty list of arguments,
                             ### but wait for a correct solution below.

Tale struttura di chiamata printlastè fissata , gli argomenti devono essere impostati nella lista di argomenti shell $1, $2ecc (stack degli argomenti) e la chiamata fatto come dato.

Se l'elenco degli argomenti deve essere modificato, basta impostarli:

set -- o1e t2o t3r f4u
printlast "$#" "$@"

Oppure crea una funzione più semplice da usare ( getlast) che consenta argomenti generici (ma non altrettanto velocemente, gli argomenti vengono passati due volte).

getlast(){ printlast "$#" "$@"; }
getlast o1e t2o t3r f4u

Si noti che gli argomenti (di getlast, o tutti inclusi in $@per printlast) potrebbero avere spazi, nuove righe, ecc. Ma non NUL.

Meglio

Questa versione non viene stampata 0quando l'elenco di argomenti è vuoto e utilizza il printf più robusto (ripiegare su echose esterno printfnon è disponibile per le vecchie shell).

printlast(){ shift  "$1"; printf '%s' "$1"; }
getlast  ()  if     [ $# -gt 0 ]
             then   printlast "$#" "$@"
                    echo    ### optional if a trailing newline is wanted.
             fi
### The {} braces were removed on purpose.

getlast 1 2 3 4    # will print 4

Utilizzando EVAL.

Se la shell Bourne è ancora più vecchia e non ci sono funzioni, o se per qualche motivo l'uso di eval è l'unica opzione:

Per stampare il valore:

if    [ $# -gt 0 ]
then  eval printf "'1%s\n'" \"\$\{$#\}\"
fi

Per impostare il valore in una variabile:

if    [ $# -gt 0 ]
then  eval 'last_arg='\"\$\{$#\}\"
fi

Se ciò deve essere fatto in una funzione, gli argomenti devono essere copiati nella funzione:

print_last_arg () {
    local last_arg                ### local only works on more modern shells.
    eval 'last_arg='\"\$\{$#\}\"
    echo "last_arg3=$last_arg"
}

print_last_arg "$@"               ### Shell arguments are sent to the function.

è meglio ma dice che non usa un ciclo - ma lo fa. una copia dell'array è un ciclo . e con due funzioni utilizza due loop. inoltre - c'è qualche motivo per cui si dovrebbe preferire iterare l'intero array piuttosto che indicizzarlo eval?
Mikeserv,

1
Ne vedi qualcuno for loopo while loop?


1
Secondo il tuo standard, suppongo che tutto il codice abbia loop, anche a echo "Hello"ha loop poiché la CPU deve leggere le posizioni di memoria in un loop. Ancora: il mio codice non ha loop aggiunti.

1
Davvero non mi interessa la tua opinione sul bene o sul male, è già stato smentito diverse volte. Ancora: il mio codice non ha for, whileo untilloop. Vedi qualche for whileo untilciclo scritto nel codice ?. No ?, quindi accetta il fatto, accettalo e impara.

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.