Як перебрати аргументи в сценарії Bash


899

У мене є складна команда, про яку я хотів би зробити сценарій shell / bash. Я можу це $1легко написати :

foo $1 args -o $1.ext

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

І, звичайно, я хочу обробляти імена файлів з пробілами в них.


67
FYI, це гарна ідея завжди ставити параметри в лапки. Ніколи не знаєш, коли в пармі може бути вбудований простір.
David R Tribble

Відповіді:


1479

Використовуйте "$@"для представлення всіх аргументів:

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

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

sh test.sh 1 2 '3 4'
1
2
3 4

38
До речі, одне з інших достоїнств "$@"полягає в тому, що він розширюється до нічого, коли немає позиційних параметрів, тоді як "$*"розширюється до порожнього рядка - і так, є різниця між аргументами і одним порожнім аргументом. Див. ${1:+"$@"} in / Бін / ш` .
Джонатан Леффлер

9
Зауважте, що це позначення також слід використовувати у функціях оболонки для доступу до всіх аргументів функції.
Джонатан Леффлер

Відкинувши подвійні лапки: $varя зміг виконати аргументи.
еоніст

як почати з другого аргументу? Я маю на увазі, мені це потрібно, але завжди починайте з другого аргументу, це - пропустити $ 1.
m4l490n

4
@ m4l490n: shiftвикидає $1і зміщує всі наступні елементи.
MSalters

239

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

Лаконічна відповідь Роберта Гембла безпосередньо стосується питання. Цей текст посилюється в деяких питаннях з назви файлів, що містять пробіли.

Дивіться також: $ {1: + "$ @"} в / бін / ш

Основна теза: "$@" правильно, і $*(без цитування) майже завжди неправильно. Це тому, що "$@"працює добре, коли аргументи містять пробіли, і працює так само, як $*коли їх немає. За певних обставин, "$*"це також нормально, але "$@"зазвичай (але не завжди) працює там же. Не цитуються $@і $*є рівнозначними (і майже завжди неправильними).

Отже, в чому різниця між $*, $@, "$*"і "$@"? Всі вони пов'язані з "усіма аргументами оболонки", але вони роблять різні речі. Коли некотируваних, $*і $@робити те ж саме. Вони розглядають кожне 'слово' (послідовність непробілу) як окремий аргумент. Проте, цитовані форми є зовсім різними: "$*"трактує список аргументів як єдиний рядок, розділений пробілом, тоді "$@"як аргументи трактує майже так, як вони були, коли вказано в командному рядку. "$@"розширюється взагалі до нічого, коли немає позиційних аргументів; "$*"розширюється до порожнього рядка - і так, є різниця, хоча це може бути важко сприйняти. Детальну інформацію див. Нижче, після введення команди (нестандартної) 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"

(Я, мабуть, не використовую втечу досить глибоко в наші дні - це не потрібно в -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 dot com). Вихідний код для alнеймовірно простий:

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

Це все. Він еквівалентний test.shсценарію, який показав Роберт Гембл, і його можна було записати як функцію оболонки (але функції оболонки не існували в локальній версії оболонки Борна, коли я вперше писав al).

Також зауважте, що ви можете писати alяк простий скрипт оболонки:

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

Умовна необхідна, щоб вона не давала результатів, коли не було передано жодних аргументів. printfКоманда буде виробляти порожній рядок з тільки рядка формату аргументу, але програма C нічого не виробляє.


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 Керніган та Пайк.

В 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 "$@".
Алок Сінгал

це те, що я шукав - як вони називають це припущення про $ @ в циклі, де вам не потрібно явно посилатися на це? Чи є недолік не використовувати $ @ посилання?
qodeninja

58

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

#this prints all arguments
while test $# -gt 0
do
    echo "$1"
    shift
done

Я вважаю, що ця відповідь краща, тому що якщо ви робите shiftцикл for, цей елемент все ще є частиною масиву, тоді як з цим shiftпрацює, як очікувалося.
DavidG

2
Зауважте, що ви повинні використовувати echo "$1"для збереження інтервалу у значенні аргументу. Крім того, (другорядне питання) це є руйнівним: ви не можете повторно використовувати аргументи, якщо ви перемістили їх усі. Часто це не проблема - так чи інакше, ви обробляєте список аргументів один раз.
Джонатан Леффлер

1
Погодьтеся, що зсув краще, тому що тоді у вас є простий спосіб захопити два аргументи у випадку комутатора для обробки аргументів прапора типу "-o вихідний файл".
Baxissimo

З іншого боку, якщо ви починаєте розбирати аргументи прапора, ви можете розглянути більш потужну мову скрипту, ніж bash :)
nuoritoveri

4
Це рішення краще, якщо вам потрібна пара-параметр-значення, наприклад --file myfile.txt , $ 1 - це параметр, $ 2 - це значення, і ви двічі дзвоните, коли вам потрібно пропустити ще один аргумент
Daniele Licitra

16

Ви також можете отримати доступ до них як до елементів масиву, наприклад, якщо ви не хочете переглядати їх усі

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

for (( j=0; j<argc; j++ )); do
    echo "${argv[j]}"
done

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 "$@"

1
Як зазначено нижче, тим [ $# != 0 ]краще. У вищесказаному #$має бути $#як inwhile [[ $# > 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

Хитрий один, я би це дослідив все ретельно. Приклад: файл: ///usr/share/doc/util-linux/examples/getopt-parse.bash, Посібник: man.jimmylandstudios.xyz/?man=getopt
JimmyLandStudios
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.