Рассмотрим следующий код:
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
, который используется для установки типов и атрибутов переменных, также делает переменную локальной в ksh
4 (в 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
)
Итак, короче говоря, если вам нужно работать с параметрами, соответствующими многословным параметрам, используйте "$@"
.