Jak iterować argumenty w skrypcie Bash

899

Mam złożone polecenie, z którego chciałbym utworzyć skrypt powłoki / bash. Mogę to napisać pod względem $1łatwości:

foo $1 args -o $1.ext

Chcę mieć możliwość przekazania wielu nazw wejściowych do skryptu. Jak to zrobić?

I oczywiście chcę obsługiwać nazwy plików ze spacjami w nich.

Thelema
źródło
67
Do Twojej wiadomości, warto zawsze umieszczać parametry w cudzysłowie. Nigdy nie wiadomo, kiedy parm może zawierać osadzoną przestrzeń.
David R Tribble,

Odpowiedzi:

1478

Służy "$@"do reprezentowania wszystkich argumentów:

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

Spowoduje to iterację każdego argumentu i wydrukowanie go w osobnej linii. $ @ zachowuje się jak $ *, z wyjątkiem tego, że podczas cytowania argumenty są poprawnie dzielone, jeśli są w nich spacje:

sh test.sh 1 2 '3 4'
1
2
3 4
Robert Gamble
źródło
38
Nawiasem mówiąc, jedną z innych zalet "$@"jest to, że rozwija się do zera, gdy nie ma parametrów pozycyjnych, podczas gdy "$*"rozwija się do pustego ciągu - i tak, istnieje różnica między brakiem argumentów a jednym pustym argumentem. Zobacz ${1:+"$@"} in / bin / sh` .
Jonathan Leffler,
9
Zauważ, że ta notacja powinna być również używana w funkcjach powłoki, aby uzyskać dostęp do wszystkich argumentów funkcji.
Jonathan Leffler,
Porzucając podwójne cudzysłowy: $varbyłem w stanie wykonać argumenty.
eonista
jak mam zacząć od drugiego argumentu? To znaczy, potrzebuję tego, ale zawsze zaczynaj od drugiego argumentu, czyli pomiń 1 $.
m4l490n
4
@ m4l490n: shiftwyrzuca $1i przesuwa wszystkie kolejne elementy w dół.
MSalters
239

Przepisz teraz usuniętą odpowiedź przez VonC .

Zwięzła odpowiedź Roberta Gamble'a dotyczy bezpośrednio pytania. Ten wzmacnia niektóre problemy z nazwami plików zawierającymi spacje.

Zobacz także: $ {1: + "$ @"} w / bin / sh

Podstawowa teza: "$@" jest poprawna, a $*(nie cytowana) prawie zawsze jest błędna. Jest tak, ponieważ "$@"działa dobrze, gdy argumenty zawierają spacje, i działa tak samo, jak $*gdy nie. W niektórych okolicznościach "$*"jest również OK, ale "$@"zwykle (ale nie zawsze) działa w tych samych miejscach. Nienotowane $@i $*są równoważne (i prawie zawsze błędne).

Więc jaka jest różnica między $*, $@, "$*", i "$@"? Wszystkie są powiązane z „wszystkimi argumentami powłoki”, ale robią różne rzeczy. Gdy nie jest cytowany $*i $@rób to samo. Traktują każde „słowo” (sekwencję spacji) jako osobny argument. Cytowane formy są jednak zupełnie różne: "$*"traktuje listę argumentów jako pojedynczy ciąg oddzielony spacjami, podczas "$@"gdy argumenty traktuje prawie dokładnie tak, jak były podane w wierszu poleceń. "$@"rozwija się do zera, gdy nie ma argumentów pozycyjnych; "$*"rozwija się do pustego ciągu - i tak, jest różnica, chociaż może być trudno ją dostrzec. Zobacz więcej informacji poniżej, po wprowadzeniu polecenia (niestandardowego) al.

Teza dodatkowa: jeśli potrzebujesz przetworzyć argumenty ze spacjami, a następnie przekazać je innym poleceniom, czasem potrzebujesz niestandardowych narzędzi do pomocy. (Lub powinieneś używać tablic ostrożnie: "${array[@]}"zachowuje się analogicznie do "$@".)

Przykład:

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

Dlaczego to nie działa? Nie działa, ponieważ powłoka przetwarza cudzysłowy przed rozwinięciem zmiennych. Aby powłoka zwracała uwagę na osadzone w niej cytaty $var, musisz użyć 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
    $ 

To staje się bardzo trudne, gdy masz nazwy plików, takie jak „ He said, "Don't do this!"” (z cudzysłowami i podwójnymi cudzysłowami i spacjami).

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

Powłoki (wszystkie) nie sprawiają, że szczególnie łatwo jest obsługiwać takie rzeczy, więc (co zabawne) wiele programów uniksowych nie radzi sobie z nimi dobrze. W systemie Unix nazwa pliku (pojedynczy składnik) może zawierać dowolne znaki oprócz ukośnika i wartości NUL '\0'. Jednak powłoki zdecydowanie nie zachęcają do spacji, znaków nowej linii ani tabulatorów w nazwach ścieżek. Z tego powodu standardowe nazwy plików uniksowych nie zawierają spacji itp.

Kiedy masz do czynienia z nazwami plików, które mogą zawierać spacje i inne kłopotliwe znaki, musisz być bardzo ostrożny, a już dawno odkryłem, że potrzebuję programu, który nie jest standardem w Uniksie. Nazywam to escape(wersja 1.1 została opatrzona datą 1989-08-23T16: 01: 45Z).

Oto przykład escapezastosowania - z systemem sterowania SCCS. Jest to skrypt tytułowy, który wykonuje zarówno delta(myśl check-in ), jak i get(myśl check-out ). Różne argumenty, zwłaszcza -y(powód, dla którego dokonałeś zmiany) zawierałyby spacje i znaki nowej linii. Zauważ, że skrypt pochodzi z 1992 roku, więc używa $(cmd ...)notacji zamiast notacji i nie używa #!/bin/shw pierwszym wierszu.

:   "@(#)$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"

(Prawdopodobnie w dzisiejszych czasach nie używałbym ucieczki aż tak dokładnie - nie jest to potrzebne -ena przykład z argumentem - ale ogólnie jest to jeden z moich prostszych skryptów escape.)

escapeProgram po prostu wyprowadza swoje argumenty, a jak echo nie, ale zapewnia, że argumenty są chronione do stosowania eval(jeden poziom eval; mam program, który zrobił na zdalne wykonanie powłoki, i że potrzebne do ucieczki wyjście escape).

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

Mam inny program o nazwie, alktóry wyświetla jego argumenty po jednym w wierszu (i jest jeszcze bardziej stary: wersja 1.1 z 1987-01-27T14: 35: 49). Jest to najbardziej przydatne podczas debugowania skryptów, ponieważ można je podłączyć do wiersza poleceń, aby zobaczyć, jakie argumenty są faktycznie przekazywane do polecenia.

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

[ Dodano: A teraz, aby pokazać różnicę między różnymi "$@"notacjami, oto jeszcze jeden przykład:

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

Zauważ, że nic nie zachowuje oryginalnych spacji między *i */*w linii poleceń. Zauważ też, że możesz zmienić „argumenty wiersza poleceń” w powłoce, używając:

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

Ustawia 4 opcje „ -new”, „ -opt”, „ and” i „ arg with space”.
]

Hmm, to dość długa odpowiedź - być może egzegeza jest lepszym terminem. Kod źródłowy escapedostępny na żądanie (e-mail do imienia i nazwiska kropka w gmail dot com). Kod źródłowy aljest niezwykle prosty:

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

To wszystko. Jest to odpowiednik test.shskryptu, który pokazał Robert Gamble, i może być napisane jako funkcja powłoki (ale funkcje powłoki nie istniały w lokalnej wersji powłoki Bourne'a, kiedy pisałem po raz pierwszy al).

Pamiętaj też, że możesz napisać aljako prosty skrypt powłoki:

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

Warunek jest potrzebny, aby nie generował danych wyjściowych po przekazaniu żadnych argumentów. printfKomenda będzie produkować pusty wiersz tylko argumentu format string, ale program C produkuje niczego.

Jonathan Leffler
źródło
132

Zauważ, że odpowiedź Roberta jest poprawna i działa shrównież. Możesz (przenośnie) uprościć to jeszcze bardziej:

for i in "$@"

jest równa:

for i

Tj. Nie potrzebujesz niczego!

Testowanie ( $jest wierszem poleceń):

$ 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

Po raz pierwszy przeczytałem o tym w Unix Programming Environment autorstwa Kernighana i Pike'a.

W bash, help fordokumenty to:

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

Jeśli 'in WORDS ...;'nie jest obecny, 'in "$@"'zakłada się.

Alok Singhal
źródło
4
Nie zgadzam się. Trzeba wiedzieć, co kryptyczne "$@"znaczy, a kiedy już wiesz, co for ioznacza, jest nie mniej czytelne niż for i in "$@".
Alok Singhal
10
Nie twierdziłem, że for ijest to lepsze, ponieważ oszczędza naciśnięcia klawiszy. Porównałem czytelność for ii for i in "$@".
Alok Singhal
właśnie tego szukałem - jak nazywają to założenie $ @ w pętlach, w których nie trzeba tego jawnie odwoływać? Czy jest wadą nieużywanie $ @ referencji?
qodeninja,
58

W prostych przypadkach można również użyć shift. Traktuje listę argumentów jak kolejkę. Każdy shiftwyrzuca pierwszy argument, a indeks każdego z pozostałych argumentów jest zmniejszany.

#this prints all arguments
while test $# -gt 0
do
    echo "$1"
    shift
done
nuoritoveri
źródło
Myślę, że ta odpowiedź jest lepsza, ponieważ jeśli robisz shiftw pętli for, ten element jest nadal częścią tablicy, podczas gdy z tym shiftdziała zgodnie z oczekiwaniami.
DavidG,
2
Zauważ, że powinieneś użyć, echo "$1"aby zachować odstępy w wartości ciągu argumentu. Ponadto (niewielki problem) jest to destrukcyjne: nie można ponownie użyć argumentów, jeśli wszystkie zostały przeniesione. Często nie stanowi to problemu - i tak lista argumentów jest przetwarzana tylko raz.
Jonathan Leffler,
1
Zgadzam się, że zmiana jest lepsza, ponieważ wtedy masz łatwy sposób na przechwycenie dwóch argumentów w przypadku przełącznika w celu przetworzenia argumentów flag, takich jak „-o plik wyjściowy”.
Baxissimo,
Z drugiej strony, jeśli zaczynasz analizować argumenty flagi, możesz rozważyć
użycie
4
To rozwiązanie jest lepsze, jeśli potrzebujesz pary parametrów i wartości, na przykład --file myfile.txt Więc 1 $ jest parametrem, 2 $ jest wartością i wywołujesz shift dwukrotnie, gdy musisz pominąć inny argument
Daniele Licitra
16

Możesz również uzyskać do nich dostęp jako elementy tablicy, na przykład, jeśli nie chcesz wykonywać iteracji przez wszystkie z nich

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

for (( j=0; j<argc; j++ )); do
    echo "${argv[j]}"
done
baz
źródło
5
Uważam, że wiersz argv = ($ @) powinien mieć postać argv = ("$ @"). Inne mądre argumenty ze spacjami nie są obsługiwane poprawnie
kdubs 15.01.2018
Potwierdzam, że poprawna składnia to argv = („$ @”). chyba że nie otrzymasz ostatnich wartości, jeśli masz podane parametry. Na przykład, jeśli użyjesz czegoś takiego ./my-script.sh param1 "param2 is a quoted param": - używając argv = ($ @) otrzymasz [param1, param2], - używając argv = ("$ @"), otrzymasz [param1, param2 to cytowany param]
jseguillon
2
aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"
g24l
źródło
1
Jak stwierdzono poniżej, [ $# != 0 ]jest lepiej. Powyżej #$powinno być $#jak w inwhile [[ $# > 0 ]] ...
hute37
1

Wzmacniając odpowiedź baz, jeśli musisz wyliczyć listę argumentów indeksem (np. W celu wyszukania określonego słowa), możesz to zrobić bez kopiowania listy lub mutowania jej.

Załóżmy, że chcesz podzielić listę argumentów podwójnym myślnikiem („-”) i przekazać argumenty przed myślnikami do jednego polecenia, a argumenty po myślnikach do drugiego:

 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:$#}"
 }

Wyniki powinny wyglądać następująco:

 $ 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
Rich Kadel
źródło
1

getopt Użyj polecenia w skryptach, aby sformatować dowolne opcje lub parametry wiersza poleceń.

#!/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
źródło
Podstępne, dokładnie to zbadałem. Przykład: plik: ///usr/share/doc/util-linux/examples/getopt-parse.bash
JimmyLandStudios