Como faço para iterar em um intervalo de números definidos por variables ​​no Bash?

Como faço para iterar em um intervalo de números no Bash quando o intervalo é dado por uma variável?

Eu sei que posso fazer isso (chamado de “expressão de seqüência” na documentação do Bash):

for i in {1..5}; do echo $i; done 

Que dá:

1
2
3
4
5

Ainda assim, como posso replace um dos endpoints do intervalo por uma variável? Isso não funciona:

 END=5 for i in {1..$END}; do echo $i; done 

Que imprime:

{1..5}

 for i in $(seq 1 $END); do echo $i; done 

edit: Eu prefiro seq sobre os outros methods, porque eu posso realmente lembrar dele;)

O método seq é o mais simples, mas Bash possui avaliação aritmética embutida.

 END=5 for ((i=1;i<=END;i++)); do echo $i done # ==> outputs 1 2 3 4 5 on separate lines 

O for ((expr1;expr2;expr3)); Constructo funciona como for (expr1;expr2;expr3) em C e linguagens semelhantes, e como outros ((expr)) casos, Bash os trata como aritmética.

discussão

Usar o seq é bom, como Jiaaro sugeriu. Pax Diablo sugeriu um loop de Bash para evitar chamar um subprocess, com a vantagem adicional de ser mais amigável à memory se $ END for muito grande. Zathrus detectou um erro típico na implementação do loop e também sugeriu que, como i é uma variável de texto, conversões contínuas para números de e para números são executadas com um slow-down associado.

aritmética inteira

Esta é uma versão melhorada do loop de Bash:

 typeset -ii END let END=5 i=1 while ((i<=END)); do echo $i … let i++ done 

Se a única coisa que queremos é o echo , então podemos escrever echo $((i++)) .

ephemient me ensinou algo: Bash permite construir for ((expr;expr;expr)) . Desde que eu nunca li a man page inteira para o Bash (como eu fiz com a página de manual do Korn shell ( ksh ), e isso foi há muito tempo atrás), eu senti falta disso.

Assim,

 typeset -ii END # Let's be explicit for ((i=1;i<=END;++i)); do echo $i; done 

parece ser a maneira mais eficiente de memory (não será necessário alocar memory para consumir a saída do seq , o que poderia ser um problema se END for muito grande), embora provavelmente não seja o “mais rápido”.

a pergunta inicial

eschercycle notou que a notação de {a..b} Bash funciona apenas com literais; verdade, de acordo com o manual do Bash. Pode-se superar este obstáculo com um único fork() interno fork() sem um exec() (como é o caso da chamada seq , que sendo outra imagem requer um fork + exec):

 for i in $(eval echo "{1..$END}"); do 

Ambos eval e echo são Bash builtins, mas um fork() é necessário para a substituição do comando (a construção $(…) ).

Eis por que a expressão original não funcionou.

Do homem bash :

A expansão de chaves é executada antes de qualquer outra expansão, e quaisquer caracteres especiais para outras expansões são preservados no resultado. É estritamente textual. Bash não aplica qualquer interpretação sintática ao contexto da expansão ou ao texto entre as chaves.

Então, a expansão de brace é algo feito cedo como uma operação de macro puramente textual, antes da expansão do parâmetro.

Shells são híbridos altamente otimizados entre macros processadores e linguagens de programação mais formais. Para otimizar os casos de uso típicos, a linguagem é bastante mais complexa e algumas limitações são aceitas.

Recomendação

Eu sugeriria ficar com os resources do Posix 1 . Isso significa usar for i in ; do se a lista já é conhecida, caso contrário, use while ou seq , como em:

 #!/bin/sh limit=4 i=1; while [ $i -le $limit ]; do echo $i i=$(($i + 1)) done # Or ----------------------- for i in $(seq 1 $limit); do echo $i done 

1. Bash é uma ótima shell e eu uso interativamente, mas eu não coloco bash-isms em meus scripts. Os scripts podem precisar de um shell mais rápido, mais seguro e mais incorporado. Eles podem precisar ser executados no que estiver instalado como / bin / sh e, em seguida, há todos os argumentos usuais de pro-padrões. Lembre-se de shellshock, também conhecido como bashdoor?

O caminho POSIX

Se você se preocupa com a portabilidade, use o exemplo do padrão POSIX :

 i=2 end=5 while [ $i -le $end ]; do echo $i i=$(($i+1)) done 

Saída:

 2 3 4 5 

Coisas que não são POSIX:

  • (( )) sem dólar, embora seja uma extensão comum como mencionado pelo próprio POSIX .
  • [[ . [ é o suficiente aqui. Veja também: Qual é a diferença entre colchetes simples e duplos no Bash?
  • for ((;;))
  • seq (GNU Coreutils)
  • {start..end} , e isso não pode funcionar com variables ​​conforme mencionado no manual do Bash .
  • let i=i+1 : POSIX 7 2. Linguagem de comando do shell não contém a palavra let e falha no bash --posix 4.3.42
  • o dólar em i=$i+1 pode ser necessário, mas não tenho certeza. POSIX 7 2.6.4 Expansão Aritmética diz:

    Se a variável de shell x contiver um valor que forma uma constante de inteiro válida, incluindo opcionalmente um sinal de mais ou menos, então as expansões aritméticas “$ ((x))” e “$ (($ x))” devem retornar o mesmo valor.

    mas ler literalmente isso não significa que $((x+1)) expande, pois x+1 não é uma variável.

Outra camada de indirecção:

 for i in $(eval echo {1..$END}); do ∶ 

Você pode usar

 for i in $(seq $END); do echo $i; done 

Se você está no BSD / OS X, você pode usar jot em vez de seq:

 for i in $(jot $END); do echo $i; done 

Isso funciona bem no bash :

 END=5 i=1 ; while [[ $i -le $END ]] ; do echo $i ((i = i + 1)) done 

Se você precisar de prefixo do que você pode gostar disso

  for ((i=7;i<=12;i++)); do echo `printf "%2.0d\n" $i |sed "s/ /0/"`;done 

que renderá

 07 08 09 10 11 12 

Eu sei que esta questão é sobre bash , mas – apenas para o registro – ksh93 é mais inteligente e implementa como esperado:

 $ ksh -c 'i=5; for x in {1..$i}; do echo "$x"; done' 1 2 3 4 5 $ ksh -c 'echo $KSH_VERSION' Version JM 93u+ 2012-02-29 $ bash -c 'i=5; for x in {1..$i}; do echo "$x"; done' {1..5} 

Essa é outra maneira:

 end=5 for i in $(bash -c "echo {1..${end}}"); do echo $i; done 

Tudo isso é bom, mas o seq é supostamente obsoleto e a maioria só funciona com intervalos numéricos.

Se você colocar o loop for entre aspas duplas, as variables ​​inicial e final serão desreferenciadas quando você fizer o eco da string, e você poderá enviar a string de volta para BASH para execução. $i precisa ser escapado com \ ‘s, por isso NÃO é avaliado antes de ser enviado para a sub-rede.

 RANGE_START=a RANGE_END=z echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \\${i}; done" | bash 

Esta saída também pode ser atribuída a uma variável:

 VAR=`echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \\${i}; done" | bash` 

A única “sobrecarga” que isso deve gerar deve ser a segunda instância do bash, portanto, deve ser adequada para operações intensivas.

Substitua {} por (( )) :

 tmpstart=0; tmpend=4; for (( i=$tmpstart; i<=$tmpend; i++ )) ; do echo $i ; done 

Rendimentos:

 0 1 2 3 4 

Se você está fazendo comandos de shell e você (como eu) tem um fetiche por pipelining, este é bom:

seq 1 $END | xargs -I {} echo {}

Se você quiser ficar o mais próximo possível da syntax da expressão de chave, experimente a function range do range.bash do bash- range.bash .

Por exemplo, todos os itens a seguir farão exatamente a mesma coisa que echo {1..10} :

 source range.bash one=1 ten=10 range {$one..$ten} range $one $ten range {1..$ten} range {1..10} 

Ele tenta suportar a syntax bash nativa com o mínimo possível de “dicas”: não apenas variables ​​são suportadas, mas o comportamento frequentemente indesejável de intervalos inválidos é fornecido como strings (por exemplo, for i in {1..a}; do echo $i; done ) é evitado também.

As outras respostas funcionarão na maioria dos casos, mas todas elas têm pelo menos uma das seguintes desvantagens:

  • Muitos deles usam subshells , o que pode prejudicar o desempenho e pode não ser possível em alguns sistemas.
  • Muitos deles dependem de programas externos. Mesmo seq é um binário que deve ser instalado para ser usado, deve ser carregado pelo bash e deve conter o programa que você espera, para que ele funcione neste caso. Ubíqua ou não, é muito mais para confiar do que apenas a própria linguagem Bash.
  • As soluções que usam apenas a funcionalidade nativa do Bash, como o @ ephemient, não funcionarão em intervalos alfabéticos, como {a..z} ; cinta expansão vai. A questão era sobre intervalos de números , então isso é um trocadilho.
  • A maioria deles não é visualmente semelhante à syntax da faixa {1..10} de extensão expandida, portanto, os programas que usam ambos podem ser um pouco mais difíceis de ler.
  • A resposta do @bobbogo usa algumas das syntaxs familiares, mas faz algo inesperado se a variável $END não for um “bookend” de intervalo válido para o outro lado do intervalo. Se END=a , por exemplo, um erro não ocorrerá e o valor textual {1..a} será ecoado. Este é também o comportamento padrão do Bash – é apenas inesperado.

Isenção de responsabilidade: Eu sou o autor do código vinculado.

Isso funciona em Bash e Korn, também pode ir de números mais altos para números mais baixos. Provavelmente não é o mais rápido nem o mais bonito, mas funciona bem o suficiente. Lida com negativos também.

 function num_range { # Return a range of whole numbers from beginning value to ending value. # >>> num_range start end # start: Whole number to start with. # end: Whole number to end with. typeset sev s=${1} e=${2} if (( ${e} >= ${s} )); then v=${s} while (( ${v} <= ${e} )); do echo ${v} ((v=v+1)) done elif (( ${e} < ${s} )); then v=${s} while (( ${v} >= ${e} )); do echo ${v} ((v=v-1)) done fi } function test_num_range { num_range 1 3 | egrep "1|2|3" | assert_lc 3 num_range 1 3 | head -1 | assert_eq 1 num_range -1 1 | head -1 | assert_eq "-1" num_range 3 1 | egrep "1|2|3" | assert_lc 3 num_range 3 1 | head -1 | assert_eq 3 num_range 1 -1 | tail -1 | assert_eq "-1" }