script - ¿Cómo itero en un rango de números definidos por variables en Bash?
scripts linux ejercicios resueltos (17)
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?
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));
las obras de construcción, al igual que for (expr1;expr2;expr3)
en C y lenguajes similares, y al igual que en otros ((expr))
casos, Bash los trata como aritméticos.
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
Esto funciona en Bash y Korn, también puede ir de números más altos a más bajos. Probablemente no sea el más rápido o 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 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"
}
Otra capa de indirección:
for i in $(eval echo {1..$END}); do
∶
Puedes usar
for i in $(seq $END); do echo $i; done
Reemplace {}
con (( ))
:
tmpstart=0;
tmpend=4;
for (( i=$tmpstart; i<=$tmpend; i++ )) ; do
echo $i ;
done
Rendimientos:
0
1
2
3
4
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 estás haciendo comandos de shell y tú (como yo) tienes un fetiche para canalizar, este es bueno:
seq 1 $END | xargs -I {} echo {}
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. SiEND=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.
Todos estos son buenos, pero seq supuestamente 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 finalización se eliminarán de las referencias cuando haga eco de la cadena, y puede enviar la cadena de nuevo a BASH para su ejecución. $i
necesita ser escapado con / ''s para que NO sea evaluado antes de ser enviado 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 debe generar debe ser la segunda instancia de bash, por lo que debe ser adecuada para operaciones intensivas.
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 palabralet
, y falla enbash --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 quex+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;)