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

¿Cómo puedo iterar 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 impresiones:

{1..5}

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

editar: prefiero seq sobre los otros métodos porque realmente puedo recordarlo;)

El método seq 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 

El for ((expr1;expr2;expr3)); construir funciona igual que for (expr1;expr2;expr3) en C y en lenguajes similares, y al igual que otros casos ((expr)) , Bash los trata como aritmética.

discusión

Usar seq está bien, como sugirió Jiaaro. Pax Diablo sugirió un bucle Bash para evitar llamar a un subproceso, con la ventaja adicional de ser más compatible 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 i es una variable de texto, las conversiones continuas de un lado a otro se realizan con una desaceleración asociada.

Aritmética entera

Esta es una versión mejorada del bucle Bash:

 typeset -ii 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 construcciones for ((expr;expr;expr)) . Como nunca he leído toda la página de manual de Bash (como he hecho con la página man de Korn shell ( ksh ), y eso fue hace mucho tiempo), me lo perdí.

Asi que,

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

parece ser la forma más eficiente de la memoria (no será necesario asignar memoria para consumir la producción de seq , 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 notó que la notación Bash { a ... b } solo funciona con literales; cierto, de acuerdo con el manual de Bash. Uno puede superar este obstáculo con una única fork() (interna) fork() sin un exec() (como es el caso de llamar a seq , que es otra imagen que requiere un fork + exec):

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

Tanto eval como echo son Bash builtins, pero se requiere un fork() para la sustitución del comando (el constructo $(…) ).

Esta es la razón por la cual la expresión original no funcionó.

De hombre bash :

La expansión de corchetes se realiza antes que cualquier otra expansión, y los caracteres especiales de otras expansiones se conservan en el resultado. Es estrictamente textual. Bash no aplica ninguna interpretación sintáctica al contexto de la expansión o el texto entre las llaves.

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

Las shells son híbridos altamente optimizados entre macroprocesadores y lenguajes de progtwigció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

Sugiero seguir con las características de Posix 1 . Esto significa usar for i in ; do for i in ; do , si la lista ya es conocida, de lo contrario, use while o 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 caparazón y lo uso de forma interactiva, pero no coloco bash-isms en mis scripts. Los scripts pueden necesitar un shell más rápido, uno más seguro y otro más incrustado. Es posible que necesiten ejecutar lo que esté instalado como / bin / sh, y luego están todos los argumentos pro estándares habituales. ¿Recuerdas shellshock, también conocido como bashdoor?

La forma 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:

  • (( )) sin dólar, aunque es una extensión común mencionada por POSIX .
  • [[ . [ es suficiente aquí. Ver también: ¿Cuál es la diferencia entre los corchetes simples y dobles en Bash?
  • for ((;;))
  • seq (GNU Coreutils)
  • {start..end} , y eso no puede funcionar con variables como se menciona en el manual de Bash .
  • let i=i+1 : POSIX 7 2. Shell Command Language no contiene la palabra let , y falla en bash --posix 4.3.42
  • el dólar en i=$i+1 podría ser requerido, pero no estoy seguro. POSIX 7 2.6.4 Arithmetic Expansion dice:

    Si la variable de shell x contiene un valor que forma una constante entera válida, que incluye opcionalmente un signo más o menos adelantado, las expansiones aritméticas “$ ((x))” y “$ (($ x))” devolverán el mismo valor.

    pero leerlo literalmente no implica que $((x+1)) expanda ya que x+1 no es una variable.

Otra capa de indirección:

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

Puedes usar

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

Si está utilizando BSD / OS X, puede usar jot en lugar de seq:

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

Esto funciona bien en bash :

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

Si lo necesitas prefijo de lo que te gustaría,

  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 

Sé que esta pregunta es sobre bash , pero, para que conste, ksh93 es más inteligente y lo 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} 

Esta es otra forma:

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

Todos son agradables, pero seq se supone que está obsoleto y la mayoría solo funciona con rangos numéricos.

Si encierra su bucle for entre comillas dobles, las variables de inicio y final serán desreferenciadas cuando haga eco de la cadena, y puede enviar la cadena de regreso a BASH para su ejecución. $i que se escape con \ ‘s así que NO se evalúa 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.

Reemplazar {} con (( )) :

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

Rendimientos:

 0 1 2 3 4 

Si estás haciendo comandos de shell y tú (como yo) tenemos un fetiche para el pipeline, este es bueno:

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

Si quieres mantenerte lo más cerca posible de la syntax de expresión de corchetes, prueba la función de range desde range.bash de bash-tricks .

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 soportar la syntax bash nativa con el menor número posible de “errores”: no solo se admiten variables, sino que el comportamiento a menudo indeseable de los intervalos no válidos se proporciona como cadenas (por ejemplo, for i in {1..a}; do echo $i; done ) también se previene.

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 es posible que no sea posible en algunos sistemas.
  • Muchos de ellos dependen de progtwigs externos. Incluso seq es un binario que debe instalarse para ser utilizado, debe ser cargado por bash, y debe contener el progtwig 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í mismo.
  • Las soluciones que solo usan la funcionalidad nativa de Bash, como @ ephemient, no funcionarán en rangos alfabéticos, como {a..z} ; la expansión ortopédica lo hará. La pregunta era sobre rangos de números , así que esto es una objeción.
  • La mayoría de ellos no son visualmente similares a la syntax de rango {1..10} , por lo que los progtwigs que usan ambos pueden ser un poco más difíciles de leer.
  • La respuesta de @bobbogo usa algo de la syntax familiar, pero hace algo inesperado si la variable $END no es un rango válido “delimitador” para el otro lado del rango. Si END=a , por ejemplo, no se producirá un error y se {1..a} valor literal {1..a} . Este es el comportamiento predeterminado de Bash, también; a menudo es inesperado.

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

Esto funciona en Bash y Korn, también puede ir de mayor a menor número. Probablemente no sea el más rápido ni el más bonito, pero funciona lo suficientemente bien. Maneja los 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 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" }