tags: ASO-GISI

La bash


Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


Introducción

La bash es una shell de UNIX/Linux. Una shell es una aplicación que facilita la interacción del usuario con el sistema operativo. Los ordenadores de propósito general y servidores se utilizan y administran generalmente a través de la shell.

Bash está basada en la shell sh (de hecho su nombre procede de “Bourne-Again SHell”, en referencia a Stephen Bourne que fue el creador de sh). Se trata de una shell textual (las hay también de tipo gráfico, como la Windows shell), utilizada por defecto, de muchos tipos de Linux y, hasta hace unos años, era la shell textual de MacOS. Además de bash y sh, existen otras shell para UNIX/Linux como ksh, dash, tsch, zsh, etc.

La bash se ejecuta desde un terminal. Un terminal es un dispositivo, virtual o físico que permite a un operador humano enviar mensajes de texto (generalmente desde un teclado) y recibir información textual de regreso (generalmente por una pantalla; puede incluir también pitidos de “alarma”).

Desde Ubuntu, para abrir una terminal virtual desde la shell gráfica (que por defecto es Gnome Shell) se puede pulsar el icono Terminal o la combinación de teclas CTRL+ ALT+T. Se pueden abrir varios terminales a la vez según sean necesarios, identificándose cada terminal como un dispositivo (virtual) distinto.

Por ejemplo:

$ ps #desde un terminal
 PID      TTY      TIME       CMD
 3261     pts/0    00:00:00   bash
 5334     pts/0    00:00:00   ps
 
 $ ps #desde un terminal
 PID      TTY      TIME       CMD
 5355     pts/1    00:00:00   bash
 5367     pts/1    00:00:00   ps

Como se ve en la columna PID, cada terminal está ejecutando una bash (procesos 3261 y 5355). Por otro lado en la columna TTY (de TeleTYpewriter) se identifica cada uno de los terminales (pts/0 y pts/1). El identificador pts viene de Pseudo-Terminal Slave.

Como casi todos los dispositivos en Linux, los terminales (incluidos los virtuales) se manejan con archivos de dispositivos, que cuelgan del directorio /dev. Por ejemplo, pts/0 se corresponde con /dev/pts/0:

$ ls -l /dev/pts
crw--w----  1  javier  tty 136, 0   sep 4 13:38   0
crw--w----  1  javier  tty 136, 1   sep 4 13:34   1

Como puede ver, los archivos 0 y 1 son de tipo carácter (a diferencia de los asociados a discos, que son de tipo bloque). Pruebe desde el terminal identificado como pts/0 a ejecutar lo siguiente:

$ echo hola > /dev/pts/1

Características de la bash

Muchas características de la bash son comunes a otras shell de Linux:

  • Edición de línea, historial de órdenes, autocompletado, etc.
  • Ejecución de órdenes internas y órdenes externas.
  • Alias.
  • Metacaracteres y expansiones.
  • Redirecciones de la E/S.
  • Estructuración de órdenes en la bash.
  • Variables de shell y personalización entorno.
  • Funciones.
  • Aritmética entera.
  • Arrays.
  • Proporciona un lenguaje de programación con el que se pueden hacer scripts.

Bash se puede utilizar de forma interactiva (con el usuario introduciendo las órdenes por teclado) o no interactiva (en este caso las órdenes suelen darse mediante un script). Se pueden ejecutar órdenes externas de forma síncrona (espera hasta que terminen) o asíncrona (en el caso de que se lancen en segundo plano).

Ejecución de órdenes internas y órdenes externas

Órdenes internas: Son nativas de la bash. La orden $ help obtiene un listado de las órdenes internas disponibles.

Órdenes externas: Son las no nativas. Deben de ser llamadas por el interprete de órdenes para ser ejecutadas. Son todas las que se encuentran accesibles desde la variable de entorno $PATH.

Para averiguar si una orden es interna o externa, además de help , puede utilizarse type y where.

$ type let && whereis let
  let is a shell builtin
  let:

$ type nano && whereis nano
  nano is /usr/bin/nano
  nano: /usr/bin/nano /usr/share/man/man1/nano.1

Las órdenes, tanto externas como internas, devuelven un valor al finalizar (exit status), cuyo valor es 0 si todo fue correcto y distinto de 0 si hubo un error. Existen algunas excepciones como grep (véase el manual).

Nótese que la mayoría de órdenes devuelven por pantalla un texto con un error, cuando este ocurre. Así, el usuario puede actuar al efecto. Sin embargo, en el caso de programar un script, el manejo de errores textuales es más complejo.

El valor de retorno de la última orden queda almacenada en la variable de shell ?. Para “acceder” al valor de una variable, como veremos más adelante, se precede de $. Por ejemplo:

$ cd dirNoExistente
  bash: cd: dirNoExistente: No such file or directory

$ echo $? #muestra el valor de retorno de la orden anterior (“cd”)
  1
  
$ echo $? #muestra el valor de retorno de la orden anterior (“echo”)
  0

Alias

Un alias es un nombre alternativo para una orden.

Imagine por ejemplo que le interesa listar los archivos de un directorio en formato largo con el nombre ll. Puede entonces definir un alias (la bash sustituirá el alias por su definición antes de la ejecución):

$ alias ll='ls -s' # cuidado, sin blancos entre =

Para ver los alias definidos en su terminal, utilice alias y para eliminar un alias, utilice unalias.

La declaración de este alias es temporal (hasta que cierre la shell). Sin embargo, para que un usuario tenga permanentemente disponible un alias debe incluirlo en su archivo ~/.bashrc que define la configuración de la bash del usuario y que se ejecuta cada vez que el usuario inicia un proceso bash.

Si se quiere que esté disponible para todos los usuarios, el administrador debe incluir el alias en el archivo /etc/bash.bashrc que también se ejecuta al abrir una bash (y lo hace previamente al archivo .bashrc del usuario).

Metacaracteres y expansiones

La bash tiene una serie de caracteres con un significado especial, por ejemplo * para expansión de nombres de archivo, & para lanzar órdenes en segundo plano, | para las tuberías o pipelines, > para redireccionar la salida, $ para sustitución de variables, # para comentarios, etc.

Cuando no queremos que se utilice este significado sino que se trate como un carácter “normal”, hay varias posibilidades a utilizar:

  • Carácter de escape \. Mantiene el literal del carácter que le sigue. Se usa también al final de una línea para continuarla. Ej.:
  $ echo \* \$? \>archivo
    * $? >archivo
  • Comillas simples ' '. Preserva el valor literal de lo contenido entre las comillas. Ej.:
  $ echo '* $? >archivo'
    * $? >archivo
  • Comillas dobles " ". Preserva el valor literal de lo contenido entre las comillas excepto para \ y $ . Ej.:
  $ echo "* $? >archivo"
    * 0 >archivo

Además, la bash proporciona múltiples expansiones que facilitan su utilización. Las principales son:

  • Llaves { }
  • Tilde ~
  • Variables (y parámetros) $
  • Sustitución de orden $( )
  • Aritmética $(( ))
  • Archivos y directorios (globbing)

Expansión de llaves

Se utiliza para generar cadenas. Opcionalmente las cadenas generadas pueden tener un “prefijo” y/o un “sufijo”.

Ej.:

$ echo /tmp/{bin,datos,log}.aux #no poner espacios entre las comas
  /tmp/bin.aux /tmp/datos.aux /tmp/log.aux
$ echo {a,e,i}{A,E,I} #piense qué saldría con {a,e,i} {A,E,I}
  aA aE aI eA eE eI iA iE iI

Permite también generar secuencias de caracteres o números, crecientes o decrecientes, con posibilidad de especificar el incremento (por defecto, 1 o -1 según corresponda). En el caso de secuencias de números, permite tamaños fijos completando con ceros (se especifica añadiendo un 0 a la izquierda al límite inferior o superior).

Ej.:

$ echo {a..c}{15..12}
  a15 a14 a13 a12 b15 b14 b13 b12 c15 c14 c13 c12
$ echo {A..D..2}y{1..9..3}
  Ay1 Ay4 Ay7 Cy1 Cy4 Cy7
$ echo {a..c..2}{00..1000..100}
  a0000 a0100 a0200 a0300 a0400 a0500 a0600 a0700 a0800 a0900 a1000 
  c0000 c0100 c0200 c0300 c0400 c0500 c0600 c0700 c0800 c0900 c1000

Expansión de tilde

Se utiliza para obtener ciertos nombres de directorio.

Ej.

$ echo ~ #directorio home usuario actual
  /home/jmc
$ echo ~pepe #directorio home usuario pepe
  /home/pepe
$ echo ~+ #directorio actual
  /home/jmc/aso
$ echo ~- #directorio anterior
  /tmp

Expansión de variables (y parámetros)

Nos vamos a centrar en la expansión de variable, aunque todo lo dicho en esta sección es también válido para los parámetros.

En la bash se pueden declarar variables. Ej.:

$ mes=9 #asignamos a la variable mes el valor 9. Cuidado: sin blancos entre =
$ mes= #asignamos a la variable mes la cadena vacía (es un valor válido)
$ unset mes #eliminamos la variable mes

Los identificadores de las variables distinguen mayúsculas y minúsculas (por tanto mes, Mes y MES son tres identificadores distintos). También, anteponiendo en la declaración readonly es posible definir constantes.

Ej.:

$ readonly IVA_NORMAL=21

Precediendo una variable con $, o escribiendo ${nombre_variable}, se sustituye por el valor de la variable.

Ej.:

$ echo Es el mes $mes ... pro${mes}a
  Es el mes 9 ... pro9a

El valor a asignar a las variables es objeto de varias expansiones si las hubiera: tilde, variable y parámetro, sustitución de orden y aritmética (nótese que no se produce expansión de llaves ni de nombres de archivos).

Ej.:

$ miDir=~/gastos${mes}.2021
$ echo $miDir
  /home/jmacias/gastos9.2021

Las variables creadas como se ha indicado aquí se denominan variables de shell y no se exportan a las subshells.

Ej.:

$ miVar=hola
$ bash #ejecutamos otra shell desde la shell original
$ echo $miVar
$ exit #volvemos a la shell inicial
$ echo $miVar
  hola

Existen las denominadas variables de entorno que están disponibles una vez definidas para todas las subshells. Para que una variable sea de entorno hay que precederla de export.

Ej.:

$ export miVar=hola
$ bash #ejecutamos otra shell desde la shell original
$ echo $miVar
  hola
$ exit #volvemos a la shell inicial
$ echo $miVar
  hola

Existen una serie de variables de entorno predefinidas. Las variables de entorno se visualizan con env. Podemos encontrar entre otras:

  • HOME: directorio de conexión del usuario.
  • PATH: Caminos en los que se buscarán los ejecutables cuando no se especifican.
  • PS1: la cadena de configuración del prompt para la primera línea.
  • PS2: la cadena de configuración del prompt a partir de la primera línea.
  • PWD: el directorio actual.

Sustitución de orden

Consiste en el reemplazo por la salida de una orden.

Su sintaxis es:$(orden).

Ej.:

$ cal $(date +%Y) #supóngase que estamos en el año 2020
                       2020
     Enero             Febrero                Marzo
do lu ma mi ju vi sá   do lu ma mi ju vi sá   do lu ma mi ju vi sá
       1  2  3  4  5                   1  2                   1  2
 6  7  8  9 10 11 12    3  4  5  6  7  8  9    3  4  5  6  7  8  9
 ...

Sustitución artimética

Evalúa una expresión aritmética entera y sustituye por el resultado. Los operadores, su precedencia, asociatividad y resultados son los mismos que en lenguaje C.

Su sintaxis es: $((expresión)).

Ej.:

$ echo $((45*(9%2)+5))
  50

Expansión de nombres de archivos

Se denomina globbing en inglés y consiste en el archivos (o directorios) con patrones que contienen caracteres comodines. Estos son:

  • *: Cualquier cadena, incluida la vacía
  • ?: Un carácter cualquiera
  • [ ]: Cualquier carácter incluido entre los corchetes. Se pueden especificar rangos de caracteres con -.
  • [:alpha:], [:digit:], [:punct:], etc.: Clases de caracteres (letras, dígitos, cualquier signo de puntuación, etc.).
  • [^]: cualquier carácter no incluido entre los corchetes.

Ej.:

$ ls
  4abc.txt a.3b.txt a222.txt abcd.txt25
$ ls [[:alpha:]][^[:number:]]??.txt*
  a.3b.txt abcd.txt25

Redirecciones de la entrada/salida

Todo proceso tiene abiertos por defecto los siguientes descriptores de archivo (file descriptors):

  • 0 => stdin (entrada estándar, del teclado)
  • 1 => stdout (salida estándar, en el terminal)
  • 2 => stderr (salida error estándar, en el terminal)

Las órdenes suelen usar la entrada y salidas estándar, lo que facilita el encadenamiento de órdenes utilizando pipelines.

Además hay varias posibilidades de redireccionar la entrada y salida a un archivo, como se verá a continuación.

Redirección de la entrada a fichero

n < archivo: Abre, para lectura, el archivo cuyo descriptor es n. Si no se especifica n, se emplea el descriptor 0, entrada estándar. Ej.:

$ cat < fileTab.txt # Lee de la entrada estándar el archivo dado
  Archivo    con        tabulaciones
$ tr "\t" " " < fileTab.txt
  Archivo con tabulaciones

Redirección de la salida a fichero

n > archivo: Abre, para escritura, el archivo cuyo descriptor es n. Si no se especifica n, es 1, salida estándar. Si fichero no existe, se crea y si existe, se trunca.

Ej.: Muestre todos los archivos que empiecen por a, b, c y d, sin el contenido de los directorios, en el archivo resultado.txt. La ejecución de esta orden no debe mostrar ningún resultado por pantalla

$ ls -d [a-d]* > resultado.txt 2>/dev/null

n >> archivo: Añade contenido a un archivo existente. Si el archivo no existe, se crea. Ej.:

$ ls -d [w-z]* >>resultado.txt

Redirección de la salida a descriptor

n > &m (o n >> &m): Abre para escritura (o añade) el mismo destino que haya en el descriptor m en el descriptor n. Ej.:

$ ls a* >resultado.txt 2>&1
$ ls b* >f1 2>f2 3>&1 1>&2 2>&3

Además, como es muy común establecer una redirección de la salida de error estándar a la salida estándar, la sintaxis reducida es & > archivo. Ej.:

$ ls j* &>resultado.txt

Aquí documento (here document)

<<ETIQUETA: La entrada estándar lee del texto indicado hasta que encuentre de nuevo ETIQUETA.

Ej.:

$ ftp -n 127.0.0.100 <<FIN
  user anonymous pepe@uah.es 
  binary
  lcd $HOME
  put archivo.dat
  bye
  FIN

Si se usa <<-ETIQUETA se ignoran tabuladores al inicio de las líneas, lo que permite, en el caso de los scripts, que el código quede más claro.

Aquí cadena (here string)

<<< cadena: La entrada estándar lee de la cadena especificada.

Ej.:

$ dc <<<"10k 10.0 3.0 / p"

Estructuración de órdenes en la bash

Las formas principales que adoptan las órdenes en la bash son:

  • Simples
  • Pipelines
  • Listas
  • Compuestas

Órdenes simples

Secuencia de palabras separadas por espacios, terminados por uno de los operadores de control de la shell (nueva línea, |, &, ||, &&, etc.). La primera palabra suele ser la orden a ejecutar y el resto los argumentos. Devuelve un exit status que indica el resultado de la orden.

Ej.:

$ grep "ASO $(($(date +%Y)-1))" *.txt

Pipelines

Secuencia de una o más órdenes simples separadas por pipes, esto es | . La salida estándar de la orden precedente pasa a ser la entrada estándar de la orden siguiente. El exit status es el de la última orden del pipeline.

Ej.:

$ tail fichero.txt | wc -w

Listas de órdenes

Secuencia de uno o más pipelines separados por ;, &, && o || y terminadas opcionalmente en ;, &o nueva línea. El exit status es el de la última orden ejecutada.

  • orden1 ; orden2: Las órdenes se ejecutan secuencialmente. La shell espera que termine cada una antes de ejecutar la siguiente. Ej.:
$ cd $home; touch fichero; echo Terminado
  • orden1 & orden2: La orden1 se ejecuta asíncronamente en una subshell y pasa a ejecutar de forma síncrona orden2. Ej.:
$ gedit fichero & date
  • orden1 && orden2: La orden2 se ejecuta solo si el exit status de orden1 ha sido 0. Ej.:
$ cd $dir && touch fichero
  • orden1 || orden2: La orden2 se ejecuta solo si el exit status de orden1 ha sido distinto de 0. Ej.:
$ cd $dir 2>/dev/null || mkdir $dir

Nota: && y || asocian por la izquierda.

Órdenes compuestas

  • Agrupación de órdenes
    Listas de órdenes ejecutadas como una unidad, que pueden ejecutarse en una subshell con la sintaxis (lista) o en la misma shell con la sintaxis { lista;}.

Ej.: (importante espacios y ; en { lista; })

 $ cd $dir 2>/dev/null || { mkdir $dir ; cd $dir; }
 $ (echo "Mi $HOME"; ls $HOME) > ver.txt

Asimismo, se revisarán en los siguientes capítulos:

  • Bucles.
  • Condicionales.

Variables de shell y personalización entorno

Los comportamientos de algunas de las expansiones se pueden configurar con la orden shopt (SHell OPTions). Por ejemplo, obsérvese el comportamiento para el uso de caracteres comodín:

$ ls
  datos1.txt datos2.txt datosZ.txt fichero.txt
$ ls dat*.txt
  datos1.txt datos2.txt datosZ.txt
$ echo *.txt
  datos1.txt datos2.txt datosZ.txt fichero.txt
$ ls prueba*
  ls: cannot access 'prueba*': No such file or directory
$ echo prueba*
  prueba*

Este comportamiento es debido a que, por defecto, la opción nullglob de shopt esta desactivada. Esto significa que, si no es posible expandir la cadena a uno o más nombres de fichero, se usa la cadena tal cual. Otra posibilidad sería que, si no es posible expandir, se devolviera “vacío”, con lo que el comportamiento sería distinto. Por ejemplo:

$ shopt nullglob #consultamos estado
  nullglob off
$ shopt -s nullglob #habilitamos (set) nullglob
$ ls prueba*
  datos1.txt datos2.txt datosZ.txt fichero.txt
$ echo prueba*
$ shopt -u nullglob #deshabilitamos (unset) nullglob

Si se quiere modificar permanentemente esta u otra opción de la shell, habría que colocar la instrucción correspondiente en el archivo ~/.bashrc del usuario (o en el archivo /etc/bash.bashrc, si se deseara para todos los usuarios).

Ciclo de ejecución de una orden

De forma resumida, el ciclo de ejecución de una orden realiza los siguientes pasos:

  1. Lee la orden desde el teclado o desde un archivo (shell script).
    Una orden puede ocupar más de una línea, usando el continuador, \, al final de cada línea.

  2. Divide la entrada en tokens (“palabras” y “operadores”) teniendo en cuenta el entrecomillado. En este paso se realiza también la expansión de los alias.

  3. Analiza los tokens diferenciando órdenes simples y compuestas.
    Las órdenes compuestas son estructuras de control del lenguaje de programación de la bash (for, if, etc.) y varias formas de agrupamiento que veremos más adelante.

  4. Realiza varias expansiones de la shell, separando los tokens expandidos en listas de nombres de archivos y órdenes/argumentos.

  5. Realiza las redirecciones de la entrada/salida que hubiera (eliminando de la lista de tokens los operadores y operandos relativos a las redirecciones).

  6. Ejecuta la orden y, si la orden no ha sido lanzada en segundo plano, espera a que finalice recogiendo su valor de retorno (exit status) al terminar.

  7. Repite (o termina shell, si la orden era exit).

Imagíne que tiene:

$ ls -l
  -rw-rw-r-- 1 javier javier 50  nov 10 2020 f1.txt
  -rw-rw-r-- 1 javier javier 200 feb 10 2021 f2.txt
  -rw-rw-r-- 1 javier javier 200 feb 15 2021 fichero
  
$ ls -l *.txt > /tmp/resultado.txt

De manera aproximada, la última línea se procesaría como sigue:

  1. Lectura de la línea.
  2. División de la línea en tokens: ls, -l, *.txt, >, /tmp/resultado.txt.
    Además, por defecto en Ubuntu, ls es un alias de ls --color=auto. Por tanto, el token ls se sustituiría por los tokens ls, --color=auto.
  3. Se identifica y se analiza cada token como una orden simple.
  4. *.txt se expande a f1.txt f2.txt f3.txt, considerando la orden y sus argumentos ($ ls -l f1.txt f2.txt f3.txt).
  5. Se redirecciona la salida estándar a el fichero /tmp/resultado.txt y se elimina de la lista de tokens > y /tmp/resultado.txt.
  6. Se ejecuta la orden en primer plano.
    Como ls es una orden externa, la bash realiza una llamada al sistema operativo, a la función exec, y almacena el valor resultante (exit status) en la variable ?.
  7. Vuelve a lanzar la bash.

Ejercicios

  1. Crear el alias listadir para la orden ls. Pruébalo con alguna opción del ls ¿Funciona?
  2. La opción -h (de human) de la orden ls hace que los tamaños de archivo aparezcan con un múltiplo adecuado, para que sean más fáciles de leer. Se quiere que, a veces, el ´ls´ lleve implícito -h y otras no.
    Cree la variable HUMANO que tendrá el valor -lh y declare un alias de ls con dicha variable.
    Observe la salida de ejecutar dicho alias. Después elimine la variable HUMANO y vuelva a ejecutar dicho alias. ¿Se obtiene el mismo resultado en ambos casos?
  3. Escribe la orden para mover, independientemente del directorio en el que te encuentres, el archivo prueba.txt de directorio $HOME al directorio /tmp. En caso de no existir, no debería aparecer ningún error y se crearía un archivo en /tmp que contendría el calendario del mes actual (cal), además del resultado de ejecutar ls en el directorio de trabajo.
  4. Escribe la orden para crear, en el directorio actual, doce archivos vacíos (touch) cuyos cuatro primeros dígitos sean el año que viene (tiene que funcionar independientemente del año en curso) y los dos siguientes el mes.
    P. ej., si se ejecuta en 2022 crearía 202301, 202302, …, 202312.
  5. Escriba una orden que utilice grep para comprobar si en la ruta completa del directorio actual de trabajo aparece la letra s. En caso de no aparecer, no muestra nada.
    Por ejemplo, si estamos en el directorio /tmp/aso, se escribirá exclusivamente un mensaje del estilo “El directorio /tmp/aso contiene la letra s” y si no aparece, no escribe nada.
    Haga dos versiones: una redireccionando la salida de echo a grep y otra usando “aquí cadena” como entrada de grep.

Referencias