Qual é a diferença entre $* e $@?

Qual é a diferença entre $* e $@?

Considere o seguinte código:

foo () {
    echo $*
}

bar () {
    echo $@
}

foo 1 2 3 4
bar 1 2 3 4

Ele produz:

1 2 3 4

1 2 3 4

Estou usando o Ksh88, mas também estou interessado em outros shells comuns. Se você conhece alguma particularidade de shells específicos, mencione-os.

Encontrei o seguinte na página de manual do Ksh no Solaris:

O significado de $* e $@ é idêntico quando não está entre aspas ou quando usado como valor de atribuição de parâmetro ou como nome de arquivo. Entretanto, quando usado como argumento de comando, $* é equivalente a ``$1d$2d...'', onde d é o primeiro caractere da variável IFS, enquanto $@ é equivalente a $1 $2 ....

Tentei modificar a IFSvariável, mas isso não modifica a saída. Talvez eu esteja fazendo algo errado?

Responder1

Quando não são citados $*e $@são iguais. Você não deve usar nenhum deles, pois eles podem quebrar inesperadamente assim que você tiver argumentos contendo espaços ou curingas.


"$*"expande para uma única palavra "$1c$2c...". cera um espaço no shell Bourne, mas agora é o primeiro caractere IFSnos shells modernos do tipo Bourne (de ksh e especificado pelo POSIX para sh), então pode ser qualquer coisa¹ que você escolher.

O único bom uso que encontrei para isso é:

juntar argumentos com vírgula(versão simples)

function join1 {
    typeset IFS=,      # typeset makes a local variable in ksh²
    print -r -- "$*"   # using print instead of unreliable echo³
}

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

juntar argumentos com o delimitador especificado(melhor versão)

function join2 {
    typeset IFS="$1"
    shift
    print -r -- "$*"
}

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

"$@"expande para palavras separadas:"$1" "$2" ...

Quase sempre é isso que você deseja. Ele expande cada parâmetro posicional para uma palavra separada, o que o torna perfeito para receber argumentos de linha de comando ou de função e depois passá-los para outro comando ou função. E porque se expande usando aspas duplas, significa que as coisas não quebram se, digamos, "$1"contiverem um espaço ou um asterisco ( *) 4 .


Vamos escrever um script chamado svimque rode vimcom sudo. Faremos três versões para ilustrar a diferença.

svim1

#!/bin/sh
sudo vim $*

svim2

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

svim3

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

Todos eles servirão para casos simples, por exemplo, um único nome de arquivo que não contenha espaços:

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

Mas só $*funciona "$@"corretamente se você tiver vários argumentos.

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"

E somente "$*"funciona "$@"corretamente se você tiver argumentos contendo espaços.

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"

Então só "$@"funcionará corretamente o tempo todo.


¹ embora tenha cuidado em alguns shells, ele não funciona para caracteres multibyte.

² typesetque é usado para definir tipos e atributos de variáveis ​​​​também torna uma variável local em ksh4 (em ksh93, isso é apenas para funções definidas com a function f {}sintaxe Korn, não com a sintaxe Bourne f() ...). Isso significa que aqui IFSserá restaurado ao valor anterior quando a função retornar. Isso é importante porque os comandos executados posteriormente podem não funcionar conforme o esperado se IFSestiverem configurados para algo fora do padrão e você se esquecer de citar algumas expansões.

³ echoirá ou pode falhar ao imprimir seus argumentos corretamente se o primeiro começar com -ou contiver barras invertidas, printpode ser instruído a não fazer processamento de barra invertida -re a se proteger contra argumentos começando com -ou +com o delimitador de opção --(ou ). seria a alternativa padrão, mas observe que ksh88 e pdksh e alguns de seus derivados ainda não possuem recursos integrados.-printf '%s\n' "$*"printf

4 Observe que "$@"não funcionou corretamente no Bourne Shell e no ksh88 quando $IFSnão continha o caractere de espaço, tão efetivamente foi implementado quanto o parâmetro posicional sendo unido a espaços "sem aspas" e o resultado sujeito a $IFSdivisão. As primeiras versões do shell Bourne também tinham aquele bug que "$@"era expandido para um argumento vazio quando não havia parâmetro posicional, que é uma das razões pelas quais você às vezes vê ${1+"$@"}no lugar de "$@". Nenhum desses bugs afeta os shells modernos do tipo Bourne.

5 O shell Almquist e boshtenha localpara isso. bash, yashe zshtambém tem typeset, alias para local(também declareem bash e zsh) com a ressalva de que in bash, localsó pode ser usado em uma função.

Responder2

Resposta curta:usar"$@"(observe as aspas duplas). As outras formas raramente são úteis.

"$@"é uma sintaxe bastante estranha. É substituído por todos os parâmetros posicionais, como campos separados. Se não houver parâmetros posicionais ( $#é 0), então "$@"se expande para nada (não uma string vazia, mas uma lista com 0 elementos), se houver um parâmetro posicional então "$@"é equivalente a "$1", se houver dois parâmetros posicionais então "$@"é equivalente a "$1" "$2", etc.

"$@"permite que você passe os argumentos de um script ou função para outro comando. É muito útil para wrappers que fazem coisas como definir variáveis ​​de ambiente, preparar arquivos de dados, etc. antes de chamar um comando com os mesmos argumentos e opções com os quais o wrapper foi chamado.

Por exemplo, a função a seguir filtra a saída de cvs -nq update. Além da filtragem de saída e do status de retorno (que é de grepe não de cvs), chamar cvssmalguns argumentos se comporta como chamar cvs -nq updatecom esses argumentos.

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

"$@"expande para a lista de parâmetros posicionais. Em shells que suportam arrays, existe uma sintaxe semelhante para expandir para a lista de elementos do array: "${array[@]}"(os colchetes são obrigatórios, exceto em zsh). Novamente, as aspas duplas são um tanto enganosas: elas protegem contra a divisão de campos e a geração de padrões dos elementos do array, mas cada elemento do array termina em seu próprio campo.

Alguns shells antigos tinham o que é indiscutivelmente um bug: quando não havia argumentos posicionais, "$@"expandiam-se para um único campo contendo uma string vazia, em vez de nenhum campo. Isto levou aoGambiarra${1+"$@"}(feitofamoso através da documentação Perl). Apenas as versões mais antigas do Bourne Shell real e a implementação do OSF1 são afetadas, nenhum de seus substitutos modernos compatíveis (ash, ksh, bash,…) é. /bin/shnão é afetado em nenhum sistema lançado no século 21 que eu conheça (a menos que você conte a versão de manutenção Tru64, e mesmo que seja /usr/xpg4/bin/shseguro, apenas #!/bin/shscripts são afetados, não #!/usr/bin/env shscripts, desde que seu PATH esteja configurado para conformidade com POSIX) . Resumindo, esta é uma anedota histórica com a qual você não precisa se preocupar.


"$*"sempre se expande para uma palavra. Esta palavra contém os parâmetros posicionais, concatenados com um espaço entre eles. (Mais geralmente, o separador é o primeiro caractere do valor da IFSvariável. Se o valor de IFSfor a sequência vazia, o separador será a sequência vazia.) Se não houver parâmetros posicionais, então "$*"será a sequência vazia, se houver dois parâmetros posicionais e IFStem seu valor padrão, então "$*"é equivalente a "$1 $2", etc.

$@e $*aspas externas são equivalentes. Eles se expandem para a lista de parâmetros posicionais, como campos separados, como "$@"; mas cada campo resultante é então dividido em campos separados que são tratados como padrões curinga de nome de arquivo, como de costume com expansões de variáveis ​​sem aspas.

Por exemplo, se o diretório atual contiver três arquivos bar, baze foo, então:

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`

Responder3

Aqui está um script simples para demonstrar a diferença entre $*e $@:

#!/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"

Saída:

% 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<==

Na sintaxe de array, não há diferença ao usar $*ou $@. Só faz sentido quando você os usa com aspas duplas "$*"e "$@".

Responder4

A diferença é importante ao escrever scripts que devem usar os parâmetros posicionais da maneira correta...

Imagine a seguinte chamada:

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

Aqui existem apenas 4 parâmetros:

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

No meu caso, myuseraddé apenas um wrapper useraddque aceita os mesmos parâmetros, mas adiciona uma cota para o usuário:

#!/bin/bash -e

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

Observe a chamada para useradd "$@", com $@aspas. Isso respeitará os parâmetros e os enviará como estão para useradd. Se você tirasse aspas $@(ou usasse $*também sem aspas), useradd veria5parâmetros, pois o terceiro parâmetro que continha um espaço seria dividido em dois:

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

(e inversamente, se você usasse "$*", useradd veria apenas um parâmetro: -m -c Carlos Campderrós ccampderros)

Então, resumindo, se você precisar trabalhar com parâmetros respeitando parâmetros multipalavras, use "$@".

informação relacionada