bash - scripts - shell script linux español




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

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 dio a entender 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 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 , podríamos escribir echo $((i++)) .

ephemient me enseñó algo: Bash permite for ((expr;expr;expr)) construcciones. Ya que nunca he leído la página de manual completa de Bash (como he hecho con la página de manual de Korn shell ( ksh ), y eso fue hace mucho tiempo), me perdí eso.

Asi que,

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 la salida de seq , lo que podría ser un problema si END es muy grande), aunque probablemente no sea la "más rápida".

la pregunta inicial

eschercycle notó que la notación de bash { a .. b } solo funciona con literales; Es cierto, de acuerdo con el manual de Bash. Uno puede superar este obstáculo con una sola fork() (interna) fork() sin un exec() (como es el caso de la llamada seq , que ser otra imagen requiere una bifurcación + exec):

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

Tanto eval como echo son elementos Bash, pero se requiere una fork() para la sustitución de comandos (la construcción $(…) ).

¿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 documentation 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 de rango con una variable? Esto no funciona:

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

Que imprime:

{1..5}


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

De man bash :

La expansión de refuerzo se realiza antes de cualquier otra expansión, y cualquier carácter especial de 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 refuerzo es algo que se hace temprano como una operación de macro puramente textual, antes de la expansión de parámetros.

Los shells son híbridos altamente optimizados entre procesadores de macros 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

Yo sugeriría seguir con las características de Posix 1 . Esto significa usar for i in <list>; do for i in <list>; do , si la lista ya es conocida, de lo contrario, usa 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 shell y lo uso interactivamente, pero no pongo bash-isms en mis scripts. Los scripts pueden necesitar un shell más rápido, uno más seguro, uno más incrustado. Es posible que necesiten ejecutarse en lo que esté instalado como / bin / sh, y luego están todos los argumentos habituales de pro-estándares. ¿Recuerdas a shashshock, también conocido como bashdoor?


Esta es otra manera:

end=5
for i in $(bash -c "echo {1..${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

Otra capa de indirección:

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

Puedes usar

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

Sé que esta pregunta es sobre bash , pero, solo para el registro, ksh93 es 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}

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

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

Si lo necesitas prefijo lo que te guste

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

que cederá

07
08
09
10
11
12

Si quieres estar lo más cerca posible de la sintaxis de la expresión de corchetes, prueba la función de range de 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 admitir la sintaxis de bash nativa con la menor cantidad de "errores" posibles: no solo se admiten variables, sino que el comportamiento a menudo indeseable de rangos no válidos se proporciona como cadenas (por ejemplo, for i in {1..a}; do echo $i; done ) se previene también.

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 utilizan subshells , que pueden dañar el rendimiento y pueden no ser posibles en algunos sistemas.
  • Muchos de ellos dependen de programas externos. Incluso seq es un binario que debe instalarse para ser utilizado, debe estar cargado por bash y debe contener el programa que espera, para que funcione en este caso. Ubicado o no, eso es mucho más en lo que confiar que solo el lenguaje Bash.
  • Las soluciones que solo usan la funcionalidad nativa de Bash, como @phemient's, no funcionarán en rangos alfabéticos, como {a..z} ; refuerzo de la voluntad. 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 ampliado, por lo que los programas que usan ambos pueden ser un poco más difíciles de leer.
  • La respuesta de @bobbogo utiliza parte de la sintaxis familiar, pero hace algo inesperado si la variable $END no es un rango válido de "final de libro" para el otro lado del rango. Si END=a , por ejemplo, no se producirá un error y el valor literal {1..a} repetirá. Este es también el comportamiento predeterminado de Bash, ya que a menudo es inesperado.

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


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 como lo menciona POSIX .
  • [[ . [ es suficiente aquí. Ver también: ¿Cuál es la diferencia entre corchetes simples y dobles en Bash?
  • for ((;;))
  • seq (GNU Coreutils)
  • {start..end} , y eso no puede funcionar con las 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 puede ser requerido, pero no estoy seguro. POSIX 7 2.6.4 Expansión aritmética dice:

    Si la variable de shell x contiene un valor que forma una constante entera válida, incluyendo opcionalmente un signo más o menos inicial, entonces las expansiones aritméticas "$ ((x))" y "$ (($ x))" devolverán lo mismo valor.

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


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

Edición: prefiero seq sobre los otros métodos porque realmente puedo recordarlo;)





syntax