Como iterar em um intervalo de números definido por variáveis ​​no Bash?

1545

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

Eu sei que posso fazer isso (chamado "expressão de sequência" na documentação do Bash ):

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

Que dá:

1
2
3
4
5

No entanto, como posso substituir qualquer um dos pontos de extremidade 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}

eschercycle
fonte
26
Olá a todos, as informações e dicas que li aqui são realmente úteis. Eu acho que é melhor evitar o uso de seq. O motivo é que alguns scripts precisam ser portáteis e devem ser executados em uma ampla variedade de sistemas unix, onde alguns comandos podem não estar presentes. Apenas para dar um exemplo, seq não está presente por padrão nos sistemas FreeBSD.
9
Não me lembro desde qual versão do Bash exatamente, mas esse comando também suporta zeros à direita. O que às vezes é realmente útil. Comando for i in {01..10}; do echo $i; donedaria números como 01, 02, 03, ..., 10.
topr
1
Para aqueles como eu, que apenas desejam iterar no intervalo de índices de uma matriz , o caminho do bash seria: myarray=('a' 'b' 'c'); for i in ${!myarray[@]}; do echo $i; done(observe o ponto de exclamação). É mais específico que a pergunta original, mas pode ajudar. Veja expansões de parâmetros do bash
PlasmaBinturong
1
A expansão entre chaves também é usada para expressões como as {jpg,png,gif}que não são abordadas diretamente aqui, embora a resposta seja idêntica. Veja expansão Brace com variável? [duplicado], que está marcado como duplicado deste.
tripleee

Respostas:

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

edit: eu prefiro seqos outros métodos, porque na verdade consigo me lembrar;)

Jiaaro
fonte
36
seq envolve a execução de um comando externo que geralmente torna as coisas mais lentas. Isso pode não importar, mas torna-se importante se você estiver escrevendo um script para lidar com muitos dados.
22468
37
Tudo bem para um one-liner. A solução de Pax também é boa, mas se o desempenho fosse realmente uma preocupação, eu não usaria um shell script.
Eschercycle 04/10/08
17
seq é chamado apenas uma vez para gerar os números. exec () não deve ser significativo, a menos que esse loop esteja dentro de outro loop apertado.
Javier
29
O comando externo não é realmente relevante: se você está preocupado com a sobrecarga da execução de comandos externos, não deseja usar scripts de shell, mas geralmente no unix a sobrecarga é baixa. No entanto, existe o problema do uso de memória se END estiver alto.
Mark Baker
18
Observe que isso seq $ENDseria suficiente, pois o padrão é iniciar a partir de 1. De man seq: "Se PRIMEIRO ou INCREMENTO for omitido, o padrão será 1".
fedorqui 'SO stop prejudying'
474

O seqmétodo é o mais simples, mas o Bash possui avaliação aritmética integrada.

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

A for ((expr1;expr2;expr3));construção funciona como for (expr1;expr2;expr3)em C e linguagens semelhantes e, como em outros ((expr))casos, Bash os trata como aritméticos.

efémero
fonte
67
Dessa maneira, evita-se a sobrecarga de memória de uma lista grande e a dependência de seq. Use-o!
bobbogo
3
@MarinSagovac Isso faz o trabalho e não há erros de sintaxe. Tem certeza de que seu shell é o Bash?
22615
3
@MarinSagovac Certifique-se de criar #!/bin/basha primeira linha do seu script. wiki.ubuntu.com/...
Melebius
7
apenas uma pergunta muito curta sobre isso: por que ((i = 1; i <= END; i ++)) AND NOT ((i = 1; i <= $ END; i ++)); por que não $ antes de END?
Baedsch 20/09/18
5
@ Baedsch: pela mesma razão, eu não é usado como $ i. Os estados da página de manual bash para avaliação aritmética: "Em uma expressão, variáveis ​​de shell também podem ser referenciadas por nome sem usar a sintaxe de expansão de parâmetro."
User3188140
193

discussão

Usar seqé bom, como Jiaaro sugeriu. Pax Diablo sugeriu um loop Bash para evitar chamar um subprocesso, com a vantagem adicional de ser mais amigável à memória 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, as conversões contínuas para números são executadas com uma desaceleração associada.

aritmético inteiro

Esta é uma versão aprimorada do loop Bash:

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

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

ephemient me ensinou algo: Bash permite for ((expr;expr;expr))construções. Como nunca li a página de manual inteira do Bash (como fiz com a kshpágina de manual do Korn shell ( ), e isso foi há muito tempo), senti falta disso.

Assim,

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

parece ser a maneira mais eficiente em termos de memória (não será necessário alocar memória para consumir seq a saída, o que pode ser um problema se END for muito grande), embora provavelmente não seja o "mais rápido".

a pergunta inicial

eschercycle observou que a notação { a .. b } Bash funciona apenas com literais; true, de acordo com o manual do Bash. Pode-se superar esse obstáculo com um único (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 são evale echoBash builtins, mas um fork()é necessário para a substituição de comando (a $(…)construção).

tzot
fonte
1
A única desvantagem do loop de estilo C que ele não pode usar argumentos de linha de comando, pois eles começam com "$".
karatedog
3
@karatedog: for ((i=$1;i<=$2;++i)); do echo $i; doneem um script funciona bem para mim no bash v.4.1.9, então não vejo problema nos argumentos da linha de comando. Você quer dizer outra coisa?
tzot
Parece que a solução eval é mais rápida do que a compilada em C para: $ time for ((i = 1; i <= 100000; ++ i)); Faz :; done real 0m21.220s usuário 0m19.763s sys 0m1.203s $ time para i em $ (eval echo "{1..100000}"); Faz :; feito; reais 0m13.881s usuário 0m13.536s sys 0m0.152s
Marcin Zaluski
3
Sim, mas eval é mau ... @MarcinZaluski time for i in $(seq 100000); do :; doneé muito mais rápido!
F. Hauri
O desempenho deve ser específico da plataforma, pois a versão eval é mais rápida na minha máquina.
Andrew Prock
103

Aqui está o porquê da expressão original não funcionar.

De man bash :

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

Portanto, a expansão entre chaves é algo feito cedo como uma operação macro puramente textual, antes da expansão dos parâmetros.

Os shells são híbridos altamente otimizados entre processadores macro 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 manter os recursos do Posix 1 . Isso significa usar for i in <list>; do, se a lista já for conhecida, caso contrário, use whileou 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. O Bash é um ótimo shell e eu o uso interativamente, mas não coloco bash-isms nos meus scripts. Os scripts podem precisar de um shell mais rápido, mais seguro, mais incorporado. Eles podem precisar executar o que estiver instalado como / bin / sh e, em seguida, existem todos os argumentos pró-padrões usuais. Lembre-se do shellshock, também conhecido como bashdoor?

DigitalRoss
fonte
13
Eu não tenho poder, mas gostaria de subir um pouco na lista, acima de tudo, observando um pouco o umbigo, mas imediatamente após o estilo C para avaliação aritmética e de loop.
mateor
2
Uma implicação é que a expansão entre chaves não economiza muita memória em comparação com as seqgrandes faixas. Por exemplo, echo {1..1000000} | wcrevela que o eco produz 1 linha, um milhão de palavras e 6.888.896 bytes. A tentativa seq 1 1000000 | wcproduz um milhão de linhas, um milhão de palavras e 6.888.896 bytes e também é mais de sete vezes mais rápido, conforme medido pelo timecomando.
George George
Nota: Eu tinha mencionado o POSIX whilemétodo anteriormente na minha resposta: stackoverflow.com/a/31365662/895245 Mas contente você concorda :-)
Ciro Santilli郝海东冠状病六四事件法轮功
Incluímos esta resposta na minha resposta de comparação de desempenho abaixo. stackoverflow.com/a/54770805/117471 (Esta é uma nota para mim mesmo para manter o controle de quais os que me resta fazer.)
de Bruno Bronosky
@mateor Eu pensei que o estilo C para loop e avaliação aritmética são a mesma solução. Estou esquecendo de algo?
Oscar Zhang
73

A maneira POSIX

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

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

Resultado:

2
3
4
5

Coisas que não são POSIX:

Ciro Santilli adicionou uma nova foto
fonte
Só tive 4 votos positivos nesta resposta, o que é altamente incomum. Se isso foi publicado em algum site de agregação de links, dê-me um link, felicidades.
Ciro Santilli escreveu
A citação se refere a x, não a expressão inteira. $((x + 1))está bem.
chepner
Embora não seja portátil e seja diferente do GNU seq(o BSD seqpermite que você defina uma sequência de terminação com -t), o FreeBSD e o NetBSD também têm seqdesde 9.0 e 3.0, respectivamente.
Adrian Günter
@CiroSantilli @chepner $((x+1))e $((x + 1))parse exatamente o mesmo, como quando o analisador tokenizes x+1será dividido em 3 fichas: x, +, e 1. xnão é um token numérico válido, mas é um token de nome de variável válido, ainda x+não é, portanto, a divisão. +é um token de operador aritmético válido, ainda +1não é, portanto o token é novamente dividido lá. E assim por diante.
Adrian Günter
Incluímos esta resposta na minha resposta de comparação de desempenho abaixo. stackoverflow.com/a/54770805/117471 (Esta é uma nota para mim mesmo para manter o controle de quais os que me resta fazer.)
de Bruno Bronosky
35

Outra camada de indireção:

for i in $(eval echo {1..$END}); do
    
bobbogo
fonte
2
+1: Além disso, eval 'for i in {1 ..' $ END '}; do ... 'eval parece a maneira natural de resolver esse problema.
William Pursell
28

Você pode usar

for i in $(seq $END); do echo $i; done
Peter Hoffmann
fonte
seq envolve a execução de um comando externo que geralmente torna as coisas mais lentas.
22468
9
Não envolve a execução de um comando externo para cada iteração, apenas uma vez. Se a hora de iniciar um comando externo for um problema, você está usando o idioma errado.
6308 Mark Baker
1
Então, o aninhamento é o único caso em que isso importa? Fiquei me perguntando se havia uma diferença de desempenho ou algum efeito colateral técnico desconhecido?
Sqeaky
@Squeaky Essa é uma pergunta separada, que foi respondida aqui: stackoverflow.com/questions/4708549/…
tripleee
Incluímos esta resposta na minha resposta de comparação de desempenho abaixo. stackoverflow.com/a/54770805/117471 (Esta é uma nota para mim mesmo para manter o controle de quais os que me resta fazer.)
de Bruno Bronosky
21

Se você precisar dele como prefixo, poderá gostar deste

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

isso renderá

07
08
09
10
11
12
hossbear
fonte
4
Não printf "%02d\n" $iseria mais fácil do que printf "%2.0d\n" $i |sed "s/ /0/"?
zb226 11/02/19
19

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

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

Isso funciona bem em bash:

END=5
i=1 ; while [[ $i -le $END ]] ; do
    echo $i
    ((i = i + 1))
done
paxdiablo
fonte
6
echo $((i++))funciona e combina em uma linha.
Bruno Bronosky
1
Isso tem extensões desnecessárias do bash. A POSIX versão: stackoverflow.com/a/31365662/895245
Ciro Santilli郝海东冠状病六四事件法轮功
1
@Ciro, já que a questão afirma especificamente bash, e tem uma tag bash, eu acho que você provavelmente vai achar que 'extensões' festança são mais do que bem :-)
paxdiablo
@paxdiablo Eu não significa que não é correto, mas por que não ser portátil quando podemos ;-)
Ciro Santilli郝海东冠状病六四事件法轮功
Em bash, podemos simplesmente fazer while [[ i++ -le "$END" ]]; doo (pós) incremento no teste #
Aaron McDaid
14

Combinei algumas das idéias aqui e medi o desempenho.

TL; DR Takeaways:

  1. seqe {..}são muito rápidos
  2. fore whileloops são lentos
  3. $( ) é lento
  4. for (( ; ; )) loops são mais lentos
  5. $(( )) é ainda mais lento
  6. Preocupar-se com os números N na memória (seq ou {..}) é bobo (pelo menos até 1 milhão).

Estas não são conclusões . Você teria que olhar o código C por trás de cada um deles para tirar conclusões. Isso é mais sobre como tendemos a usar cada um desses mecanismos para fazer loop sobre o código. A maioria das operações individuais está perto o suficiente para ter a mesma velocidade que não importa na maioria dos casos. Mas um mecanismo como esse for (( i=1; i<=1000000; i++ ))é muitas operações, como você pode ver visualmente. Também há muito mais operações por loop do que você obtém for i in $(seq 1 1000000). E isso pode não ser óbvio para você, e é por isso que fazer testes como esse é valioso.

Demonstrações

# show that seq is fast
$ time (seq 1 1000000 | wc)
 1000000 1000000 6888894

real    0m0.227s
user    0m0.239s
sys     0m0.008s

# show that {..} is fast
$ time (echo {1..1000000} | wc)
       1 1000000 6888896

real    0m1.778s
user    0m1.735s
sys     0m0.072s

# Show that for loops (even with a : noop) are slow
$ time (for i in {1..1000000} ; do :; done | wc)
       0       0       0

real    0m3.642s
user    0m3.582s
sys 0m0.057s

# show that echo is slow
$ time (for i in {1..1000000} ; do echo $i; done | wc)
 1000000 1000000 6888896

real    0m7.480s
user    0m6.803s
sys     0m2.580s

$ time (for i in $(seq 1 1000000) ; do echo $i; done | wc)
 1000000 1000000 6888894

real    0m7.029s
user    0m6.335s
sys     0m2.666s

# show that C-style for loops are slower
$ time (for (( i=1; i<=1000000; i++ )) ; do echo $i; done | wc)
 1000000 1000000 6888896

real    0m12.391s
user    0m11.069s
sys     0m3.437s

# show that arithmetic expansion is even slower
$ time (i=1; e=1000000; while [ $i -le $e ]; do echo $i; i=$(($i+1)); done | wc)
 1000000 1000000 6888896

real    0m19.696s
user    0m18.017s
sys     0m3.806s

$ time (i=1; e=1000000; while [ $i -le $e ]; do echo $i; ((i=i+1)); done | wc)
 1000000 1000000 6888896

real    0m18.629s
user    0m16.843s
sys     0m3.936s

$ time (i=1; e=1000000; while [ $i -le $e ]; do echo $((i++)); done | wc)
 1000000 1000000 6888896

real    0m17.012s
user    0m15.319s
sys     0m3.906s

# even a noop is slow
$ time (i=1; e=1000000; while [ $((i++)) -le $e ]; do :; done | wc)
       0       0       0

real    0m12.679s
user    0m11.658s
sys 0m1.004s
Bruno Bronosky
fonte
1
Agradável! Não concorde com o seu resumo. Parece-me que $(seq)é sobre a mesma velocidade que {a..b}. Além disso, cada operação leva aproximadamente o mesmo tempo, portanto adiciona cerca de 4μs a cada iteração do loop para mim. Aqui, uma operação é um eco no corpo, uma comparação aritmética, um incremento etc. Isso é surpreendente? Quem se importa com quanto tempo a parafernália do loop leva para fazer seu trabalho - é provável que o tempo de execução seja dominado pelo conteúdo do loop.
22419 bobbogo
@bobbogo você está certo, é realmente sobre a contagem de operações. Eu atualizei minha resposta para refletir isso. Muitas chamadas que fazemos realizam mais operações do que esperávamos. Eu reduzi isso a partir de uma lista de cerca de 50 testes que executei. Eu esperava que minha pesquisa fosse muito nerd, mesmo para essa multidão. Como sempre, sugiro priorizar seus esforços de codificação da seguinte maneira: diminua; Torne legível; Faça isso mais rápido; Torne-o portátil. Muitas vezes, o nº 1 causa o nº 3. Não perca seu tempo no # 4 até que você precise.
Bruno Bronosky
8

Eu sei que essa pergunta é sobre bash, mas - apenas para constar - ksh93é mais inteligente e a implementa conforme o 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}
Adrian Frühwirth
fonte
8

Esta é outra maneira:

end=5
for i in $(bash -c "echo {1..${end}}"); do echo $i; done
Jahid
fonte
1
Isso tem a sobrecarga de gerar outra concha.
codeforester
1
Na verdade, isso é extremamente terrível porque gera 2 conchas quando 1 é suficiente.
Bruno Bronosky 19/02/19
8

Se você deseja permanecer o mais próximo possível da sintaxe da expressão entre chaves, tente a rangefunção em bash-tricks 'range.bash .

Por exemplo, tudo o que se segue fará 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 sintaxe nativa do bash com o mínimo possível de "truques": não apenas as variáveis ​​são suportadas, mas o comportamento muitas vezes indesejável de intervalos inválidos sendo fornecidos como strings (por exemplo, for i in {1..a}; do echo $i; done ).

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

  • Muitos deles usam subcascas , o que pode prejudicar o desempenho e pode não ser possível em alguns sistemas.
  • Muitos deles contam com programas externos. Até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 nesse caso. Onipresente 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 @ ephemient, não funcionarão em intervalos alfabéticos, como {a..z}; cinta expansão vontade. A pergunta era sobre faixas de números , no entanto, então isso é uma queixa.
  • A maioria deles não é visualmente semelhante à {1..10}sintaxe da faixa expandida, então os programas que usam os dois podem ser um pouco mais difíceis de ler.
  • A resposta de @ bobbogo usa algumas das sintaxes familiares, mas faz algo inesperado se a $ENDvariável 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 literal {1..a}for repetido. Esse também é o comportamento padrão do Bash - geralmente é inesperado.

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

Zac B
fonte
7

Substitua {}por (( )):

tmpstart=0;
tmpend=4;

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

Rendimentos:

0
1
2
3
4
BashTheKeyboard
fonte
Incluímos esta resposta na minha resposta de comparação de desempenho abaixo. stackoverflow.com/a/54770805/117471 (Esta é uma nota para mim mesmo para manter o controle de quais os que me resta fazer.)
de Bruno Bronosky
6

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

Se você colocar o loop for entre aspas duplas, as variáveis ​​de início e fim serão desreferenciadas quando você repetir a sequência e poderá enviá-la de volta ao BASH para execução. $iprecisa ser escapado com \ 's, para NÃO ser avaliado antes de ser enviado ao subshell.

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.

SuperBob
fonte
5

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

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

Alex Spangher
fonte
3

Existem muitas maneiras de fazer isso, mas as que eu prefiro são apresentadas abaixo

Usando seq

Sinopse de man seq

$ seq [-w] [-f format] [-s string] [-t string] [first [incr]] last

Sintaxe

Comando completo
seq first incr last

  • primeiro é o número inicial na sequência [é opcional, por padrão: 1]
  • incr é incremento [é opcional, por padrão: 1]
  • last é o último número na sequência

Exemplo:

$ seq 1 2 10
1 3 5 7 9

Somente com o primeiro e o último:

$ seq 1 5
1 2 3 4 5

Somente com o último:

$ seq 5
1 2 3 4 5

Usando {first..last..incr}

Aqui, o primeiro e o último são obrigatórios e o incr é opcional

Usando apenas o primeiro e o último

$ echo {1..5}
1 2 3 4 5

Usando incr

$ echo {1..10..2}
1 3 5 7 9

Você pode usar isso mesmo para caracteres como abaixo

$ echo {a..z}
a b c d e f g h i j k l m n o p q r s t u v w x y z
theBuzzyCoder
fonte
3

se você não quiser usar o formato de expansão ' seq' ou ' eval' jotou aritmético, por exemplo. for ((i=1;i<=END;i++)), ou outros loops, por exemplo. while, e você não quer ' printf' e gosta de ' echo' apenas, essa solução alternativa simples pode caber no seu orçamento:

a=1; b=5; d='for i in {'$a'..'$b'}; do echo -n "$i"; done;' echo "$d" | bash

PS: Meu bash não tem seqcomando ' ' de qualquer maneira.

Testado no Mac OSX 10.6.8, Bash 3.2.48

Zimba
fonte
0

Isso funciona em Bash e Korn, também pode ir de números maiores para menores. Provavelmente não é o mais rápido ou 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 s e v
   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"
}
Ethan Post
fonte