jueves, 5 de octubre de 2017

Cadenas y Números

Todos los programas de ordenador trabajan con datos. En los capítulos anteriores, nos hemos enfocado en procesamiento de datos a nivel de archivo. Sin embargo, muchos problemas de programación necesitan solventarse usando unidades de datos más pequeñas como cadenas y números.

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)

 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.

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.

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

 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.
Para saber más

No hay comentarios:

Publicar un comentario

Nota: solo los miembros de este blog pueden publicar comentarios.