So erhalten Sie das letzte Argument für eine / bin / sh-Funktion


11

Was ist ein besserer Weg, um zu implementieren print_last_arg?

#!/bin/sh

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

print_last_arg foo bar baz
# baz

(Wenn dies beispielsweise wäre, #!/usr/bin/zshanstatt zu #!/bin/shwissen, was zu tun ist. Mein Problem besteht darin, einen vernünftigen Weg zu finden, dies zu implementieren #!/bin/sh.)

EDIT: Das obige ist nur ein dummes Beispiel. Mein Ziel ist es nicht, das letzte Argument zu drucken , sondern eine Möglichkeit zu haben, auf das letzte Argument innerhalb einer Shell-Funktion zu verweisen.


EDIT2: Ich entschuldige mich für eine so unklar formulierte Frage. Ich hoffe, es diesmal richtig zu machen.

Wenn dies /bin/zshstattdessen wäre /bin/sh, könnte ich so etwas schreiben

#!/bin/zsh

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

Der Ausdruck $argv[$#]ist ein Beispiel für das, was ich in meiner ersten EDIT beschrieben habe , um auf das letzte Argument innerhalb einer Shell-Funktion zu verweisen .

Deshalb hätte ich mein ursprüngliches Beispiel wirklich so schreiben sollen:

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

... um klar zu machen, dass das, wonach ich suche, eine weniger schreckliche Sache ist, die ich rechts von der Aufgabe setzen muss.

Beachten Sie jedoch, dass in allen Beispielen zerstörungsfrei auf das letzte Argument zugegriffen wird . IOW, der Zugriff auf das letzte Argument lässt die Positionsargumente insgesamt unberührt.


unix.stackexchange.com/q/145522/117549 weist auf die verschiedenen Möglichkeiten von #! / bin / sh hin - können Sie es eingrenzen ?
Jeff Schaller

Ich mag die Bearbeitung, aber vielleicht haben Sie auch bemerkt, dass es hier eine Antwort gibt, die ein zerstörungsfreies Mittel bietet, um auf das letzte Argument zu verweisen ...? Sie sollten nicht gleichsetzen var=$( eval echo \${$#})zu eval var=\${$#}- die beiden nichts gleicht dem anderen .
Mikeserv

1
Ich bin mir nicht sicher, ob ich Ihre letzte Notiz erhalte, aber fast alle bisher gegebenen Antworten sind in dem Sinne zerstörungsfrei, dass sie die laufenden Skriptargumente beibehalten. Nur shiftund set -- ...basierte Lösungen können destruktiv sein, wenn sie nicht in Funktionen verwendet werden, in denen sie auch harmlos sind.
Jlliagre

@jlliagre - aber sie sind im Wesentlichen immer noch destruktiv - sie müssen Einwegkontexte erstellen, damit sie zerstören können, um sie zu entdecken. aber ... wenn Sie trotzdem einen zweiten Kontext erhalten - warum nicht einfach den, mit dem Sie indizieren können? Stimmt etwas nicht mit dem für den Job vorgesehenen Werkzeug? Das Interpretieren von Shell-Erweiterungen als erweiterbare Eingabe ist Aufgabe von eval. und es ist nichts wesentlich anderes daran zu tun, eval "var=\${$#}"als var=${arr[evaled index]}dass dies $#ein garantierter sicherer Wert ist. Warum das ganze Set kopieren und dann zerstören, wenn Sie es einfach direkt indizieren können?
Mikeserv

1
@mikeserv Eine for-Schleife im Hauptteil der Shell lässt alle Argumente unverändert. Ich bin damit einverstanden, dass das Schleifen aller Argumente sehr unoptimiert ist, insbesondere wenn Tausende von ihnen an die Shell übergeben werden, und ich stimme auch zu, dass der direkte Zugriff auf das letzte Argument mit dem richtigen Index die beste Antwort ist (und ich verstehe nicht, warum es abgelehnt wurde) ) aber darüber hinaus gibt es nichts wirklich Destruktives und keinen zusätzlichen Kontext.
Jlliagre

Antworten:


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

... gibt entweder die Zeichenfolge the last arg isgefolgt von einem <Leerzeichen> , dem Wert des letzten Arguments und einem nachfolgenden <Newline> aus, wenn mindestens 1 Argument vorhanden ist, oder gibt bei Nullargumenten überhaupt nichts aus.

Wenn du. .. getan hast:

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

... dann würden Sie entweder der Shell-Variablen den Wert des letzten Arguments zuweisen, $lastargwenn mindestens 1 Argument vorhanden ist, oder Sie würden überhaupt nichts tun. In jedem Fall würden Sie es sicher tun, und es sollte sogar für die Olde Bourne-Shell tragbar sein, denke ich.

Hier ist eine andere, die ähnlich funktionieren würde, obwohl das gesamte arg-Array zweimal kopiert werden muss (und ein printfIn $PATHfür die Bourne-Shell erforderlich ist ) :

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

Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Terdon

2
Zu Ihrer Information, Ihr erster Vorschlag schlägt unter der alten Bourne-Shell mit einem "Bad Substitution" -Fehler fehl, wenn keine Argumente vorhanden sind. Beide verbleibenden funktionieren wie erwartet.
Jlliagre

@ Jillagre - danke. Ich war mir bei diesem Top nicht so sicher - aber bei den anderen beiden war ich mir ziemlich sicher. Es sollte nur demonstrieren, wie auf Argumente durch Inline-Referenz zugegriffen werden kann. vorzugsweise würde sich eine Funktion mit so etwas wie ${1+":"} returnsofort öffnen - denn wer möchte, dass sie etwas tut oder Nebenwirkungen jeglicher Art riskiert, wenn sie umsonst aufgerufen wird? Mathe ist genauso schön - wenn Sie sicher sein können, dass Sie eine positive ganze Zahl erweitern können, können Sie das den evalganzen Tag. $OPTINDist toll dafür.
Mikeserv

3

Hier ist ein vereinfachter Weg:

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

(aktualisiert basierend auf dem Punkt von @ cuonglm, dass das Original fehlgeschlagen ist, wenn keine Argumente übergeben wurden; dies gibt jetzt eine leere Zeile wieder - ändern Sie dieses Verhalten in der elseKlausel, falls gewünscht)


Dies erforderte die Verwendung von / usr / xpg4 / bin / sh unter Solaris. Lassen Sie mich wissen, ob Solaris #! / bin / sh erforderlich ist.
Jeff Schaller

Bei dieser Frage geht es nicht um ein bestimmtes Skript, das ich zu schreiben versuche, sondern um eine allgemeine Anleitung. Ich suche ein gutes Rezept dafür. Je tragbarer, desto besser.
Kjo

Die $ (()) -Mathematik schlägt in Solaris / bin / sh fehl. Verwenden Sie etwas wie: verschieben Sie `expr $ # - 1`, wenn dies erforderlich ist.
Jeff Schaller

alternativ für Solaris / bin / sh,echo "$# - 1" | bc
Jeff Schaller

1
Genau das wollte ich schreiben, aber es ist bei der zweiten Shell, die ich ausprobiert habe, fehlgeschlagen - NetBSD, anscheinend eine Almquist-Shell, die es nicht unterstützt :(
Jeff Schaller

3

Am Beispiel des Eröffnungspostens (Positionsargumente ohne Leerzeichen):

print_last_arg foo bar baz

Für den Standard IFS=' \t\n', wie etwa:

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

Für eine sicherere Erweiterung von "$*"setzen Sie IFS (per @ StéphaneChazelas):

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

Das Obige schlägt jedoch fehl, wenn Ihre Positionsargumente Leerzeichen enthalten können. Verwenden Sie in diesem Fall stattdessen Folgendes:

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

Beachten Sie, dass diese Techniken die Verwendung vermeiden evalund keine Nebenwirkungen haben.

Getestet bei shellcheck.net


1
Das erste Argument schlägt fehl, wenn das letzte Argument Leerzeichen enthält.
Cuonglm

1
Beachten Sie, dass die erste auch nicht in Pre-POSIX-Shell
funktioniert

@cuonglm Gut gesehen, Ihre korrekte Beobachtung ist jetzt enthalten.
AsymLabs

Es wird auch angenommen, dass das erste Zeichen $IFSein Leerzeichen ist.
Stéphane Chazelas

1
Dies ist der Fall, wenn sich in der Umgebung kein $ IFS befindet, sofern nicht anders angegeben. Da Sie IFS jedoch praktisch jedes Mal festlegen müssen, wenn Sie den Operator split + glob verwenden (lassen Sie eine Erweiterung nicht in Anführungszeichen), besteht ein Ansatz beim Umgang mit IFS darin, es festzulegen, wann immer Sie es benötigen. Es würde nicht schaden, IFS=' 'hier zu sein, nur um klar zu machen, dass es für die Erweiterung von verwendet wird "$*".
Stéphane Chazelas

2

POSIXly:

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

printf '%s\n' "$1"

(Dieser Ansatz funktioniert auch in der alten Bourne-Shell)

Mit anderen Standardwerkzeugen:

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

(Dies funktioniert nicht mit alten awk, die nicht hatten ARGV)


Wo es noch eine Bourne-Shell gibt, awkkönnte auch die alte Awk sein, die es nicht gab ARGV.
Stéphane Chazelas

@ StéphaneChazelas: Stimme zu. Zumindest funktionierte es mit Brian Kernighans eigener Version. Hast du ein Ideal?
Cuonglm

Dadurch werden die Argumente gelöscht. Wie kann man sie behalten?

@BinaryZebra: Verwenden Sie den awkAnsatz oder eine andere einfache forSchleife wie andere oder übergeben Sie sie an eine Funktion.
Cuonglm

2

Dies sollte mit jeder POSIX-kompatiblen Shell funktionieren und auch mit der älteren Solaris Bourne-Shell vor POSIX:

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

und hier ist eine Funktion, die auf dem gleichen Ansatz basiert:

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

PS: Sag mir nicht, dass ich die geschweiften Klammern um den Funktionskörper vergessen habe ;-)


2
Du hast die geschweiften Klammern um den Funktionskörper vergessen ;-).

@BinaryZebra Ich habe dich gewarnt ;-) Ich habe sie nicht vergessen. Die Zahnspangen sind hier überraschend optional.
Jlliagre

1
@jlliagre Ich wurde tatsächlich gewarnt ;-): P ...... Und: auf jeden Fall!

Welcher Teil der Syntaxspezifikation erlaubt dies?
Tom Hale

@TomHale Die Shell-Grammatikregeln erlauben sowohl das Fehlen einer in ...in einer forSchleife als auch eine geschweifte Klammer um eine ifAnweisung, die als Funktionskörper verwendet wird. Siehe for_clauseund compound_commandin pubs.opengroup.org/onlinepubs/9699919799 .
Jlliagre

2

Aus "Unix - Häufig gestellte Fragen"

(1)

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

Wenn die Anzahl der Argumente Null sein könnte, wird dem Argument Null $0(normalerweise der Name des Skripts) zugewiesen $last. Das ist der Grund für das Wenn.

(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"

Um zu vermeiden, dass eine leere Zeile gedruckt wird, wenn keine Argumente vorhanden sind, ersetzen Sie das echo "$last"for:

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

Eine Anzahl von Nullargumenten wird durch vermieden if [ $# -gt 0 ].

Dies ist keine exakte Kopie dessen, was auf der Seite verlinkt ist. Einige Verbesserungen wurden hinzugefügt.


2

Obwohl diese Frage etwas mehr als 2 Jahre alt ist, dachte ich, ich würde eine etwas kompakte Option teilen.

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

Lass es uns laufen

print_last_arg foo bar baz
baz

Erweiterung der Bash- Shell-Parameter .

Bearbeiten

Noch prägnanter: echo "${@: -1}"

(Achten Sie auf den Raum)

Quelle

Getestet unter macOS 10.12.6, sollte aber auch das letzte Argument für die meisten verfügbaren * nix-Varianten zurückgeben ...

Tut viel weniger weh ¯\_(ツ)_/¯


Dies sollte die akzeptierte Antwort sein. Noch besser wäre: echo "${*: -1}"worüber man shellchecksich nicht beschweren wird.
Tom Hale

4
Dies funktioniert nicht mit einem einfachen POSIX sh, ${array:n:m:}ist eine Erweiterung. (Die Frage wurde ausdrücklich erwähnt /bin/sh)
Ilkkachu

0

Dies sollte auf allen perlinstallierten Systemen funktionieren (daher die meisten UNICES):

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

Der Trick besteht darin printf, \0nach jedem Shell-Argument und dann perlden -0Schalter einzufügen, der das Datensatztrennzeichen auf NULL setzt. Dann iterieren wir über die Eingabe, entfernen die \0und speichern jede NULL-terminierte Zeile als $l. Der ENDBlock (das ist, was der }{ist) wird ausgeführt, nachdem alle Eingaben gelesen wurden, und gibt die letzte "Zeile" aus: das letzte Shell-Argument.


@mikeserv ah, stimmt, ich hatte in keinem anderen als dem letzten Argument mit einem Zeilenumbruch getestet. Die bearbeitete Version sollte für so ziemlich alles funktionieren, ist aber für eine so einfache Aufgabe wahrscheinlich zu kompliziert.
Terdon

Ja, das Aufrufen einer externen ausführbaren Datei (wenn dies nicht bereits Teil der Logik ist) ist für mich ein wenig hartnäckig. aber wenn der Punkt ist dieses Argument weitergeben zu sed, awkoder perldann könnte es trotzdem wertvolle Informationen sein. Trotzdem können Sie dies auch sed -zfür aktuelle GNU-Versionen tun .
Mikeserv

Warum wurdest du herabgestimmt? das war gut ... dachte ich?
Mikeserv

0

Hier ist eine Version mit Rekursion. Keine Ahnung, wie POSIX-konform dies ist ...

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

0

Ein einfaches Konzept, keine Arithmetik, keine Schleife, keine Auswertung , nur Funktionen.
Denken Sie daran, dass die Bourne-Shell keine Arithmetik hatte (extern benötigt expr). Wenn Sie eine arithmetikfreie evalWahl treffen möchten , ist dies eine Option. Benötigte Funktionen bedeutet SVR3 oder höher (kein Überschreiben von Parametern).
Unten finden Sie eine robustere Version mit 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.

Diese Struktur Anruf printlastwird fixiert , die Argumente in der Liste der Argumente Schale festgelegt werden müssen $1, $2usw. (das Argument stack) und der Anruf wird als gegeben getan.

Wenn die Liste der Argumente geändert werden muss, legen Sie sie einfach fest:

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

Oder erstellen Sie eine benutzerfreundlichere Funktion ( getlast), die generische Argumente zulässt (aber nicht so schnell, Argumente werden zweimal übergeben).

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

Bitte beachten Sie, dass Argumente (von getlastoder alle in $@printlast enthaltenen) Leerzeichen, Zeilenumbrüche usw. enthalten können, jedoch nicht NUL.

Besser

Diese Version wird nicht gedruckt, 0wenn die Liste der Argumente leer ist, und verwendet das robustere printf (greifen Sie auf zurück, echowenn printffür alte Shells kein externes verfügbar ist).

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

EVAL verwenden.

Wenn die Bourne-Shell noch älter ist und keine Funktionen vorhanden sind oder wenn aus irgendeinem Grund die Verwendung von eval die einzige Option ist:

So drucken Sie den Wert:

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

So legen Sie den Wert in einer Variablen fest:

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

Wenn dies in einer Funktion erfolgen muss, müssen die Argumente in die Funktion kopiert werden:

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.

es ist besser, aber es heißt, es wird keine Schleife verwendet - aber es tut es. Eine Array-Kopie ist eine Schleife . und mit zwei Funktionen werden zwei Schleifen verwendet. Auch - gibt es einen Grund, warum das Iterieren des gesamten Arrays der Indizierung vorgezogen werden sollte eval?
Mikeserv

1
Sehen Sie welche for loopoder while loop?


1
Nach Ihrem Standard hat der gesamte Code Schleifen, sogar a echo "Hello"hat Schleifen, da die CPU Speicherstellen in einer Schleife lesen muss. Nochmals: Mein Code hat keine zusätzlichen Schleifen.

1
Ihre Meinung von gut oder schlecht ist mir wirklich egal, sie hat sich bereits mehrmals als falsch erwiesen. Auch hier: mein Code hat keine for, whileoder untilSchleifen. Sehen Sie eine for whileoder eine untilSchleife im Code?. Nein?, Dann akzeptiere einfach die Tatsache, nimm sie an und lerne.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.