En este capítulo, veremos varias funcionalidades del shell que se usan para manejar cadenas y números. El shell proporciona una variedad de expansiones de parámetros que realizan operaciones con cadenas. Además de la expansión aritmética (que vimos en el Capítulo 7), hay un programa de línea de comandos muy común llamado bc, que realiza matemáticas de alto nivel.
Expansión de parámetros
Aunque la expansión de parámetros surgió en el Capítulo 7, no lo vimos en detalle porque la mayoría de las expansiones de parámetros se usan en scripts en lugar de en la línea de comandos. Ya hemos trabajado con algunas formas de expansión de parámetros; por ejemplo, las variables de shell. El shell ofrece muchas más.
Parámetros básicos
La forma más simple de expansión de parámetros se refleja en el uso ordinario de variables. Por ejemplo:
$a
cuando se expande, se convierte en lo que contenga la variable. Los parámetros simples también pueden incluirse entre llaves:
${a}
Esto no tiene efecto en la expansión, pero se requiere si la variable es adyacente a otro texto, que pueda confundir al shell. En este ejemplo, intentaremos crear un nombre de archivo añadiendo la cadena "_file" al contenido de la variable a.
[me@linuxbox ~]$ a="foo"
[me@linuxbox ~]$ echo "$a_file"
Si ejecutamos esta secuencia, el resultado será nada, porque el shell intentará expandir la variable a_file en lugar de a. Este problema puede solucionarse añadiendo llaves:
[me@linuxbox ~]$ echo "${a}_file"
foo_file
También hemos visto que podemos acceder a los parámetros posicionales mayores de 9 incluyendo el número entre llaves. Por ejemplo, para acceder al parámetro posicional undécimo, podemos hacer esto:
${11}
Expansiones para manejar variables vacías
Varias expansiones de parámetros manejan variables inexistentes o vacías. Estas expansiones son útiles para manejar parámetros posicionales perdidos y asignar valores por defecto a parámetros.
${parámetro:-palabra}
Si parámetro está sin definir (p.ej., no existe) o está vacío, esta expansión tiene como resultado el valor de palabra. Si parámetro no está vacío, la expansión tiene como resultado el valor de parámetro.
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:-"substitute value if unset"}
substitute value if unset
[me@linuxbox ~]$ echo $foo
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:-"substitute value if unset"}
bar
[me@linuxbox ~]$ echo $foo
bar
${parámetro:=palabra}
Si parámetro está sin definir o vacío, esta expansión tiene como resultado el valor de palabra. Además, el valor de palabra se asigna a parámetro. Si parámetro no está vacío, la expansión tiene como resultado el valor deparámetro.
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:="default value if unset"}
default value if unset
[me@linuxbox ~]$ echo $foo
default value if unset
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:="default value if unset"}
bar
[me@linuxbox ~]$ echo $foo
bar
Nota: Los posicionales y otros parámetros no pueden asignarse de esta forma.
${parámetro:?palabra}
Si parámetro no está establecido o está vacío, esta expansión hace que el script termine con un error, y el contenido de palabra se envíe al error estándar. Si parámetro no está vacío, la expansión da como resultado el valor de parámetro.
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:?"parameter is empty"}
bash: foo: parameter is empty
[me@linuxbox ~]$ echo $?
1
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:?"parameter is empty"}
bar
[me@linuxbox ~]$ echo $?
0
${parámetro:+palabra}
Si parámetro no está establecido o está vacío, la expansión tiene como resultado nada. Si parámetro no está vacío, el valor de palabra se sustituye por parámetro; sin embargo, el valor de parámetro no cambia.
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:+"substitute value if set"}
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:+"substitute value if set"}
substitute value if set
Expansiones que devuelven nombres de variables
El shell tiene la capacidad de devolver los nombres de las variables. Esto se usa en algunas situaciones algo exóticas.
${!prefijo*}
${!prefijo@}
Esta expansión devuelve los nombres de variables existentes con nombres que empiecen con prefijo. Según la documentación de bash, ambas formas de expansión se comportan idénticamente. Aquí, listamos ttodas las variables en el entorno con nombres que comiencen con BASH:
[me@linuxbox ~]$ echo ${!BASH*}
BASH BASH_ARGC BASH_ARGV BASH_COMMAND BASH_COMPLETION
BASH_COMPLETION_DIR BASH_LINENO BASH_SOURCE BASH_SUBSHELL
BASH_VERSINFO BASH_VERSION
Operaciones con cadenas
Hay una gran colección de expansiones que pueden usarse para operar con cadenas. Muchas de estas expansiones son particularmente adecuadas para operaciones con rutas.
${#parámetro}
se expande en la longitud de la cadena contenida en parámetro. Normalmente, parámetro es una cadena; sin embargo, si parámetro es @ o *, la expansión da como resultado el número de parámetros posicionales.
[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo "'$foo' is ${#foo} characters long."
'This string is long.' is 20 characters long.
${parámetro:margen}
${parámetro:margen:longitud}
Estas expansiones se usan para extraer una porción de la cadena contenida en parámetro. La extracción comienza en margen caracteres desde el principio de la cadena y continua hasta el final de la cadena, a no ser que se especifique longitud.
[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo ${foo:5}
string is long.
[me@linuxbox ~]$ echo ${foo:5:6}
string
Si el valor de margen es negativo, se considera que empieza por el final de la cadena en lugar de por el principio. Fíjate que los valores negativos deben ir precedidos por un espacio para evitar la confusión con la expansión ${parámetro:-palabra}. longitud, si está presente, no debe ser menor de cero.
Si parámetro es @, el resultado de la expansión es longitud parámetros posicionales, comenzando en margen.
[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo ${foo: -5}
long.
[me@linuxbox ~]$ echo ${foo: -5:2}
lo
${parámetro#patrón}
${parámetro##patrón}
Estas expansiones eliminan la parte delantera de la cadena contenida en parámetro definida por patrón. patrón es un patrón comodín como los que se usan en expansiones de rutas. La diferencia entre las dos formas es que la # elimina el resultado más corto, mientras que la forma ## elimina el resultado más largo.
[me@linuxbox ~]$ foo=file.txt.zip
[me@linuxbox ~]$ echo ${foo#*.}
txt.zip
[me@linuxbox ~]$ echo ${foo##*.}
zip
${parámetro%patrón}
${parámetro%%patrón}
Estas expansiones son iguales que las # y ## anteriores, excepto que eliminan texto desde el final de la cadena contenida en parámetro en lugar que desde el principio.
[me@linuxbox ~]$ foo=file.txt.zip
[me@linuxbox ~]$ echo ${foo%.*}
file.txt
[me@linuxbox ~]$ echo ${foo%%.*}
file
${parámetro/patrón/cadena}
${parámetro//patrón/cadena}
${parámetro/#patrón/cadena}
${parámetro/%patrón/cadena}
Esta expansión realiza un "buscar y reemplazar" dentro del contenido de parámetro. Si se encuentra texto que coincida con el comodín patrón, se reemplaza con el contenido de cadena. En la forma normal, solo se reemplaza la primera coincidencia de patrón. En la forma //, se reemplazan todas las coincidencias. La forma /# requiere que la coincidencia ocurra al principio de la cadena, y la forma /% requiere que la coincidencia ocurra al final de la cadena. /cadena puede omitirse, lo que causa que el texto señalado por patrón se borre.
[me@linuxbox ~]$ foo=JPG.JPG
[me@linuxbox ~]$ echo ${foo/JPG/jpg}
jpg.JPG
[me@linuxbox ~]$ echo ${foo//JPG/jpg}
jpg.jpg
[me@linuxbox ~]$ echo ${foo/#JPG/jpg}
jpg.JPG
[me@linuxbox ~]$ echo ${foo/%JPG/jpg}
JPG.jpg
La expansión de parámetros es algo bueno de conocer. Las expansiones con manipulación de cadenas pueden usarse como sustitutos de otros comandos comunes como sed o cut. Las expansiones mejoran la eficiencia de los scripts eliminando el uso de programas externos. Como ejemplo, modificaremos el programa longest-word que vimos en el capítulo anterior para usar la expansión de parámetros ${#j} en lugar de la sustitución de comandos $(echo $j | wc -c) y su subshell resultante, así:
#!/bin/bash
# longest-word3 : find longest string in a file
for i; do
if [[ -r $i ]]; then
max_word=
max_len=
for j in $(strings $i); do
len=${#j}
if (( len > max_len )); then
max_len=$len
max_word=$j
fi
done
echo "$i: '$max_word' ($max_len characters)"
fi
done
A continuación, compararemos la eficiencia de las dos versiones usando el comando time:
[me@linuxbox ~]$ time longest-word2 dirlist-usr-bin.txt
dirlist-usr-bin.txt: 'scrollkeeper-get-extended-content-list'
(38 characters)
real 0m3.618s
user 0m1.544s
sys 0m1.768s
[me@linuxbox ~]$ time longest-word3 dirlist-usr-bin.txt
dirlist-usr-bin.txt: 'scrollkeeper-get-extended-content-list'
(38 characters)
user 0m1.544s
sys 0m1.768s
[me@linuxbox ~]$ time longest-word3 dirlist-usr-bin.txt
dirlist-usr-bin.txt: 'scrollkeeper-get-extended-content-list'
(38 characters)
real 0m0.060s
user 0m0.056s
sys 0m0.008s
La versión original del script tarda 3,618 segundos en escanear el archivo, mientras que la nueva versión, usando expansión de parámetros, tarda sólo 0,06 segundos - una mejora significativa.
user 0m0.056s
sys 0m0.008s
La versión original del script tarda 3,618 segundos en escanear el archivo, mientras que la nueva versión, usando expansión de parámetros, tarda sólo 0,06 segundos - una mejora significativa.
Conversión de mayúsculas y minúsculas
Las versiones recientes de bash tiene soporte para conversión mayúsculas/minúsculas de cadenas. bash tiene cuatro expansiones de parámetros y dos opciones para el comando declare para soportarlo.
¿Y para que sirve la conversión mayúsculas-minúsculas? Además del obvio valor estético, tiene un importante papel en programación. Consideremos el caso de una búsqueda en base de datos. Imagina que un usuario ha introducido una cadena en un campo de entrada de texto que querernos buscar en una base de datos. Es posible que el usuario introduzca el valor todo en mayúsculas o todo en minúsculas o una combinación de ambas. Ciertamente no queremos abarrotar nuestra base de datos con cada combinación posible de letras mayúsculas y minúsculas. ¿qué hacemos?
Una aproximación común a este problema es normalizar la entrada del usuario. O sea, convertirla en una forma estandarizada antes de intentar la búsqueda en la base de datos. Podemos hacer esto convirtiendo todos los caracteres de la entrada del usuario a mayúsculas o minúsculas y asegurarnos de que las entradas en la base de datos están normalizadas de la misma forma.
El comando declare puede usarse para normalizar cadenas a mayúsculas o minúsculas. Usando declare, podemos forzar que una variable siempre contenga el formato deseado sin importar lo que tenga asignado:
#!/bin/bash
# ul-declare: demonstrate case conversion via declare
declare -u upper
declare -l lower
if [[ $1 ]]; then
upper="$1"
lower="$1"
echo $upper
echo $lower
fi
En el script anterior, usamos declare para crear dos variables, upper y lower. Asignamos el valor del primer argumento de la línea de comandos (parámetro posicional 1) a cada variable y luego mostrarlas en la pantalla:
[me@linuxbox ~]$ ul-declare aBc
ABC
abc
Como podemos ver, el argumento de línea de comandos ("aBc") ha sido normalizado.
Hay cuatro expansiones de parámetros que realizan la conversión mayúsuclas/minúsculas:
Expansión de parámetros para conversión mayúsculas/minúsculas
${parámetro,,}
Expande el valor de parámetro todo en minúsculas.
${parámetro,}
Expande el valor de parámetro cambiando sólo el primer carácter a minúsculas.
${parámetro^^}
Expande el valor de parámetro todo en mayúsculas.
${parámetro^}
Expande el valor de parámetro cambiando sólo el primer carácter a mayúsculas (Nombre propio).
Aquí tenemos un script que demuestra estas expansiones:
#!/bin/bash
# ul-param - demonstrate case conversion via parameter expansion
if [[ $1 ]]; then
echo ${1,,}
echo ${1,}
echo ${1^^}
echo ${1^}
fi
Aquí está el script en acción:
[me@linuxbox ~]$ ul-param aBc
abc
aBc
ABC
ABc
De nuevo, procesamos el primer argumento de la línea de comandos y obtenemos las cuatro variantes soportadas por las expansiones de parámetros. Aunque este script usa el primer parámetro posicional, parámetro puede ser una cadena, una variable o una expresión.
Evaluación aritmética y expansión
Hemos visto la expansión aritmética en el Capítulo 7. Se usa para realizar varias operaciones aritméticas con enteros. Su forma básica es:
$((expresión))
donde expresión es una expresión aritmética válida.
Esto está relacionado con el comando compuesto (( )) usado para evaluación aritmética (pruebas de verdad) que vimos en el Capítulo 27.
En capítulos anteriores, vimos algunos de los tipos comunes de expresiones y operadores. Aquí, veremos una lista más completa.
Bases numéricas
Cuando estábamos en el Capítulo 9, echamos un vistazo a los octales (base 8) y hexadecimales (base 16). En las expresiones aritméticas, el shell soporta constantes enteras en cualquier base.
Especificando diferentes bases numéricas
número
Por defecto, los números sin notación se tratan como enteros decimales (base 10).
0número
En expresiones aritméticas, números que comienzan con un cero se consideran octales.
0xnúmero
Notación hexadecimal
base#número
número en base base
Algunos ejemplos:
[me@linuxbox ~]$ echo $((0xff))
255
[me@linuxbox ~]$ echo $((2#11111111))
255
En los ejemplos anteriores, mostramos el valor de un número hexadecimal ff(el mayor número de dos dígitos) y el mayor número binario (base 2) de ocho dígitos.
Operadores unarios
Hay dos operadores unarios, el + y el -, que se usan para indicar si un número es positivo o negativo, respectivamente. Por ejemplo, -5.
Aritmética simple
Los operadores aritméticos ordinarios se listan en la siguiente tabla:
Operadores aritméticos
Operador Descripción
+ Adición
- Sustracción
* Multiplicación
/ División entera
** Exponenciación
% Módulo (resto)
La mayoría son autoexplicativos, pero la división entera y el módulo requieren una explicación.
Como la aritmética del shell sólo opera con enteros, los resultados de la división son siempre números enteros:
[me@linuxbox ~]$ echo $(( 5 / 2 ))
2
Esto hace que la determinación del resto de una división sea más importante:
[me@linuxbox ~]$ echo $(( 5 % 2 ))
1
Usando los operadores división y módulo, podemos determinar que 5 dividido entre 2 da como resultado 2, con un resto de 1.
Calcular el resto es útil en bucles. Permite que una operación se realice en intervalos especificados durante la ejecución del bucle. En el ejemplo siguiente, mostramos una línea de números, destacando cada múltiplo de 5:
#!/bin/bash
# modulo : demonstrate the modulo operator
for ((i = 0; i <= 20; i = i + 1)); do
remainder=$((i % 5))
if (( remainder == 0 )); then
printf "<%d> " $i
else
printf "%d " $i
fi
done
printf "\n"
Cuando se ejecuta, el resultado es el siguiente:
[me@linuxbox ~]$ modulo
<0> 1 2 3 4 <5> 6 7 8 9 <10> 11 12 13 14 <15> 16 17 18 19 <20>
Asignación
A pesar de que su utilidad puede no ser inmediatamente aparente, las expresiones aritméticas pueden realizar asignación. Hemos realizado asignación muchas veces, pero en un contexto diferente. Cada vez que damos un valor a una variable, estamos realizando asignación. Podemos hacerlo también dentro de expresiones aritméticas:
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo $foo
[me@linuxbox ~]$ if (( foo = 5 ));then echo "It is true."; fi
It is true.
[me@linuxbox ~]$ echo $foo
5
En el ejemplo anterior, primero asignamos un valor vacío a la variable foo y verificamos que está realmente vacía. A continuación, realizamos un if con el comando compuesto (( foo = 5 )). Este proceso hace dos cosas interesantes: 1) asigna el valor 5 a la variable foo, y 2) se evalúa como verdadera porque se le asignó a foo un valor distinto de cero.
Nota: Es importante recordar el significado exacto del = en la expresión anterior. Un = individual realiza asignación. foo = 5 dice "haz a foo igual a 5", mientras == evalúa equivalencia. foo == 5 dice "¿es foo igual a 5? Esto puede ser muy confuso porque el comando test acepta un = individual para equivalencia de cadenas. Esta es otra razón más para usa los comandos compuestos más modernos [[ ]] y (( )) en lugar de test.
Además del =, el shell también ofrece notaciones que realizan algunos asignaciones muy útiles:
Operadores de asignación
parámetro = valor
Asignación simple. Asigna valor a parámetro.
parámetro +=valor
Adición. Equivale a parámetro = parámetro + valor.
parámetro -=valor
Sustracción. Equivale a parámetro = parámetro - valor.
parámetro *=valor
Multiplicación. Equivale a parámetro =
parámetro * valor.
parámetro /=valor
División entera. Equivale a parámetro = parámetro / valor.
División entera. Equivale a parámetro = parámetro / valor.
parámetro %=valor
Módulo. Equivale a parámetro = parámetro % valor.
parámetro ++
Variable post-incremental. Equivale a parámetro =parámetro +1 (de todas formas, lo veremos continuación).
parámetro --
Variable post-decremental. Equivale a parámetro =parámetro - 1.
++parámetro
Variable pre-incremental. Equivale a parámetro =parámetro +1.
--parámetro
Variable pre-decremental. Equivale a parámetro =parámetro -1.
Estos operadores de asignación aportan un atajo para muchas de las tareas aritméticas comunes. De especial interés son los operadores incrementales (++) y decrementales (--), que aumentan o disminuyen el valor de sus parámetros en uno. Este estilo de notación se toma del lenguaje de programación C y ha sido incorporado a otros lenguajes de programación, incluido bash.
Los operadores pueden aparecer delante o detrás de un parámetro. Aunque ambos aumentan o disminuyen el parámetro en uno, las dos localizaciones tiene una diferencia sutil. Si se coloca delante del parámetro , el parámetro aumenta (o disminuye) antes de que se devuelva el parámetro. Si se coloca después, la operación se realiza después de que el parámetro se devuelva. Esto es un poco extraño, pero está hecho a propósito. Aquí tenemos una demostración:
[me@linuxbox ~]$ foo=1
[me@linuxbox ~]$ echo $((foo++))
1
[me@linuxbox ~]$ echo $foo
2
Si asignamos el valor de uno a la variable foo y luego la incrementamos con el operador ++ situado tras el nombre del parámetro, foo es devuelto con el valor de uno. Sin embargo, si miramos el valor de la variable una segunda vez, vemos el valor incrementado. Si colocamos el operador ++ delante del parámetro, tenemos el comportamiento esperado:
[me@linuxbox ~]$ foo=1
[me@linuxbox ~]$ echo $((++foo))
2
[me@linuxbox ~]$ echo $foo
2
Para la mayoría de las aplicaciones de shell, colocar un prefijo al operador será lo más útil.
Los operadores ++ y -- se usan a menudo junto con los bucles. Haremos algunas mejoras en nuestro módulo de script ajustándolo un poco:
#!/bin/bash
# modulo2 : demonstrate the modulo operator
for ((i = 0; i <= 20; ++i )); do
if (((i % 5) == 0 )); then
printf "<%d> " $i
else
printf "%d " $i
fi
done
printf "\n"
Operaciones con bits
Hay una clase de operadores que manipulan números de un modo inusual. Estos operadores funcionan a nivel de bits. Se usan para ciertos tipos de tareas de bajo nivel, a menudo implican configuración o lectura de banderas-bit:
Operadores de bits
~ Negación bit a bit. Niega todos los bit de un número.
<< Cambia bit a bit hacia la izquierda. Cambia todos los bits en un número hacia la izquierda.
>> Cambia bit a bit hacia la derecha. Cambia todos los bits en un número hacia la derecha.
& AND bit a bit. Realiza una operación AND en todos los bits en dos números.
| OR bit a bit. Realiza una operación OR en todos los bits en dos números.
^ XOR bit a bit. Realiza una operación OR exclusiva en todos los bits en dos números.
Fíjate que también hay los correspondientes operadores de asignación (por ejemplo, <<==) para todos excepto para la negación bit a bit.
Aquí lo comprobaremos produciendo una lista de potencias de 2, usando el operador de cambio bit a bit hacia la izquierda:
[me@linuxbox ~]$ for ((i=0;i<8;++i)); do echo $((1<<i)); done
12
48
16
32
64
128
Lógica
Como descubrimos en el Capítulo 27, el comando compuesto (( )) soporta una variedad de operadores de comparación. Hay unos pocos más que pueden usarse par evaluar lógica. Aquí tenemos la lista completa:
Operadores de comparación
<= Menor o igual que
>= Mayor o igual que
< Menor que
> Mayor que
== Igual a
!= No igual a
&& AND lógico
|| OR lógico
expr1?expr2?:expr3
Operación de comparación (ternario). Si expr1 se evalúa como no-cero (verdad aritmética) entonces expr2, si no expr3.
Cuando se usan para operaciones lógicas, las expresiones siguen las reglas de la lógica aritmética; o sea, las expresiones que se evalúan como cero se consideran falsas, mientras que las expresiones no-cero se consideran verdaderas. El comando compuesto (( )) mapea los resultados dentro de los códigos de salida normales de shell:
[me@linuxbox ~]$ if ((1)); then echo "true"; else echo "false"; fi
true
[me@linuxbox ~]$ if ((0)); then echo "true"; else echo "false"; fi
false
El operador lógico más extraño es el operador ternario. Este operador (que se modeló después del que hay en el lenguaje de programación C) realiza un test lógico autónomo. Puede usarse como un tipo de sentencia if/then/else. Actúa en tres expresiones aritméticas (las cadenas no funcionarán), y si la primera expresión es verdadera (o no-cero) se ejecuta la segunda expresión. Si no, se ejecuta la tercera expresión. Podemos probarlo en la línea de comandos:
[me@linuxbox ~]$ a=0
[me@linuxbox ~]$ ((a<1?++a:--a))
[me@linuxbox ~]$ echo $a
1
[me@linuxbox ~]$ ((a<1?++a:--a))
[me@linuxbox ~]$ echo $a
0
Aquí vemos al operador ternario en acción. Este ejemplo implementa un interruptor. Cada vez que el operador se ejecuta, el valor de la variable a cambia de cero a uno o viceversa.
Por favor, fíjate que realizar asignación dentro de expresiones no es sencillo.
Cuando lo intentamos, bash devuelve un error:
[me@linuxbox ~]$ a=0
[me@linuxbox ~]$ ((a<1?a+=1:a-=1))
bash: ((: a<1?a+=1:a-=1: attempted assignment to non-variable (error
token is "-=1")
Este problema puede evitarse incluyendo la expresión de asignación entre paréntesis:
[me@linuxbox ~]$ ((a<1?(a+=1):(a-=1)))
A continuación, vemos un ejemplo más completo del uso de operadores aritméticos en un script que produce una tabla simple de números:
#!/bin/bash
# arith-loop: script to demonstrate arithmetic operators
finished=0
a=0
printf "a\ta**2\ta**3\n"
printf "=\t====\t====\n"
until ((finished)); do
b=$((a**2))
c=$((a**3))
printf "%d\t%d\t%d\n" $a $b $c
((a<10?++a:(finished=1)))
done
En este script, implementamos un bucle until basado en el valor de la variable finished. Inicialmente, la variable está establecida en cero (falso aritmético) y continuamos el bucle hasta que se convierta en no-cero. Dentro del bucle, calculamos el cuadrado y el cubo de la variable contador a. Al final del bucle, el valor de la variable contador se evalúa. Si es menor de 10 (el número máximo de iteraciones), se incrementa en uno, si no, se le da a la variable finished el valor de uno, haciendo a finished aritméticamente verdadera, y de esta forma terminando el bucle. Ejecutar el script no da este resultado:
[me@linuxbox ~]$ arith-loop
a a**2 a**3
= ==== ====
0 0 0
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
6 36 216
7 49 343
8 64 512
9 81 729
10 100 1000
bc - Un lenguaje de cálculo de precisión arbitraria
Hemos visto como el shell puede manejar todos los tipos de aritmética con enteros, pero ¿qué pasa si necesitamos realizar matemáticas más complejas o incluso usar números de coma flotante? La respuesta es, no podemos. Al menos no directamente con el shell. Para hacerlo, necesitamos usar un programa externo. Hay varios enfoques que podemos tomar. Incluir programas Perl o AWK es una solución posible, pero desafortunadamente, están fuera del objetivo de este libro.
Otro acercamiento es usar un programa de cálculo especializado. Uno de estos programas que encontramos en la mayoría de los sistemas Linux se llama bc.
El programa bc lee un archivo escrito en su propio lenguaje tipo C y lo ejecuta. Un script bc puede ser una archivo por separado o puede leerse de la entrada estándar. El lenguaje bc tiene bastantes características incluyendo variables, bucles y funciones definidas por el programador. No veremos bc completamente aquí, sólo lo probaremos. bc está bien documentado por su man page.
Empecemos con un ejemplo simple. Escribiremos un script bc para sumar 2 más 2:
/* A very simple bc script */
2 + 2
La primera línea del script es un comentario. bc usa la misma sintáxis para comentarios que el lenguaje de programación C. Comentarios que se pueden expandir en múltiples líneas, comenzando con /* y finalizando con /*.
Usando bc
Si guardamos el script bc anterior como foo.bc, podemos ejecutarlo de la siguiente manera:
[me@linuxbox ~]$ bc foo.bc
bc 1.06.94
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software
Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
4
Si miramos con atención, podemos ver el resultado abajo del todo, tras el mensaje de copyright. Este mensaje puede suprimirse con la opción -q (quiet).
bc también puede usarse interactivamente:
[me@linuxbox ~]$ bc -q
2 + 2
4
quit
Cuando usamos bc interactivamente, simplemente tecleamos los cálculos que queremos realizar, y los resultados se muestran inmediatamente. El comando debc quit finaliza la sesión interactiva.
También es posible pasar un script a bc a través de la entrada estándar:
[me@linuxbox ~]$ bc < foo.bc
4
La capacidad de tomar entrada estándar significa que podemos usar documentos-aquí, cadenas-aquí y entubados para pasar scripts. Aquí tenemos un ejemplo de cadena:
[me@linuxbox ~]$ bc <<< "2+2"4
Un script de ejemplo
Como ejemplo de la vida real, construiremos un script que realice un cálculo común, el pago mensual de un préstamo. En el siguiente script, usamos un documento-aquí para pasar un script a bc:
#!/bin/bash
# loan-calc : script to calculate monthly loan payments
PROGNAME=$(basename $0)
usage () {
cat <<- EOF
Usage: $PROGNAME PRINCIPAL INTEREST MONTHS
Where:
PRINCIPAL is the amount of the loan.
INTEREST is the APR as a number (7% = 0.07).
MONTHS is the length of the loan's term.
EOF
}
if (($# != 3)); then
usage
exit 1
fi
INTEREST is the APR as a number (7% = 0.07).
MONTHS is the length of the loan's term.
EOF
}
if (($# != 3)); then
usage
exit 1
fi
principal=$1
interest=$2
months=$3
bc <<- EOF
scale = 10
i = $interest / 12
p = $principal
n = $months
a = p * ((i * ((1 + i) ^ n)) / (((1 + i) ^ n) - 1))
print a, "\n"
EOF
Cuando lo ejecutamos, el resultado aparece así:
[me@linuxbox ~]$ loan-calc 135000 0.0775 180
1270.7222490000
Este ejemplo calcula el pago mensual de un préstamo de 135.000 $ al 7.75% TAE en 180 meses (15 años). Fíjate la precisión de la respuesta. Viene determinada por el valor dado a la variable especial scale en el script bc. La man page de bcproporciona una descripción completa del lenguaje de scripting de bc. Aunque su notación matemática es ligeramente diferente de la del shell (bc se parece más C), en su mayoría es muy familiar, basándonos en lo aprendido hasta ahora.
Resumiendo
En este capítulo, hemos aprendido muchas pequeñas cosas que pueden usarse para hacer "trabajo real" en scripts. A la vez que nuestra experiencia en scripts aumenta, la capacidad de manipular eficientemente cadenas y números nos será de mucho valor. Nuestro script loan-calc demuestra que incluso scripts simples pueden crearse para hacer cosas realmente útiles.
Crédito extra
Aunque la funcionalidad básica del script loan-calc está en su sitio, el script está lejos de estar completo. Como crédito extra, prueba a mejorar el script loan-calc con las siguientes características:
- Verificación completa de los argumentos de la línea de comandos.
- Una opción de línea de comandos para implementar un modo "interactivo" que pregunte al usuario el principal, el tipo de interes y el plazo del prestamo.
- Un mejor formateo de la salida.
- La Bash Hackers Wiki tiene un buen tema sobre expansión de parámetros:
http://wiki.bash-hackers.org/syntax/pe - El Manual de Referencia de Bash lo trata también:
http://www.gnu.org/software/bash/manual/bashref.html#Shell-Parameter-Expansion - Wikipedia tiene un buen artículo sobre las operaciones con bits:
https://es.wikipedia.org/wiki/Operador_a_nivel_de_bits - y un artículo sobre operaciones ternarias:
https://es.wikipedia.org/wiki/Operaci%C3%B3n_ternaria - así como una descripción de la fórmula para calcular pagos de prestamos usada en nuestro script loan-calc:
http://en.wikipedia.org/wiki/Amortization_calculator
No hay comentarios:
Publicar un comentario
Nota: solo los miembros de este blog pueden publicar comentarios.