← Automatización con Scripts en Bash Bloque 5 · Funciones y argumentos · Tema 20 de 42

Tema 20

Argumentos de un script

Los argumentos son la forma principal de darle datos a un script, y la que lo hace reutilizable: el mismo respaldo sirve para cualquier directorio si el directorio llega como argumento. Los scripts del curso los usan desde el tema 2, con $1, "$@" y ${1:-…}. Este tema los estudia a fondo: cómo se accede a ellos, la diferencia crucial entre "$@" y "$*", cómo recorrerlos y validarlos, y cómo escribir a mano un analizador de opciones.

1. Los parámetros posicionales

Cuando se ejecuta un script, Bash guarda sus argumentos en variables especiales numeradas, llamadas parámetros posicionales. Junto con ellas, otras tres dan información sobre la llamada:

VariableContieneEn respaldo -v ~/docs /mnt/usb
$0La ruta con que se invocó el script./home/ana/bin/respaldo
$1, $2…Cada argumento, por posición.-v, /home/ana/docs, /mnt/usb
${10}, ${11}…Del décimo en adelante, con llaves.—
$#La cantidad de argumentos.3
"$@"Todos los argumentos, cada uno como palabra separada.Tres palabras.
"$*"Todos los argumentos unidos en un solo texto.Una palabra.

La virgulilla de ~/docs no llega al script: la expandió la shell que lo ejecutó, como vimos en el tema 3. Lo mismo ocurre con los comodines y las variables: el script recibe los argumentos ya expandidos, sin comillas.

Las llaves de ${10} son obligatorias, porque $10 se lee como $1 seguido del texto 0:

terminal
$ set -- a b c d e f g h i j k          # fija once argumentos (sección 3)
$ echo "$10 ${10}"
a0 j

Como vimos en el tema 18, dentro de una función estas variables se refieren a los argumentos de la función, no a los del script. Si una función necesita los argumentos del script, hay que pasárselos: es lo que hace main "$@".

2. "$@" y "$*"

Las dos variables contienen todos los argumentos, pero entre comillas se comportan de forma muy distinta. Con la función args del tema 3, y tres argumentos, uno de ellos con un espacio:

terminal
$ set -- uno "dos tres" cuatro
$ args "$@"
3 argumentos: <uno> <dos tres> <cuatro>
$ args "$*"
1 argumentos: <uno dos tres cuatro>
$ args $@
4 argumentos: <uno> <dos> <tres> <cuatro>
FormaProduceCuándo usarla
"$@"Exactamente los argumentos recibidos, cada uno intacto.Casi siempre: para recorrerlos o pasarlos a otro comando.
"$*"Un solo texto, con los argumentos unidos por el primer carácter de IFS.Para un mensaje, o para unirlos con un separador.
$@ o $*Los argumentos, divididos otra vez por los espacios y expandidos como comodines.Nunca.

Es la misma diferencia que entre "${lista[@]}" y "${lista[*]}" en el tema 8: los argumentos de un script se comportan como un array. Y "$@" es la otra excepción a la regla de que una expansión entre comillas da una sola palabra. Si no hay ningún argumento, "$@" no produce nada, ni siquiera una palabra vacía, que es justamente lo correcto.

3. set --: cambiar los argumentos

El comando set -- reemplaza los parámetros posicionales por las palabras que lo siguen. En la terminal es la forma de probar cómo se comportaría un script con ciertos argumentos, como en los ejemplos anteriores. En un script tiene un uso más concreto: reorganizar los argumentos, por ejemplo para agregar valores por defecto.

usos de set --
set -- uno "dos tres"              # ahora $1=uno, $2="dos tres", $#=2
set --                             # sin argumentos: $# vale 0
(( $# > 0 )) || set -- .           # si no hay argumentos, usar el directorio actual
set -- "$@" --verbose              # agregar un argumento al final

El doble guion es importante: sin él, set interpretaría un argumento que empiece con guion como una de sus opciones, como -e o -x.

4. shift: consumir argumentos

shift descarta el primer argumento y corre los demás un lugar: lo que estaba en $2 pasa a $1, y $# disminuye en uno. Con un número, shift 2, descarta varios a la vez.

terminal
$ set -- a b c
$ shift; echo "$# argumentos: $*"
2 argumentos: b c
$ shift 5; echo "código $?, quedan $#"        # no hay tantos: no hace nada
código 1, quedan 2

Si se pide descartar más argumentos de los que hay, shift no hace nada y termina con código 1, lo que con set -e detiene el script. shift sirve para separar los primeros argumentos, que tienen un papel especial, del resto, que es una lista:

el primer argumento es el destino; el resto, los archivos
# Uso: copiar-a DESTINO ARCHIVO...
destino="$1"
shift
for archivo in "$@"; do                 # "$@" ya no incluye el destino
  cp -- "$archivo" "$destino/"
done

Y es la pieza central de los analizadores de opciones de la sección 10, que procesan los argumentos de a uno, consumiendo cada uno con shift hasta que no queda ninguno.

5. Validar la cantidad

Lo primero que debe hacer un script con argumentos es comprobar que recibió la cantidad correcta, y si no, explicar cómo se usa. $# y la aritmética del tema 7 lo resuelven en una línea:

comprobar la cantidad de argumentos
uso() {
  echo "Uso: ${0##*/} ORIGEN DESTINO" >&2
  exit 2
}

(( $# == 2 )) || uso                     # exactamente dos
(( $# >= 1 )) || uso                     # al menos uno
(( $# >= 1 && $# <= 3 )) || uso         # entre uno y tres

El mensaje de uso sigue una convención muy extendida: los argumentos obligatorios en mayúsculas, los opcionales entre corchetes, y los que pueden repetirse seguidos de puntos suspensivos. Uso: copiar [-n] DESTINO ARCHIVO... se lee: una opción -n opcional, un destino obligatorio y uno o más archivos. Va a la salida de errores, y el script termina con código 2, el de uso incorrecto del tema 10.

Para un argumento suelto, el ${1:?mensaje} del tema 6 es más breve, aunque su mensaje es menos cuidado. Ya lo usamos en varios scripts.

6. Validar el contenido

La cantidad correcta no garantiza que los argumentos tengan sentido. Antes de actuar, un script debe comprobar que cada uno es lo que espera, con las pruebas que ya conoces, y fallar con un mensaje que diga qué recibió y qué esperaba:

Se esperaComprobación
Un directorio existente[[ -d "$1" ]]
Un archivo que se pueda leer[[ -f "$1" && -r "$1" ]]
Un número entero[[ "$1" =~ ^[0-9]+$ ]]
Un número dentro de un rangoLa anterior, y (( 10#$1 >= 1 && 10#$1 <= 365 ))
Uno de varios valorescase "$1" in gzip|xz|zstd) ;; *) …; esac
Un destino donde no se pisará nada[[ ! -e "$2" ]]

Hay un caso que merece atención especial: un argumento que empieza con guion. Si un script recibe el nombre de archivo -r y lo pasa a rm, rm lo tomará como una opción. Por eso, al pasar nombres de archivo a otros comandos, se escribe -- antes de ellos, como venimos haciendo desde el tema 4: rm -- "$archivo".

7. Argumentos opcionales

Un argumento que puede faltar se lee con un valor por defecto. Como los argumentos se identifican por posición, los opcionales van al final: si faltan, faltan los últimos.

argumentos con valores por defecto
# Uso: limpiar [DIRECTORIO] [DÍAS]
dir="${1:-.}"
dias="${2:-30}"

Este esquema tiene un límite: no se puede indicar el segundo sin el primero. Si un script tiene más de uno o dos argumentos opcionales, conviene convertirlos en opciones con nombre, como -d DÍAS, que pueden darse en cualquier orden y omitirse por separado. Es lo que resuelven la sección 10 y el tema 21. Y para configuración que rara vez cambia, las variables de entorno del tema 5 siguen siendo una buena alternativa.

8. Rebanadas y el último argumento

Las rebanadas del tema 8 funcionan también con los argumentos, y permiten tomar una parte sin consumirlos con shift:

terminal
$ set -- p q r s
$ echo "${@:2}"                         # desde el segundo
q r s
$ echo "${@:2:2}"                       # dos, desde el segundo
q r
$ echo "${@: -1}"                       # el último (con espacio)
s
$ echo "${@:1:$#-1}"                    # todos menos el último
p q r

Las dos últimas formas resuelven el caso de los comandos con el destino al final, como cp y mv: copiar ARCHIVO... DESTINO. El destino es "${@: -1}" y los archivos son "${@:1:$#-1}". Como en el tema 6, el espacio antes del -1 es obligatorio.

9. Pasar los argumentos a otro comando

Muchos scripts son envoltorios: preparan algo, llaman a otro programa con los argumentos que recibieron, y quizá hacen algo después. La forma de pasar los argumentos sin alterarlos es "$@", que conserva cada uno exactamente como llegó:

un envoltorio de rsync con opciones fijas
#!/usr/bin/env bash
# sincronizar — rsync con las opciones habituales; el resto de los argumentos, sin cambios.
exec rsync --archive --human-readable --exclude '.cache/' "$@"

exec reemplaza el proceso del script por el del comando, en lugar de crear uno nuevo. Es apropiado como última línea de un envoltorio: el script no tiene nada más que hacer, el código de salida del comando pasa a ser directamente el del script, y las señales, como Ctrl+C, le llegan al comando sin intermediarios. Lo que haya después de un exec no se ejecuta nunca.

El script reintentar del tema 15 es otro ejemplo de envoltorio: ejecuta "$@" como un comando completo, con su nombre y sus argumentos.

10. Analizar opciones a mano

Cuando un script admite opciones, como -n o -d DESTINO, hay que separarlas de los demás argumentos y procesarlas. La forma más flexible de hacerlo a mano combina tres piezas ya conocidas: un while que se repite mientras queden argumentos, un case sobre el primero, y shift para consumirlo.

un analizador de opciones
simular=0
destino=.

while (( $# > 0 )); do
  case "$1" in
    -n|--simular)                     # opción sin valor
      simular=1
      shift
      ;;
    -d|--destino)                     # opción con valor: consume dos argumentos
      (( $# >= 2 )) || error_uso "la opción $1 necesita un valor"
      destino="$2"
      shift 2
      ;;
    -h|--help)
      uso
      exit 0
      ;;
    --)                               # fin de las opciones
      shift
      break
      ;;
    -*)
      error_uso "opción desconocida: $1"
      ;;
    *)                                # el primer argumento que no es opción
      break
      ;;
  esac
done
# a partir de aquí, "$@" contiene solo los argumentos que no son opciones
  1. Mientras quede algún argumento, se mira el primero. bucle
  2. Si es una opción conocida, se registra en una variable y se consume con shift; si lleva un valor, se comprueba que exista y se consumen los dos. opción
  3. Si es --, se consume y se termina: lo que sigue no son opciones, aunque empiece con guion. fin
  4. Si empieza con guion pero no es conocida, es un error de uso. error
  5. Si no empieza con guion, terminaron las opciones, y lo que queda en "$@" son los argumentos. fin

Este analizador admite opciones cortas y largas, en cualquier orden, y el separador --. Lo que no admite son las opciones cortas agrupadas, como -nv en lugar de -n -v, ni el valor pegado a la opción, como -d/mnt o --destino=/mnt. Para las opciones cortas, el comando getopts del tema 21 resuelve todo eso de forma estándar.

11. Convenciones de la línea de comandos

Los programas de Unix siguen unas convenciones que los usuarios dan por sentadas. Un script que las respeta se usa sin leer su ayuda:

ConvenciónEjemplo
Opciones cortas de una letra con un guion; largas con dos.-n, --simular
Las opciones van antes de los argumentos.copiar -n archivo
-- marca el fin de las opciones.copiar -- -raro.txt
-h y --help muestran la ayuda en la salida estándar y terminan con 0.copiar --help | less
Un error de uso se informa en la salida de errores, con una sugerencia, y termina con 2.Prueba «copiar --help»…
-n o --dry-run simula sin hacer cambios.Como en el tema 1.
-v o --verbose da más detalle; -q o --quiet, menos.—
-f o --force omite confirmaciones o sobrescribe.Como rm -f.

La ayuda pedida explícitamente va a la salida estándar, porque es la respuesta a lo que se pidió, y puede leerse con less. La que aparece por un error de uso va a la de errores, porque es parte del error.

12. Un script completo

Este script copia archivos agregando la fecha de hoy al nombre, de modo que informe.pdf se convierte en informe-20261014.pdf. Admite opciones para simular y para elegir el destino, con el analizador de la sección 10, y procesa todos los archivos que recibe, informando los que no pudo copiar.

copiar-con-fecha
#!/usr/bin/env bash
#
# copiar-con-fecha — copia archivos agregando la fecha de hoy al nombre.
#
set -euo pipefail

uso() {
  cat <<FIN
Uso: ${0##*/} [-n] [-d DESTINO] ARCHIVO...

Copia cada ARCHIVO agregando la fecha de hoy al nombre:
informe.pdf → informe-AAAAMMDD.pdf

Opciones:
  -n, --simular       muestra lo que haría, sin copiar
  -d, --destino DIR   directorio de destino (por defecto, el actual)
  -h, --help          muestra esta ayuda
FIN
}

error_uso() {
  echo "${0##*/}: $*" >&2
  echo "Prueba «${0##*/} --help» para más información." >&2
  exit 2
}

simular=0
destino=.

while (( $# > 0 )); do
  case "$1" in
    -n|--simular) simular=1; shift ;;
    -d|--destino)
      (( $# >= 2 )) || error_uso "la opción $1 necesita un valor"
      destino="$2"
      shift 2
      ;;
    -h|--help) uso; exit 0 ;;
    --) shift; break ;;
    -*) error_uso "opción desconocida: $1" ;;
    *)  break ;;
  esac
done

(( $# > 0 )) || error_uso "falta al menos un archivo"
[[ -d "$destino" ]] || { echo "${0##*/}: no existe el directorio $destino" >&2; exit 1; }

fecha=$(date +%Y%m%d)
errores=0
for archivo in "$@"; do
  if [[ ! -f "$archivo" ]]; then
    echo "${0##*/}: no es un archivo: $archivo" >&2
    errores=$(( errores + 1 ))
    continue
  fi

  nombre="${archivo##*/}"
  if [[ "$nombre" == ?*.* ]]; then
    nuevo="${nombre%.*}-$fecha.${nombre##*.}"       # informe-20261014.pdf
  else
    nuevo="$nombre-$fecha"                          # LEEME-20261014
  fi

  if (( simular )); then
    echo "copiaría $archivo → $destino/$nuevo"
  else
    cp -p -- "$archivo" "$destino/$nuevo"
    echo "copiado $archivo → $destino/$nuevo"
  fi
done

(( errores == 0 ))
terminal
$ copiar-con-fecha -n -d /respaldos "informe final.pdf" LEEME notas.odt
copiaría informe final.pdf → /respaldos/informe final-20261014.pdf
copiaría LEEME → /respaldos/LEEME-20261014
copiar-con-fecha: no es un archivo: notas.odt
$ copiar-con-fecha --destino /respaldos -- -raro.txt
copiado -raro.txt → /respaldos/-raro-20261014.txt
$ copiar-con-fecha -x informe.pdf; echo "código $?"
copiar-con-fecha: opción desconocida: -x
Prueba «copiar-con-fecha --help» para más información.
código 2
$ copiar-con-fecha -d
copiar-con-fecha: la opción -d necesita un valor
Prueba «copiar-con-fecha --help» para más información.

13. Errores frecuentes

EscribisteQué pasaCorrección
for a in $@ o $*Los argumentos con espacios se parten.for a in "$@"
comando "$*"El comando recibe un solo argumento con todo.comando "$@"
$10Se lee como $1 seguido de 0.${10}
Usar $1 dentro de una función esperando el del scriptEs el argumento de la función.Pasar los argumentos: main "$@".
set uno dos sin --Un argumento con guion se toma como opción de set.set -- uno dos
shift 2 con un solo argumentoNo hace nada y devuelve 1; con set -e, el script termina.Comprobar (( $# >= 2 )) antes.
${@:-1} para el últimoEs un valor por defecto, no una rebanada."${@: -1}", con espacio.
Un mensaje de uso sin código de salidaEl script sigue, o termina con 0.exit 2 tras el mensaje.
rm "$archivo" con un nombre que empieza con guionrm lo toma como una opción.rm -- "$archivo"

14. Resumen

15. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. En script uno "dos tres", ¿qué valen $#, $1 y $2?

$# vale 2, $1 es uno y $2 es dos tres. Las comillas las procesó la shell que llamó al script; el script recibe el texto sin ellas.

2. ¿Qué diferencia hay entre "$@" y "$*"?

"$@" produce cada argumento como una palabra separada, intacta. "$*" los une todos en una sola palabra, separados por el primer carácter de IFS.

3. ¿Qué muestra echo "$10" si el primer argumento es a?

a0: Bash lee $1 seguido del texto 0. Para el décimo argumento se escribe ${10}.

4. ¿Qué hace shift, y qué pasa si no quedan argumentos?

Descarta el primer argumento y corre los demás un lugar, disminuyendo $#. Si no hay suficientes argumentos, no hace nada y termina con código 1.

5. ¿Para qué sirve set --?

Para reemplazar los parámetros posicionales por las palabras que lo siguen. El -- evita que una palabra con guion se interprete como opción de set.

6. ¿Cómo se comprueba que un script recibió entre uno y tres argumentos?

Con (( $# >= 1 && $# <= 3 )) || uso, donde uso muestra el mensaje de uso y termina con código 2.

7. ¿Qué significa Uso: copiar [-n] DESTINO ARCHIVO...?

Que -n es opcional, DESTINO es obligatorio, y se puede indicar uno o más archivos.

8. ¿Cómo se obtienen el último argumento y todos los anteriores a él?

El último con "${@: -1}", con un espacio antes del -1, y los anteriores con "${@:1:$#-1}".

9. ¿Qué hace exec comando "$@" al final de un script?

Reemplaza el proceso del script por el del comando, pasándole los argumentos intactos. El código de salida del comando pasa a ser el del script, y nada de lo que siga se ejecuta.

10. En el analizador de opciones, ¿por qué una opción con valor hace shift 2?

Porque consume dos argumentos: la opción, como -d, y su valor, como /mnt. Antes se comprueba con (( $# >= 2 )) que el valor exista.

11. ¿Para qué sirve -- en la línea de comandos?

Para indicar que terminaron las opciones: todo lo que sigue es un argumento, aunque empiece con guion. Permite, por ejemplo, procesar un archivo llamado -raro.txt.

12. ¿Por qué la ayuda pedida con --help va a la salida estándar, y la de un error de uso a la de errores?

Porque en el primer caso la ayuda es la respuesta a lo que se pidió, y puede querer leerse con less o guardarse. En el segundo, es parte de un mensaje de error.

Para practicar

  1. Escribe un script que muestre $0, $# y cada argumento numerado. Pruébalo con argumentos con espacios, con comodines y con más de diez argumentos.
  2. Usa set -- en la terminal para comparar args "$@", args "$*", args $@ y args $*.
  3. Escribe copiar-a DESTINO ARCHIVO... usando shift, y luego una versión copiar ARCHIVO... DESTINO usando rebanadas.
  4. Escribe un envoltorio de grep que agregue siempre --color=auto -n y pase el resto de los argumentos con exec.
  5. Agrega a copiar-con-fecha una opción -v que muestre los mensajes de copia solo si se la indica, y otra -f que permita sobrescribir un destino existente (y que, sin ella, se niegue a hacerlo).
  6. Toma un script de un tema anterior que use variables de entorno para su configuración y agrégale opciones equivalentes en la línea de comandos.