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:
| Variable | Contiene | En respaldo -v ~/docs /mnt/usb |
|---|---|---|
$0 | La 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:
$ 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:
$ 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>
| Forma | Produce | Cuá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.
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.
$ 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:
# 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:
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 espera | Comprobació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 rango | La anterior, y (( 10#$1 >= 1 && 10#$1 <= 365 )) |
| Uno de varios valores | case "$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.
# 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:
$ 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ó:
#!/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.
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
- Mientras quede algún argumento, se mira el primero. bucle
- 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 - Si es
--, se consume y se termina: lo que sigue no son opciones, aunque empiece con guion. fin - Si empieza con guion pero no es conocida, es un error de uso. error
- 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ón | Ejemplo |
|---|---|
| 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.
#!/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 ))
$ 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.
- El analizador deja en
"$@"solo los archivos. Después del bucle de opciones, el script trabaja con los argumentos restantes como si nunca hubiera habido opciones. --permite copiar un archivo cuyo nombre empieza con guion, como-raro.txt, que de otro modo sería una opción desconocida. Ycp -p --hace lo mismo al pasárselo acp.- Los errores de uso y los de ejecución tienen códigos distintos: 2 para una opción desconocida o un archivo faltante en la línea de comandos, 1 si algún archivo no pudo copiarse. Ante un archivo inválido, el script informa y sigue con los demás, como
cp. - La ayuda se pide con
-hy va a la salida estándar con código 0; ante un error, se sugiere en la de errores. El documento en línea<<FINpermite escribirla tal como se verá, con${0##*/}expandido; lo veremos en el tema 31. (( simular ))es cierto si la variable vale algo distinto de cero, una forma breve de usar una variable como indicador de sí o no.
13. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
for a in $@ o $* | Los argumentos con espacios se parten. | for a in "$@" |
comando "$*" | El comando recibe un solo argumento con todo. | comando "$@" |
$10 | Se lee como $1 seguido de 0. | ${10} |
Usar $1 dentro de una función esperando el del script | Es 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 argumento | No hace nada y devuelve 1; con set -e, el script termina. | Comprobar (( $# >= 2 )) antes. |
${@:-1} para el último | Es un valor por defecto, no una rebanada. | "${@: -1}", con espacio. |
| Un mensaje de uso sin código de salida | El script sigue, o termina con 0. | exit 2 tras el mensaje. |
rm "$archivo" con un nombre que empieza con guion | rm lo toma como una opción. | rm -- "$archivo" |
14. Resumen
- Los argumentos de un script están en
$1,$2… y${10}en adelante;$#es la cantidad y$0la ruta del script. "$@"da cada argumento intacto, como palabra separada;"$*"los une en un texto; sin comillas, se vuelven a dividir.set --reemplaza los argumentos;shiftdescarta el primero y corre los demás.- Lo primero es validar la cantidad con
$#y, si no es correcta, mostrar el uso en la salida de errores y terminar con 2. - Después se valida el contenido de cada argumento, con mensajes que digan qué llegó y qué se esperaba.
- Los argumentos opcionales van al final y se leen con
${n:-valor}; si son varios, conviene convertirlos en opciones. "${@:2}","${@: -1}"y"${@:1:$#-1}"toman partes de los argumentos sin consumirlos.- Un envoltorio pasa los argumentos con
"$@", y puede terminar conexecpara que el comando lo reemplace. - Un analizador a mano usa
while (( $# > 0 )),case "$1"yshift, admite opciones cortas y largas y se detiene en--o en el primer argumento que no es opción. - Las convenciones de Unix (
-h,--,-n, código 2 ante un error de uso) hacen que un script se use sin leer su ayuda.
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
- 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. - Usa
set --en la terminal para compararargs "$@",args "$*",args $@yargs $*. - Escribe
copiar-a DESTINO ARCHIVO...usandoshift, y luego una versióncopiar ARCHIVO... DESTINOusando rebanadas. - Escribe un envoltorio de
grepque agregue siempre--color=auto -ny pase el resto de los argumentos conexec. - Agrega a
copiar-con-fechauna opción-vque muestre los mensajes de copia solo si se la indica, y otra-fque permita sobrescribir un destino existente (y que, sin ella, se niegue a hacerlo). - 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.