Argumentos y opciones en Bash: $1, $@, shift y getopts

Cómo recibe un script de Bash lo que escribes detrás de su nombre: los parámetros posicionales $1, $# y "$@", por qué las comillas importan tanto, cómo consumirlos con shift, cómo analizar opciones cortas con getopts y largas con un bucle, y qué código de salida devolver en cada caso.

$1, $#, $@ y $0

Lo que escribes detrás del nombre del script llega numerado: $1 es el primer argumento, $2 el segundo, y así sucesivamente; $# dice cuántos hay, "$@" los da todos y $0 es el nombre con el que se llamó al script. Este se ejecutó como ./basico.sh uno 'dos tres' cuatro:

#!/usr/bin/env bash
echo "Script: $0"
echo "Número de argumentos: $#"
echo "Primero: $1"
echo "Segundo: $2"
i=1
for argumento in "$@"; do
    echo "  $i: $argumento"
    i=$((i + 1))
done
salidaScript: ./basico.sh
Número de argumentos: 3
Primero: uno
Segundo: dos tres
  1: uno
  2: dos tres
  3: cuatro

Las comillas al llamar al script deciden qué es un argumento: 'dos tres' llega como uno solo, con su espacio. A partir del décimo hay que usar llaves: ${10}, porque $10 es $1 seguido de un cero.

«$@» frente a $* y «$*»

#!/usr/bin/env bash
echo 'Con "$@":'
printf '  [%s]\n' "$@"
echo 'Con $*:'
printf '  [%s]\n' $*
echo 'Con "$*":'
printf '  [%s]\n' "$*"
salidaCon "$@":
  [uno]
  [dos tres]
  [cuatro]
Con $*:
  [uno]
  [dos]
  [tres]
  [cuatro]
Con "$*":
  [uno dos tres cuatro]

Solo "$@", con comillas, conserva los argumentos tal como llegaron. $* sin comillas los vuelve a partir por los espacios, y "$*" los junta todos en una sola cadena, útil para mensajes pero no para pasarlos a otra orden. ShellCheck avisa del segundo caso:

shellcheckIn comillas.sh line 5:
printf '  [%s]\n' $*
                  ^-- SC2048 (warning): Use "$@" (with quotes) to prevent whitespace problems.
                  ^-- SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean:
printf '  [%s]\n' "$*"

For more information:
  https://www.shellcheck.net/wiki/SC2048 -- Use "$@" (with quotes) to prevent...
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...

Recorrer y consumir con shift

#!/usr/bin/env bash
while (( $# > 0 )); do
    echo "Quedan $#, el primero es «$1»"
    shift
done
echo "Ya no queda ninguno"
salidaQuedan 3, el primero es «-v»
Quedan 2, el primero es «informe.txt»
Quedan 1, el primero es «otro archivo.txt»
Ya no queda ninguno

shift descarta $1 y corre los demás una posición: el antiguo $2 pasa a ser $1. Es la base de cualquier analizador de opciones hecho a mano, y shift 2 descarta dos de golpe.

Opciones cortas con getopts

Para opciones del estilo -v -n 3 -o salida.txt, Bash trae getopts, que entiende opciones agrupadas (-vn3), opciones con valor y el -- que marca el final de las opciones. Este script muestra las primeras líneas de varios archivos:

#!/usr/bin/env bash
uso() {
    cat <<FIN
Uso: ${0##*/} [-v] [-n NÚMERO] [-o ARCHIVO] ARCHIVO...
Muestra las primeras líneas de uno o varios archivos.
  -v          mostrar más detalles
  -n NÚMERO   cuántas líneas mostrar (por omisión, 5)
  -o ARCHIVO  añadir el resultado a ARCHIVO en vez de mostrarlo
  -h          mostrar esta ayuda
FIN
}

detalle=0
lineas=5
salida=""

while getopts ":vn:o:h" opcion; do
    case $opcion in
        v) detalle=1 ;;
        n) lineas=$OPTARG ;;
        o) salida=$OPTARG ;;
        h) uso; exit 0 ;;
        :) echo "Error: la opción -$OPTARG necesita un valor" >&2; uso >&2; exit 2 ;;
        \?) echo "Error: opción desconocida -$OPTARG" >&2; uso >&2; exit 2 ;;
    esac
done
shift $((OPTIND - 1))

if [[ ! $lineas =~ ^[0-9]+$ ]]; then
    echo "Error: -n espera un número, no «$lineas»" >&2
    exit 2
fi
if (( $# == 0 )); then
    echo "Error: falta al menos un archivo" >&2
    uso >&2
    exit 2
fi

if (( detalle )); then
    echo "Leyendo $lineas líneas de $# archivo(s)" >&2
fi
for archivo in "$@"; do
    if [[ ! -r $archivo ]]; then
        echo "Error: no puedo leer «$archivo»" >&2
        exit 1
    fi
    if [[ -n $salida ]]; then
        head -n "$lineas" -- "$archivo" >> "$salida"
    else
        head -n "$lineas" -- "$archivo"
    fi
done
if (( detalle )); then
    echo "Hecho" >&2
fi

Las claves:

  • En ":vn:o:h", cada letra es una opción y los dos puntos detrás de una letra indican que lleva valor, que llega en $OPTARG.
  • Los dos puntos del principio activan el modo silencioso: getopts no imprime sus propios mensajes y te deja decidir qué decir. Una opción desconocida llega como ? y una opción a la que le falta el valor, como :; en los dos casos, $OPTARG contiene la letra.
  • Al terminar, shift $((OPTIND - 1)) quita las opciones ya leídas, y en "$@" quedan solo los archivos.
  • El valor de -n se valida con una expresión regular antes de usarlo. ${0##*/} quita la ruta del nombre del script para el mensaje de uso.

Así responde a distintas llamadas, con el código de salida de cada una:

terminal$ bash opciones.sh -h
Uso: opciones.sh [-v] [-n NÚMERO] [-o ARCHIVO] ARCHIVO...
Muestra las primeras líneas de uno o varios archivos.
  -v          mostrar más detalles
  -n NÚMERO   cuántas líneas mostrar (por omisión, 5)
  -o ARCHIVO  añadir el resultado a ARCHIVO en vez de mostrarlo
  -h          mostrar esta ayuda
$ bash opciones.sh -n 2 frutas.txt
manzana
pera
$ bash opciones.sh -vn3 -o resumen.txt frutas.txt; echo "código: $?"
Leyendo 3 líneas de 1 archivo(s)
Hecho
código: 0
$ cat resumen.txt
manzana
pera
uva
$ bash opciones.sh -n; echo "código: $?"
Error: la opción -n necesita un valor
Uso: opciones.sh [-v] [-n NÚMERO] [-o ARCHIVO] ARCHIVO...
Muestra las primeras líneas de uno o varios archivos.
  -v          mostrar más detalles
  -n NÚMERO   cuántas líneas mostrar (por omisión, 5)
  -o ARCHIVO  añadir el resultado a ARCHIVO en vez de mostrarlo
  -h          mostrar esta ayuda
código: 2
$ bash opciones.sh -x frutas.txt; echo "código: $?"
Error: opción desconocida -x
Uso: opciones.sh [-v] [-n NÚMERO] [-o ARCHIVO] ARCHIVO...
Muestra las primeras líneas de uno o varios archivos.
  -v          mostrar más detalles
  -n NÚMERO   cuántas líneas mostrar (por omisión, 5)
  -o ARCHIVO  añadir el resultado a ARCHIVO en vez de mostrarlo
  -h          mostrar esta ayuda
código: 2
$ bash opciones.sh -n tres frutas.txt; echo "código: $?"
Error: -n espera un número, no «tres»
código: 2
$ bash opciones.sh -- -raro.txt; echo "código: $?"
Soy un archivo cuyo nombre empieza por guion
código: 0

El último caso muestra para qué sirve --: lo que viene detrás ya no son opciones, aunque empiece por un guion. Sin él, getopts tomaría -raro.txt por un grupo de opciones (-r, -a…) y se quejaría de la primera. Y como el script pasa también -- a head, el archivo se lee sin que head lo confunda con una de sus opciones.

Opciones largas: un bucle con case

getopts no entiende opciones largas como --salida=informe.txt. Existe la orden getopt (sin la s) de util-linux, que sí las entiende, pero solo en Linux: la de macOS es otra. Lo más portable es un bucle con case y shift:

#!/usr/bin/env bash
detalle=0
numero=5
salida=""

while (( $# > 0 )); do
    case $1 in
        -h|--ayuda)    echo "Uso: ${0##*/} [--detalle] [--numero N] [--salida ARCHIVO] ARCHIVO..."; exit 0 ;;
        -v|--detalle)  detalle=1; shift ;;
        -n|--numero)
            if (( $# < 2 )); then echo "Error: $1 necesita un valor" >&2; exit 2; fi
            numero=$2; shift 2 ;;
        --numero=*)    numero=${1#*=}; shift ;;
        -o|--salida)
            if (( $# < 2 )); then echo "Error: $1 necesita un valor" >&2; exit 2; fi
            salida=$2; shift 2 ;;
        --salida=*)    salida=${1#*=}; shift ;;
        --)            shift; break ;;
        -*)            echo "Error: opción desconocida $1" >&2; exit 2 ;;
        *)             break ;;
    esac
done

echo "detalle=$detalle numero=$numero salida=$salida"
printf 'Archivo: %s\n' "$@"
salidadetalle=1 numero=3 salida=informe.txt
Archivo: -raro.txt
Archivo: notas.txt

${1#*=} se queda con lo que va detrás del primer =, así que se aceptan las dos formas, --numero 3 y --numero=3.

Los códigos de salida

Un script que acepta argumentos debería terminar con un código que otros scripts puedan comprobar con $? o con if. Estas son las convenciones más extendidas:

  • CódigoSignificaQuién lo usa
  • 0todo fue bientodas las órdenes
  • 1error generalla mayoría de los programas
  • 2uso incorrecto: opciones o argumentos mal puestosBash con sus órdenes internas, grep, ls y buena parte de las de GNU
  • 126la orden existe pero no se puede ejecutarBash, p. ej. un script sin permiso de ejecución
  • 127orden no encontradaBash
  • 128 + Nterminado por la señal N (130 = Ctrl+C, 137 = kill -9)Bash

Todos los scripts de esta página se ejecutaron con Bash 5.3 en Ubuntu 26.04, en un contenedor limpio sin acceso a la red, y pasaron ShellCheck 0.11 sin avisos antes de publicarla; las salidas están copiadas de esa ejecución.