Как перебирать аргументы в скрипте Bash

901

У меня есть сложная команда, из которой я хотел бы сделать скрипт shell / bash. Я могу написать это с точки зрения $1легко:

foo $1 args -o $1.ext

Я хочу иметь возможность передать несколько входных имен в сценарий. Какой правильный способ сделать это?

И, конечно же, я хочу обрабатывать имена файлов с пробелами в них.

Телема
источник
67
К вашему сведению, это хорошая идея - всегда помещать параметры в кавычки. Вы никогда не знаете, когда parm может содержать встроенное пространство.
Дэвид Р. Триббл

Ответы:

1483

Используйте "$@"для представления всех аргументов:

for var in "$@"
do
    echo "$var"
done

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

sh test.sh 1 2 '3 4'
1
2
3 4
Роберт Гэмбл
источник
38
Кстати, одним из других достоинств "$@"является то, что он расширяется до нуля, когда нет позиционных параметров, тогда как "$*"расширяется до пустой строки - и да, есть разница между отсутствием аргументов и одним пустым аргументом. Смотрите ${1:+"$@"} in / bin / sh` .
Джонатан Леффлер
9
Обратите внимание, что эта запись также должна использоваться в функциях оболочки для доступа ко всем аргументам функции.
Джонатан Леффлер
Отбросив двойные кавычки: $varя смог выполнить аргументы.
eonist
как мне начать со второго аргумента? Я имею в виду, мне это нужно, но всегда начинаю со второго аргумента, то есть пропускаем 1 доллар.
m4l490n
4
@ m4l490n: shiftвыбрасывает $1и сдвигает все последующие элементы.
MSalters
239

Переписать из теперь удален ответ по VonC .

Сжатый ответ Роберта Гэмбла касается непосредственно вопроса. Это усиливает некоторые проблемы с именами файлов, содержащими пробелы.

Смотрите также: $ {1: + "$ @"} в / bin / sh

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

Итак, в чем разница между $*, $@, "$*"и "$@"? Все они связаны со «всеми аргументами оболочки», но делают разные вещи. Когда без кавычек,$* и $@делать то же самое. Они рассматривают каждое «слово» (последовательность без пробелов) как отдельный аргумент. Однако формы в кавычках совершенно разные: "$*"обрабатывает список аргументов как одну строку, разделенную пробелами, тогда "$@"как аргументы обрабатывают почти точно так, как они были указаны в командной строке. "$@"вообще ничего не расширяется, когда нет позиционных аргументов; "$*"расширяется до пустой строки - и да, есть разница, хотя это может быть трудно воспринимать. См. Дополнительную информацию ниже, после введения (нестандартной) командыal .

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

Пример:

    $ mkdir "my dir" anotherdir
    $ ls
    anotherdir      my dir
    $ cp /dev/null "my dir/my file"
    $ cp /dev/null "anotherdir/myfile"
    $ ls -Fltr
    total 0
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 my dir/
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 anotherdir/
    $ ls -Fltr *
    my dir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 my file

    anotherdir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 myfile
    $ ls -Fltr "./my dir" "./anotherdir"
    ./my dir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 my file

    ./anotherdir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 myfile
    $ var='"./my dir" "./anotherdir"' && echo $var
    "./my dir" "./anotherdir"
    $ ls -Fltr $var
    ls: "./anotherdir": No such file or directory
    ls: "./my: No such file or directory
    ls: dir": No such file or directory
    $

Почему это не работает? Это не работает, потому что оболочка обрабатывает кавычки, прежде чем она расширяет переменные. Итак, чтобы оболочка обратила внимание на кавычки $var, вы должны использовать eval:

    $ eval ls -Fltr $var
    ./my dir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 my file

    ./anotherdir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 myfile
    $ 

Это действительно сложно, когда у вас есть имена файлов, такие как " He said, "Don't do this!"" (с кавычками и двойными кавычками и пробелами).

    $ cp /dev/null "He said, \"Don't do this!\""
    $ ls
    He said, "Don't do this!"       anotherdir                      my dir
    $ ls -l
    total 0
    -rw-r--r--   1 jleffler  staff    0 Nov  1 15:54 He said, "Don't do this!"
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 anotherdir
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 my dir
    $ 

Оболочки (все они) не облегчают работу с такими вещами, поэтому (как ни странно) многие программы Unix не справляются с ними. В Unix имя файла (один компонент) может содержать любые символы, кроме косой черты и NUL '\0'. Тем не менее, оболочки настоятельно рекомендуют не использовать пробелы, символы новой строки или табуляции нигде в именах путей. Именно поэтому стандартные имена файлов Unix не содержат пробелов и т. Д.

При работе с именами файлов, которые могут содержать пробелы и другие неприятные символы, вы должны быть чрезвычайно осторожны, и я давно обнаружил, что мне нужна программа, которая не является стандартной для Unix. Я называю этоescape (версия 1.1 датирована 1989-08-23T16: 01: 45Z).

Вот пример escapeиспользования - с системой управления SCCS. Это сценарий обложки, который выполняет как delta(думайте о регистрации ), так и get(думайте о регистрации ). Различные аргументы, особенно -y(причина, по которой вы внесли изменения) будут содержать пробелы и переводы строк. Обратите внимание, что сценарий датируется 1992 годом, поэтому он использует обратные тики вместо $(cmd ...)обозначений и не использует их #!/bin/shв первой строке.

:   "@(#)$Id: delget.sh,v 1.8 1992/12/29 10:46:21 jl Exp $"
#
#   Delta and get files
#   Uses escape to allow for all weird combinations of quotes in arguments

case `basename $0 .sh` in
deledit)    eflag="-e";;
esac

sflag="-s"
for arg in "$@"
do
    case "$arg" in
    -r*)    gargs="$gargs `escape \"$arg\"`"
            dargs="$dargs `escape \"$arg\"`"
            ;;
    -e)     gargs="$gargs `escape \"$arg\"`"
            sflag=""
            eflag=""
            ;;
    -*)     dargs="$dargs `escape \"$arg\"`"
            ;;
    *)      gargs="$gargs `escape \"$arg\"`"
            dargs="$dargs `escape \"$arg\"`"
            ;;
    esac
done

eval delta "$dargs" && eval get $eflag $sflag "$gargs"

(Я бы, вероятно, не использовал escape сейчас так тщательно - -eнапример, в аргументе это не нужно - но в целом, это один из моих более простых сценариев, использующихescape .)

escapeПрограмма просто выводит свои аргументы, а как echo делает, но он гарантирует , что аргументы защищены для использования с eval(один уровень eval, у меня есть программа , которая сделала удаленное выполнение оболочки, и что необходимо , чтобы избежать вывода escape).

    $ escape $var
    '"./my' 'dir"' '"./anotherdir"'
    $ escape "$var"
    '"./my dir" "./anotherdir"'
    $ escape x y z
    x y z
    $ 

У меня есть другая программа, alкоторая называет свои аргументы по одному на строку (и она еще более древняя: версия 1.1 от 1987-01-27T14: 35: 49). Это наиболее полезно при отладке сценариев, поскольку его можно подключить к командной строке, чтобы увидеть, какие аргументы фактически передаются команде.

    $ echo "$var"
    "./my dir" "./anotherdir"
    $ al $var
    "./my
    dir"
    "./anotherdir"
    $ al "$var"
    "./my dir" "./anotherdir"
    $

[ Добавлено: А теперь, чтобы показать разницу между различными "$@"обозначениями, вот еще один пример:

$ cat xx.sh
set -x
al $@
al $*
al "$*"
al "$@"
$ sh xx.sh     *      */*
+ al He said, '"Don'\''t' do 'this!"' anotherdir my dir xx.sh anotherdir/myfile my dir/my file
He
said,
"Don't
do
this!"
anotherdir
my
dir
xx.sh
anotherdir/myfile
my
dir/my
file
+ al He said, '"Don'\''t' do 'this!"' anotherdir my dir xx.sh anotherdir/myfile my dir/my file
He
said,
"Don't
do
this!"
anotherdir
my
dir
xx.sh
anotherdir/myfile
my
dir/my
file
+ al 'He said, "Don'\''t do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file'
He said, "Don't do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file
+ al 'He said, "Don'\''t do this!"' anotherdir 'my dir' xx.sh anotherdir/myfile 'my dir/my file'
He said, "Don't do this!"
anotherdir
my dir
xx.sh
anotherdir/myfile
my dir/my file
$

Обратите внимание, что ничего не сохраняет оригинальные пробелы между *и */*в командной строке. Также обратите внимание, что вы можете изменить «аргументы командной строки» в оболочке, используя:

set -- -new -opt and "arg with space"

Это устанавливает 4 опции, ' -new', ' -opt', ' and' и ' arg with space'.
]

Хм, это довольно длинный ответ - возможно, толкование - лучший термин. Исходный код escapeдоступен по запросу (электронная почта на имя, точка, фамилия на gmail, точка com). Исходный код alневероятно прост:

#include <stdio.h>
int main(int argc, char **argv)
{
    while (*++argv != 0)
        puts(*argv);
    return(0);
}

Это все. Он эквивалентен test.shсценарию, который показал Роберт Гэмбл, и может быть написан как функция оболочки (но функции оболочки не существовали в локальной версии оболочки Bourne, когда я впервые писалal ).

Также обратите внимание, что вы можете написать alв виде простого сценария оболочки:

[ $# != 0 ] && printf "%s\n" "$@"

Условное условие необходимо для того, чтобы оно не выдавало выходных данных при отсутствии аргументов. Команда printfвыдаст пустую строку только с аргументом строки формата, но программа на Си ничего не выдаст.

Джонатан Леффлер
источник
132

Обратите внимание, что ответ Роберта верен, и он shтоже работает. Вы можете (переносимо) упростить это еще больше:

for i in "$@"

эквивалентно:

for i

Т.е. тебе ничего не нужно!

Тестирование ( $это командная строка):

$ set a b "spaces here" d
$ for i; do echo "$i"; done
a
b
spaces here
d
$ for i in "$@"; do echo "$i"; done
a
b
spaces here
d

Я впервые прочитал об этом в среде программирования Unix Kernighan и Pike.

В bash, help forдокументы это:

for NAME [in WORDS ... ;] do COMMANDS; done

Если 'in WORDS ...;'нет, то 'in "$@"'предполагается.

Алок Сингхал
источник
4
Я не согласен. Нужно знать, что "$@"означает загадочное , и как только вы узнаете, что это for iзначит, оно не менее читабельно, чем for i in "$@".
Алок
10
Я не утверждал, что for iлучше, потому что это экономит нажатия клавиш. Я сравнил удобочитаемость for iи for i in "$@".
Алок
это то, что я искал - как они называют это предположение о $ @ в циклах, когда вам не нужно явно ссылаться на него? Есть ли недостаток в том, чтобы не использовать $ @ reference?
qodeninja
58

Для простых случаев вы также можете использовать shift. Он обрабатывает список аргументов как очередь. Каждый shiftвыбрасывает первый аргумент, и индекс каждого из оставшихся аргументов уменьшается.

#this prints all arguments
while test $# -gt 0
do
    echo "$1"
    shift
done
nuoritoveri
источник
Я думаю, что этот ответ лучше, потому что, если вы делаете shiftв цикле for, этот элемент все еще является частью массива, тогда как с этим shiftработает как ожидалось.
DavidG
2
Обратите внимание, что вы должны использовать, echo "$1"чтобы сохранить интервал в значении строки аргумента. Кроме того, (незначительная проблема) это разрушительно: вы не можете повторно использовать аргументы, если вы удалили их все. Часто это не проблема - вы все равно обрабатываете список аргументов только один раз.
Джонатан Леффлер
1
Согласитесь, что сдвиг лучше, потому что тогда у вас есть простой способ получить два аргумента в случае переключателя для обработки аргументов флага, таких как «-o outputfile».
Baxissimo
С другой стороны, если вы начинаете анализировать аргументы флага, вы можете рассмотреть более мощный язык сценариев, чем bash :)
nuoritoveri
4
Это решение лучше, если вам нужна пара параметров-значений, например. --file myfile.txt Итак, $ 1 - это параметр, $ 2 - это значение, и вы дважды вызываете shift, когда вам нужно пропустить другой аргумент
Daniele Licitra
16

Вы также можете обращаться к ним как к элементам массива, например, если вы не хотите перебирать их все

argc=$#
argv=("$@")

for (( j=0; j<argc; j++ )); do
    echo "${argv[j]}"
done
Baz
источник
5
Я считаю, что строка argv = ($ @) должна быть argv = ("$ @"), другие мудрые аргументы с пробелами обрабатываются неправильно
kdubs
Я подтверждаю, что правильный синтаксис - argv = ("$ @"). если вы не получите последние значения, если у вас есть параметры в кавычках. Например, если вы используете что-то вроде ./my-script.sh param1 "param2 is a quoted param": - используя argv = ($ @), вы получите [param1, param2], - используя argv = ("$ @"), вы получите [param1, param2 - это цитируемый параметр]
jseguillon
2
aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"
g24l
источник
1
Как указано ниже, [ $# != 0 ]это лучше. Выше #$должно быть $#какwhile [[ $# > 0 ]] ...
hute37
1

Усиление ответа baz: если вам нужно перечислить список аргументов с индексом (например, для поиска определенного слова), вы можете сделать это, не копируя список или не изменяя его.

Допустим, вы хотите разделить список аргументов по двойному тире («-») и передать аргументы перед тире одной команде, а аргументы после тире другой:

 toolwrapper() {
   for i in $(seq 1 $#); do
     [[ "${!i}" == "--" ]] && break
   done || return $? # returns error status if we don't "break"

   echo "dashes at $i"
   echo "Before dashes: ${@:1:i-1}"
   echo "After dashes: ${@:i+1:$#}"
 }

Результаты должны выглядеть так:

 $ toolwrapper args for first tool -- and these are for the second
 dashes at 5
 Before dashes: args for first tool
 After dashes: and these are for the second
Рич Кадель
источник
1

getopt Используйте команду в ваших сценариях для форматирования любых параметров или параметров командной строки.

#!/bin/bash
# Extract command line options & values with getopt
#
set -- $(getopt -q ab:cd "$@")
#
echo
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option" ;;
-b) param="$2"
echo "Found the -b option, with parameter value $param"
shift ;;
-c) echo "Found the -c option" ;;
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift
JimmyLandStudios
источник
Сложно, я бы тщательно изучить это. Пример: file: ///usr/share/doc/util-linux/examples/getopt-parse.bash, руководство: man.jimmylandstudios.xyz/?man=getopt
JimmyLandStudios