В чем разница между $* и $@?

В чем разница между $* и $@?

Рассмотрим следующий код:

foo () {
    echo $*
}

bar () {
    echo $@
}

foo 1 2 3 4
bar 1 2 3 4

Выводит:

1 2 3 4

1 2 3 4

Я использую Ksh88, но мне интересны и другие распространенные оболочки. Если вы знаете какие-то особенности для определенных оболочек, пожалуйста, упомяните их.

На странице руководства Ksh по Solaris я нашел следующее:

Значение $* и $@ идентично, если они не заключены в кавычки или используются в качестве значения назначения параметра или имени файла. Однако при использовании в качестве аргумента команды $* эквивалентно ``$1d$2d...'', где d — первый символ переменной IFS, тогда как $@ эквивалентно $1 $2 ....

Я пробовал изменять IFSпеременную, но это не меняет вывод. Может я что-то не так делаю?

решение1

Когда они не заключены в кавычки $*и $@одинаковы. Вам не следует использовать ни один из них, потому что они могут неожиданно сломаться, как только у вас появятся аргументы, содержащие пробелы или подстановочные знаки.


"$*"расширяется до одного слова "$1c$2c...". cбыл пробелом в оболочке Bourne, но теперь является первым символом IFSв современных оболочках, подобных Bourne (начиная с ksh и определено POSIX для sh), поэтому это может быть что угодно¹ по вашему выбору.

Единственное хорошее применение, которое я когда-либо находил для него, это:

соедините аргументы запятой(простая версия)

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

объединить аргументы с указанным разделителем(улучшенная версия)

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

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

"$@"расширяется до отдельных слов:"$1" "$2" ...

Это почти всегда то, что вам нужно. Он расширяет каждый позиционный параметр до отдельного слова, что делает его идеальным для приема аргументов командной строки или функции и последующей передачи их другой команде или функции. И поскольку он расширяется с использованием двойных кавычек, это означает, что ничего не сломается, если, скажем, "$1"содержит пробел или звездочку ( *) 4 .


Давайте напишем скрипт под названием , svimкоторый работает vimс sudo. Мы сделаем три версии, чтобы проиллюстрировать разницу.

svim1

#!/bin/sh
sudo vim $*

svim2

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

svim3

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

Все они подойдут для простых случаев, например, для одного имени файла, не содержащего пробелов:

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

Но $*это будет "$@"работать правильно только в том случае, если у вас есть несколько аргументов.

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"

И только "$*"и "$@"работают правильно, если у вас есть аргументы, содержащие пробелы.

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"

Только так "$@"он будет работать правильно все время.


¹ хотя будьте осторожны, в некоторых оболочках это не работает для многобайтовых символов.

² typeset, который используется для установки типов и атрибутов переменных, также делает переменную локальной в ksh4 (в ksh93 это только для функций, определенных с помощью function f {}синтаксиса Korn, а не синтаксиса Bourne f() ...). Это означает, что здесь IFSбудет восстановлено предыдущее значение, когда функция вернет значение. Это важно, потому что команды, которые вы запускаете после этого, могут работать не так, как ожидается, если IFSустановлено что-то нестандартное, и вы забыли заключить в кавычки некоторые расширения.

³ echoможет или не может правильно вывести свои аргументы, если первый начинается с -обратной косой черты или содержит ее, printможно указать не выполнять обработку обратной косой черты -rи защититься от аргументов, начинающихся с -разделителя +опций --(или -). printf '%s\n' "$*"Это было бы стандартной альтернативой, но следует отметить, что ksh88 и pdksh, а также некоторые из их производных по-прежнему не имеют printfвстроенной функции.

4 Обратите внимание, что это "$@"не работало должным образом в оболочке Bourne и ksh88, когда $IFSне содержало символ пробела, так как эффективно это было реализовано как позиционный параметр, объединенный с "некавычками" пробелами, и результат подлежал $IFSразделению. Ранние версии оболочки Bourne также имели эту ошибку, которая "$@"расширялась до одного пустого аргумента, когда не было позиционного параметра, что является одной из причин, по которой вы иногда видите ${1+"$@"}вместо "$@". Ни одна из этих ошибок не влияет на современные оболочки типа Bourne.

5 Оболочка Almquist и boshимеет localдля этого вместо этого. bash, yashа zshтакже имеет typeset, псевдоним local(также declareв bash и zsh) с оговоркой, что в bash, localможет использоваться только в функции.

решение2

Короткий ответ:использовать"$@"(обратите внимание на двойные кавычки). Другие формы очень редко бывают полезны.

"$@"довольно странный синтаксис. Он заменяется всеми позиционными параметрами, как отдельными полями. Если позиционных параметров нет ( $#равно 0), то "$@"расширяется до ничего (не пустой строки, а списка с 0 элементами), если есть один позиционный параметр, то "$@"эквивалентно "$1", если есть два позиционных параметра, то "$@"эквивалентно "$1" "$2", и т. д.

"$@"позволяет передавать аргументы скрипта или функции другой команде. Это очень полезно для оберток, которые выполняют такие действия, как установка переменных среды, подготовка файлов данных и т. д. перед вызовом команды с теми же аргументами и опциями, с которыми была вызвана обертка.

Например, следующая функция фильтрует вывод cvs -nq update. Помимо фильтрации вывода и статуса возврата (который соответствует статусу grep, а не статусу cvs), вызов cvssmнекоторых аргументов ведет себя как вызов cvs -nq updateс этими аргументами.

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

"$@"расширяется до списка позиционных параметров. В оболочках, поддерживающих массивы, есть похожий синтаксис для расширения до списка элементов массива: "${array[@]}"(фигурные скобки обязательны, за исключением zsh). Опять же, двойные кавычки несколько вводят в заблуждение: они защищают от разделения полей и генерации шаблонов элементов массива, но каждый элемент массива оказывается в своем собственном поле.

Некоторые древние оболочки имели то, что, возможно, является ошибкой: когда не было позиционных аргументов, "$@"расширялось до одного поля, содержащего пустую строку, а не до отсутствия поля. Это привело кобходной путь${1+"$@"}(сделализвестный по документации Perl). Затрагиваются только старые версии фактической оболочки Bourne и реализация OSF1, ни одна из ее современных совместимых замен (ash, ksh, bash, …) не затронута. /bin/shне затронута ни одна из известных мне систем, выпущенных в 21 веке (если не считать релиз обслуживания Tru64, и даже там /usr/xpg4/bin/shбезопасно, поэтому #!/bin/shзатронуты только скрипты, а не #!/usr/bin/env shскрипты, если ваш PATH настроен на соответствие POSIX). Короче говоря, это исторический анекдот, о котором вам не нужно беспокоиться.


"$*"всегда расширяется до одного слова. Это слово содержит позиционные параметры, соединенные пробелом между ними. (В более общем смысле разделитель — это первый символ значения переменной IFS. Если значение IFS— пустая строка, разделитель — пустая строка.) Если позиционных параметров нет, то "$*"— пустая строка, если есть два позиционных параметра и IFSимеет значение по умолчанию, то "$*"эквивалентно "$1 $2"и т. д.

$@и $*внешние кавычки эквивалентны. Они расширяются до списка позиционных параметров, как отдельные поля, например "$@"; но каждое результирующее поле затем разделяется на отдельные поля, которые обрабатываются как шаблоны подстановочных знаков имени файла, как обычно с расширениями переменных без кавычек.

Например, если текущий каталог содержит три файла bar, bazи foo, то:

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`

решение3

Вот простой скрипт, демонстрирующий разницу между $*и $@:

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

Выход:

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

В синтаксисе массива нет разницы при использовании $*или $@. Это имеет смысл только при использовании их с двойными кавычками "$*"и "$@".

решение4

Разница важна при написании скриптов, которые должны правильно использовать позиционные параметры...

Представьте себе следующий звонок:

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

Здесь всего 4 параметра:

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

В моем случае myuseraddэто просто оболочка, useraddкоторая принимает те же параметры, но добавляет квоту для пользователя:

#!/bin/bash -e

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

Обратите внимание на вызов useradd "$@", с $@кавычками. Это позволит учесть параметры и отправить их как есть в useradd. Если бы вы сняли кавычки $@(или использовали $*также без кавычек), useradd увидел бы5параметры, так как третий параметр, содержащий пробел, будет разделен на два:

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

(и наоборот, если бы вы использовали "$*", useradd увидел бы только один параметр: -m -c Carlos Campderrós ccampderros)

Итак, короче говоря, если вам нужно работать с параметрами, соответствующими многословным параметрам, используйте "$@".

Связанный контент