domingo, 31 de diciembre de 2017

Introducción

Quiero contarte una historia.

No, no es la historia de cómo, en 1991, Linus Torvalds escribió la primera versión del kernel de Linux. Puedes leer esa historia en montones de libros sobre Linux. Tampoco voy a contarte la historia de cómo, algunos años antes, Richard Stallman comenzó el Proyecto GNU para crear un sistema operativo libre parecido a Linux. Esa también es una historia importante, pero la mayoría de los libros de Linux también la incluyen.

No, quiero contarte la historia de como tú puedes recuperar el control de tu ordenador.
Cuando, estudiando en el instituto, comencé a trabajar con ordenadores había una revolución en marcha. La invención del microprocesador había hecho posible para la gente normal, como tú y yo, tener un ordenador. Es difícil para mucha gente hoy imaginar cómo era el mundo cuando sólo las grandes empresas y grandes gobiernos manejaban ordenadores. Digamos que no se podía hacer mucho más.

Hoy, el mundo es muy diferente. Los ordenadores están en todas partes, desde pequeños relojes de pulsera a gigantescos centros de datos. Además de ordenadores repartidos por todas partes también tenemos redes conectándolos. Ésto ha creado una maravillosa nueva era de empoderamiento personal y libertad creativa, pero en las dos últimas décadas algo más ha sucedido. Algunas grandes corporaciones han impuesto su control sobre la mayoría de ordenadores del mundo y han decidido lo que puedes o no puedes hacer con ellos. Afortunadamente, gente de todo el mundo está haciendo algo al respecto. Están luchando por mantener el control de sus ordenadores escribiendo su propio software. Están construyendo Linux.

Mucha gente habla de "libertad" con respecto a Linux, pero no creo que la mayoría de la gente sepa que significa esta libertad en realidad. Libertad es el poder de decidir lo que tu ordenador hace, y la única forma de tener esta libertad es saber que está haciendo tu ordenador. La libertad es un ordenador que no tiene secretos, en el que todo puede saberse si te interesa averiguarlo.


¿Por qué usar la línea de comandos?

¿Te has dado cuenta alguna vez que, en las películas, cuando el "súper hacker" - ya sabes, el tío que puede entrar en la computadora militar ultra-segura en menos de treinta segundos - se sienta en el ordenador, nunca toca un ratón? Es por que los que hacen las películas saben que nosotros, como seres humanos, instintivamente sabemos que la única forma de hacer algo en un ordenador ¡es escribiendo en el teclado!

La mayoría de los usuarios de ordenadores de hoy sólo están familiarizados con la interfaz gráfica de usuario o GUI (del inglés graphical user interface) y los vendedores y los expertos les han enseñado que la interfaz de línea de comandos o CLI (del inglés command line interface) es una cosa espantosa del pasado. Es una pena, porque una buena interfaz de línea de comandos es una maravillosa y expresiva forma de comunicarse con el ordenador, muy parecida a lo que el lenguaje escrito es para los seres humanos. Se ha dicho que "las interfaces gráficas de usuario hacen fáciles las tareas fáciles, mientras que las interfaces de línea de comandos hacen posibles las tareas difíciles" y eso es muy cierto aún hoy.

Desde que Linux fue desarrollado desde la familia de sistemas operativos Unix, comparte la misma rica herencia de herramientas de línea de comandos que Unix. Unix saltó a la fama en los primeros años ochenta (aunque fue desarrollado una década antes), antes de que se extendiera la adopción de las interfaces gráficas de usuario, y por eso, se desarrolló una amplia interfaz de línea de comandos en su lugar.


¿De qué trata este libro?

Este libro es una visión amplia sobre "vivir" en la línea de comandos de Linux. Al contrario que algunos libros que se concentran en un sólo programa, como el programa shell o interfaz, bash, este libro tratará de explicar como utilizar la interfaz de línea de comandos en un sentido amplio. ¿cómo funciona? ¿qué puede hacer? ¿cuál es la mejor forma de usarla?

Éste no es un libro sobre la administración de sistemas Linux. Mientras que cualquier debate serio sobre la línea de comandos lleva inevitablemente a asuntos de administración de sistemas, este libro sólo tratará algunas cuestiones de administración. Sin embargo, preparará al lector para un estudio posterior proporcionando unos cimientos sólidos en el uso de la línea de comandos, una herramienta esencial para cualquier tarea seria de administración de sistemas.

Este libro está muy centrado en Linux. Muchos otros libros tratan de ser más atractivos incluyendo otras plataformas como Unix genérico y OS X. Al hacerlo, se convierten en una presentación de tópicos generales. Este libro, por otra parte, sólo cubre distribuciones Linux actuales. El noventa y cinco por ciento del contenido es útil para usuarios de otros sistemas parecidos a Linux, pero este libro está muy enfocado a las interfaces de línea de comandos de Linux modernas.

¿Quién debería leer este libro?


Este libro es para nuevos usuarios de Linux que hayan migrado desde otras plataformas. Lo más probable es que seas un "usuario avanzado" de alguna versión de Microsoft Windows. Quizás tu jefe te ha dicho que administres un servidor Linux, o quizás eres sólo un usuario de escritorio que está cansado de todos los problemas de seguridad y quieres darle una oportunidad a Linux. Eso está bien. Todos sois bienvenidos.

Dicho esto, no hay atajos para dominar Linux. Aprender la línea de comandos es un reto y requiere un esfuerzo real. No es que sea muy duro, más bien es muy extenso. El sistema Linux medio tiene literalmente miles de programas que puedes usar en la linea de comandos. Considérate avisado; aprender la linea de comandos no es un esfuerzo puntual.

Por otro lado, aprender la línea de comandos de Linux es extremadamente provechoso. Si crees que eres un "usuario avanzado" ahora, espera. No sabes cual es su poder real - todavía. Y al contrario que otras habilidades informáticas, el conocimiento de la linea de comandos es para mucho tiempo. Las habilidades aprendidas hoy serán útiles todavía dentro de diez años. La línea de comandos ha sobrevivido al paso del tiempo.

Se asume también que no tienes experiencia programando, pero no te preocupes, te iniciaremos en ese camino también.

¿Qué hay en este libro?


Este material está presentado en un orden cuidadosamente escogido, como si tuvieras un tutor sentado junto a ti para guiarte. Muchos autores tratan esta materia de una forma “sistemática”, lo que tiene sentido desde la perspectiva del autor, pero puede ser confuso para los nuevos usuarios.

Otro objetivo es familiarizarte con la forma de pensar de Unix, que es distinta de la forma de pensar de Windows. A lo largo de nuestro camino, tomaremos algunos desvíos para ayudarte a comprender por qué algunas cosas funcionan de un modo concreto y por qué lo hacen así. Linux no es sólo una pieza de software, es también una pequeña parte de la gran cultura Unix, que tiene su propio lenguaje e historia. Podría hacer alguna reflexión también.

Este libro está dividido en cuatro partes, cada una cubre un aspecto de la experiencia con la línea de comandos:
  • Parte 1 – Aprendiendo el Shell comienza nuestra exploración del lenguaje básico de la línea de comandos incluyendo cosas como la estructura de los comandos, el sistema de navegación de ficheros, edición en línea de comandos, y encontrar ayuda y documentación sobre los comandos.
  • Parte 2 – Configuración y Entorno cubre la edición de ficheros de configuración que controlan el funcionamiento del ordenador desde la línea de comandos.
  • Parte 3 – Tareas comunes y herramientas esenciales explora muchas de las tareas ordinarias que se realizan normalmente desde la línea de comandos. Los sistemas operativos como-Unix (similares a Unix), uno de ellos es Linux, contienen muchos programas de línea de comandos “clásicos” que se usan para realizar grandes operaciones con datos.
  • Parte 4 – Escribiendo Scripts de Shell presenta la programación en Shell, una técnica, rudimentaria pero fácil de aprender, para automatizar muchas tareas comunes de computación. Aprendiendo programación en Shell, te familiarizarás con conceptos que pueden ser aplicados a muchos otros lenguajes de programación.

¿Cómo leer este libro?

Empieza por el principio del libro y síguelo hasta el final. No está escrito como un trabajo de referencia, es más como una historia que tiene un inicio, una cumbre y un desenlace.

Prerrequisitos

Para usar este libro, todo lo que necesitas es una instalación de Linux funcionando. Puedes conseguirlo de cualquiera de estas dos formas:

  1. Instala Linux en tu computadora (no tiene que ser muy potente). No importa la distribución que elijas, aunque la mayoría de las personas hoy en día empiezan con Ubuntu, Fedora o OpenSuse. Si estás en duda, prueba primero Ubuntu. Instalando una distribución de Linux moderna puede ser ridículamente fácil o ridículamente difícil dependiendo de tu hardware. Recomiendo un ordenador de hace un par de años y que tenga al menos 256 megabytes de RAM y 6 gigabytes libres de disco duro. Evita portátiles y redes inalámbricas si es posible, ya que suelen ser más difíciles de hacerlas funcionar. (Nota del traductor: estos requisitos se refieren a la fecha de edición del libro, en la actualidad se necesita una hardware algo más potente.)
  2. Utiliza un “Live CD”. Una de las cosas interesantes que puedes hacer con muchas distribuciones Linux es ejecutarlas directamente desde un CDROM (o una unidad USB) sin instalar nada. Sólo entra en la configuración de tu BIOS y configura tu ordenador para que “Arranque desde el CDROM”, introduce el Live CD, y reinicia. Usar un Live CD es una gran forma de probar la compatibilidad de un ordenador antes de instalarlo. La desventaja de usar un Live CD es que podrá ser mucho más lento que un Linux instalado en el disco duro. Ubuntu y Fedora (además de otros) tienen versiones en Live CD.
Independientemente de como instales Linux, tendrás que tener ocasionalmente privilegios de súper-usuario (p.ej. administrativos) para llevar a cabo las lecciones de este libro.

Cuando tengas una instalación funcionando, empieza leyendo y siguiendo el libro con tu propio ordenador. La mayoría del material de este libro es “manos a la obra”, así que ¡siéntate y empieza a teclear!


¿Por qué no lo llamo “GNU/Linux”?

En algunos ámbitos, es políticamente correcto llamar al sistema operativo Linux “Sistema Operativo GNU/Linux”. El problema con “Linux” es que no hay una manera correcta del todo de nombrarlo, porque ha sido escrito por mucha gente distinta en un esfuerzo de desarrollo enorme y distribuido. Técnicamente hablando, Linux es el nombre del kernel (núcleo) del sistema operativo, nada más. Por supuesto, el kernel es muy importante, ya que hace que el sistema operativo funcione, pero no es suficiente para formar un sistema operativo completo.

Richard Stallman, es el genio-filósofo que fundó el Free Software Movement (Movimiento por el Software Libre), comenzó la Free Software Foundation (Fundación por el Software Libre), formó el Proyecto GNU, escribió la primera versión del Compilador GNU C (GCC: GNU C Compiler), creó la Licencia Pública General GNU (GPL: General Public License), etc., etc., etc. Él insiste en que se le llama “GNU/Linux” para reflejar apropiadamente las contribuciones del Proyecto GNU. Mientras que el Proyecto GNU es anterior al kernel de Linux, y las contribuciones hechas por el proyecto son muy dignas de reconocimiento, colocarlo en el nombre es injusto para todos aquellos otros que hayan hecho contribuciones significantes. Además, pienso que “Linux/GNU” sería más preciso técnicamente ya que el kernel arranca primero y todo lo demás funciona sobre él.

Popularmente, “Linux” se refiere al kernel y todo el software libre y de código abierto que viene en cualquier distribución de Linux; o sea, todo el ecosistema Linux, no solo los componentes GNU. El mercado de sistemas operativos parece que prefieren nombres de una sola palabra como DOS, Windows, Solaris, Irix, AIX. Yo he elegido usar el formato popular. Si, de todas formas, prefieres utilizar “GNU/Linux”, por favor haz un “buscar y reemplazar” mentalmente mientras lees este libro. No me importará.

Agradecimientos


Quiero dar las gracias a las siguientes personas, quienes me ayudaron a hacer posible este libro:

Jenny Watson, Acquisitions Editor en Wiley Publishing quien en primer lugar me sugirió que escribiera un libro sobre scripts de shell.

John C. Dvorak, conocido y respetado columnista. En un episodio de su vídeo podcast, “Cranky Geeks,” Mr. Dvorak describió el proceso de escribir: “Cielos. Escribe 200 palabras al día y en un año tendrás una novela.” Este consejo me llevó a escribir una página al día hasta que tuve un libro.

Dmitri Popov escribió un artículo en Free Software Magazine titulado, “Creating a book template with Writer,” que me inspiró a usar OpenOffice.org Writer para maquetar el texto. Al final resultó que funcionó maravillosamente.

Mark Polesky realizó una extraordinaria revisión del texto.

Jesse Becker, Tomasz Chrzczonowicz, Michael Levin, Spence Miner también leyeron y revisaron partes del texto.

Karen M. Shotts contribuyó con muchas horas, puliendo mi supuesto Inglés, editando el texto.

Y finalmente, los lectores de LinuxCommand.org, que me han enviado muchos amables correos.

Sus ánimos me dieron la idea de que ¡Realmente merecía la pena!


¿Qué hay nuevo en The Second Internet Edition?

Esta versión de The Linux Command Line ha sufrido algún pulido adicional y modernización. En particular, se asume que la versión 4.x del bash es la versión estándar y el texto ha sido actualizado para reflejarlo. El número de capítulos en The Second Internet Edition ahora se corresponden con los de la edición No Starch Press. También he arreglado algunos bugs ;-)

Agradecimiento especial para las siguientes personas que me hicieron valiosos comentarios en la primera edición: Adrian Arpidez, Hu Bo, Heriberto Cantú, Joshua Escamilla, Bruce Fowler, Ma Jun, Seth King, Mike O'Donnell, Parviz Rasoulipour, Gabriel Stutzman, and Christian Wuethrich.

Para saber más

Algunos artículos de Wikipedia sobre la gente famosa que aparecen en este capítulo:
La Free Software Foundation y el GNU Project:
Richard Stallman ha escrito mucho sobre el asunto del nombre“GNU/Linux”:


Colofón


Este libro fue originalmente escrito utilizando OpenOffice.org Writer con las fuentes Liberation Serif y Sans en un Dell Inspiron 530N, configurado de fábrica con Ubuntu 8.04. La versión PDF de este texto fue generada directamente con OpenOffice.org Writer. The Second Internet Edition fue producida en el mismo ordenador utilizando LibreOffice Writer en Ubuntu 12.04.

jueves, 5 de octubre de 2017

Cosas Exóticas

En este, el último capítulo de nuestro viaje, veremos algunos flecos. Aunque hemos cubierto mucho terreno en los capítulos anteriores, hay muchas características de bash que no hemos cubierto. Muchas son algo confusas, y útiles principalmente para los que integran bash en una distribución Linux. Sin embargo, hay unas pocas que, aunque no son de uso común, son de ayuda para algunos problemas de programación. Las veremos aquí.

Comandos agrupados y subshells

bash permite agrupar comandos. Esto puede hacerse de dos formas; con un comando agrupado o con un subshell. Aquí tenemos ejemplos de la sintáxis de cada uno de ellos:

Grupo de comandos:

 { comando1; comando2; [comando3; ...] }

Subshell:

 (comando1; comando2; [comando3;...])

Las dos formas difieren en que el grupo de comandos rodea sus comandos con llaves y el subshell usa paréntesis. Es importante fijarse en que, debido a la forma en que bash implementa los grupos de comandos, las llaves deben separarse de los comandos por un espacio y el último comando debe terminar con un punto y coma o con una nueva línea antes de la llave de cierre.

Entonces ¿para qué sirven los grupos de comandos y los subshells? Aunque tienen una diferencia importante (que veremos en un momento), ambos se usan para gestionar redirecciones. Consideremos un segmento de script que realiza redirecciones. Consideremos un segmento de script que realiza redirecciones en múltiples comandos:

 ls -l > output.txt
 echo "Listing of foo.txt" >> output.txt
 cat foo.txt >> output.txt

Esto es bastante directo. Tres comandos con su salida redireccionada a un archivo llamado output.txt. Usando un grupo de comandos, podríamos codificarlo de la siguiente forma:

 { ls -l; echo "Listing of foo.txt"; cat foo.txt; } > output.txt

Usando un subshell es similar:

 (ls -l; echo "Listing of foo.txt"; cat foo.txt) > output.txt

Usando esta técnica nos hemos ahorrado algo de escritura, pero donde brilla un comando agrupado o un subshell realmente es en los entubados. Cuando construimos un entubado de comandos, es útil a menudo combinar el resultado de varios comandos en una única cadena. Los comandos agrupados y los subshell hacen esto de forma fácil:

 { ls -l; echo "Listing of foo.txt"; cat foo.txt; } | lpr

Aquí hemos combinado la salida de nuestros tres comandos y la hemos entubado dentro de la salida de lpr para producir un informe impreso.

En el script que sigue, usaremos comandos agrupados y veremos varias técnicas de programación que pueden emplearse junto con arrays asociativos. Este script, llamado array-2, cuando se el da el nombre de un directorio, imprime una lista de archivos en el directorio junto a los nombres de los propietarios de los archivos y de los grupos de los propietario. Al final del listado, el script imprime un listado del número de archivos que pertenecen a cada usuario y grupo. Aquí vemos el resultados (resumidos para abreviar) cuando se le da al script el directorio /usr/bin:

 [me@linuxbox ~]$ array-2 /usr/bin
 /usr/bin/2to3-2.6 root root
 /usr/bin/2to3 root root
 /usr/bin/a2p root root
 /usr/bin/abrowser root root
 /usr/bin/aconnect root root
 /usr/bin/acpi_fakekey root root
 /usr/bin/acpi_listen root root
 /usr/bin/add-apt-repository root root
 .
 .

 .
 /usr/bin/zipgrep root root
 /usr/bin/zipinfo root root
 /usr/bin/zipnote root root
 /usr/bin/zip root root
 /usr/bin/zipsplit root root
 /usr/bin/zjsdecode root root
 /usr/bin/zsoelim root root

 File owners:
 daemon : 1 file(s)
 root : 1394 file(s)

 File group owners:
 crontab : 1 file(s)
 daemon : 1 file(s)
 lpadmin : 1 file(s)
 mail : 4 file(s)
 mlocate : 1 file(s)
 root : 1380 file(s)
 shadow : 2 file(s)
 ssh : 1 file(s)
 tty : 2 file(s)
 utmp : 2 file(s)

Aquí tenemos un listado (con los números de línea) del script:

  1  #!/bin/bash
  2
  3  # array-2: Use arrays to tally file owners
  4
  5  declare -A files file_group file_owner groups owners
  6
  7  if [[ ! -d "$1" ]]; then
  8      echo "Usage: array-2 dir" >&2
  9      exit 1
 10  fi
 11
 12  for i in "$1"/*; do
 13      owner=$(stat -c %U "$i")
 14      group=$(stat -c %G "$i")
 15      files["$i"]="$i"
 16      file_owner["$i"]=$owner
 17      file_group["$i"]=$group
 18      ((++owners[$owner]))
 19      ((++groups[$group]))
 20  done
 21
 22  # List the collected files
 23  { for i in "${files[@]}"; do
 24      printf "%-40s %-10s %-10s\n" \
 25          "$i" ${file_owner["$i"]} ${file_group["$i"]}
 26  done } | sort

Echemos un vistazo a la mecánica de este script:

Línea 5: Los arrays asociativos deben crearse con el comando declare usando la opción -A. en este script creamos los cinco arrays siguientes:

files contiene los nombres de los archivos en el directorio, indexados por nombre de archivo
file_group contiene el grupo del propietario de cada archivo, indexado por nombre de archivo
file_owner contiene el propietario de cada archivo, indexado por nombre de archivo
group contiene el número de archivos pertenecientes al grupo indexado
owner contiene el número de archivos pertenecientes al propietario indexado

Líneas 7-10: Comprueba para ver que se ha pasado un nombre de directorio válido como parámetro posicional. Si no, se muestra un mensaje de uso y el script sale con un estado de salida de 1.

Líneas 12-20: Hace un bucle a través de los archivos del directorio. Usando el comando stat, las lineas 13 y 14 extraen los nombres del propietario del archivo y del grupo del propietario y asigna valores a sus arrays respectivos (líneas 16, 17) usando el nombre del archivo como índice del array. De otra forma el propio nombre del archivo se asigna al array files (línea 15).

Líneas 18-19: El número total de archivos pertenecientes al propietario del archivo y al grupo del propietario se incrementan en uno.

Líneas 22-27: Se muestra la lista de archivos. Esto se hace usando la expansión de parámetros "${array[@]}" que se expande en la lista completa de elementos del array cada uno tratado como una palabra separada. Esto permite la posibilidad de que un nombre de archivo contenga espacios en blanco. Fíjate también que el bucle completo está incluido en llaves para que forme un comando agrupado. Esto permite que la salida completa del bucle sea entubada en el comando sort. Esto es necesario porque la expansión de los elementos del array no está ordenada.

Líneas 29-40: Estos dos bucles son similares al bucle de la lista de archivos excepto que usan la expansión "${!array[@]}" que se expande en la lista de índices del array en lugar de en la lista de elementos del array.

Sustitución de procesos

Aunque parecen iguales y ambos pueden usarse para combinar cadenas para redireccionarlas, hay una diferencia importante entre los comandos agrupados y lo subshells. Mientras que un comando agrupado ejecuta todos sus comandos en el shell actual, un subshell (como su nombre indica) ejecuta sus comandos en una copia hijo del shell actual. Esto significa que el entorno se copia y se pasa a una instancia del shell. Cuando el subshell termina, la copia del entorno se pierde, por lo que cualquier cambio hecho al entorno del subshell (incluyendo la asignación de variables) se pierde también. Por lo tanto, en la mayoría de los casos, a menos que un script requiera un subshell, los comandos agrupados son preferibles a los subshells. Los comandos agrupados son también más rápidos y requieren menos memoria.

Vimos un ejemplo del problema del entorno del subshell en el capítulo 28, cuando descubrimos que el comando read en un entubado no funciona como esperaríamos intuitivamente. Para resumir, si construimos un entubado como este:

 echo "foo" | read
 echo $REPLY

El contenido de la variable REPLY siempre está vacío porque el comando readse ejecuta en un subshell, y su copia de REPLY se destruye cuando el subshell termina.

Como los comandos en entubados siempre se ejecutan en subshells, cualquier comando que asigne variables se encontrará con este problema. Afortunadamente, el shell ofrece una forma exótica de expansión llamada sustitución de procesos que puede usarse para solucionar este problema.

La sustitución de procesos se expresa de dos formas:

Para procesos que producen salida estándar:
 <(lista)

o, para procesos que toman entrada estándar:
 >(lista)

donde lista es una lista de comandos.

Para resolver nuestro problema con read, podemos emplear sustitución de procesos así:

 read < <(echo "foo")
 echo $REPLY

La sustitución de procesos nos permite tratar la salida de un subshell como un archivo ordinario para propósitos de redirección. De hecho, como es una forma de expansión, podemos examinar su valor real:

 [me@linuxbox ~]$ echo <(echo "foo")
 /dev/fd/63

Usando echo para ver el resultado de la expansión, vemos que la salida del subshell está proporcionada por el archivo llamado /dev/fd/63.

La sustitución de procesos se una a menudo con bucles que contienen read.Aquí tenemos un ejemplo de un bucle read que procesa el contenido de un listado de directorio creado por un subshell:

 #!/bin/bash


 # pro-sub : demo of process substitution

 while read attr links owner group size date time filename; do
     cat <<- EOF
         Filename:   $filename
        
         Size:       $size        
         Owner:      $owner        
         Group:      $group        
         Modified:   $date $time        
         Links:      $links        
         Attributes: $attr
     EOF
 done < <(ls -l | tail -n +2)

El bucle ejecuta read para cada línea de un listado de directorio. El propio listado se produce en la línea final del script. Esta línea redirige la salida de la sustitución de procesos en la entrada estándar del bucle. El comando tail está incluido en el entubado de la sustitución de procesos para eliminar la primera línea del listado, que no se necesita.

Cuando se ejecuta, el script produce salida como esta:

 [me@linuxbox ~]$ pro_sub | head -n 20
 Filename:    addresses.ldif
 Size:        14540
 Owner:       me
 Group:       me
 Modified:    2009-04-02 11:12
 Links:       1
 Attributes:  -rw-r--r--


 Filename:    bin
 Size:        4096
 Owner:       me
 Group:       me
 Modified:    2009-07-10 07:31
 Links:       2
 Attributes:  drwxr-xr-x


 Filename:    bookmarks.html
 Size:        394213
 Owner:       me
 Group:       me

Trampas

En el Capítulo 10, vimos cómo, los programas, puede responder a señales. Podemos añadir esta capacidad a nuestros scripts también. Aunque los scripts que hemos escrito hasta ahora no han necesitado esta capacidad (porque tienen tiempos de ejecución muy cortos, y no crean archivos temporales), los scripts más largos y complicados pueden beneficiarse de tener una rutina de manejo de señales.

Cuando diseñamos un script largo y complicado, es importante considerar que ocurre si el usuario cierra la sesión o apaga el ordenador mientras el script se está ejecutando. Cuando ocurre un evento como este, debe enviarse una señal a todos los procesos afectados. En respuesta, los programas que representan estos procesos pueden realizar acciones para asegurar una terminación apropiada y ordenada del programa. Digamos, por ejemplo, que hemos escrito un script que crea un archivo temporal durante su ejecución. En nombre del buen diseño, habríamos hecho que el script borre el archivo cuando el script termine su trabajo. También sería inteligente hacer que el script borre el archivo si recibe una señal indicando que el programa va a terminar prematuramente.

bash ofrece un mecanismo para este propósito conocido como trampa. Las trampas se implementan con el apropiadamente denominado comando incluido, trap. trap usa la siguiente sintaxis:

 trap argumento señal [señal...]

donde argumento es una cadena que se leerá y tratará como un comando y señales la especificación de una señal que pone en funcionamiento la ejecución el comando interpretado.

Aquí tenemos un ejemplo simple:

 #!/bin/bash


 # trap-demo : simple signal handling demo

 trap "echo 'I am ignoring you.'" SIGINT SIGTERM
 for i in {1..5}; do
     echo "Iteration $i of 5"
     sleep 5
 done

Este script define una trampa que ejecutará un comando echo cada vez que recibe la señal SIGINT o SIGTERM mientras el script se está ejecutando. La ejecución del programa aparece así cuando el usuario intenta detener el script presionando Ctrl-c:

 [me@linuxbox ~]$ trap-demo
 Iteration 1 of 5
 Iteration 2 of 5
 I am ignoring you.
 Iteration 3 of 5
 I am ignoring you.
 Iteration 4 of 5
 Iteration 5 of 5

Como podemos ver, cada vez que el usuario intenta interrumpir el programa, se muestra el mensaje en su lugar.

Construir una cadena para formar una secuencia útil de comandos puede ser complicado, por lo que es una práctica habitual especificar una función de shell como comandos. En este ejemplo, se especifica una función de shell separada para manejar cada señal:

 #!/bin/bash
 # trap-demo2 : simple signal handling demo

 exit_on_signal_SIGINT () {
     echo "Script interrupted." 2>&1
     exit 0
 }

 exit_on_signal_SIGTERM () {
     echo "Script terminated." 2>&1
     exit 0
 }

 trap exit_on_signal_SIGINT SIGINT
 trap exit_on_signal_SIGTERM SIGTERM
 for i in {1..5}; do
     echo "Iteration $i of 5"
     sleep 5
 done

Este script presenta dos comandos trap, uno para cada señal. Cada trampa, por turnos, especifica una función de shell a ejecutar cuando se recibe una señal en particular. Fíjate en la inclusión de un comando exit en cada una de las señales de manejo de señales. Si un exit, el script continuaría tras completar la función.

Cuando el usuario presione Ctrl-c durante la ejecución de este script, el resultado aparece así:

 [me@linuxbox ~]$ trap-demo2
 Iteration 1 of 5
 Iteration 2 of 5
 Script interrupted.

Archivos temporales

Una razón por la que los gestores de señales se incluyen en los scripts es para eliminar los archivos temporales que el script pueda crear para manejar resultados intermedios durante su ejecución. Hay algo artístico en la denominación de los archivos temporales. Tradicionalmente, los programas en sistemas como-Unix crean sus archivos temporales en el directorio /tmp, un directorio compartido creado para tales archivos. Sin embargo, como el directorio está compartido, esto conlleva algunos problemas de seguridad, particularmente para programas que se ejecutan con privilegios de superusuario. Mas allá del paso obvio de establecer permisos apropiados para los archivos expuestos a todos los usuarios del sistema, es importante dar a los archivos temporales nombres no predecibles. Esto evita un exploit conocido como temp race attack. Una forma de crear un nombre no predecible (pero descriptivo) es hacer algo como esto:


archivotemporal=/tmp($(nombrebase $0).$$.$RANDOM

Esto creará un archivo consistente en el nombre del programa, seguido por su ID de proceso (PID), seguido por un entero aleatorio. Fíjate, sin embargo, que la variable de shell $RANDOM solo devuelve un valor del rango 1-32767, que no es un rango muy grande en términos informáticos, por lo que una única instancia no es suficiente para vencer a un posible atacante.

Una forma mejor es usar el programa mktemp (no confundir con la función de librería estándar mktemp) para crear y nombrar el archivo temporal. El progrma mktemp acepta una plantilla como argumento que se usa para construir el nombre del archivo. La plantilla debe incluir una serie de "X" caracteres, que se reemplazan con un número correspondiente de letras y números aleatorios. Cuanto más larga sea la serie de "X" caracteres, más larga será la serie de caracteres aleatorios. Aquí tenemos un ejemplo:

archivotemporal=$(mktemp /tmp/foobar.$$.XXXXXXXXXX)

Esto crea un archivo temporal y asigna su nombre a la variable tempfile. Los "X" caracteres en la plantilla se reemplazan con letras y números aleatorios de forma que el nombre del archivo final (que, en este ejemplo, también incluye el valor expandido del parámetro especial $$ para obtener el PID) debería ser algo así:

/tmp/foobar. 6593,UOZuvM6654

Para scripts que se ejecutan por usuarios normales, sería prudente evitar el uso del directorio /tmp y crear un directorio para archivos temporales dentro del directorio home del usuario, con una línea de código como esto:

[[ -d $HOME/tmp ]] || mkdir $HOME/tmp


Ejecución asíncrona

A veces es preferible realizar más de una tarea al mismo tiempo. Hemos visto como los sistemas operativos modernos son al menos multitarea incluso multiusuario. Los script pueden construirse para comportarse de forma multitarea.

Normalmente, esto implica arrancar un script, y por turnos, arrancar uno o más script hijos que realicen una tarea adicional mientras que el script padre continua ejecutando. Sin embargo, cuando una serie de scripts se ejecutan de esta forma, puede haber problemas en la coordinación entre el script padre y los hijos. O sea, ¿qué pasa si el padre o el hijo son dependientes el uno del otro, y un script debe esperar a que el otro termine su tarea ante de finalizar la suya propia?

bash tiene un comando interno para ayudarnos a manejar ejecución asíncronacomo esta. El comando wait hace que un script padre se pause hasta que un proceso especificado (p.ej., el script hijo) termine.

wait

Probaremos el comando wait primero. Para hacerlo, necesitaremos dos scripts. un script padre:

 #!/bin/bash 

 # async-parent : Asynchronous execution demo (parent) 

 echo "Parent: starting..." 

 echo "Parent: launching child script..."   async-child & 
 pid=$! 
 echo "Parent: child (PID= $pid) launched."

 echo "Parent: continuing..." 
 sleep 2

 echo "Parent: pausing to wait for child to finish..." 
 wait $pid

 echo "Parent: child is finished.   Continuing..." 
 echo "Parent: parent is done. Exiting."

y un script hijo:

 #!/bin/bash 

 # async-child : Asynchronous execution demo (child) 

 echo "Child: child is running..." 
 sleep 5 
 echo "Child: child is done. Exiting."

En este ejemplo, vemos que el script hijo es muy simple. La acción real la realiza el padre. En el script padre, se arranca el script hijo y se envía al fondo. El ID de proceso del script hijo se graba asignando a la variable pid el valor del parámetro de shell $!, que siempre cotendrá el ID de proceso del último trabajo puesto en el fondo.

El script padre continua y luego ejecuta un comando wait con el PID del proceso hijo. Esto hace que el script padre se pause hasta que el script hijo salga, punto en el cual el script padre concluye.

Cuando se ejecutan, los script padre e hijo producen la siguiente salida:

 [me@linuxbox ~]$ async-parent
 Parent: starting...
 Parent: launching child script...
 Parent: child (PID= 6741) launched.
 Parent: continuing...
 Child: child is running...
 Parent: pausing to wait for child to finish...
 Child: child is done. Exiting.
 Parent: child is finished. Continuing...
 Parent: parent is done. Exiting.

Entubados con nombre

En la mayoría de los sistemas como-Unix, es posible crear un tipo especial de archivo llamado un entubado con nombre. Los entubados con nombre se usan para crear una conexión entre dos procesos y pueden usarse igual que otros tipos de archivo. No son muy populares, pero es bueno conocerlos.

Hay una arquitectura de programación común llamada cliente-servidor, que puede hacer uso de un método de comunicación como los entubados con nombre, así como de otros tipos de comunicación entre procesos tales como conexiones de red.

El tipo más ampliamente usado de sistema cliente-servidor es, claramente, la comunicación entre un navegador web y un servidor web. El navegador web actua como cliente, realizando peticiones al servidor y el servidor responde al navegador con páginas web.

Los entubados con nombre se comportan como archivos, pero en realidad forman buffers "el primero en entrar es el primero en salir" (FIFO - first in firt out). Igual que los entubados normales (sin nombre), los datos entran por un extremo y salen por el otro. Con los entubados con nombre, es posible configurar algo como esto:

proceso1 > entubado_con_nombre

y

proceso2 < entubado_con_nombre

y se comportará como si fuera:

proceso1 | proceso2


Configurando un entubado con nombre

Primero, debemos crear un entubado con nombre. Esto se hace usando el comando mkfifo:

 [me@linuxbox ~]$ mkfifo pipe1
 [me@linuxbox ~]$ ls -l pipe1
 prw-r--r-- 1 me me 0 2009-07-17 06:41 pipe1

Aquí hemos usado mkfifo para crear un entubado con nombre llamado pipe1. Usando ls, examinamos el archivo y vemos que la primera letra en el campo atributos es "p", indicando que es un entubado con nombre.


Usando entubados con nombre

Para demostrar como funciona el entubado con nombre, necesitaremos dos ventanas de terminal (o alternativamente, dos consolas virtuales). En el primer terminal, introducimos un comando simple y redirigimos su salida al entubado con nombre:

 [me@linuxbox ~]$ ls -l > pipe1

Tras pulsar la tecla Enter, el comando parecerá que se ha colgado. Esto es porque no está recibiendo nada desde el otro extremo del entubado aún. Cuando ocurre esto, se dice que el entubado está bloqueado. Esta condición se aclarará una vez que apliquemos un proceso al otro extremo y empiece a leer entrada desde el entubado. Usando la segunda ventana de terminal, introducimos este comando:

 [me@linuxbox ~]$ cat < pipe1

y el listado de directorio producido desde la primera ventana de terminal aparece en el segundo terminal como salida desde el comando cat. El comando ls en el primer terminal se completa con éxito una vez que ya no está bloqueado.


Resumiendo

Bien, hemos completado nuestro viaje. Lo único que queda por hacer ahora es practicar, practicar y practicar. Aunque hemos cubierto bastante terreno en nuestra excursión, apenas hemos rozado la superficie de lo que es la línea de comandos. Hay todavía miles de programas de línea de comandos pendientes de descubrir y disfrutar. ¡Comienza excavando en /usr/bin y veras!

Para saber más

Arrays

En el último capítulo, vimos como el shell puede manipular cadenas y números. El tipo de datos que hemos visto hasta ahora se conocen en los círculos informáticos como variables escalares; o sea, variables que contienen un valor individual.

En este capítulo, veremos otro tipo de estructura de datos llamados arrays (vectores), que contienen valores múltiples. Los arrays son una característica de prácticamente todos los lenguajes de programación. El shell los soporta también, sólo que de una forma algo limitada. Incluso así, pueden ser muy útiles para resolver problemas de programación.


¿Qué son los arrays?

Los arrays son variables que almacenan más de un valor a la vez. Los arrays se organizan como una tabla. Consideremos una hoja de cálculo como ejemplo. Una hoja de cálculo funciona como un array de dos dimensiones. Tiene tanto filas como columnas, y una celda individual de la hoja de cálculo puede localizarse según su dirección de fila y columna. Un array se comporta de la misma forma. Un array tiene celdas, que se llaman elementos, y cada elemento contiene datos. Un elemento individual de un array es accesible usando una dirección llamada un indice o subscript.

La mayoría de lenguajes de programación soportan arrays multidimensionales. Una hoja de cálculo es un ejemplo de un array multidimensional de dos dimensiones, anchura y altura. Muchos lenguajes de programacion soportan arrays con un número arbitrario de dimensiones, aunque los arrays de dos y tres dimensiones son probablemente los más usados.

Los arrays en bash se limitan a una única dimensión. Podemos pensar que son hojas de cálculo con una única columna. Incluso con esta limitación, hay muchas aplicaciones para ellos. El soporte para arrays apareció por primera vez en bash versión 2. El programa de shell original de unix, sh, no soporta arrays de ningún tipo.


Creando un array

Las variables array se nombran igual que otras variables de bash, y se crean automáticamente cuando se accede a ellas. Aquí tenemos un ejemplo:

 [me@linuxbox ~]$ a[1]=foo
 [me@linuxbox ~]$ echo ${a[1]}
 foo

Aquí vemos un ejemplo tanto de asignación como de acceso a un elemento de un array. Con el primer comando, al elemento 1 del array a se le asigna el valor "foo". El segundo comando muestra el valor almacenado en el elemento 1. El uso de llaves en el segundo comando se requiere para evitar que el shell intente una expansión de ruta en el nombre del elemento del array.

Un array puede también crearse con el comando declare:

 [me@linuxbox ~]$ declare -a a

Usando la opción -a, este ejemplo de declare crea el array a.


Asignando valores a un array

Los valores pueden asignarse de dos formas. Valores individuales pueden asignarse usando la siguiente sintaxis:

 nombre[índice]=valor

donde nombre es el nombre del array e índice es un entero (o una expresión aritmética) mayor o igual que cero. Fíjate que el primer elemento de un array es el índice cero, no uno. valor es una cadena o un entero asignado al elemento del array.

Valores múltiples pueden asignarse usando la siguiente sintaxis:

 nombre=(valor1 valor2 ...)

donde nombre es el nombre del array y valor... son los valores asignados secuencialmente a elementos del array, comenzando por el elemento cero. Por ejemplo, si queremos asignar los días de las semana en abreviaturas al array days, podríamos hacer esto:

 [me@linuxbox ~]$ days=(Sun Mon Tue Wed Thu Fri Sat)

También es posible asignar valores a un elemento en concreto especificando un índice para cada valor:

[me@linuxbox ~]$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu 

[5]=Fri [6]=Sat)

Accediendo a los elementos de un array

Entonces ¿para qué sirven los arrays? De la misma forma que muchas tareas de manejo de datos pueden realizarse con un programa de hojas de cálculo, muchas tareas de programación pueden realizarse con arrays.

Consideremos un ejemplo simple de recogida y presentación de datos. Construiremos un script que examine la hora de modificación de los archivos de un directorio determinado. Desde estos datos, nuestro script mostrará una tabla con la hora en que los datos fueron modificados por última vez. Dicho script podría usarse para determinar cuando está más activo un sistema. Este script, llamado hours, produce este resultado:

 [me@linuxbox ~]$ hours
 Hour  Files  Hour  Files
 ----  -----  ----  -----
 00    0      12    11
 01    1      13    7
 02    0      14    1
 03    0      15    7
 04    1      16    6
 05    1      17    5
 06    6      18    4
 07    3      19    4
 08    1      20    1
 09    14     21    0
 10    2      22    0
 11    5      23    0


 Total files = 80

Ejecutamos el programa hours, especificando el directorio actual como destino. Produce una tabla mostrando, para cada hora del día (0-23), cuantos archivos han sido modificados por última vez. El código para producir esto es el que sigue:

 #!/bin/bash

 # hours : script to count files by modification time

 usage () {
     echo "usage: $(basename $0) directory" >&2
 }

 # Check that argument is a directory
 if [[ ! -d $1 ]]; then
     usage
     exit 1
 fi

 # Initialize array
     for i in {0..23}; do hours[i]=0; done
 # Collect data
 for i in $(stat -c %y "$1"/* | cut -c 12-13); do
     j=${i/#0}
     ((++hours[j]))
     ((++count))
 done

 # Display data
 echo -e "Hour\tFiles\tHour\tFiles"
 echo -e "----\t-----\t----\t-----"
 for i in {0..11}; do
     j=$((i + 12))
     printf "%02d\t%d\t%02d\t%d\n" $i ${hours[i]} $j ${hours[j]}
 done
 printf "\nTotal files = %d\n" $count

El script consiste en una función (usage) y un cuerpo principal con cuatro secciones. En la primera sección, comprobamos que hay un argumento en la línea de comandos y que es un directorio. Si no, mostramos el mensaje de uso y salimos.

La segunda sección inicializa el array hours. Lo hace asignando a cada elemento un valor cero. No hay ningún requerimiento especial para preparar arrays antes de usarlos, pero nuestro script necesita asegurarse de que ningún elemento se queda vacío. Fíjate la interesante forma en que el bucle se construye. Empleado expansión con llaves ({0..23}), podemos generar fácilmente una secuencia de palabras para el comando for.

La siguiente sección recoge los datos ejecutando el programa stat en cada archivo del directorio. Usamos cut para extraer los dígitos de la hora del resultado. Dentro del bucle, necesitamos eliminar los ceros a la izquierda de nuestro campo hora, ya que el shell trata (y finalmente falla) de interpretar los valores del "00" al "99" como números octales (ver Tabla 34-1). A continuación, incrementamos el valor del elemento del array correspondiente a la hora del día. Finalmente, incrementamos un contador (count) para seguir la pista del número total de archivos en el directorio.

La última sección del script muestra el contenido del array. Primero mostramos un par de líneas de encabezado y luego entramos en un bucle que produce una salida en dos columnas. Finalmente, mostramos la lista completa de archivos.

Operaciones con arrays

Hay muchas operaciones comunes con arrays. Cosas como borrar arrays, determinar su tamaño, su orden, etc. tienen muchas aplicaciones en scripting.

Mostrando todo el contenido de un array

Los subscripts * y @ pueden usarse para acceder a todos los elementos de un array. Al igual que con los parámetros posicionales, la notación @ es la más útil de las dos. Aquí tenemos una prueba:

 [me@linuxbox ~]$ animals=("a dog" "a cat" "a fish")
 [me@linuxbox ~]$ for i in ${animals[*]}; do echo $i; done
 a
 dog
 a
 cat
 a
 fish
 [me@linuxbox ~]$ for i in ${animals[@]}; do echo $i; done
 a
 dog
 a
 cat
 a
 fish
 [me@linuxbox ~]$ for i in "${animals[*]}"; do echo $i; done
 a dog a cat a fish
 [me@linuxbox ~]$ for i in "${animals[@]}"; do echo $i; done
 a dog
 a cat
 a fish

Creamos el array animals y le asignamos tres cadenas de dos palabras. Luego ejecutamos cuatro bucles para ver el efecto de la separación de palabras en el contenido del array. El comportamiento de las notaciones ${animals[*]} y${animals[@]}es idéntico hasta que se entrecomillen. La notación * da como resultado una palabra individual con el contenido del array, mientras que la notación @ da como resultado tres palabras, lo que coincide con el contenido "real" del array.


Determinando el número de elementos de una array

Usando expansión de parámetros, podemos determinar el número de elementos en un array de forma muy parecida a determinar la longitud de una cadena. Aquí tenemos un ejemplo:

 [me@linuxbox ~]$ a[100]=foo
 [me@linuxbox ~]$ echo ${#a[@]} # number of array elements
 1
 [me@linuxbox ~]$ echo ${#a[100]} # length of element 100
 3

Creamos un array a y le asignamos la cadena "foo" al elemento 100. A continuación, usamos expansión de parámetros para examinar la longitud del array, usando la notación @. Finalmente, vemos la longitud del elemento 100 que contiene la cadena "foo". Es interesante fijarse que mientras que asignamos nuestra cadena al elemento 100, bash sólo reporta un elemento en el array. Esto difiere del comportamiento de otros lenguajes en los que los elementos sin uso del array (elementos 0-99) serían inicializados con valores vacíos y se cuentan.


Encontrando los subscripts usados por un array

Como bash permite que los arrays contengan "huecos" en la asignación de subscripts, a veces es útil para determinar qué elementos existen en realidad. Esto puede hacerse con una expansión de parámetros usando las siguientes fórmulas:

 ${!array[*]}
 ${!array[@]}

donde array es el nombre de una variable array. Como en las otras expansiones que usan * y @, la forma @ entre comillas es la más útil, ya que se expande en palabras separadas:

 [me@linuxbox ~]$ foo=([2]=a [4]=b [6]=c)
 [me@linuxbox ~]$ for i in "${foo[@]}"; do echo $i; done
 ab
 c
 [me@linuxbox ~]$ for i in "${!foo[@]}"; do echo $i; done
 2

 4
 6

Añadiendo elementos al final de un array

Saber el número de elementos de un array no ayuda si necesitamos añadir valores al final del array, ya que los valores devueltos por las notaciones * y @ no nos dicen el máximo índice del array en uso. Afortunadamente, el shell nos da una solución. Usando el operador de asignación +=, podemos añadir valores automáticamente al final de un array. Aquí, asignamos tres valores al array foo, y luego le añadimos tres más.

 [me@linuxbox ~]$ foo=(a b c)
 [me@linuxbox ~]$ echo ${foo[@]}
 a b c
 [me@linuxbox ~]$ foo+=(d e f)
 [me@linuxbox ~]$ echo ${foo[@]}
 a b c d e f


Ordenando un array

Como en las hojas de cálculo, a menudo es necesario ordenar los valores de una columna de datos. El shell no tiene una forma directa de harcelo, pero no es complicado hacerlo con un poco de código:

 #!/bin/bash


 # array-sort : Sort an array

 a=(f e d c b a)

 echo "Original array: ${a[@]}"
 a_sorted=($(for i in "${a[@]}"; do echo $i; done | sort))
 echo "Sorted array: ${a_sorted[@]}"

Cuando lo ejecutamos, el script produce esto:

 [me@linuxbox ~]$ array-sort
 Original array:  f e d c b a
 Sorted array:    a b c d e f

El script opera copiando el contenido del array original (a) en un segundo array (a_sorted) con un un pequeño truco con sustitución de comandos. Esta técnica básica puede usarse para realizar muchos tipos de operaciones en el array cambiando el diseño del entubado.

Borrando un array

Para borrar un array, usa el comando unset:

 [me@linuxbox ~]$ foo=(a b c d e f)
 [me@linuxbox ~]$ echo ${foo[@]}
 a b c d e f
 [me@linuxbox ~]$ unset foo
 [me@linuxbox ~]$ echo ${foo[@]}

 [me@linuxbox ~]$

unset puede usarse también para borrar elementos individuales del array:

 [me@linuxbox ~]$ foo=(a b c d e f)
 [me@linuxbox ~]$ echo ${foo[@]}
 a b c d e f
 [me@linuxbox ~]$ unset 'foo[2]'
 [me@linuxbox ~]$ echo ${foo[@]}
 a b d e f

En este ejemplo, borramos el tercer elemento del array, subscript 2. Recuerda, los arrays comienzan con el subscript cero, ¡no uno! Fíjate también que el elemento del array debe entrecomillarse para evitar que el shell realice expansión de rutas.

Es interesante cómo, la asignación de un valor vacío a un array no vacía su contenido:

 [me@linuxbox ~]$ foo=(a b c d e f)
 [me@linuxbox ~]$ foo=
 [me@linuxbox ~]$ echo ${foo[@]}
 b c d e f

Cualquier referencia a una variable array sin un subscript se refiere al elemento cero del array:

 [me@linuxbox ~]$ foo=(a b c d e f)
 [me@linuxbox ~]$ echo ${foo[@]}
 a b c d e f
 [me@linuxbox ~]$ foo=A
 [me@linuxbox ~]$ echo ${foo[@]}
 A b c d e f

Arrays asociativos

Versiones recientes de bash soportan ahora arrays asociativos. Los arrays asociativos usan cadenas en lugar de enteros como índices del array. Esta capacidad permite nuevos enfoques interesantes en el manejo de datos. Por ejemplo, podemos crear un array llamado "colors" y usar nombres de colores como índices:

 declare -A colors
 colors["red"]="#ff0000"
 colors["green"]="#00ff00"
 colors["blue"]="#0000ff"

Al contrario de los arrays indexados con enteros, que se crean simplemente referenciándolos, los arrays asociativos deben crearse con el comando declareusando la nueva opción -A. Los elementos de arrays asociativos son accesibles de forma muy parecida a los arrays indexados por enteros:

 echo ${colors["blue"]}

En el próximo capítulo, veremos un script que hace un buen uso de arrays asociativos para producir un interesante informe.


Resumiendo

Si buscamos en la man page de bash la palabra "array", encontramos muchas instancias donde bash hace uso de variables array. Muchas de ellas son algo confusas, pero pueden ofrecer una utilidad ocasional en algunas circunstancias especiales. De hecho, todo el tema de los arrays está algo infrautilizado en la programación shell más allá del hecho de que los programas del shell Unix tradicional (como sh) carecen de soporte para arrays. Es una desafortunada falta de popularidad ya que los arrays se usan ampliamente en otros lenguajes de programación y proporcionan una herramienta poderosa para resolver muchos tipos de problemas de programación.

Los arrays y los bucles tiene una afinidad popular y a menudo se usan juntos. El formato de bucle

 for ((expr; expr; expr))

está particularmente bien adecuado para calcular subscripts de arrays.


Para saber más

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