¿Cómo itero sobre un rango de números definidos por variables en Bash?


1545

¿Cómo itero sobre un rango de números en Bash cuando el rango viene dado por una variable?

Sé que puedo hacer esto (llamado "expresión de secuencia" en la documentación de Bash ):

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

Lo que da:

1
2
3
4
5

Sin embargo, ¿cómo puedo reemplazar cualquiera de los puntos finales del rango con una variable? Esto no funciona

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

Que imprime:

{1..5}


26
Hola a todos, la información y las sugerencias que he leído aquí son realmente útiles. Creo que es mejor evitar el uso de seq. La razón es que algunos scripts deben ser portátiles y deben ejecutarse en una amplia variedad de sistemas Unix, donde algunos comandos pueden no estar presentes. Solo para hacer un ejemplo, seq no está presente por defecto en los sistemas FreeBSD.


99
No recuerdo exactamente desde qué versión de Bash, pero este comando también admite ceros finales. Lo que a veces es realmente útil. El comando for i in {01..10}; do echo $i; donedaría números como 01, 02, 03, ..., 10.
topr

1
Para aquellos como yo que solo quieren iterar sobre el rango de índices de una matriz , la forma bash sería: myarray=('a' 'b' 'c'); for i in ${!myarray[@]}; do echo $i; done(observe el signo de exclamación). Es más específico que la pregunta original, pero podría ayudar. Ver expansiones de parámetros bash
PlasmaBinturong

1
La expansión de llaves también se usa para expresiones como las {jpg,png,gif}que no se abordan directamente aquí, aunque la respuesta será idéntica. Ver expansión de llaves con variable? [duplicado] que está marcado como un duplicado de este.
tripleee

Respuestas:


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

editar: prefiero seqlos otros métodos porque realmente puedo recordarlo;)


36
seq implica la ejecución de un comando externo que generalmente ralentiza las cosas. Esto puede no importar, pero se vuelve importante si está escribiendo un script para manejar una gran cantidad de datos.
paxdiablo

37
Muy bien para una sola línea. La solución de Pax también está bien, pero si el rendimiento fuera realmente una preocupación, no estaría usando un script de shell.
eschercycle

17
seq se llama solo una vez para generar los números. exec () 'no debería ser significativo a menos que este ciclo esté dentro de otro ciclo cerrado.
Javier

29
El comando externo no es realmente relevante: si le preocupa la sobrecarga de ejecutar comandos externos, no desea utilizar scripts de shell, pero en general en Unix, la sobrecarga es baja. Sin embargo, existe el problema del uso de memoria si END es alto.
Mark Baker, el

18
Tenga en cuenta que seq $ENDsería suficiente, ya que el valor predeterminado es comenzar desde 1. Desde man seq: "Si se omite FIRST o INCREMENT, el valor predeterminado es 1".
Fedorqui 'SO deja de dañar'

475

El seqmétodo es el más simple, pero Bash tiene una evaluación aritmética incorporada.

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

La for ((expr1;expr2;expr3));construcción funciona igual que for (expr1;expr2;expr3)en C y lenguajes similares, y como en otros ((expr))casos, Bash los trata como aritméticos.


68
De esta forma se evita la sobrecarga de memoria de una lista grande y una dependencia de seq. Úsalo!
bobbogo

3
@MarinSagovac Esto hace el trabajo y no hay errores de sintaxis. ¿Estás seguro de que tu caparazón es Bash?
gniourf_gniourf

3
@MarinSagovac Asegúrate de hacer #!/bin/bashla primera línea de tu script. wiki.ubuntu.com/…
Melebius

77
solo una pregunta muy breve sobre eso: por qué ((i = 1; i <= END; i ++)) AND NOT ((i = 1; i <= $ END; i ++)); ¿Por qué no $ antes de END?
Baedsch

55
@Baedsch: por la misma razón, no se usa como $ i. Los estados de la página de manual de bash para la evaluación aritmética: "Dentro de una expresión, las variables de shell también se pueden referenciar por nombre sin utilizar la sintaxis de expansión de parámetros".
user3188140

193

discusión

Usar seqestá bien, como sugirió Jiaaro. Pax Diablo sugirió un bucle Bash para evitar llamar a un subproceso, con la ventaja adicional de ser más amigable con la memoria si $ END es demasiado grande. Zathrus detectó un error típico en la implementación del bucle y también insinuó que, dado que ies una variable de texto, las conversiones continuas de un lado a otro se realizan con una desaceleración asociada.

aritmética de enteros

Esta es una versión mejorada del bucle Bash:

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

Si lo único que queremos es el echo, entonces podríamos escribir echo $((i++)).

Ephemient me enseñó algo: Bash permite for ((expr;expr;expr))construcciones. Como nunca leí toda la página de manual de Bash (como lo hice con la kshpágina de manual de Korn shell ( ), y eso fue hace mucho tiempo), me lo perdí.

Entonces,

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

parece ser la forma más eficiente de memoria (no será necesario asignar memoria para consumir seqla salida, lo que podría ser un problema si END es muy grande), aunque probablemente no sea el "más rápido".

la pregunta inicial

eschercycle señaló que la notación Bash { a .. b } funciona solo con literales; cierto, de acuerdo con el manual de Bash. Uno puede superar este obstáculo con un solo (interno) fork()sin un exec()(como es el caso con la llamada seq, que ser otra imagen requiere un tenedor + ejecutivo):

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

Ambos evaly echoson Bash incorporados, pero fork()se requiere a para la sustitución del comando (la $(…)construcción).


1
El único inconveniente del bucle de estilo C es que no puede usar argumentos de línea de comando, ya que comienzan con "$".
karatedog

3
@karatedog: for ((i=$1;i<=$2;++i)); do echo $i; doneen un script funciona bien para mí en bash v.4.1.9, por lo que no veo un problema con los argumentos de la línea de comandos. ¿Te refieres a algo más?
tzot

Parece que la solución de evaluación es más rápida que la integrada en C para: $ time for ((i = 1; i <= 100000; ++ i)); hacer: hecho real 0m21.220s usuario 0m19.763s sys 0m1.203s $ tiempo para i en $ (eval echo "{1..100000}"); hacer: hecho; usuario real 0m13.881s 0m13.536s sys 0m0.152s
Marcin Zaluski

3
Sí, pero evaluar es malo ... ¡@MarcinZaluski time for i in $(seq 100000); do :; donees mucho más rápido!
F. Hauri

El rendimiento debe ser específico de la plataforma, ya que la versión de evaluación es la más rápida en mi máquina.
Andrew Prock

103

Aquí es por qué la expresión original no funcionó.

De man bash :

La expansión de llaves se realiza antes que cualquier otra expansión, y cualquier personaje especial para otras expansiones se conserva en el resultado. Es estrictamente textual. Bash no aplica ninguna interpretación sintáctica al contexto de la expansión o al texto entre llaves.

Entonces, la expansión de llaves es algo que se hace temprano como una operación macro puramente textual, antes de la expansión de parámetros.

Los shells son híbridos altamente optimizados entre macroprocesadores y lenguajes de programación más formales. Para optimizar los casos de uso típicos, el lenguaje se hace bastante más complejo y se aceptan algunas limitaciones.

Recomendación

Sugeriría seguir con las características de Posix 1 . Esto significa usar for i in <list>; do, si la lista ya se conoce, de lo contrario, usar whileo seq, como en:

#!/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 es un gran shell y lo uso de forma interactiva, pero no pongo bash-isms en mis scripts. Los scripts pueden necesitar un shell más rápido, uno más seguro y uno más integrado. Es posible que necesiten ejecutarse en lo que esté instalado como / bin / sh, y luego están todos los argumentos habituales a favor de los estándares. ¿Recuerdas shellshock, también conocido como bashdoor?


13
No tengo el poder, pero movería esto un poco en la lista, sobre todo la mirada bash del ombligo, pero inmediatamente después del estilo C para la evaluación de bucle y aritmética.
mateor

2
Una implicación es que la expansión de llaves no ahorra mucha memoria en comparación con seqgrandes rangos. Por ejemplo, echo {1..1000000} | wcrevela que el eco produce 1 línea, un millón de palabras y 6.888.896 bytes. Intentar seq 1 1000000 | wcproduce un millón de líneas, un millón de palabras y 6,888,896 bytes y también es más de siete veces más rápido, según lo medido por el timecomando.
George

Nota: había mencionado el whilemétodo POSIX anteriormente en mi respuesta: stackoverflow.com/a/31365662/895245 Pero me alegra que esté de acuerdo :-)
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

He incluido esta respuesta en mi respuesta de comparación de rendimiento a continuación. stackoverflow.com/a/54770805/117471 (Esta es una nota para mí mismo para hacer un seguimiento de las que me quedan por hacer).
Bruno Bronosky

@mateor Pensé que el estilo C para la evaluación aritmética y de bucle es la misma solución. ¿Me estoy perdiendo de algo?
Oscar Zhang

73

La manera POSIX

Si le importa la portabilidad, use el ejemplo del estándar POSIX :

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

Salida:

2
3
4
5

Cosas que no son POSIX:


Acabo de recibir 4 votos a favor en esta respuesta, lo cual es muy inusual. Si esto fue publicado en algún sitio web de agregación de enlaces, por favor deme un enlace, saludos.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

La cita se refiere a x, no a toda la expresión. $((x + 1))está bien
chepner

Si bien no es portátil y difiere de GNU seq(BSD le seqpermite establecer una secuencia de terminación de secuencia con -t), FreeBSD y NetBSD también tienen seqdesde 9.0 y 3.0, respectivamente.
Adrian Günter

@CiroSantilli @chepner $((x+1))y $((x + 1))de análisis exactamente lo mismo, como cuando el analizador tokenizes x+1será dividido en 3 fichas: x, +, y 1. xno es un token numérico válido, pero es un token de nombre de variable válido, pero x+no lo es, de ahí la división. +es un token de operador aritmético válido, pero +1no lo es, por lo que el token se divide nuevamente allí. Etcétera.
Adrian Günter

He incluido esta respuesta en mi respuesta de comparación de rendimiento a continuación. stackoverflow.com/a/54770805/117471 (Esta es una nota para mí mismo para hacer un seguimiento de las que me quedan por hacer).
Bruno Bronosky

35

Otra capa de indirección:

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

2
+1: Además, eval 'for i en {1 ..' $ END '}; do ... 'eval parece la forma natural de resolver este problema.
William Pursell

28

Puedes usar

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

seq implica la ejecución de un comando externo que generalmente ralentiza las cosas.
paxdiablo

99
No implica la ejecución de un comando externo para cada iteración, solo una vez. Si el momento de iniciar un comando externo es un problema, está utilizando el idioma incorrecto.
Mark Baker, el

1
Entonces, ¿es el anidamiento el único caso donde esto importa? Me preguntaba si había una diferencia de rendimiento o algún efecto secundario técnico desconocido.
Sqeaky

@Squeaky Esa es una pregunta separada que se responde aquí: stackoverflow.com/questions/4708549/…
tripleee

He incluido esta respuesta en mi respuesta de comparación de rendimiento a continuación. stackoverflow.com/a/54770805/117471 (Esta es una nota para mí mismo para hacer un seguimiento de las que me quedan por hacer).
Bruno Bronosky

21

Si necesita un prefijo, entonces le gustaría esto

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

eso rendirá

07
08
09
10
11
12

44
¿No printf "%02d\n" $isería más fácil que printf "%2.0d\n" $i |sed "s/ /0/"?
zb226

19

Si estás en BSD / OS X puedes usar jot en lugar de seq:

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

17

Esto funciona bien en bash:

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

66
echo $((i++))funciona y lo combina en una sola línea.
Bruno Bronosky

1
Esto tiene extensiones de bash innecesarias. Una versión POSIX: stackoverflow.com/a/31365662/895245
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

1
@Ciro, dado que la pregunta especifica específicamente bash y tiene una etiqueta bash, creo que probablemente encontrarás que las 'extensiones' de bash están más que bien :-)
paxdiablo

@paxdiablo No quiero decir que no sea correcto, pero ¿por qué no ser portátil cuando podemos?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

En bash, simplemente podemos while [[ i++ -le "$END" ]]; dohacer el incremento (posterior) en la prueba
Aaron McDaid

14

He combinado algunas de las ideas aquí y he medido el rendimiento.

TL; DR Para llevar:

  1. seqy {..}son realmente rápidos
  2. fory los whilebucles son lentos
  3. $( ) es lento
  4. for (( ; ; )) los bucles son más lentos
  5. $(( )) es aún más lento
  6. Preocuparse por los N números en la memoria (seq o {..}) es una tontería (al menos hasta 1 millón).

Estas no son conclusiones . Tendría que mirar el código C detrás de cada uno de estos para sacar conclusiones. Esto es más acerca de cómo tendemos a usar cada uno de estos mecanismos para recorrer el código. La mayoría de las operaciones individuales están lo suficientemente cerca de ser la misma velocidad que no va a importar en la mayoría de los casos. Pero un mecanismo como for (( i=1; i<=1000000; i++ ))es muchas operaciones como puede ver visualmente. También es muchas más operaciones por ciclo de las que obtiene for i in $(seq 1 1000000). Y eso puede no ser obvio para usted, por lo que hacer pruebas como esta es valioso.

Población

# 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

1
¡Agradable! Sin embargo, no esté de acuerdo con su resumen. Me parece que $(seq)tiene aproximadamente la misma velocidad que {a..b}. Además, cada operación lleva aproximadamente el mismo tiempo, por lo que agrega aproximadamente 4 μs a cada iteración del bucle para mí. Aquí una operación es un eco en el cuerpo, una comparación aritmética, un incremento, etc. ¿Algo de esto es sorprendente? A quién le importa cuánto tiempo lleva la parafernalia del bucle para hacer su trabajo: es probable que el tiempo de ejecución esté dominado por el contenido del bucle.
bobbogo

@bobbogo tienes razón, realmente se trata de contar la operación. Actualicé mi respuesta para reflejar esto. Muchas llamadas que realizamos realmente realizan más operaciones de las que podríamos esperar. Reduje esto de una lista de aproximadamente 50 pruebas que realicé. Esperaba que mi investigación fuera demasiado nerd incluso para esta multitud. Como siempre, sugiero priorizar sus esfuerzos de codificación de la siguiente manera: hágalo más corto; Hazlo legible; Hazlo mas rapido; Hazlo portátil. A menudo # 1 causa # 3. No pierdas tu tiempo en el n. ° 4 hasta que debas hacerlo.
Bruno Bronosky

8

Sé que esta pregunta se trata bash, pero, solo para el registro, ksh93es más inteligente y la implementa como se esperaba:

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

8

Esta es otra forma:

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

1
Esto tiene la sobrecarga de generar otro caparazón.
codeforester

1
En realidad, esto es extra terrible porque genera 2 proyectiles cuando 1 sería suficiente.
Bruno Bronosky

8

Si desea permanecer lo más cerca posible de la sintaxis de la expresión de llaves, pruebe la rangefunción de bash-tricks 'range.bash .

Por ejemplo, todo lo siguiente hará exactamente lo mismo que echo {1..10}:

source range.bash
one=1
ten=10

range {$one..$ten}
range $one $ten
range {1..$ten}
range {1..10}

Intenta admitir la sintaxis nativa de bash con la menor cantidad posible de "problemas": no solo se admiten variables, sino for i in {1..a}; do echo $i; doneque también se evita el comportamiento a menudo indeseable de rangos no válidos que se suministran como cadenas (por ejemplo ).

Las otras respuestas funcionarán en la mayoría de los casos, pero todas tienen al menos uno de los siguientes inconvenientes:

  • Muchos de ellos usan subcapas , lo que puede dañar el rendimiento y puede no ser posible en algunos sistemas.
  • Muchos de ellos dependen de programas externos. Even seqes un binario que debe instalarse para usarse, debe cargarse mediante bash y debe contener el programa que espera para que funcione en este caso. Ubicuo o no, es mucho más en lo que confiar que solo el lenguaje Bash en sí.
  • Las soluciones que solo utilizan la funcionalidad nativa de Bash, como @ ephemient, no funcionarán en rangos alfabéticos, como {a..z}; la expansión de la abrazadera lo hará. Sin embargo, la pregunta era sobre rangos de números , por lo que esta es una objeción.
  • La mayoría de ellos no son visualmente similares a la {1..10}sintaxis de rango expandido, por lo que los programas que usan ambos pueden ser un poco más difíciles de leer.
  • La respuesta de @ bobbogo usa parte de la sintaxis familiar, pero hace algo inesperado si la $ENDvariable no es un "bookend" de rango válido para el otro lado del rango. Si END=a, por ejemplo, no se produce un error y {1..a}se repite el valor literal . Este es también el comportamiento predeterminado de Bash: a menudo es inesperado.

Descargo de responsabilidad: soy el autor del código vinculado.


7

Reemplazar {}con (( )):

tmpstart=0;
tmpend=4;

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

Rendimientos:

0
1
2
3
4

He incluido esta respuesta en mi respuesta de comparación de rendimiento a continuación. stackoverflow.com/a/54770805/117471 (Esta es una nota para mí para hacer un seguimiento de las que me quedan por hacer).
Bruno Bronosky

6

Todos estos son agradables, pero supuestamente seq está en desuso y la mayoría solo funciona con rangos numéricos.

Si encierra su bucle for entre comillas dobles, las variables de inicio y final se desreferenciarán cuando haga eco de la cadena, y puede enviar la cadena de regreso a BASH para su ejecución. $idebe escaparse con \ 's para que NO se evalúe antes de enviarse a la subshell.

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

Esta salida también se puede asignar a una variable:

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

La única "sobrecarga" que esto debería generar debería ser la segunda instancia de bash, por lo que debería ser adecuada para operaciones intensivas.


5

Si está haciendo comandos de shell y usted (como yo) tiene un fetiche por canalizar, este es bueno:

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


3

Hay muchas formas de hacer esto, sin embargo, las que prefiero se dan a continuación

Utilizando seq

Sinopsis de man seq

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

Sintaxis

Comando completo
seq first incr last

  • primero es el número inicial en la secuencia [es opcional, por defecto: 1]
  • incr es incremento [es opcional, por defecto: 1]
  • último es el último número de la secuencia

Ejemplo:

$ seq 1 2 10
1 3 5 7 9

Solo con primero y último:

$ seq 1 5
1 2 3 4 5

Solo con el último:

$ seq 5
1 2 3 4 5

Utilizando {first..last..incr}

Aquí primero y último son obligatorios e incr es opcional

Usando solo el primero y el último

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

Usando incr

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

Puedes usar esto incluso para los personajes como a continuación

$ 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

3

si no quiere usar ' seq' o ' eval' o el jotformato de expansión aritmética, por ejemplo. for ((i=1;i<=END;i++)), u otros bucles, por ejemplo. while, y no quiere ' printf' y está contento ' echo' solo, entonces esta solución simple podría ajustarse a su presupuesto:

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

PD: Mi bash no tiene seqel comando ' ' de todos modos.

Probado en Mac OSX 10.6.8, Bash 3.2.48


0

Esto funciona en Bash y Korn, también puede ir de mayor a menor número. Probablemente no sea el más rápido o el más bonito, pero funciona lo suficientemente bien. Maneja negativos también.

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"
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.