Como iterar sobre argumentos em um script Bash

900

Eu tenho um comando complexo que eu gostaria de criar um script shell / bash. Eu posso escrever em termos de $1facilidade:

foo $1 args -o $1.ext

Quero poder passar vários nomes de entrada para o script. Qual é o caminho certo para fazer isso?

E, é claro, quero lidar com nomes de arquivos com espaços neles.

Thelema
fonte
67
Para sua informação, é recomendável sempre colocar parâmetros entre aspas. Você nunca sabe quando um parm pode conter um espaço incorporado.
David R Tribble

Respostas:

1482

Use "$@"para representar todos os argumentos:

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

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

sh test.sh 1 2 '3 4'
1
2
3 4
Robert Gamble
fonte
38
Aliás, um dos outros méritos "$@"disso é que ele se expande para nada quando não há parâmetros posicionais, enquanto se "$*"expande para a string vazia - e sim, há uma diferença entre nenhum argumento e um argumento vazio. Veja ${1:+"$@"} in / bin / sh` .
31816 Jonathan Leffler
9
Observe que essa notação também deve ser usada em funções de shell para acessar todos os argumentos da função.
Jonathan Leffler
Ao soltar as aspas duplas: $vareu era capaz de executar os argumentos.
eonist
como começo do segundo argumento? Quero dizer, eu preciso disso, mas sempre comece pelo segundo argumento, ou seja, pule $ 1.
M4l490n
4
@ m4l490n: shiftjoga fora $1e desloca todos os elementos subsequentes.
MSalters
239

Reescreva uma resposta agora excluída pelo VonC .

A resposta sucinta de Robert Gamble lida diretamente com a questão. Este amplifica algumas questões com nomes de arquivos contendo espaços.

Veja também: $ {1: + "$ @"} em / bin / sh

Tese básica: "$@" está correta e $*(sem aspas) quase sempre está errada. Isso ocorre porque "$@"funciona bem quando os argumentos contêm espaços e funciona da mesma maneira que $*quando não. Em algumas circunstâncias, tudo "$*"bem também, mas"$@" geralmente (mas nem sempre) funciona nos mesmos lugares. Não citado $@e $*é equivalente (e quase sempre errado).

Então, qual é a diferença entre $*, $@, "$*", e "$@"? Todos eles estão relacionados a 'todos os argumentos do shell', mas fazem coisas diferentes. Quando não citado, $*e $@faça a mesma coisa. Eles tratam cada 'palavra' (sequência de espaço não em branco) como um argumento separado. As formas citadas são bem diferentes, no entanto: "$*"trata a lista de argumentos como uma única cadeia de caracteres separada por espaço, enquanto "$@"trata os argumentos quase exatamente como eram quando especificados na linha de comando. "$@"expande para nada quando não há argumentos posicionais; "$*"expande para uma string 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 passá-los para outros comandos, às vezes precisará de ferramentas não padrão para ajudá-lo. (Ou você deve usar matrizes com cuidado: "${array[@]}"comporta - se de maneira análoga 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
    $

Por que isso não funciona? Não funciona porque o shell processa aspas antes de expandir variáveis. Portanto, para que o shell preste atenção nas aspas incorporadas $var, você deve usar 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 realmente complicado quando você tem nomes de arquivos como " He said, "Don't do this!"" (com aspas e aspas e espaços duplos).

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

Os shells (todos eles) não facilitam o manuseio de tais coisas, então (engraçado) muitos programas Unix não fazem um bom trabalho em lidar com eles. No Unix, um nome de arquivo (componente único) pode conter qualquer caractere, exceto barra e NUL'\0' . No entanto, os shells não incentivam espaços, novas linhas ou tabulações em nenhum lugar nos nomes de um caminho. É também por isso que os nomes padrão de arquivos Unix não contêm espaços, etc.

Ao lidar com nomes de arquivos que podem conter espaços e outros caracteres problemáticos, você precisa ser extremamente cuidadoso, e descobri há muito tempo que precisava de um programa que não fosse padrão no Unix. Eu chamo issoescape (a versão 1.1 foi datada de 1989-08-23T16: 01: 45Z).

Aqui está um exemplo de escapeuso - com o sistema de controle SCCS. É um script de capa que faz um delta(pense no check-in ) e um get(pense no check-out ). Vários argumentos, especialmente -y(o motivo pelo qual você fez a alteração), conteriam espaços em branco e novas linhas. Observe que o script data de 1992, portanto, usa back-ticks em vez de $(cmd ...)notação e não usa #!/bin/shna 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"

(Eu provavelmente não usaria a fuga tão completamente hoje em dia - não é necessário com o -e argumento, por exemplo - mas, no geral, esse é um dos meus scripts mais simples usandoescape ).

O escapeprograma simplesmente exibe seus argumentos, da mesma forma que echo faz, mas garante que os argumentos sejam protegidos para uso com eval(um nível de eval; eu tenho um programa que executou a shell remotamente e que era necessário para escapar da saída de escape).

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

Eu tenho outro programa chamado alque lista seus argumentos um por linha (e é ainda mais antigo: versão 1.1 de 1987-01-27T14: 35: 49). É mais útil na depuração de scripts, pois pode ser conectado a uma linha de comandos para ver quais argumentos são realmente transmitidos 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 "$@"notações, eis 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
$

Observe que nada preserva os espaços em branco originais entre *e */*na linha de comando. Além disso, observe que você pode alterar os 'argumentos da linha de comando' no shell usando:

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

Isso define 4 opções, ' -new', ' -opt', ' and' e ' arg with space'.
]

Hmm, essa é uma resposta bastante longa - talvez exegese seja o melhor termo. Código-fonte escapedisponível a pedido (email para o ponto de nome sobrenome no gmail ponto com). O código fonte para alé incrivelmente simples:

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

Isso é tudo. É equivalente aotest.sh script que Robert Gamble mostrou e pode ser escrito como uma função shell (mas as funções shell não existiam na versão local do Bourne shell quando escrevi pela primeira vezal ).

Observe também que você pode escrever alcomo um simples shell script:

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

O condicional é necessário para que não produza saída quando não são transmitidos argumentos. O printfcomando produzirá uma linha em branco apenas com o argumento de seqüência de formato, mas o programa C não produz nada.

Jonathan Leffler
fonte
132

Observe que a resposta de Robert está correta e também funciona sh. Você pode (portably) simplificá-lo ainda mais:

for i in "$@"

é equivalente a:

for i

Ou seja, você não precisa de nada!

Teste ( $é o 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 pela primeira vez sobre isso no Unix Programming Environment por Kernighan e Pike.

Em bash, help fordocumenta isso:

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

Se 'in WORDS ...;'não estiver presente, 'in "$@"'será assumido.

Alok Singhal
fonte
4
Discordo. É preciso saber o que "$@"significa o enigmático e, depois que você souber o que for isignifica, não é menos legível do que for i in "$@".
Alok Singhal
10
Eu não afirmei que isso for ié melhor porque salva as teclas digitadas. Eu comparei a legibilidade de for ie for i in "$@".
Alok Singhal
era isso que eu estava procurando - como eles chamam essa suposição de $ @ in loops, na qual você não precisa fazer referência explícita? Existe uma desvantagem em não usar a referência $ @?
Qodeninja 19/08/19
58

Para casos simples, você também pode usar shift. Trata a lista de argumentos como uma fila. Cada um shiftjoga fora o primeiro argumento e o índice de cada um dos argumentos restantes é diminuído.

#this prints all arguments
while test $# -gt 0
do
    echo "$1"
    shift
done
nuoritoveri
fonte
Eu acho que essa resposta é melhor porque se você fizer shiftum loop for, esse elemento ainda faz parte da matriz, enquanto que com isso shiftfunciona conforme o esperado.
DavidG
2
Observe que você deve usar echo "$1"para preservar o espaçamento no valor da sequência de argumentos. Além disso, (uma questão menor) isso é destrutivo: você não pode reutilizar os argumentos se tiver mudado todos eles. Muitas vezes, isso não é um problema - você só processa a lista de argumentos uma vez.
27616 Jonathan Leffler
1
Concorde que essa mudança é melhor, porque você tem uma maneira fácil de pegar dois argumentos em um caso de switch para processar argumentos de flag como "-o outputfile".
Baxissimo 5/17
Por outro lado, se você está começando a analisar argumentos bandeira, você pode querer considerar uma linguagem de script mais poderoso do que o bash :)
nuoritoveri
4
Esta solução é melhor se você precisar de algum parâmetro de valor par, por exemplo --file myfile.txt Então, US $ 1 é o parâmetro, $ 2 é o valor e você chamar Shift duas vezes quando você precisa pular outro argumento
Daniele Licitra
16

Você também pode acessá-los como elementos de uma matriz, por exemplo, se você não deseja iterar por todos eles

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

for (( j=0; j<argc; j++ )); do
    echo "${argv[j]}"
done
baz
fonte
5
Eu acredito que o argv linha = ($ @) deve ser argv = ( "$ @") outros argumentos sábios com espaços não são tratados corretamente
kdubs
Confirmo que a sintaxe correta é argv = ("$ @"). a menos que você não obtenha os últimos valores se tiver parâmetros entre aspas. Por exemplo, se você usar algo como ./my-script.sh param1 "param2 is a quoted param": - usando argv = ($ @) você obterá [param1, param2], - usando argv = ("$ @"), você obterá [param1, param2 é um parâmetro citado]
jseguillon
2
aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"
g24l
fonte
1
Como indicado abaixo, o [ $# != 0 ]é melhor. No exemplo acima, #$deve ser $#como nawhile [[ $# > 0 ]] ...
hute37
1

Ampliando a resposta do baz, se você precisar enumerar a lista de argumentos com um índice (como procurar uma palavra específica), poderá fazer isso sem copiar a lista ou alterá-la.

Digamos que você queira dividir uma lista de argumentos em um traço duplo ("-") e passar os argumentos antes dos traços para um comando e os argumentos após os traços para outro:

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

Os resultados devem ficar assim:

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

getopt Use o comando em seus scripts para formatar qualquer opção ou parâmetro de linha de comando.

#!/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
fonte
Tricky, eu pesquisaria isso completamente. Exemplo: file: ///usr/share/doc/util-linux/examples/getopt-parse.bash, Manual: man.jimmylandstudios.xyz/?man=getopt
JimmyLandStudios