Jaka jest różnica między $ * a $ @?

72

Rozważ następujący kod:

foo () {
    echo $*
}

bar () {
    echo $@
}

foo 1 2 3 4
bar 1 2 3 4

Wyprowadza:

1 2 3 4

1 2 3 4

Używam Ksh88, ale interesują mnie również inne popularne powłoki. Jeśli zdarzyło Ci się znać jakąś szczegółowość konkretnych powłok, proszę o nich wspomnieć.

Znalazłem następujący link na stronie podręcznika Ksh w systemie Solaris:

Znaczenie $ * i $ @ jest identyczne, gdy nie jest cytowane lub gdy jest używane jako wartość przypisania parametru lub nazwa pliku. Jednak gdy użyty jako argument polecenia, $ * jest równoważne do `` $ 1d $ 2d ... '', gdzie d jest pierwszym znakiem zmiennej IFS, podczas gdy $ @ jest równoważne 1 $ 2 $ ...

Próbowałem zmodyfikować IFSzmienną, ale nie modyfikuje ona wyniku. Może robię coś złego?

rahmu
źródło

Odpowiedzi:

95

Gdy nie są cytowane $*i $@są takie same. Nie powinieneś używać żadnego z nich, ponieważ mogą one niespodziewanie ulec uszkodzeniu, gdy tylko pojawią się argumenty zawierające spacje lub symbole wieloznaczne.


"$*"rozwija się do jednego słowa "$1c$2c...". Zazwyczaj cjest to spacja, ale tak naprawdę to pierwsza postać IFS, więc może to być dowolna wybrana przez Ciebie postać .

Jedyne dobre zastosowanie, jakie kiedykolwiek znalazłem, to:

połącz argumenty przecinkiem (wersja prosta)

join1() {
    typeset IFS=,
    echo "$*"
}

join1 a b c   # => a,b,c

łączyć argumenty z określonym ogranicznikiem (lepsza wersja)

join2() {
    typeset IFS=$1   # typeset makes a local variable in ksh (see footnote)
    shift
    echo "$*"
}

join2 + a b c   # => a+b+c

"$@" rozwija się do oddzielnych słów: "$1" "$2" ...

To jest prawie zawsze to, czego chcesz. Rozwija każdy parametr pozycyjny do osobnego słowa, co czyni go idealnym do przyjmowania argumentów wiersza poleceń lub funkcji, a następnie przekazywania ich do innego polecenia lub funkcji. A ponieważ rozszerza się za pomocą podwójnych cudzysłowów, oznacza to, że rzeczy się nie psują, jeśli, powiedzmy, "$1"zawiera spację lub gwiazdkę ( *).


Załóżmy napisać skrypt o nazwie svim, która biegnie vimz sudo. Zrobimy trzy wersje, aby zilustrować różnicę.

svim1

#!/bin/sh
sudo vim $*

svim2

#!/bin/sh
sudo vim "$*"

svim3

#!/bin/sh
sudo vim "$@"

Wszystkie będą odpowiednie dla prostych przypadków, np. Pojedyncza nazwa pliku, która nie zawiera spacji:

svim1 foo.txt             # == sudo vim foo.txt
svim2 foo.txt             # == sudo vim "foo.txt"
svim2 foo.txt             # == sudo vim "foo.txt"

Ale tylko $*i "$@"działa poprawnie, jeśli masz wiele argumentów.

svim1 foo.txt bar.txt     # == sudo vim foo.txt bar.txt
svim2 foo.txt bar.txt     # == sudo vim "foo.txt bar.txt"   # one file name!
svim3 foo.txt bar.txt     # == sudo vim "foo.txt" "bar.txt"

I tylko "$*"i "$@"działać prawidłowo, jeśli masz argumenty zawierające spacje.

svim1 "shopping list.txt" # == sudo vim shopping list.txt   # two file names!
svim2 "shopping list.txt" # == sudo vim "shopping list.txt"
svim3 "shopping list.txt" # == sudo vim "shopping list.txt"

Więc tylko "$@"będzie działał poprawnie cały czas.


typesetjest jak utworzyć zmienną lokalną w ksh( bashi zamiast tego ashużyć local). Oznacza to IFS, że po przywróceniu funkcji zostanie przywrócona poprzednia wartość. Jest to ważne, ponieważ polecenia uruchamiane później mogą nie działać poprawnie, jeśli IFSsą ustawione na coś niestandardowego.

Mikel
źródło
2
Cudowne wyjaśnienie, bardzo dziękuję.
rahmu
Dziękujemy za przykład użycia $*. Zawsze uważałem to za całkowicie bezużyteczne ... połączenie z separatorem to dobry przypadek użycia.
anishsane
35

Krótka odpowiedź: użyj"$@" (zwróć uwagę na podwójne cudzysłowy). Inne formy są bardzo rzadko przydatne.

"$@"jest raczej dziwną składnią. Jest on zastępowany przez wszystkie parametry pozycyjne, jako osobne pola. Jeśli nie ma parametrów pozycyjnych ( $#wynosi 0), wówczas "$@"rozwija się do zera (nie jest to pusty ciąg, ale lista z 0 elementami), jeśli istnieje jeden parametr pozycyjny, to "$@"jest równoważny "$1", jeśli są dwa parametry pozycyjne, to "$@"jest równoważny z "$1" "$2"itd.

"$@"pozwala przekazać argumenty skryptu lub funkcji do innego polecenia. Jest to bardzo przydatne w przypadku opakowań, które wykonują takie czynności, jak ustawianie zmiennych środowiskowych, przygotowywanie plików danych itp. Przed wywołaniem polecenia z tymi samymi argumentami i opcjami, które zostały wywołane przez opakowanie.

Na przykład poniższa funkcja filtruje dane wyjściowe cvs -nq update. Oprócz filtrowania danych wyjściowych i statusu zwracanego (który ma grepraczej status niż zamiast cvs), wywoływanie cvssmniektórych argumentów zachowuje się jak wywoływanie cvs -nq updatez tymi argumentami.

cvssm () { cvs -nq update "$@" | egrep -v '^[?A]'; }

"$@"rozwija się do listy parametrów pozycyjnych. W powłokach obsługujących tablice istnieje podobna składnia, aby rozwinąć listę elementów tablicy: "${array[@]}"(nawiasy klamrowe są obowiązkowe oprócz zsh). Ponownie, podwójne cudzysłowy są nieco mylące: chronią przed podziałem pola i generowaniem wzorca elementów tablicy, ale każdy element tablicy kończy się na swoim własnym polu.

Niektóre starożytne powłoki miały coś, co jest prawdopodobnie błędem: gdy nie było argumentów pozycyjnych, "$@"rozszerzono je do pojedynczego pola zawierającego pusty ciąg, zamiast do żadnego pola. Doprowadziło to do obejścia${1+"$@"} ( rozsławionego za pomocą dokumentacji Perla ). Wpływa to tylko na starsze wersje rzeczywistej powłoki Bourne'a i implementacji OSF1, a żadna z jej nowoczesnych kompatybilnych zamienników (ash, ksh, bash,…) nie jest zagrożona. /bin/shnie ma wpływu na żaden system, który został wydany w XXI wieku, o którym wiem (chyba że policzysz wersję aktualizacyjną Tru64, a nawet /usr/xpg4/bin/shjest bezpieczny, więc #!/bin/shdotyczy to tylko skryptu, a nie #!/usr/bin/env shskryptów, o ile twoja ŚCIEŻKA jest skonfigurowana pod kątem zgodności z POSIX) . Krótko mówiąc, jest to historyczna anegdota, o którą nie musisz się martwić.


"$*"zawsze rozwija się do jednego słowa. To słowo zawiera parametry pozycyjne, połączone ze spacją pomiędzy nimi. (Mówiąc bardziej ogólnie, separator jest pierwszym znakiem wartości IFSzmiennej. Jeśli wartość IFSjest pustym łańcuchem, separator jest pustym łańcuchem.) Jeśli nie ma parametrów pozycyjnych, to "$*"jest to pusty ciąg, jeśli są dwa parametry pozycyjne, a IFSich wartość domyślna "$*"jest wówczas równoważna "$1 $2"itp.

$@a $*cytaty zewnętrzne są równoważne. Rozwijają się do listy parametrów pozycyjnych, jako oddzielne pola, takie jak "$@"; ale każde wynikowe pole jest następnie dzielone na osobne pola, które są traktowane jak wzorce symboli wieloznacznych nazw plików, jak zwykle z niecytowanymi rozszerzeniami zmiennych.

Na przykład jeśli bieżący katalog zawiera trzy pliki bar, baza foonastępnie:

set --         # no positional parameters
for x in "$@"; do echo "$x"; done  # prints nothing
for x in "$*"; do echo "$x"; done  # prints 1 empty line
for x in $*; do echo "$x"; done    # prints nothing
set -- "b* c*" "qux"
echo "$@"      # prints `b* c* qux`
echo "$*"      # prints `b* c* qux`
echo $*        # prints `bar baz c* qux`
for x in "$@"; do echo "$x"; done  # prints 2 lines: `b* c*` and `qux`
for x in "$*"; do echo "$x"; done  # prints 1 lines: `b* c* qux`
for x in $*; do echo "$x"; done    # prints 4 lines: `bar`, `baz`, `c*` and `qux`
Gilles
źródło
1
Uwaga historyczna: w niektórych starożytnych muszlach Bourne'a "$@"rzeczywiście rozwinął się do listy zawierającej pusty ciąg: unix.stackexchange.com/questions/68484/…
ninjalj
25

Oto prosty skrypt pokazuje różnicę między $*i $@:

#!/bin/bash

test_param() {
  echo "Receive $# parameters"
  echo Using '$*'

  echo
  for param in $*; do
    printf '==>%s<==\n' "$param"
  done;

  echo
  echo Using '"$*"'
  for param in "$*"; do
    printf '==>%s<==\n' "$param"
  done;

  echo
  echo Using '$@'
  for param in $@; do
    printf '==>%s<==\n' "$param"
  done;

  echo
  echo Using '"$@"';
  for param in "$@"; do
  printf '==>%s<==\n' "$param"
  done
}

IFS="^${IFS}"

test_param 1 2 3 "a b c"

Wynik:

% cuonglm at ~
% bash test.sh
Receive 4 parameters

Using $*
==>1<==
==>2<==
==>3<==
==>a<==
==>b<==
==>c<==

Using "$*"
==>1^2^3^a b c<==

Using $@
==>1<==
==>2<==
==>3<==
==>a<==
==>b<==
==>c<==

Using "$@"
==>1<==
==>2<==
==>3<==
==>a b c<==

W składni tablicowej nie ma różnicy przy użyciu $*lub $@. Ma to sens tylko wtedy, gdy używasz ich z podwójnymi cudzysłowami "$*"i "$@".

Cuonglm
źródło
Doskonały przykład! Czy potrafisz wyjaśnić użycie IFS="^${IFS}"?
Russ
@Russ: Pokazuje, jak wartość jest konkatowana z pierwszym znakiem w IFS.
cuonglm
W ten sam sposób, który IFS="^xxxxx"by to zrobił? Końcowy ${IFS}sufiks sprawił, że pomyślałem, że robisz coś trudniejszego, jak na przykład automatyczne odzyskanie oryginalnego IFS na końcu (np. Pierwszy znak automatycznie się zmienił lub coś takiego).
Russ
11

Podany kod da ten sam wynik. Aby to lepiej zrozumieć, spróbuj tego:

foo () {
    for i in "$*"; do
        echo "$i"
    done
}

bar () {
    for i in "$@"; do
        echo "$i"
    done
}

Wynik powinien być teraz inny. Oto, co otrzymuję:

$ foo() 1 2 3 4
1 2 3 4
$ bar() 1 2 3 4
1
2
3
4

To działało dla mnie bash. O ile mi wiadomo, ksh nie powinien się bardzo różnić. Zasadniczo cytowanie $*potraktuje wszystko jako jedno słowo, a cytowanie $@potraktuje listę jako osobne słowa, jak widać w powyższym przykładzie.

Jako przykład użycia IFSzmiennej z $*, rozważ to

fooifs () {
    IFS="c"            
    for i in "$*"; do
        echo "$i"
    done
    unset IFS          # reset to the original value
}

Otrzymuję to w wyniku:

$ fooifs 1 2 3 4
1c2c3c4

Poza tym właśnie potwierdziłem, że działa tak samo ksh. Zarówno testowane tutaj, jak bashi kshpod OSX, ale nie widzę, jak to by miało znaczenie.

Wojtek Rzepala
źródło
„zmieniony w ramach funkcji - nie wpływa na globalny”. Testowałeś to?
Mikel
Tak, sprawdziłem to. Dla spokoju dodam unset IFSna końcu, aby zresetować go do oryginału, ale nie działało to dla mnie bez problemu i dzięki temu otrzymałem echo $IFSstandardowe wyjście, które z niego otrzymuję. Ustawienie za IFSpomocą nawiasów klamrowych wprowadza nowy zakres, więc jeśli go nie wyeksportujesz, nie wpłynie to na zewnątrz IFS.
Wojtek Rzepala
echo $IFSnic nie dowodzi, ponieważ powłoka widzi ,, ale potem dzieli słowa za pomocą IFS! Spróbować echo "$IFS".
Mikel
Słuszna uwaga. rozbrojenie IFSpowinno rozwiązać ten problem.
Wojtek Rzepala
Chyba IFSże miał inną niestandardową wartość przed wywołaniem funkcji. Ale tak, przez większość czasu niepokojące IFS będzie działać.
Mikel
6

Różnica jest ważna przy pisaniu skryptów, które powinny prawidłowo używać parametrów pozycyjnych ...

Wyobraź sobie następujące połączenie:

$ myuseradd -m -c "Carlos Campderrós" ccampderros

Oto tylko 4 parametry:

$1 => -m
$2 => -c
$3 => Carlos Campderrós
$4 => ccampderros

W moim przypadku myuseraddjest to tylko opakowanie, useraddktóre akceptuje te same parametry, ale dodaje limit dla użytkownika:

#!/bin/bash -e

useradd "$@"
setquota -u "${!#}" 10000 11000 1000 1100

Zwróć uwagę na połączenie useradd "$@"z $@cytowanym. Spowoduje to przestrzeganie parametrów i wysyłanie ich takimi, jakimi są useradd. Gdyby cofnąć cudzysłowy $@(lub użyć $*również cudzysłowu), useradd zobaczyłby 5 parametrów, ponieważ trzeci parametr zawierający spację zostałby podzielony na dwa:

$1 => -m
$2 => -c
$3 => Carlos
$4 => Campderrós
$5 => ccampderros

(i odwrotnie, jeśli były w użyciu "$*", useradd zobaczy tylko jeden parametr: -m -c Carlos Campderrós ccampderros)

Krótko mówiąc, jeśli chcesz pracować z parametrami odnoszącymi się do parametrów wielowyrazowych, użyj "$@".

Carlos Campderrós
źródło
4
   *      Expands  to  the positional parameters, starting from one.  When
          the expansion occurs within double quotes, it expands to a  sin
          gle word with the value of each parameter separated by the first
          character of the IFS special variable.  That is, "$*" is equiva
          lent to "$1c$2c...", where c is the first character of the value
          of the IFS variable.  If IFS is unset, the parameters are  sepa
          rated  by  spaces.   If  IFS  is null, the parameters are joined
          without intervening separators.
   @      Expands to the positional parameters, starting from  one.   When
          the  expansion  occurs  within  double  quotes,  each  parameter
          expands to a separate word.  That is, "$@" is equivalent to "$1"
          "$2"  ...   If the double-quoted expansion occurs within a word,
          the expansion of the first parameter is joined with  the  begin
          ning  part  of  the original word, and the expansion of the last
          parameter is joined with the last part  of  the  original  word.
          When  there  are no positional parameters, "$@" and $@ expand to
          nothing (i.e., they are removed).

// man bash . to ksh, afair, podobne zachowanie.

wysypka
źródło
2

Mówiąc o różnicach między zshi bash:

Z cytatów wokół $@i $*, zshi bashzachowują się tak samo, i myślę, że wynik jest dość standardowy wśród wszystkich muszli:

 $ f () { for i in "$@"; do echo +"$i"+; done; }; f 'a a' 'b' ''
 +a a+
 +b+
 ++
 $ f () { for i in "$*"; do echo +"$i"+; done; }; f 'a a' 'b' ''
 +a a b +

Bez cudzysłowu wyniki są takie same dla $*i $@, ale różne dla bashi dla zsh. W tym przypadku zshpokazuje dziwne zachowanie:

bash$ f () { for i in $*; do echo +"$i"+; done; }; f 'a a' 'b' ''
+a+
+a+
+b+
zsh% f () { for i in $*; do echo +"$i"+; done; }; f 'a a' 'b' ''  
+a a+
+b+

(Zsh zwykle nie dzieli danych tekstowych za pomocą IFS, chyba że jest to wyraźnie wymagane, ale zauważ, że tutaj nieoczekiwanie brakuje pustego argumentu na liście).

Stéphane Gimenez
źródło
1
W Zsh $@nie jest wyjątkowy pod tym względem: $xrozwija się do co najwyżej jednego słowa, ale puste zmienne rozwijają się do zera (nie jest pustym słowem). Spróbuj print -l a $foo bz foopustym lub niezdefiniowanym.
Gilles,
0

Jedna z odpowiedzi mówi $*(co uważam za „ikonę”) rzadko jest przydatna.

Przeszukuję google za pomocą G() { IFS='+' ; w3m "https://encrypted.google.com/search?q=$*" ; }

Ponieważ adresy URL są często dzielone z +, ale moja klawiatura sprawia, że   łatwiej osiągnąć niż +, $*+ $IFSczują się opłaca.

izomorfizmy
źródło