Apuntes mientras aprendo sobre software y computadoras.

Linux

Guía mínima para empezar a programar en Bash

Con este apunte doy por iniciada la “Guía mínima para empezar a programar en Bash”.

Lo que voy a practicar es bash scripting. Programas creados con el lenguaje para el entorno de bash, una linea de comandos común a muchos sistemas Linux.

La idea central de mi proyecto: aprender a programar mientras escribo estos apuntes. Efectivamente, estoy tratando de hacer lo que se conoce como “aprender enseñando”.

¿A quién le estoy enseñando? Me estoy enseñando a mi mismo. Se trata de un apunte autodidacta.

La verdad es que ya estuve a punto de empezar a estudiar programación unas… cinco o seis veces antes.

Cinco o seis mil veces antes , quiero decir.

Es cierto, no es un número alentador… pero por eso mismo escribo estos apuntes.

Si te interesa este proyecto, no olvides escribirme un correo electrónico para contarme tu opinión de mi experimento. Y para corregirme si cometo algún error en las explicaciones o si utilizo un ejemplo que resulta confuso/equivocado.

Capítulo 1: ¿Qué es la programación en bash?

¿Qué son los shell scripts?

La shell es un programa para interpretar comandos. Se ingresan comandos en la terminal y la shell (concha, caparazón, etc.) pasa a ejecutarlos.

Se puede decir que “shell” hace referencia al programa que interactuá con el sistema operativo y la palabra “terminal” quiere decir la pieza de equipo o hardware que usamos para ingresar los comandos.

En lo que a mi respecta utilizo las palabras “shell”, “terminal” y “linea de comandos” de manera intercambiable para que la lectura resulte menos aburrida. De otra forma tendría que repetir “bash” mil millones de veces a lo largo del texto.

Al momento de escribir una serie de comandos, por lo general los ingreso uno por uno. Escribo, ejecuto, y luego vuelvo a escribir.

Ejemplo: primero uso un comando para crear una carpeta. Después otro para entrar dentro de la nueva carpeta. Luego otro para crear un documento en la carpeta. Y más tarde un comando distinto para apagar la computadora e irme a dormir.

Sin embargo la shell ofrece un ambiente de programación que cuenta con lo que se conoce como un lenguaje de alto nivel.

En lugar de escribir los comandos del ejemplo por separado, tengo la opción de escribirlos todos juntos dentro de un mismo archivo.

Ese archivo forma un script para la shell. Un shell script.

De este modo puedo combinar los comandos por medio de una sintaxis específica, cosa que con el tiempo me va a permitir construir ideas más complejas.

¿Qué es Bash?

Bash es una Shell. Según el manual de instrucciones:

bash – GNU Bourne-Again SHell

Bash is an sh-compatible command language interpreter that executes
commands read from the standard input or from a file.

Esto sigue en la linea de lo que mencioné antes. Bash es una shell, y como tal interpreta los comandos que le damos directamente o a través de un archivo, como un shell script.

Seguramente hay una historia sobre la creación de esta shell… pero eso excede el propósito de este apunte.

Capítulo 2: empezar a programar

Dos tipos de comandos

Todo lo que ingreso en la terminal, cualquier input, es un comando. Pero no todos los comandos son interpretados de la misma forma.

Tengo que saber que existen:

– Comandos built-in (internos, son parte del sistema)
– Comandos externos (son creados o instalados de forma extra a los originales)

Hasta ahora siempre utilicé comandos internos de la shell. Estos también son conocidos como “comandos built-in” y son parte del código interno de bash.

Ejemplo, escribo pwd en la consola (Print Working Directory o “mostrar directorio de trabajo”) y la terminal me devuelve la dirección del directorio en el que me encuentro trabajando ahora.

pwd
/home/apunte

Hay muchos comandos internos en la terminal como ese. Si quiero saber si un comando es interno o no, lo puedo comprobar con el comando type:

type pwd
pwd es una orden interna del intérprete de ordenes (pwd is a shell builtin)

Pero ahora, al trabajar con mis propios scripts, voy a generar nuevos comandos externos.

Los programas que voy a crear son comandos ejecutables que elijo desde el sistema de archivos. Este archivo puede ser un código binario ejecutable, o un archivo de texto al que le di el formato de script.

Pero este shell script no tiene que ser solo generado por mi para ser externo, podria ser parte tambien de un nuevo programa instalado por separado.

Hay algunas diferencias entre usar un comando interno y uno externo, en teoría los primeros son más rápidos que los segundos. Pero en la práctica su comportamiento es casi lo mismo, al menos para los sencillos ejemplos que estoy utilizando.

Luego voy a ver como darle permiso de ejecución a estos programas, y como invocar esos archivos como si fueran un comando más de la terminal.

Cómo encabezar un programa en bash

Lo primero que necesito para empezar a escribir mi programa en un documento de texto sin formato. Un clásico archivo “txt” es más que suficiente.

No puedo escribir el código en un procesador de texto como “Word” o “LibreOffice Writter”, porque traen detalles extra que van a hacer imposible su ejecución. Esto es interlineado, información en la tipografía y similares.

Si puedo escribir el código en un procesador “plain” o sin formato. Ejemplo: el editor de texto del sistema o programas como Nano.

Voy a utilizar Nano, es muy fácil de aprender. Pero puedo usar otros programas más complejos como por ejemplo Visual Studio Code o VSCodium.

Digamos que entonces creo un nuevo documento de texto y me dispongo a escribir mi programa para bash.

No puedo comenzar un script de cualquier manera. Necesito encabezarlo con una linea especial conocida como “shebang”.

Esta linea especial tiene estos dos símbolos:

#!

Esos dos símbolos comprenden la linea shebang. Pero no termina con eso.

La función de este encabezado es decirle a la terminal el camino hacia el interprete que se encarga de ejecutar el script. Por ejemplo:

#!/bin/bash

La parte /bin/bash es la que expresa el camino simbólico al directorio de la shell que va a interpretar el programa.

En mi caso, como estoy escribiendo un script para bash, el encabezado específico va a quedar de esa forma.

Pero hay otros ejemplos. Digamos que uso otra shell disponible como la “Korn shell”. En ese caso, la shebang va a quedar expresada de otra manera, apuntando a ser interpretado por esa otra shell:

#!/bin/ksh

Agregar comentarios en el script

Un comentario se utiliza para agregar información extra o para explicar algún concepto dentro del script. Un comentario no es parte de lo que va a ejecutar el programa, sirve solamente para mejorar su compresión.

Para crear un comentario utilizo el signo de numeral o almohadilla (#). Todo lo que va después del numeral va a ser ignorado por la linea de comandos.

# Esto es un comentario al programa

Es cierto que la linea shebang también va detrás de un signo numeral, pero ese caso específico no cuenta como comentario.

Los comentarios pueden ir en distintas partes del código, arriba, al final o acompañando una linea de texto especifica. Ejemplo:

mkdir ejemplo  #necesito recordar hacer ejemplos más originales.

Al revisar algunos archivos de configuración en Linux, es fácil notar la presencia de comentarios siempre detrás de signos #, indicando que esperar de cada opción.

Más aun, esta notación del numeral para separar comentarios del cuerpo del código no es único a la programación en la shell. Es una convención general para otros lenguajes de programación.

Sumando todo en un primer programa

Con las cosas que conozco hasta el momento, esta es la lista de pasos que seguí para crear mi primer programa.

Paso 01: crear directorio y archivo

Lo primero que hago es crear un directorio específico para guardar el programas. En este caso voy a crear esa carpeta en mi directorio personal.

Para eso utilizo el comando mkdir.

mkdir ejemplos_bash

Si, el nombre de la carpeta es ejemplos_bash. No es un nombre muy creativo, pero va a servir

Ahora entro en la carpeta. Voy a crear el archivo un texto sin formato y a llamarlo nuevo_script. Para eso utilizo el comando touch.

touch nuevo_script

Perfecto, ya tengo un directorio dedicado a mi experimento de código, y tengo también el archivo en el que voy a guardar el código.

Si quiero también puedo guardar el programa con la extensión “.sh”, lo que en este caso quedaría como “nuevo_script.sh”. Pero la extensión no modifica la forma en que el programa va a ser ejecutado, simplemente sirve para poder identificar de que se trata el archivo de forma fácil.

Paso 02: abrir el script con Nano

Al fin, luego de tanto, tanto tiempo… empezar a programar… esa promesa de éxito, creatividad, dinero infinito…

Perdón, no tengo que distraerme.

Necesito abrir el archivo, y escribir el código.

Voy a usar el editor de texto Nano que es fácil de usar y se ajusta perfectamente a la búsqueda de velocidad que me empuja a seguir con esto. Pero como dije antes, cualquier otro programa va a servir bien. VSCode o también VSCodium son opciones interesanets.

nano nuevo_script

Paso 03: encabezar el script

Para comenzar el script, tengo que encabezarlo. Para eso creo la linea shebang:

#!/bin/bash

Esto indica a la shell el interprete necesario para leer el código. En este caso, el lenguaje es el de la terminal Bash. Ya lo mencioné antes.

Si guardo el archivo y vuelvo a la terminal, puedo inspeccionar el tipo de archivo que tengo. Para eso utilizo el comando file.

Hago entonces:

file nuevo_script

La respuesta va a ser algo como:

script: Bourne-Again shell script, ASCII text executable

Y eso me dice que es un script de bash (Bourne-Again shell script).

Paso 04: comentar el programa

Voy a ponerle un comentario al programa. Aprendí eso antes.

El código entonces queda de es esta forma:

#!/bin/bash
# Este programa crea un directorio, crea un nuevo archivo dentro y luego me informa que terminó su trabajo.

Pero todavía puedo mejorar este comentario de apertura. Ejemplo: si agrego la fecha en que fue creado, y la última fecha en la que fue modificado. También puedo mi nombre.

#!/bin/bash 

# Fecha de escritura: 08/09/21
# Nombre de referencia: apunteimpensado
# Este programa crea un directorio, un nuevo archivo y luego me informa que terminó su trabajo.

De este modo, si alguna vez en el futuro vuelvo a abrir este archivo, voy a entender rápidamente en que estaba trabajando sin necesidad de ejecutar el código.

Paso 05: bosquejar y escribir

Como puede leerse en el comentario anterior, mi programa no va a ser realmente complejo.

El objetivo del script es correr tres comandos, haciendo una lectura consecutiva de las instrucciones que le doy.

Esto último no siempre es así. No siempre puedo leer un programa de arriba hacia abajo sin detenerme, como si se tratara de un texto narrativo.

A veces el programa lee condiciones, y va moviéndose según las distintas variables.

Pero no en este caso. Este programa va directamente en secuencia, cumpliendo los comandos que le pedimos. No escribimos nada que lo haga chequear variables (ejemplo, no le preguntamos si la carpeta ya existe o no).

Esto va a ir así:

– se crea el directorio: nueva_carpeta

– la terminal entra al nuevo directorio.

– se crea el documento: nuevo_script

– Se imprime en la terminal la frase “el trabajo fue terminado con éxito.”

Todo eso en formato script me queda así:

#!/bin/bash

#Fecha de escritura: 08/09/21
#Nombre de referencia: Gustavo
#Este programa crea un directorio, crea un nuevo archivo y luego me informa que terminó su trabajo.

mkdir nueva_carpeta

cd nueva_carpeta

touch nuevo_script

echo “el trabajo fue terminado con éxito.”

Guardo el documento y lo cierro.

Paso 06: ejecutar el script

Lo que me falta es darme permiso de ejecución para el script. Para eso tengo que usar el comando chmod.

Voy a volver sobre este tema luego, pero para resolverlo rápidamente ahora uso el comando:

chmod +rx nuevo_script

Y ahora si, voy a ejecutar el programa.

Me encuentro en la misma carpeta donde empecé a trabajar.

Como sigo en el directorio “ejemplos_bash” no tengo especificar el camino al programa al momento de ejecutarlo.

Entonces hago:

./nuevo_script

Y con eso el programa tiene que ejecutarse.

Paso 07: observar el resultado

Bien, dos opciones.

En la primera todo fue como lo esperaba. En ese caso la terminal tiene la frase:

el trabajo fue terminado con éxito.

Sin ninguna clase de error. Al revisar veo que se creó un directorio llamado nueva_carpeta y en su interior hay un archivo llamado nuevo_script.

Lo reviso rápido con el comando ls.

Tengo que recordar que cuando un programa funciona bien, la terminal no da ningún tipo de mensaje. El mensaje que ahora dio de «trabajo terminado» lo agregué a propósitom para saber que el programa terminó de ejecutarse.

Segunda opción: el programa no funcionó correctamente o me devolvió algún tipo de error.

Errores en mi primer programa:

-Problema 01: No such file or directory

Al ejecutar el script la terminal me devuelve:

line 1: !/bin/bash: No such file or directory

Este es el primer error que encontré al empezar a practicar. raducido quiere decir “el archivo o directorio no existen”.

Por lo que entiendo se puede explica fácil, me faltó agregar el signo numeral delante de la linea shebang. O tal vez escribí con errores el nombre del programa al invocarlo.

Lo mejor va a ser revisar todos esos pequeños detalles y probar de nuevo.

Problema 02: Mi programa solamente funciona la primera vez que lo ejecuto.

El programa que terminamos de escribir tiene un gran problema de diseño. Pero eso en realidad no es un problema realmente, porque es un error buscado.

Este script solamente hace lo que se le pide si “nueva_carpeta” no existe en el directorio donde lo ejecuto.

En realidad puedo ejecutar el script todas las veces que se me ocurra, pero la shell solamente puede crear el directorio nueva_carpeta una única vez antes de encontrar problemas.

Eso es porque no puede haber dos archivos que compartan el mismo nombre, y no se puede borrar de forma simple un directorio con algo en su interior.

Cuando intento ejecutarlo una segunda vez me devuelve un error: la terminal no puede escribir arriba de un directorio que ya existe.

Pero no pasa nada, voy a tratar de arreglar este defecto un poco más adelante.

Anexo: Cómo convertir código de Bash en comandos personalizados

En este apunte extra reviso en que forma puedo agregar comandos personalizados en Bash, para conseguir una experiencia de uso más directa.

De esta manera puedo transformar uno de mis nuevos programas en un comando ejecutable desde la terminal, sin importar en que directorio me encuentro trabajando.

Y este otro apunte explora la idea de crear alias, con el objetivo de crear atajos a la hora de ejecutar comandos:

Qué son y cómo crear alias de comandos en Linux

Capítulo 3: variables y valores

El manual de Referencia para Bash (link al final del texto) dice:

A parameter is an entity that stores values. (…) A variable is a parameter denoted by a name.

Manual de Bash

Mi traducción: un parámetro es una entidad que almacena un valor. Y una variable es un parámetro denotado por nombre .

Entonces en resumen: Una variable tiene un nombre y hace referencia a un valor.

Y cuando dice “hace referencia” quiero decir que la variable “guarda” un valor.

Cuando guardo algo en el bolsillo, por ejemplo las llaves, conozco donde tengo que ir a buscar ese algo. De este modo se que “en el bolsillo de adelante” llevo el elemento “llaves”. Puedo llevar una cosa, o muchas.

Se puede pensar que una variable es un contenedor de otra cosa, como nuestros bolsillos son contenedores de llaves, papeles u otros elementos.

Vamos a entender más lo anterior revisandolo con algunos ejemplos. Estos ejemplos pueden escribirse directamente en la terminal, o se pueden agregar dentro de un script como el que revisamos antes.

La sintaxis para escribir una variable es:

variable=valor

Le damos el nombre a la variable y al mismo tiempo le asignamos un valor.

Crear una variable es como crear un contenedor, uno que puede llevar en su interior distintas cosas.

Siguiendo nuestro ejemplo:

bolsillo=llaves

En este caso la variable “bolsillo” guarda en su interior un llamado, una referencia, al valor “llaves”.

Esto mismo se puede revisar fácilmente en la terminal. Hay que escribir, sin dejar espacios, una variable y su valor, conectadas por el signo igual.

Entonces la shell va a guardar la variable bolsillo con el valor llaves.

Ahora para llamar al valor de la variable, tengo la opción de usar el comando echo. Este comando sirve para mostrar un mensaje de texto en la terminal.

Para eso tengo que acompañar el nombre de la variable con el signo $

echo $bolsillo
llaves

Y la linea de comandos me devuelve el valor de la variable.

Entonces lo importante de la sintaxis es:

– No dejar espacio entre el nombre de la variable y su valor, y conectar ambas con un singo igual.

– Para llamar una variable, tengo que anteponer a su nombre el signo $.

Eso último muy importante. Cuando guardo la variable no utilizo el signo “$”. Pero cuando se trae la variable con un comando, cualquier comando, ahí tengo que recordar agregar el signo $ por delante.

A modo de ejemplo, ahora quiero llamar el valor de mi variable dentro de una oración. Queda:

echo “no tengo que olvidar las $bolsillo”
no tengo que olvidar las llaves

¿Y qué sucede si el valor de la variable esta compuesto por más de una palabra?

Para que ese valor funcione, tengo que encabezarlo entre comillas dobles.

bolsillo="llaves de la nave espacial"
echo $bolsillo
[llaves de la nave espacial]

En lineas generales, al escribir algo que contenga espacios vacíos (o necesita espacios vacíos), utilizar las comillas.

¿Para qué me sirven las variables?

Bueno, las variables me sirven entre otras cosas para:

  • Guardar y asociar distintos tipos de datos.

Por ejemplo si el programa le pregunta a alguien que adivine el contenido de nuestro bolsillo, puedo contrastar el valor de nuestra variable con la respuesta que recibimos.

  • Ahorranos tiempo para repetir información.

Una misma variable puede generarse una única vez, pero puede referenciarse varias veces a lo largo del programa.

Distintos tipos de variables

En el mundo de la programación existen distintas clases de datos: texto (string), números enteros (integers), números con decimales (floats) y algunas otras clases.

Esto es un ultra resumen, existen diferencias de lenguaje a lenguaje.

Pero tengo que recordar que en un primer momento, bash va a interpretar cualquier dato que le demos a una variable como si ese dato fuera un string.

Incluso si le doy un número a una variable, en un primer momento va a ser tomado por un string y no por un integer.

Declarar variables como integers en la shell:

Dado el caso, también puedo especificar que el valor de una de las variables sea tratado como un número y no como texto, por ejemplo para realizar operaciones matemáticas.

El tipo de dato considerado como número (esto es, como número entero) se conoce como integer.

En inglés integer quiere decir entero. Y en matemática los números enteros comprenden a los números naturales (1,2,3…), los naturales negativos (…-3, -2, -1) y también al cero.

Digamos que tengo las variables:

hola=1
chau=4

¿Que número me va a dar resultado de la suma entre las variables hola(1) y chau(4)? Voy a averiguarlo:

echo $hola+$chau
1+4

Bien, no era lo que yo buscaba. Pensaba más bien en conseguir el resultado de la suma numérica de 1+4=5.

Para conseguir eso necesito usar el comando declare con la opción -i (de integer). Con eso voy a declarar los valores de la nueva variable “resultado” como números y no como texto.

declare -i resultado=hola+chau
echo $resultado
5

Bien, finalmente pude pasar el valor de la variable que en principio eran interpreadas como texto a números enteros con los que puedo hacer operaciones.

– Variables globales y locales

Las variables globales son las que se definen al escribir un script, y se encuentra por fuera de una función.

Segun entiendo, estas varaibles pueedn llamarse y modificarse desde cualquier parte del código.

Este tipo de variables son las que mencioné hasta el momento. Por ejemplo si hago:

insecto="abeja gigante"
echo $insecto
abeja gigante

Y luego hago dentro de la misma terminal:

insecto=hormiga
echo $insecto
hormiga

Puedo darme cuenta que “escribí por arriba” del valor anterior de la variable insecto.

Para ver esto de vuelta. Si cierro la terminal y abro una nueva, luego hago:

echo $insecto

No recibo ningún valor. Se perdió cuando cerré la primera terminal.

Pero tambien tengo las llamadas variables locales.

Estas variables son las que escribo por ejemplo dentro de una función en un script. Voy a revisar eso un poco más en profundidad luego.

– Variables del entorno

Las variablesde entorno también pueden encontrarse escritas con su nombre en inglés, como enviroment variables.

Son las variables que el sistema tiene disponible desde su configuración, ya guardan valores que pueden ser llamados para el uso de cualquier script en la shell.

Una ejemplo de este tipo de variable puede ser LANG, que me muestra el lenguaje en el que se encuentra configurado el equipo.

Por ejemplo, si le pido a la terminal que me muestre el valor de la variable LANG, obtengo:

echo $LANG
en_US.UTF-8

Puedo usar esa información a mi favor al momento de crear un programa, por ejemplo contrastando o verificando datos.

Hay muchas variables de entorno y puedo ver un listado completo usando el comando env y también usando el comando printenv.

– Variables especiales

Las variables especiales (special variables) son predefinidas.

Su valor no es único, depende del uso que se hace de la variable y del contexto en que se encuentran.

Por ejemplo la variable especial “$0” tiene como valor el nombre del programa en el que se encuentra (o al que le hacés referencia con su uso). Si, el nombre de la variable es 0.

Por ejemplo, si recupero el valor de esa variable directamente dede la consola de comandos consigo:

echo $0
bash

Y es cierto, bash es el programa que estoy usando.

Pero si escribo rapidamente un nuevo programa llamado “ejemplo_500” y el contenido del script es:

#!/bin/bash
echo El nombre de este programa es $0

Y luego ejecuto el programa en la terminal, el resultado que obtengo es:

El nombre de este programa es ./ejemplo_500

Entonces la variable 0 corresponde siempre al valor del nombre del programa que la esta invocando.

Este es un listado de las variables especiales:

Nombre de la variableValor asignado
$*El valor de todos los argumentos.
$!PID del último background process.
$?Estatus de salida del último child process.
$$PID del proceso activo en este momento.
$#Cantidad del número de comandos.
$0Nombre del programa

No es necesario aprender todo esto de memoria, creo que lo más importante ahora es recordar que este tipo especial de variable existe y se puede usar en algunas oportunidades.

Capítulo 4: Parámetros de posición | Positional parameters

La verdad es que tardé bastante en entender como funcionaba este tema, por eso voy a intentar explicarlo de la forma en que me resulta más sencilla.

De alguna forma se pueden pensar a estos parametros como una suerte de “comodines”a los que puedo asignarles distintos valores según el lugar que ocupan.

Un ejemplo puede mejorar la explicación:

Tengo un script llamado “completar.sh” y el mismo esta escrito de la siguiente manera:

#!/bin/bash
echo este script agrega $1 a modo de $2
echo y gracias a eso consigo $3
echo $4

Si tan solo lo ejecuto obtengo:

./completar.sh

este script agrega y
y gracias a eso consigo

Pero digamos que al momento de ejecutarlo en la terminal agrego algunos argumentos, de esta forma:

./completar.sh palabras argumentos terminarlo saludos

El resultado es que al ejecutarlo, cada palabra toma la “posición” del valor asignado en el programa ($1, $2…) de este modo:

./completar.sh palabras argumentos finalizar saludos

este script agrega palabras y argumentos
y gracias a eso consigo terminarlo
saludos

Las variables que dejé en el programa adquieren los argumentos que escribí al ejecutar el script.

Para acceder a los distintos argumentos de un script puedo valerme de las variables $1, $2, $3 y así hasta llegar a $9.

No se puede usar $0, porque responde a una variable especial.

Pero como dije antes, solamente se puede acceder a nueve argumentos de este modo. Ese es el límite. Para tener más argumentos extra, hay que utilizar el comando shift.

El comando shift remueve el primer argumento y lo convierte en el segundo, el segundo se convierte en el tercero, y así sucesivamente (convierte a $1 en $2, $2 en $3 y así sucesivamente).

Por ejemplo, este es mi nuevo programa llamado “nuevo_parametro.sh”. El contenido del mismo es:

#!/bin/bash
echo este es el primer $1
echo y también el nuevo $1

Se ejecuta como:

./nuevo_parametro.sh argumento ejemplo
este es el primer argumento
y también el nuevo argumento

La palabra «argumento» se repite dos veces, y la palabra «ejemplo» no entra. Pero si escribo el programa como:

#!/bin/bash
echo este es el primer $1
shift
echo y también el nuevo $1

Se ejecuta de este modo:

./nuevo_argumento.sh argumento ejemplo

este es el primer argumento
y también el nuevo ejemplo

Si me fijo bien ahora puedo ver como la posición $1 de la segunda oración se modificó por lo que viene a ser una posición “$2” al usar el comando shift.

Capítulo 5: sustitución de comandos

La sustitución de comandos (command substitution) me sirve para guardar un comando (o su output) como una variable. La idea por detrás es que luego voy a poder usar esa variable como parte de un nuevo comando diferente.

Para conseguirlo necesito escribir el comando utilizando el operador $() de este modo:

$(comando)

Y acompañado de una variable se presenta de esta forma:

variable=$(comando)

Según entiendo, hay al menos dos formas de utilizar la sustitución de comandos.

Guardar el resultado de un comando como una variable

Un detalle de la sustitución de comandos es la oportunidad de guardar un comando como una variable, lo que me permite volver a utilizarlo luego en subsiguientes partes del código.

Para probar esto voy a crear un nuevo script desde cero. Entonces voy a crear un nuevo programa, aunque uno muy sencillo.

Lo que me interesa es que el valor de esta variable sea un comando, para usar luego su resultado de otro modo.

Entonces voy a crear la variable lugar con una expansión para el comando pwd:

#!/bin/bash

#Este programa te informa un listado del contenido del directorio.

listado=$(ls)
echo "$listado"

Luego guardo el programa y le doy permiso de ejecución.

Al ejecutarlo, si todo funciona bien, voy a tener como resultado una lista de los archivos y carpetas presentes en el directorio de trabajo.

Por la sustitución de comandos, la variable “listado” guarda el resultado del comando “ls”.

Al guardar el output de un comando como una variable, la misma se va a modificar según el resultado del comando. Esto le agrega gran versatilidad al programa.

Correr comandos dentro de comandos

Bash permite ejecutar un comando (o varios) y utilizar su salida dentro de otro comando.

Algo similar a lo anterior, pero sin necesidad de crear una variable.

Por ejemplo:

echo "Los comandos presentes en el directorio son: $(ls)"

Pero por medio de la sustitución de comandos conseguí anidar las dos acciones en una sola:

Los comandos presentes en el directorio son: archivo1
archivo2
aarchivo3

De este modo bash utiliza el comando “ls” para listar el contenido del directorio. Luego usa ese output dentro del siguiente comando “echo” para mostrar el resultado.

Es importante recordar que puedo sustituir múltiples comandos en un mismo movimiento, “englobándolos” uno dentro de otro.

Según entiendo, formalmente esto se ve de un modo similar a esto:

comando3 $(comando2 $(comando1))

Voy a tratar de leer esto en “modo humano”. Primero se ejecuta el comando1 y luego el argumento de salida de ese comando pasa a ser usado por el comando2. El output de este segundo entonces es utilizado por el comando 3.

Extra 01: Pipelines en Bash

Este apartado extra quedo fuera de este texto principal para no volverlo más extenso.

Las pipelines sirven para crear secuencias entre comandos. Si te interesa leer más del tema podés leerlo en el siguiente apunte:

¿Qué son las Pipelines en Bash?

Capítulo 6: Clausulas de condición en la shell bash

Creo que primero puedo hacer una definición ultra rápida basada en lo que entendí en mi primer repaso del tema: los condicionales le ponen condiciones a lo que va a ocurrir.

En este caso, siendo que el tema es programación, un condicional le pone condiciones a la forma en que el programa va a continuar.

De algún modo es similar a poner una condición en la vida diaria antes de elegir llevar o no adelante una acción.

Por ejemplo “si me va bien en el examen voy a festejar” o “si me pongo a cocinar ahora voy a poder permitirme leer un libro un poco más tarde”.

Es mismo, pero volcado dentro del lenguaje programa.

Si ocurre tal condición, entonces ocurre tal comando. Si ocurre tal otra condición, en ese caso voy a usar o verificar otro comando.

Condicional ” if”

Mi traducción literal de la palabra if es la siguiente: si.

Y según el diccionario Word Reference, una de las multiples definiciones de la palabra “si” es:

“conj. cond. Que introduce la prótasis de la clausula condicional.”

diccionario word reference

Ejemplo de una clausula condicional: Si ocurre una cosa, entonces luego hago esto otro.

Ahora me pregunto que carambas es una “prótasis”. Y al parecer prótasis significa:

f. gram. Parte de la oración subordinada condicional donde se expresa la condición:
la prótasis y la apódosis son los dos elementos de la condición.

diccionario Word reference

Y bueno, apódosis quiere decir:

Se refiere principalmente a la oración principal de las proposiciones adverbiales condicionales:
si no llega pronto (prótasis), me iré (apódosis).

diccionario word reference

Entonces, creo que eso explica que quiere decir “if”…. Pero en resumen: De ahora en adelante cuando utilizo la palabra “if” al programar, voy a pensarlo como un significado de la palabra “si”.

La palabra if entonces pasa a abrir el condicional. Pero por supuesto, el asunto no termina ahí.

Al momento de hablar, por ejemplo en una conversación informal, el condicional if se usa en conjunto con la palabra “then” para cerrar el sentido de la oración. La traducción literal de la palabra then es: entonces.

Ya puedo entender como queda la oración completa:

Si ocurre algo, entonces hago esto otro.

Con esto creo que es fácil entender, al menos en la teoría, en que forma se piensan los condicionales. Dentro de un programa, o por fuera.

Voy a revisar esta misma lógica, pero ahora pensándolo para la terminal. Por ejemplo, pare decir “si variable A ocurre entonces invocar al comando B”

si ocurre la “variable A”
entonces
se va a ejecutar por respuesta el “comando B”

Evidentemente no puedo dejarlo escrito de esta forma. tengo que traducirlo para la terminal. El condicional en el programa se consigue traducir con un bloque de la siguiente forma:

if variable A
then
comando 1
fi

Y puedo notar que esa pieza de código se cierra con la palabra “fi.

De este modo fi consigue finalizar el bloque iniciado por el condicional if. De alguna forma es casi poético.

Pero en todo caso, entre if y fi se pueden agregar los comandos que tenga necesidad de evaluar.

¿Cómo entiende la terminal lo que escribí con este condicional?

Primero va a revisar si es verdadera la “variable A” que aparece junto al condicional if. En Linux, un comando exitoso regresa “cero” por respuesta.

Entonces si la “variable A” regresa cero al ser comprobada (es decir, es exitosa en su respuesta), entonces Bash ejecuta el “comando 1 que aparece luego del condicional then.

¿Qué ocurre entonces si el comando que sigue después de if no es exitoso? (Da una respuesta distinta a cero: no es verdadero, resulta en un error, etc).

Entonces en ese caso, el manual de Bash dice:

“If test-commands returns a non-zero status, each elif list is executed in turn (…)”

Manual de Bash

Es decir: si al probar la parte de “if” el resultado es distinto a cero, se va a pasar a ejecutar la parte “elif” listada a continuación.

¿Qué quiere decir con “elif”? Esto también a continuación.

Condicional “elif”

El condicional elif no es otra cosa que una combinación de if y else en una sola palabra clave.

Lo que hace es agregarle otra rama en el “árbol” de condicionales. Si el comando que sigue a if no resulta exitoso, entonces elif actuá como un “pero si”.

En nuestro lenguaje cotidiano, podría ser algo como:

Si apruebo el examen (variable A)
entonces hago una fiesta. (Comando 1)
Pero si no apruebo el examen pero si consigo un trabajo (variable B)
entonces compro libros de danzas extrañas.” (Comando 2)

No tengo que concentrarme lo extravagante del ejemplo, solo en su sentido.

Voy a probar escribir lo anterior, pero ahora en el lenguaje similar de Bash para que pueda interpretarlo la terminal:

if variable A
then
    comando 1
elif variable B
then
    comando 2
else
    comando 3
fi

¿Cómo se lee esto?

La shell revisa la validez de la variable A, y si tiene éxito entonces ejecuta el comando 1.

Si la variable A no es verdadera, entonces revisa la validez de la variable B. Y si esa variable es verdadera, entonces pasa entonces al comando 2.

Si todo lo de arriba no funciona, entonces pasa a la clausula luego de “else” y termina.

¿Qué es la clausula Else? Eso a continuación.

Condicional “else”

La palabra else la puedo entender como “si no”.

Y según comprendo, esta es la condición que ocurre cuando todo lo demás que estuvimos evaluando resultó falso.

En conjunto con el comando if puedo usar el comando else. De hecho puedo recurrir a esta clausula incluso si mi código no hace uso del condicional “elif”.

Si la clausula para if no resulta exitosa o verdadera, la terminal va a correr los comandos que siguan luego de else.

Un ejemplo en nuestro lenguaje cotidiano quedaría mas o menos así:

Si gano un premio (Variable A)
entonces voy a comprar libros de bailes extraños. (Comando 1)
Si No gano un premio (lo opuesto a la “Variable A”)
voy a salir a tomar un café. (Comando 2)

Un ejemplo algo extraño, pero creo que se entiende el punto.

Pensado desde un posible programa en Bash, esto queda:

if variable A
then
    comando 1
else
    comando 2
fi

Entonces básicamente, la shell revisa la variable A.

Si la variable A es verdadera, entonces ejecuta el comando B.

Pero si el comando A no es exitoso, Bash pasa a ejecutar el comando C.

Siendo que cada comando se ejecuta en orden, else va al final. Si la primera parte del bloque no fue exitosa, la terminal revisa el comando luego de else.

Tengo que recordar que “verdadero” es true y “falso” es false.

En principio, con eso ya puedo hacer un programa más complejo. Antes en mis primeros programas los comandos se iban ejecutando uno detrás del otro. Ahora puedo ejecutarlos según distintas ramas de eventos.

Anexo: estado de salida

Este otro apunte tiene más informaciobre “exit status” y el comando exit:

Qué es el estado de salida en bash

Es un tema que ayuda a entender mejor cómo funcionana las distintas ramas de un programa y sirve para encontrar posibles errores en el código.

Capítulo 7: Cómo testear condicionales en la shell

El comando test

Test es un comando para evaluar expresiones.

Si la expresión es verdadera regresa un cero y si es falsa regresa un número distinto (o digamos, regresa un “no cero”).

Dicho de una forma más directa, test evalúa si algo es verdadero o falso. Es como revisar el pronóstico del clima cuando nuestra proposición condicional era “si hoy llueve, entonces voy a dibujar un insecto”.

Este comando tiene tres formas básicas de uso, dependiendo de eso que estoy evaluando.

Puedo usarlo para evaluar texto (strings), para evaluar números (integers) y para evaluar archivos.

Un ejemplo rápido, test puede revisar si un archivo existe o no. También si un archivo esta vacío o no. O si un archivo permite que se escriba en el o no.

Comparar texto con test:

Cuando le doy un valor a una variable, estoy utilizando el signo igual de este modo:

cuader=cuaderno

Sin separación entre la variable, el signo igual y el valor.

Por otra parte, cuando comparo dos piezas de texto (strings), utilizo también el signo igual pero de otro modo:

test cuader = cuaderno

Al usar test necesito dejar espacios entre el signo igual, el valor y la variable. Un ejemplo de programa usando esto me queda de este modo:

#!/bin/bash

#Este programa revisa si dos strings son iguales.

if test cuader = cuaderno
then
        echo “los dos textos son iguales.”
else
        echo “los dos textos son distintos.”
fi

Comparar archivos con el comando test:

De una forma similar se puede usar el comando “test” para comparar números y variables.

Pero una de las funcionalidades que encuentro más útil es la de comparar archivos.

Voy a mejorar brevemente nuestro programa original. Voy a traerlo de vuelta.

En un principio tenía que:

#!/bin/bash

#Fecha de escritura: 08/09/21
#Este programa crea un directorio, crea un nuevo archivo y luego me informa que terminó su trabajo.

mkdir nueva_carpeta

cd nueva_carpeta

touch nuevo_script

echo “el trabajo fue terminado con éxito.”

El programa funciona bien, pero solamente la primera vez. Solamente funciona cuando el directorio “nueva_carpeta” no existe.

Luego me devuelve directamente un error, dando a entender que la carpeta existe y no puede sobre escribirse.

Tal vez hoy no tengo las herramientas para hacer un programa verdaderamente complejo, pero si puedo volverlo más elegante el que ya tengo agregando una comparación.

En conjunto, el programa terminado queda como:

#!/bin/bash

#Fecha de escritura: 08/09/21
#Este programa crea el directorio “nueva_carpeta”, entra en la carpeta y luego crea el documento “nuevo_script”.Luego me informa que terminó su trabajo.

if test -a nueva_carpeta
then
    echo la carpeta ya existe, no es necesario crearla.
else
    mkdir nueva_carpeta
fi
cd nueva_carpeta

touch nuevo_script

echo el trabajo fue terminado con éxito.

No tengo que olvidar darle al programa un permiso para volverlo ejecutable.

Si todo sale bien, lo primero que bash va a comprobar es el comando junto a if para ver si realmente existe un archivo llamado nueva_carpeta.

Cuando la condición el archivo existe se cumple (el test es verdadero) entonces va a pasar a imprimir la leyenda “la carpeta ya existe, no es necesario crearla”.

Por otra parte, si el archivo no existe entonces el programa va a pasar a la clausula else y va a correr el comando mkdir para crear el documento.

Luego el programa sigue, entra en el nuevo directorio y crea un nuevo documento.

Ahora, con esta bifurcación en el programa, contemplando dos posibilidades distintas: la existencia o no de una carpeta específica.

[] Una alternativa a test

Hay otro modo de conseguir averiguar si una variable es verdadera o falsa. Y es por medio del comando [].

Si, el comando que tengo que usar es “[]” .

Supongo que puedo llamarlo corchetes rectos, para diferenciarlos de los redondeados. O simplemente puedo llamarlo [].

Existe un link simbólico entre el comando test y este otro.

Entonces puedo decir que [] es otra forma de escribir test.

Voy a verlo en acción, transformando el ejemplo anterior.

#!/bin/bash

if [ a = a ]
then
        echo los dos textos son iguales
else
        echo los dos textos son distintos.
fi

Y listo, con eso lo tengo. Reemplazamos el comando test por [] y consigo el mismo resultado.

Ahora bien, hay que tener cuidado con los espacios.

Hay que dejar espacio entre los corchetes y la variable. Por ejemplo si escribo:

[ a = a]

me va a devolver un error porque falta un espacio libre al final.

Pero en cambio si respeto los espacios adelante y atrás:

[ a = a ]

El programa va a evaluar la variable sin errores.

Conectores lógicos

Este tema no tiene que ver completamente con los condicionales, puede usarse también para otras cosas como operaciones matemáticas.

Pero quiero incluirlo en alguna parte, bien puedo usarlo ahora.

Hay dos conectores lógicos (logical constructs) que me gustaría mencionar. Sirven para “combinar” condicionales y pueden ir muy bien al comparar comandos.

Hasta ahora solamente hice la evaluación de una condición a la vez. Pero puedo ampliar eso.

Tengo por un lado el conector:

&&

Y este conector significa “and” (en castellano: y).

Por eso si veo:

If [ a = a ] && [ c = h ]
then …

Puedo saber que el condicional if solamente va a resultar verdadero si las condiciones a= a y c = h son verdaderas al mismo tiempo.

A la vez, puedo conseguir lo mismo utilizando el conector lógico -a que actúa de la misma forma que &&. Pero tengo que variar un poco la sintaxis:

if [ b =b -a c = h ]
then …

Por otra parte tengo el conector:

||

Son dos barras verticales. Significa or. En castellano podría traducirlo como “o”.

Ahora:

if [ a = a ] || [ c = h ]
then …

Puede leerse de este modo: Si la letra a es igual a la letra a o la letra c es igual a h, entonces…

En ese nuevo ejemplo, el condicional va a devolvernos verdadero si la primera comparación es verdadera. Si la primera no fuera verdadera, pasaría a evaluar la segunda.

Alcanza con que cualquiera de las dos condiciones sea verdadera para que el código de salida sea cero.

Una alternativa de “||” es el conector lógico “-o” ya que cumple la misma función, aunque también tengo que prestar atención a la forma en la que se escribe:

if [ a = a -o c = h ]
then …

Dejando espacios entre el conector y lo que estoy evaluando.

Capítulo 8: Expansión de parametros | Expansion aritmética

Otro dos apuntes que van a su propia entrada para liberar espacio dentro de esta guía:

Expansión de parámetros e índice en bash

Programación en Bash: operaciones matemáticas

Dentro de esos temas reviso algunas de las herramientas que me van a permitir trabajar con números en bash. Además voy a poder aprender cómo interactuar con las variables y voy a revisar el concepto de “índice”.

Capítulo 9: Bucles (loops)

En este otro apunte reviso la idea de loop dentro de la programación en Bash:

Programar en bash: definición de loop

Conclusión

Este es el final de esta guía mínima para empezar a programar con bash.

Me quedaron afuera muchos temas, pero de haberlos incluido la guía hubiera dejado de ser mínima.

Otra cosa que me gustaría decir es la siguiente: estos son los temas que a mi me parecen importantes para empezar a programar en bash. No soy un experto, simplemente estoy intentando aprender mientras escribo estos “apuntes impensados”.

¿Me sirvió escribir todo esto? Me parece que si, aprendí mucho con este proyecto. Cuando empecé con el tema, no sabia nada de la programación en bash. Hoy al menos puedo leer un programa por encima y tener alguna idea sobre lo que esta ocurriendo.

Pero las verdaderas preguntas tienen que ser… ¿ te sirvió de algo leer todo esto? ¿fue muy confuso? ¿muy mal escrito? ¿plagado de errores?

Cualquier error que encuentres, u opinión que quieras darme del apunte, te pido por favor que me envíes un correo electrónico para que pueda mejorar el texto. La dirección es:

gustavo (arroba) apunteimpensado.com

La seguimos en el próximo apunte.

Recursos:

Leave a Reply