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

568

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

foo $1 args -o $1.ext

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

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

  • 56
    К вашему сведению, это хорошая идея - всегда помещать параметры в кавычки. Вы никогда не знаете, когда parm может содержать встроенное пространство.
Теги:
command-line

6 ответов

1042
Лучший ответ

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

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

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

sh test.sh 1 2 '3 4'
1
2
3 4
  • 20
    Кстати, одним из других достоинств "$@" является то, что он расширяется до нуля, когда нет позиционных параметров, тогда как "$*" расширяется до пустой строки - и да, есть разница между отсутствием аргументов и одним пустым аргументом , Смотрите ${1:+"$@"} in / bin / sh` .
  • 8
    Обратите внимание, что эта запись также должна использоваться в функциях оболочки для доступа ко всем аргументам функции.
Показать ещё 3 комментария
202

Перепишите теперь удаленный ответ 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. Это обложка script, которая выполняет как delta (думаю, заезд), так и get (думаю, выписка). Различные аргументы, особенно -y (причина, по которой вы внесли изменения) будет содержать пробелы и новые строки. Обратите внимание, что script датируется 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"

(Я бы, вероятно, не использовал бы бегство настолько тщательно в эти дни - это не требуется с аргументом -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 доступен по запросу (email to firstname dot lastname в gmail dot com). Исходный код для al невероятно прост:

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

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

Также обратите внимание, что вы можете написать al как простую оболочку script:

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

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

  • 2
    длинный пост ... но я все еще проголосовал, потому что это необходимо для чтения, когда вы реализуете что-то, что использует входные параметры.
93

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

for i in "$@"

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

for i

I.e., вам ничего не нужно!

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

$ 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 Керниганом и Пайком.

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

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

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

  • 3
    Приятно знать для чтения сценариев других людей. Страшно использовать. Явный способ лучше. Избежать путаницы!
  • 4
    Я не согласен. Нужно знать, что означает загадочный "$@" , и как только вы поймете, for i чего for i , это не менее читабельно, чем for i in "$@" .
Показать ещё 7 комментариев
40

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

#this prints all arguments
while test $# -gt 0
do
    echo $1
    shift
done
  • 0
    Я думаю, что этот ответ лучше, потому что, если вы делаете shift в цикле for, этот элемент все еще является частью массива, тогда как с этим shift работает как ожидалось.
  • 2
    Обратите внимание, что вы должны использовать echo "$1" чтобы сохранить интервал в значении строки аргумента. Кроме того, (незначительная проблема) это разрушительно: вы не можете повторно использовать аргументы, если вы удалили их все. Часто это не проблема - вы все равно обрабатываете список аргументов только один раз.
Показать ещё 5 комментариев
3
aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"
  • 1
    Как указано ниже, [ $# != 0 ] лучше. В приведенном выше примере #$ должно быть $# как в while [[ $# > 0 ]] ...
  • 0
    Будет ли это работать в золе?
1

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

argc=$#
argv=($@)

for (( j=0; j<argc; j++ )); do
    echo ${argv[j]}
done
  • 0
    Эти скобки вокруг $ @ важны
  • 1
    Я считаю, что строка argv = ($ @) должна быть argv = ("$ @"), другие мудрые аргументы с пробелами обрабатываются неправильно
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню