Jak uzyskać ostatni argument do funkcji / bin / sh

11

Jaki jest lepszy sposób wdrożenia print_last_arg?

#!/bin/sh

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

print_last_arg foo bar baz
# baz

(Gdyby tak było, powiedzmy, #!/usr/bin/zshzamiast #!/bin/shwiedzieć, co robić. Moim problemem jest znalezienie rozsądnego sposobu na wdrożenie tego #!/bin/sh.)

EDYCJA: Powyższe to tylko głupi przykład. Moim celem nie jest wydrukowanie ostatniego argumentu, ale raczej sposób na odniesienie się do ostatniego argumentu w funkcji powłoki.


EDYCJA 2: Przepraszam za tak niejasno sformułowane pytanie. Mam nadzieję, że tym razem wszystko będzie dobrze.

Gdyby tak było /bin/zsh, zamiast /bin/sh, mogę napisać coś takiego

#!/bin/zsh

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

Wyrażenie $argv[$#]jest przykładem tego, co opisałem w moim pierwszym EDYCJI jako sposobu na odniesienie do ostatniego argumentu w funkcji powłoki .

Dlatego naprawdę powinienem był napisać mój oryginalny przykład w ten sposób:

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

... aby wyjaśnić, że to, czego szukam, jest mniej okropnym posunięciem na prawo od zadania.

Należy jednak pamiętać, że we wszystkich przykładach do ostatniego argumentu można uzyskać dostęp nieniszczący . IOW, dostęp do ostatniego argumentu pozostawia argumenty pozycyjne jako całość bez zmian.

kjo
źródło
unix.stackexchange.com/q/145522/117549 wskazuje na różne możliwości #! / bin / sh - czy możesz je zawęzić?
Jeff Schaller
podoba mi się edycja, ale być może zauważyłeś, że jest tutaj odpowiedź, która oferuje nieniszczące sposoby odwoływania się do ostatniego argumentu ...? nie należy utożsamiać var=$( eval echo \${$#})się eval var=\${$#}- obaj są niczym podobni.
mikeserv
1
Nie jestem pewien, czy dostaję twoją ostatnią notatkę, ale prawie wszystkie dotychczasowe odpowiedzi nie są destrukcyjne w tym sensie, że zachowują argumenty skryptu. Tylko shifti set -- ...rozwiązań opartych może być destrukcyjna, chyba że wykorzystywane w funkcjach, gdzie są one nieszkodliwe też.
jlliagre
@jlliagre - ale nadal są głównie destrukcyjne - wymagają stworzenia kontekstów jednorazowych, aby mogli je zniszczyć, aby je odkryć. ale ... jeśli i tak otrzymujesz drugi kontekst - dlaczego nie ten, który pozwala na indeksowanie? czy jest coś złego w korzystaniu z narzędzia przeznaczonego do pracy? interpretacja rozszerzeń powłoki jako danych wejściowych rozwijanych jest zadaniem eval. i nie ma nic istotnie odmiennego w eval "var=\${$#}"porównaniu var=${arr[evaled index]}z tym, że $#jest to gwarantowana bezpieczna wartość. po co kopiować cały zestaw, a potem go niszczyć, skoro można go po prostu bezpośrednio zindeksować?
mikeserv
1
@mikeserv Pętla for wykonana w głównej części powłoki pozostawia wszystkie argumenty bez zmian. Zgadzam się, że zapętlenie wszystkich argumentów jest bardzo niezoptymalizowane, zwłaszcza jeśli tysiące z nich są przekazywane do powłoki, a także zgadzam się, że bezpośredni dostęp do ostatniego argumentu z odpowiednim indeksem jest najlepszą odpowiedzią (i nie rozumiem, dlaczego został odrzucony ), ale poza tym nie ma nic naprawdę destrukcyjnego i nie powstaje dodatkowy kontekst.
jlliagre

Odpowiedzi:

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

... wypisze ciąg znaków, the last arg ispo którym następuje <spacja> , wartość ostatniego argumentu i końcowy <nowyline>, jeśli jest co najmniej 1 argument, lub w przypadku argumentów zerowych nic nie wypisze.

Jeśli zrobiłeś:

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

... wtedy albo przypisujesz wartość ostatniego argumentu do zmiennej powłoki, $lastargjeśli jest co najmniej 1 argument, albo nic nie robisz. Tak czy inaczej, zrobiłbyś to bezpiecznie i powinien być przenośny nawet dla skorupy Olde Bourne, tak myślę.

Oto jeszcze jeden, który działa podobnie, choć wymaga kopiując cały szereg arg dwukrotnie (i wymaga printfw $PATHdla powłoki Bourne) :

if   [ "${1+:}" ]
then set "$#" "$@" "$@"
     shift    "$1"
     printf %s\\n "$1"
     shift
fi
mikeserv
źródło
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
terdon
2
Do Twojej wiadomości, twoja pierwsza sugestia zawodzi pod starszą powłoką Bourne'a z błędem „złego zastąpienia”, jeśli nie ma żadnych argumentów. Oba pozostałe działają zgodnie z oczekiwaniami.
jlliagre
@jillagre - dziękuję. nie byłam taka pewna co do tego pierwszego - ale byłam całkiem pewna dwóch pozostałych. miało to jedynie na celu pokazanie, w jaki sposób można uzyskać dostęp do argumentów poprzez wstawienie referencji. najlepiej funkcja otworzyłaby się od ${1+":"} returnrazu jak coś takiego - bo kto chciałby, aby coś robił lub ryzykowałby jakiekolwiek skutki uboczne, gdyby nie był potrzebny? Matematyka jest podobna - jeśli możesz być pewien, że możesz rozwinąć dodatnią liczbę całkowitą, możesz to zrobić przez evalcały dzień. $OPTINDjest do tego świetny.
mikeserv
3

Oto uproszczony sposób:

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

(zaktualizowano w oparciu o punkt @ cuonglm, że oryginał zawiódł, gdy nie przekazał żadnych argumentów; teraz wyświetla się pusty wiersz - w elserazie potrzeby zmień to zachowanie w klauzuli)

Jeff Schaller
źródło
Wymagało to użycia / usr / xpg4 / bin / sh w systemie Solaris; daj mi znać, jeśli Solaris #! / bin / sh jest wymagany.
Jeff Schaller
To pytanie nie dotyczy konkretnego skryptu, który próbuję napisać, ale raczej ogólne instrukcje. Szukam dobrego przepisu na to. Oczywiście im bardziej przenośny, tym lepiej.
kjo
matematyka $ (()) kończy się niepowodzeniem w systemie Solaris / bin / sh; użyj czegoś takiego: shift `expr $ # - 1`, jeśli jest to wymagane.
Jeff Schaller
alternatywnie dla Solaris / bin / sh,echo "$# - 1" | bc
Jeff Schaller
1
dokładnie to chciałem napisać, ale nie powiodło się na drugiej próbowanej przeze mnie powłoce - NetBSD, która najwyraźniej jest powłoką Almquista, która jej nie obsługuje :(
Jeff Schaller
3

Biorąc pod uwagę przykład postu otwierającego (argumenty pozycyjne bez spacji):

print_last_arg foo bar baz

Domyślnie IFS=' \t\n', co powiesz na:

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

Aby zwiększyć bezpieczeństwo "$*", ustaw IFS (per @ StéphaneChazelas):

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

Ale powyższe nie powiedzie się, jeśli argumenty pozycyjne mogą zawierać spacje. W takim przypadku użyj tego zamiast:

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

Należy pamiętać, że techniki te pozwalają uniknąć stosowania evalefektów ubocznych i nie powodują ich.

Testowany w shellcheck.net

AsymLabs
źródło
1
Pierwszy nie powiedzie się, jeśli ostatni argument zawiera spację.
cuonglm
1
Zauważ, że pierwszy również nie działał w powłoce sprzed POSIX-a
cuonglm,
@cuonglm Dobrze zauważony, Twoja poprawna obserwacja jest teraz włączona.
AsymLabs
Zakłada również, że pierwszą postacią $IFSjest spacja.
Stéphane Chazelas
1
Będzie tak, jeśli w środowisku nie ma $ IFS, nie określono inaczej. Ponieważ jednak musisz ustawić IFS praktycznie za każdym razem, gdy korzystasz z operatora split + glob (pozostaw rozszerzenie bez cudzysłowu), jednym z podejść do obsługi IFS jest ustawienie go zawsze, gdy tego potrzebujesz. Nie zaszkodzi to IFS=' 'tutaj, aby wyjaśnić, że jest on używany do rozszerzenia "$*".
Stéphane Chazelas
3

Chociaż to pytanie ma nieco ponad 2 lata, pomyślałem, że podzielę się nieco bardziej złożoną opcją.

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

Uruchommy to

print_last_arg foo bar baz
baz

Rozszerzenie parametru powłoki Bash .

Edytować

Jeszcze bardziej zwięzłe: echo "${@: -1}"

(Uważaj na przestrzeń)

Źródło

Przetestowano na macOS 10.12.6, ale powinien również zwrócić ostatni argument na większości dostępnych smaków * nix ...

Boli znacznie mniej ¯\_(ツ)_/¯

użytkownik2243670
źródło
To powinna być zaakceptowana odpowiedź. Jeszcze lepiej byłoby: na echo "${*: -1}"co shellchecknie będzie narzekać.
Tom Hale
4
To nie będzie działać z zwykłym sh POSIX, ${array:n:m:}jest rozszerzeniem. (w pytaniu wyraźnie wspomniano /bin/sh)
ilkkachu
2

POSIXly:

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

printf '%s\n' "$1"

(To podejście działa również w starej powłoce Bourne'a)

Z innymi standardowymi narzędziami:

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

(To nie będzie działać ze starymi awk, które nie miały ARGV)

Cuonglm
źródło
Tam, gdzie wciąż jest skorupa Bourne'a, awkmoże być też stary awk, którego nie miał ARGV.
Stéphane Chazelas
@ StéphaneChazelas: Zgadzam się. Przynajmniej działało z własną wersją Briana Kernighana. Czy masz jakiś ideał?
cuonglm
To usuwa argumenty. Jak je zatrzymać ?.
@BinaryZebra: użyj awkpodejścia lub użyj innej zwykłej forpętli, jak inne, lub przekazując je do funkcji.
cuonglm
2

Powinno to działać z dowolną powłoką zgodną z POSIX i działać również z wcześniejszą powłoką Solaris Bourne starszą od POSIX:

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

a oto funkcja oparta na tym samym podejściu:

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

PS: nie mów mi, że zapomniałem nawiasów klamrowych wokół korpusu funkcji ;-)

jlliagre
źródło
2
Zapomniałeś nawiasów klamrowych wokół ciała funkcji ;-).
@BinaryZebra Ostrzegałem cię ;-) Nie zapomniałem o nich. Szelki są tutaj zaskakująco opcjonalne.
jlliagre
1
@jlliagre I rzeczywiście zostałem ostrzeżony ;-): P ...... I: na pewno!
Która część specyfikacji składni pozwala na to?
Tom Hale
@TomHale Reguły gramatyczne powłoka umożliwia zarówno brakuje in ...w forpętli i bez nawiasów klamrowych wokół z ifoświadczeniem używane jako ciała funkcji. Zobacz for_clausei compound_commandw pubs.opengroup.org/onlinepubs/9699919799 .
jlliagre
2

Z „Unix - często zadawane pytania”

(1)

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

Jeśli liczba argumentów może wynosić zero, wówczas $0przypisany zostanie argument zero (zwykle nazwa skryptu) $last. To jest powód, dla którego.

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

Aby uniknąć drukowania pustej linii, gdy nie ma argumentów, zamień echo "$last"na:

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

Unika się zerowej liczby argumentów if [ $# -gt 0 ].

To nie jest dokładna kopia tego, co znajduje się w linku na stronie, dodano pewne ulepszenia.


źródło
0

Powinno to działać na wszystkich systemach z perlzainstalowanym (więc większość UNICES):

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

Sztuką jest, aby użyć printf, aby dodać \0po każdym argumencie powłoki, a następnie perl„s -0przełącznik, który ustawia swój rekord separator NULL. Następnie iterujemy dane wejściowe, usuwamy \0i zapisujemy każdą linię zakończoną znakiem NULL jako $l. ENDBlok (to właśnie }{jest) będą realizowane po wszystkim wejście została odczytana tak drukuje ostatnią „linia”: ostatni argument powłoki.

terdon
źródło
@ mikeserv ah, prawda, nie testowałem z nową linią w żadnym z ostatnich argumentów. Edytowana wersja powinna działać praktycznie na wszystko, ale prawdopodobnie jest zbyt skomplikowana, aby wykonać tak proste zadanie.
terdon
tak, wywoływanie zewnętrznego pliku wykonywalnego (jeśli nie jest to już częścią logiki) jest dla mnie trochę ciężkie. ale jeśli chodzi o to, aby zdać ten argument na sed, awkczy perlto może być i tak cennych informacji. wciąż możesz zrobić to samo z sed -zaktualnymi wersjami GNU.
mikeserv
dlaczego zostałeś przegłosowany? to było dobre ... myślałem?
mikeserv
0

Oto wersja z rekurencją. Nie mam pojęcia, jak to jest zgodne z POSIX ...

print_last_arg()
{
    if [ $# -gt 1 ] ; then
        shift
        echo $( print_last_arg "$@" )
    else
        echo "$1"
    fi
}
brm
źródło
0

Prosta koncepcja, bez arytmetyki, bez pętli, bez ewaluacji , tylko funkcje.
Pamiętaj, że powłoka Bourne'a nie miała arytmetyki (potrzebna zewnętrzna expr). Jeśli chcesz uzyskać arytmetykę wolny, evalwolny wybór, jest to opcja. Potrzebowanie funkcji oznacza SVR3 lub wyższy (bez nadpisywania parametrów).
Poniżej znajdziesz bardziej solidną wersję z 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.

Ta struktura na wezwanie printlastjest stałe , argumenty muszą być ustawione w liście argumentów powłoki $1, $2itd (stos argument) i rozmowa odbywa się, jak podano.

Jeśli lista argumentów wymaga zmiany, wystarczy je ustawić:

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

Lub stwórz łatwiejszą w użyciu funkcję ( getlast), która może dopuszczać ogólne argumenty (ale nie tak szybko, argumenty są przekazywane dwa razy).

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

Zauważ, że argumenty ( getlastlub wszystkie zawarte w $@printlast) mogą mieć spacje, znaki nowej linii itp. Ale nie NUL.

Lepszy

Ta wersja nie jest drukowana, 0gdy lista argumentów jest pusta, i użyj bardziej niezawodnego printf (cofnij się, echojeśli opcja zewnętrzna printfnie jest dostępna dla starych powłok).

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

Korzystanie z EVAL.

Jeśli powłoka Bourne'a jest jeszcze starsza i nie ma żadnych funkcji, lub jeśli z jakiegoś powodu użycie eval jest jedyną opcją:

Aby wydrukować wartość:

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

Aby ustawić wartość w zmiennej:

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

Jeśli trzeba to zrobić w funkcji, argumenty należy skopiować do funkcji:

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.

źródło
jest lepiej, ale mówi, że nie używa pętli - ale tak jest. kopia tablicy jest pętlą . i z dwiema funkcjami wykorzystuje dwie pętle. także - czy jest jakiś powód, dla którego iteracja całej tablicy powinna być preferowana niż jej indeksowanie eval?
mikeserv
1
Widzisz coś for looplub while loop?
tak robię
mikeserv
1
Według twojego standardu wydaje mi się, że cały kod ma pętle, a nawet echo "Hello"pętle, ponieważ procesor musi czytać lokalizacje pamięci w pętli. Znowu: mój kod nie ma dodanych pętli.
1
Naprawdę nie dbam o twoją opinię o dobrym lub złym, już kilka razy udowodniono, że się myli. Znowu: mój kod nie ma for, whilelub untilpętle. Czy widzisz jakieś for whilelub untilpętle zapisane w kodzie ?. Nie ?, po prostu zaakceptuj fakt, zaakceptuj go i naucz się.