Como iterar sobre argumentos em um script Bash

Eu tenho um comando complexo que I'gostaria de fazer um script shell/bash de. Eu posso escrevê-lo em termos de $1 facilmente:

foo $1 args -o $1.ext

Eu quero poder passar vários nomes de entrada para o script. Qual'é a maneira correcta de o fazer?

E, claro, eu quero lidar com nomes de arquivo com espaços neles.

Solução

Utilize "$@" para representar todos os argumentos:

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

Isto irá iterar sobre cada argumento e imprimi-lo em uma linha separada. $@ comporta-se como $*, exceto que quando citados os argumentos são quebrados corretamente se houver espaços neles:

sh test.sh 1 2 '3 4'
1
2
3 4
Comentários (7)

Re-escrever uma resposta já eliminada resposta por VonC. A resposta sucinta de Robert Gamble lida diretamente com a pergunta. Esta amplifica em alguns assuntos com nomes de arquivo contendo espaços. Veja também: ${1:+"$@"} in /bin/sh Tese básica: "$@" está correta, e $* (não cotada) está quase sempre errada. Isto porque "$@" funciona bem quando os argumentos contêm espaços, e funciona da mesma forma que $* quando não funciona. Em algumas circunstâncias, "$*" também funciona bem, mas "$@" geralmente (mas não sempre) trabalha nos mesmos lugares. Não cotados, $@ e $* são equivalentes (e quase sempre errados). Então, qual é a diferença entre $*, $@, "$*", e "$@"? Todos eles estão relacionados com "todos os argumentos para a concha", mas fazem coisas diferentes. Quando não citados, $* e $@ fazem a mesma coisa. Eles tratam cada 'palavra' (sequência de espaços não citados) como um argumento separado. As formas citadas são bem diferentes, no entanto: "$*" trata a lista de argumentos como uma única string separada por espaço, enquanto "$@" trata os argumentos quase exatamente como eles eram quando especificados na linha de comando. "$@" expande para nada quando não há argumentos posicionais; "$*" expande para uma string &mdash vazia; e sim, há uma diferença, embora possa ser difícil percebê-la. Veja mais informações abaixo, após a introdução do comando (não-padrão) al. Tese secundária: se você precisar processar argumentos com espaços e depois passe-os para outros comandos, então às vezes você precisa de comandos não-padronizados ferramentas para ajudar. (Ou você deve usar arrays, com cuidado: "${array[@]}"comporta-se analogamente a "$@".) Exemplo:

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

Porque é que isso não funciona? Não funciona porque os processos de citação de conchas antes de se expandirem variáveis. Então, para que a shell preste atenção às aspas embutidas em $var, você tem que usar o "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
    $ 

Isso fica muito complicado quando você tem nomes de arquivos como "Ele disse, "Não faças isso!" (com aspas e aspas duplas e espaços).

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

As conchas (todas elas) não facilitam particularmente o manuseio de tais coisas, então (curiosamente) muitos programas Unix não fazem um bom trabalho de a lidar com eles. No Unix, um nome de arquivo (componente único) pode conter quaisquer caracteres, exceto e NUL '\0'``. No entanto, as conchas encorajam fortemente a ausência de espaços ou novas linhas ou separadores. em qualquer lugar em um caminho nomes. É também por isso que os nomes de arquivos Unix padrão não contêm espaços, etc. Ao lidar com nomes de arquivos que podem conter espaços e outros personagens problemáticos, você tem que ser extremamente cuidadoso, e eu achei há muito tempo que eu precisava de um programa que não é padrão no Unix. Eu o chamo deescape(a versão 1.1 foi datada de 1989-08-23T16:01:45Z). Aqui está um exemplo doescapeem uso - com o sistema de controle SCCS. É um script de capa que faz umdelta(pense *check-in*) e um get(pensa *check-out*). Vários argumentos, especialmente-y(a razão pela qual você fez a mudança) conteria espaços em branco e linhas novas. Note que o script data de 1992, então ele usa back-ticks ao invés de $(cmd ...)notação e não utiliza#!/bin/sh` na primeira linha.

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

(Provavelmente eu não usaria a fuga tão bem hoje em dia - ela é não necessário com o argumento -e', por exemplo - mas, em geral, isto é um dos meus scripts mais simples utilizando oescape). O programaescape' simplesmente produz seus argumentos, como o echo'. mas assegura que os argumentos são protegidos para uso com (um nível de "eval"; eu tenho um programa que fez shell remoto execução, e que era necessário para escapar da saída doescape`).

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

Eu tenho outro programa chamado al que lista seus argumentos um por linha (e é ainda mais antiga: versão 1.1 datada de 1987-01-27T14:35:49). É mais útil na depuração de scripts, pois pode ser ligado a um linha de comando para ver que argumentos são realmente passados ao comando.

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

[Adicionado: E agora para mostrar a diferença entre as várias notas de "$@", aqui está mais um exemplo:

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

Note que nada preserva os espaços em branco originais entre os * e */* na linha de comando. Note também que você pode alterar os 'argumentos da linha de comando' na shell utilizando:

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

Isto define 4 opções, '-novo', '-opt', 'e', e 'arg com espaço'.
] Hmm, isso é um longo resposta - talvez exegese seja o melhor termo. Código fonte para escape disponível a pedido (e-mail para o primeiro ponto do nome último nome no gmail dot com). O código fonte do `al' é incrivelmente simples:

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

É tudo. É equivalente ao script test.sh' que Robert Gamble mostrou, e poderia ser escrito como uma função shell (mas funções shell não existiam na versão local de Bourne shell quando eu escrevial'). Note também que você pode escrever `al' como um simples script shell:

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

O condicional é necessário para que não produza nenhum resultado quando passado sem argumentos. O comando `printf' irá produzir uma linha em branco com apenas o argumento de formatação de string, mas o programa em C não produz nada.

Comentários (1)

Note que a resposta de Robert está correta, e funciona também em sh. Você pode (portabilidade) simplificá-la ainda mais:

for i in "$@"

é equivalente a:

for i

Isto é, não precisas de nada!

Teste ($ é um prompt de comando):

$ 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

Eu li sobre isso pela primeira vez no Unix Programming Environment de Kernighan e Pike.

Em bash',ajuda para' documentar isto:

"para NOME [em PALAVRAS ... ;] do COMMANDS; done

Se 'in WORDS ...;'`` não está presente, então'in "$@"'' é assumido.

Comentários (10)