Volver a la página principal
sábado 10 agosto 2024
40

Cómo manejar de argumentos en scripts de Bash

Algunos trucos básicos utilizando variables integradas para agregar un poco de poder extra a tus scripts de Bash.

Usar scripts de Bash para automatizar un conjunto de comandos es el primer paso en un viaje para crear herramientas y hacer tu vida más fácil. Los scripts simples, de arriba a abajo, que se ejecutan de la misma manera cada vez son poderosos y pueden ahorrarte tiempo. Sin embargo, llegará un punto en el que querrás personalizar el comportamiento de tu script sobre la marcha: crear directorios con nombres personalizados, descargar archivos de repositorios git específicos, especificar direcciones IP o puertos, y más. Ahí es donde entran en juego los argumentos del script.

Variables Integradas

Bash nos proporciona algunas variables que están presentes en cualquier script que escribas. Aquí tienes algunas de las más útiles:

$1, $2, $3, ...: Argumentos Posicionales

Estos son argumentos posicionales. Contienen los argumentos dados después de tu script cuando se ejecuta en la línea de comandos. Por ejemplo, si tienes un script que se ejecuta de esta manera:

$ ./mi_script 200 cabras

La variable $1 contendrá el valor "200", y la variable $2 contendrá el valor "cabras".

Por ejemplo, utilizo argumentos posicionales en uno de mis scripts más simples, pero es uno que ejecuto casi todos los días en el trabajo (simplificado por el momento):

#!/usr/bin/env bash

nombre_proyecto="$1"

mkdir -p ${nombre_proyecto}/{CAD,dibujos,molde,recursos}

echo "¡Nuevo proyecto '$nombre_proyecto' creado!"

Las expansiones de llaves no deben estar entre comillas, o no se expandirán. No pongas comillas alrededor del argumento para mkdir.

¿Ves cómo tomé la variable especial $1 y almacené su valor en una variable nombrada? No es necesario hacerlo de esta manera. Podría haber dicho mkdir -p "$1/{CAD,dibujos,molde,recursos}" y todo seguiría funcionando bien. Sin embargo, me gusta almacenar argumentos posicionales en variables nombradas cerca de la parte superior de mi script para que cualquiera que lea mi script tenga una idea de lo que se espera del script. Por supuesto, esto no sustituye una buena documentación y un manejo robusto de errores, pero es un pequeño bono de auto-documentación que ayuda un poco en la legibilidad. Definitivamente es una buena práctica.

Cuando lo ejecuto de esta manera:

$ nuevo_proyecto "catéter-01"

Genera la estructura de directorios:

- catéter-01
|- CAD
|- dibujos
|- molde
|- recursos

$0: El Nombre del Script

Esto proporciona el nombre del script tal como fue llamado. Este es un buen atajo que es especialmente útil para cosas como mostrar mensajes de uso y ayuda.

#!/usr/bin/env bash

function uso() {
  echo "Uso: $0 <nombre> [opciones]"
}

# Manejo de errores omitido (por ahora)

if [[ "$1" == -h ]]; then
  uso
  exit 0
fi

nombre="$1"
echo "¡Hola, ${nombre}!"

Entonces, podemos ejecutarlo así:

$ ./saludo -h
Uso: ./saludo <nombre> [opciones]

$ bash saludo -h
Uso: saludo <nombre> [opciones]

$ ./saludo "Ryan"
¡Hola, Ryan!

$#: Conteo de Argumentos

Este es excelente para el manejo de errores. ¿Qué pasa cuando nuestro script no recibe los argumentos que necesita? Vamos a actualizar el script de saludo anterior con algo de manejo de errores.

#!/usr/bin/env bash

function uso() {
  echo "Uso: $0 <nombre> [opciones]"
}

### ¡Nuevo manejo de errores!
# Esperamos un argumento. De lo contrario, le decimos al usuario cómo
# llamar a tu script.
if [[ "$#" -ne 1 ]]; then
  uso
  exit 1
fi

if [[ "$1" == -h ]]; entonces
  uso
  exit 0
fi

nombre="$1"
echo "¡Hola, ${nombre}!"

$?: Código de Salida Más Reciente

Personalmente, no uso mucho esta variable en scripts, pero la uso mucho en la línea de comandos. Muchos comandos ni siquiera dan salida cuando fallan. Simplemente no hacen nada. Entonces, ¿cómo sabes si falló? El código de salida del último comando ejecutado se almacena en la variable $?.

$ ls
prueba.txt    código    strudel.py
$ echo $?
0
$ ls directoriofalso
ls: no se puede acceder a 'directoriofalso': No existe tal archivo o directorio
$ echo $?
2

Aquí tienes un ejemplo en un script:

#!/usr/bin/env bash

nombre_directorio="$1"

mkdir "$nombre_directorio"  # Esto fallará si el directorio ya existe

if [[ "$?" -ne 0 ]]; then
  # Si el directorio ya está creado, está bien
  # solo muestra un mensaje para alertar al usuario
  echo "El directorio '$nombre_directorio' ya existe. No se creará uno nuevo."
fi

$@ y $*: Todos los Argumentos

Estas variables parecen causar la mayor confusión en los principiantes de Bash, ¡y con razón! Hacen casi exactamente lo mismo, pero las diferencias pueden ser importantes según la situación. Aquí tienes la explicación.

Cuando no citas estas variables, ambas hacen lo mismo, colocando todos los argumentos proporcionados en esa ubicación.

#!/usr/bin/env bash

echo "===================="
echo "Esto es dólar estrella."
echo "===================="
for arg in $*; do
  echo "$arg"
done

echo "===================="
echo "Esto es dólar arroba."
echo "===================="
for arg in $@; do
    echo "$arg"
done

Ejecutando esto:

$ ./impresor_argumentos abba dabba "dooby doo"
====================
Esto es dólar estrella.
====================
abba
dabba
dooby
doo
====================
Esto es dólar arroba.
====================
abba
dabba
dooby
doo

¿Ves cómo incluso el argumento entre comillas se dividió en espacios? A veces esto es lo que quieres, pero muy a menudo no lo es. La versión entre comillas de estas variables es donde las cosas se ponen interesantes.

Cuando citas $*, generará todos los argumentos recibidos como una sola cadena, separados por un espacio, independientemente de cómo se citaron originalmente, pero citará esa cadena para que no se divida más tarde.

#!/usr/bin/env bash

echo "===================="
echo "Esto es dólar estrella entre comillas."
echo "===================="
for arg in "$*"; do
  echo "$arg"
done

Ejecutándolo:

$ ./impresor_argumentos abba dabba "dooby doo"
====================
Esto es dólar estrella entre comillas.
====================
abba dabba dooby doo

¿Ves? ¡Un argumento! ¿Quieres implementar echo nosotros mismos?

#!/usr/bin/env bash

printf '%s\n' "$*"
$ ./mi_echo hola mi nombre es Ryan
hola mi nombre es Ryan

¿Genial, verdad?

En contraste, cuando citas $@, Bash recorrerá y citará cada argumento tal como se dieron originalmente. Esta es probablemente la versión más útil, en mi opinión, porque te permite pasar todos los argumentos a subcomandos manteniendo los espacios y comillas exactamente como estaban sin que la división automática de cadenas de Bash lo estropee.

#!/usr/bin/env bash

echo "===================="
echo "Esto es dólar arroba entre comillas."
echo "===================="
for arg in "$@"; do
  echo "$arg"
done
$ ./impresor_argumentos abba dabba "dooby doo"
====================
Esto es dólar arroba entre comillas.
====================
abba
dabba
dooby doo

Verás esto mucho en scripts que tienen un montón de funciones. Es tradicional, si tienes muchas funciones, hacer que la última función en el script sea una función main que maneje todos los argumentos y contenga la lógica de orquestación del script. Luego, para ejecutar la función principal, típicamente, la última línea del script es main "$@". Así:

#!/usr/bin/env bash

function uso() {
  echo "Uso: $0 <primero> <segundo> [opciones]"
}

function mayor() {
  local primero="$1"
  local segundo="$2"
  if [[ "$primero" -gt "$segundo" ]]; entonces
    echo "$primero"
  else
    echo "$segundo"
  fi
}

function main() {
  si [[ "$#" -ne 2 ]]; entonces
    uso
    exit 1
  fi

  local primero="$1"
  local segundo="$2"
  mayor "$primero" "$segundo"
}

main "$@"

Mirando Hacia Adelante: Argumentos Más Avanzados

Espero que ahora estés empezando a sentir el poder de la personalización. Puedes abstraer algunas tareas usando scripts de Bash, incluso si contienen lógica específica para determinadas situaciones, ¡porque puedes proporcionar argumentos al momento de la llamada! Pero esto es solo la punta del iceberg. Si has estado en la línea de comandos durante un tiempo, probablemente hayas visto algunos comandos que tienen un montón de banderas, opciones, configuraciones y subcomandos que todos necesitan ser procesados, y hacerlo con simples argumentos posicionales no será lo suficientemente poderoso para hacer el trabajo.

Etiquetas:
bash
Compartir:
Creado por:
Author photo

Jorge García

Fullstack developer