Jak iterować argumenty w skrypcie Bash

Mam złożone polecenie, z którego chciałbym zrobić skrypt powłoki / Basha. Mogę to napisać w kategoriach $1 łatwo:

foo $1 args -o $1.ext

Chcę móc przekazać skryptowi wiele nazw wejściowych. Jak to zrobić?

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

Author: codeforester, 2008-11-01

9 answers

Użyj "$@", aby przedstawić wszystkie argumenty:

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

Spowoduje iterację każdego argumentu i wydrukowanie go w osobnej linii. $@ zachowuje się jak $ * z tą różnicą, że podczas cytowania argumenty są odpowiednio dzielone, jeśli są w nich spacje:

sh test.sh 1 2 '3 4'
1
2
3 4
 1597
Author: Robert Gamble,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-01 23:52:48

przepisanie usuniętej odpowiedzi przez VonC .

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

Zobacz: ${1:+"$@"} in / bin / sh

Podstawowe prace magisterskie: "$@" jest poprawny, a $* (nienotowany) prawie zawsze się myli. Dzieje się tak dlatego, że "$@" działa dobrze, gdy argumenty zawierają spacje i działa na tak samo jak $* Kiedy nie. W pewnych okolicznościach "$*" też jest OK, ale "$@" zazwyczaj (ale nie zawsze) działa w tych samych miejscach. Nie cytowane, $@ i {[11] } są równoważne (i prawie zawsze błędne).

Więc jaka jest różnica między $*, $@, "$*", i "$@"? Wszystkie są związane z "wszystkimi argumentami do powłoki", ale robią różne rzeczy. Kiedy nie cytowane, $* i $@ robią to samo. Traktują każde "słowo" (sekwencję nie-białych znaków) jako odrębną kłótnia. Formularze cytowane są jednak zupełnie inne: "$*" traktuje listę argumentów jako pojedynczy łańcuch oddzielony spacjami, podczas gdy "$@" traktuje argumenty prawie dokładnie tak, jak były podane w wierszu poleceń. "$@" rozszerza się do niczego, gdy nie ma argumentów pozycyjnych; "$*" rozszerza się do pustego ciągu-i tak, jest różnica, choć może być trudno ją dostrzec. Zobacz więcej informacji poniżej, po wprowadzeniu (niestandardowego) polecenia al.

Teza Drugorzędna: jeśli trzeba przetworzyć argumenty ze spacjami i wtedy przekaż je innym poleceniom, wtedy czasami potrzebujesz niestandardowych Narzędzia wspomagające. (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 cytaty przed rozszerzeniem zmienne. Tak więc, aby powłoka zwracała uwagę na cytaty osadzone w $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
    $ 

Robi się to naprawdę trudne, gdy masz nazwy plików, takie jak " He said, "Don't do this!" "(z cudzysłowami, 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
    $ 
[63]}muszle (Wszystkie) nie sprawiają, że jest to szczególnie łatwe w obsłudze takie rzeczy, więc (co zabawne) wiele programów uniksowych nie wykonuje dobrej pracy radzenie sobie z nimi. W systemie Unix nazwa pliku (jednoskładnikowego) może zawierać dowolne znaki z wyjątkiem slash i NUL '\0'. Jednak powłoki zdecydowanie nie zachęcają do spacji lub nowych linii lub tabs gdziekolwiek w nazwie ścieżki. Dlatego też standardowe Unixowe nazwy plików nie zawierają spacji, itd.

Gdy mamy do czynienia z nazwami plików, które mogą zawierać spacje i inne kłopotliwe postacie, trzeba być niezwykle ostrożnym, a ja znalazłem dawno temu potrzebowałem programu, który nie jest standardem w Unixie. Nazywam to escape (Wersja 1.1 była datowana na 1989-08-23T16:01:45Z).

Oto przykład escape w użyciu-z systemem sterowania SCCS. Jest to skrypt okładki, który wykonuje zarówno delta (think check-in ) i get (think check-out ). Różne argumenty, zwłaszcza -y (powód, dla którego dokonałeś zmiany) zawiera spacje i znaki nowego wiersza. Zauważ, że skrypt pochodzi z 1992 roku, więc używa back-ticków zamiast $(cmd ...) notacji i nie używa #!/bin/sh w pierwszej linii.

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

(w dzisiejszych czasach raczej nie używałbym escape ' a tak dokładnie - jest nie jest potrzebny np. argument -e - ale ogólnie jest to jeden z moich prostszych skryptów przy użyciu escape.)

escape program po prostu wypisuje swoje argumenty, raczej jak echo robi, ale zapewnia, że argumenty są chronione do użycia z eval (jeden poziom eval; mam program, który zrobił zdalną powłokę execution, i że potrzebne do ucieczki wyjścia escape).

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

Mam inny program o nazwie {[28] } który wyświetla swoje argumenty po jednej linii (a jest jeszcze bardziej starożytna: Wersja 1.1 z dnia 1987-01-27T14: 35: 49). Jest to najbardziej przydatne, gdy debugowania skryptów, ponieważ można go podłączyć do wiersz 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 pomiędzy * i */* w wierszu poleceń. Zauważ również, że możesz zmienić "argumenty wiersza poleceń" w powłoce używając:

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

To ustawia 4 opcje, '-new', '-opt', 'and', i "arg with space".
]

Hmm, to dość długa odpowiedź - być możeegzegeza jest lepszym określeniem. Kod źródłowy dla escape dostępny na życzenie (email do firstname dot lastname at gmail dot com). Kod źródłowy dla al jest niesamowicie prosty:

#include <stdio.h>
int main(int argc, char **argv)
{
    while (*++argv != 0)
        puts(*argv);
    return(0);
}
To wszystko. Jest to odpowiednik skryptu test.sh, który pokazał Robert Gamble, i może być napisany jako funkcja powłoki (ale funkcje powłoki nie istniały w lokalnym wersja powłoki Bourne ' a kiedy po raz pierwszy napisałem al).

Zauważ również, że możesz napisać al jako prosty skrypt powłoki:

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

Warunek jest potrzebny, aby nie generował żadnych wyników, gdy nie podano żadnych argumentów. Polecenie printf wyświetli pustą linię tylko z argumentem format string, ale program C nic nie produkuje.

 246
Author: Jonathan Leffler,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-05-13 07:39:36

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

for i in "$@"

Jest równoważne:

for i

Czyli niczego nie potrzebujesz!

Testowanie ($ jest wierszem polecenia):

$ 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 środowisku programowania Unix Kernighan i Pike.

W bash, help for dokumentuje to:

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

Jeśli 'in WORDS ...;' nie występuje, to 'in "$@"' zakłada się.

 138
Author: Alok Singhal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-16 09:28:14

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

#this prints all arguments
while test $# -gt 0
do
    echo "$1"
    shift
done
 64
Author: nuoritoveri,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-08-20 14:24:33

Możesz również uzyskać do nich dostęp jako do elementów tablicy, na przykład, jeśli nie chcesz iterować przez wszystkie

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

for (( j=0; j<argc; j++ )); do
    echo "${argv[j]}"
done
 20
Author: baz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-08-20 14:20:40
aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"
 4
Author: g24l,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-27 18:00:20

Jeśli chcesz wyliczyć listę argumentów za pomocą indeksu (np. wyszukać konkretne słowo), możesz to zrobić bez kopiowania listy lub mutowania jej.

Powiedz, że chcesz podzielić listę argumentów za podwójnym myślnikiem ( " -- " ) i przekazać argumenty przed myślnikami do jednego polecenia, a argumenty za myślnikami do innego:

 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ć tak:

 $ 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
 3
Author: Rich Kadel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-02-02 03:48:46

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

#!/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
 1
Author: JimmyLandStudios,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-03-03 15:31:25

Loop against $#, the number of arguments variable, również działa.

#! /bin/bash

for ((i=1; i<=$#; i++))
do
  printf "${!i}\n"
done
test.sh 1 2 '3 4'

Ouput:

1
2
3 4
 0
Author: Sully,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2021-01-31 17:53:52