← Automatización con Scripts en Bash Bloque 2 · Variables y datos · Tema 6 de 42

Tema 6

Expansión de parámetros

Leer una variable no tiene por qué ser todo o nada. Entre las llaves de ${…}, Bash puede dar un valor por defecto si la variable está vacía, detener el script si falta un dato, quitar la extensión de un nombre de archivo, reemplazar un texto o pasar todo a minúsculas, sin llamar a ningún programa externo. Este tema recorre esas transformaciones, que aparecen en casi cualquier script real.

1. Transformar sin salir de Bash

Supón que un script recibe la ruta /home/ana/docs/informe.pdf y necesita tres datos: el nombre del archivo, el directorio y el nombre sin extensión. Con los comandos del curso de Terminal se resolvería así:

con programas externos
nombre="$(basename "$ruta")"                 # informe.pdf
carpeta="$(dirname "$ruta")"                 # /home/ana/docs
base="$(basename "$ruta" .pdf)"              # informe

Funciona, pero cada línea crea un proceso nuevo para hacer un trabajo mínimo sobre un texto que el script ya tiene. Con la expansión de parámetros, Bash lo hace por sí mismo:

con expansión de parámetros
nombre="${ruta##*/}"                         # informe.pdf
carpeta="${ruta%/*}"                         # /home/ana/docs
base="${nombre%.*}"                          # informe

La expansión de parámetros es la forma ${…} de leer una variable aplicándole, en el mismo paso, una transformación: dar un valor alternativo, medirla, recortarla, extraer una parte o reemplazar texto. El valor original de la variable no cambia, salvo en la forma ${var:=valor}.

La diferencia de velocidad es irrelevante para una sola línea, pero no dentro de un bucle: aplicar basename a mil archivos significa crear mil procesos, mientras que ${ruta##*/} se resuelve en memoria. En un sistema típico, lo primero tarda alrededor de un segundo y lo segundo, unas centésimas. A cambio, la sintaxis es críptica al principio: está hecha de símbolos, no de palabras. La tabla de la sección 14 reúne todas las formas para consultarlas.

2. Valores por defecto

La forma más usada es ${var:-valor}: si la variable está vacía o no existe, la expansión da valor; si tiene contenido, da ese contenido. La variable no se modifica.

terminal
$ unset destino
$ echo "Copiando a ${destino:-/respaldos}"
Copiando a /respaldos
$ destino=/mnt/usb
$ echo "Copiando a ${destino:-/respaldos}"
Copiando a /mnt/usb

Es la pieza que permite que un script tenga parámetros opcionales. Ya la usamos en los temas anteriores, con argumentos y con variables de entorno:

usos típicos
dir="${1:-.}"                              # primer argumento, o el directorio actual
dias="${2:-30}"                            # segundo argumento, o 30
readonly DESTINO="${DESTINO:-/respaldos}"  # variable de entorno, o un valor fijo
editor="${EDITOR:-nano}"                   # el editor preferido del usuario, o nano

El valor por defecto puede ser cualquier cosa que se expanda: otra variable, una sustitución de comandos, incluso otro valor por defecto. ${DESTINO:-${HOME}/respaldos} y ${FECHA:-$(date +%F)} son válidos, y la sustitución solo se ejecuta si hace falta.

Hay otras tres formas de la misma familia:

FormaSi la variable está vacía o no existeSi tiene valor
${var:-valor}Da valor.Da su valor.
${var:=valor}Da valor y lo asigna a la variable.Da su valor.
${var:?mensaje}Muestra mensaje y termina el script.Da su valor.
${var:+valor}Da una cadena vacía.Da valor, no el de la variable.

${var:=valor} se usa para fijar un valor por defecto una sola vez, al comienzo, y no repetirlo en cada lectura. Como la expansión produce un resultado que habría que ejecutar, se la suele escribir como argumento del comando :, que no hace nada:

fijar valores por defecto al comienzo
: "${DESTINO:=/respaldos}"
: "${DIAS:=30}"
# desde aquí, $DESTINO y $DIAS tienen siempre un valor

${var:+valor} es la forma inversa, y la menos intuitiva. Sirve para agregar algo solo cuando una variable tiene contenido, como un separador o una opción:

terminal
$ nombre="Ana"; apellido=""
$ echo "${nombre}${apellido:+ $apellido}"    # el espacio solo si hay apellido
Ana
$ apellido="Pérez"
$ echo "${nombre}${apellido:+ $apellido}"
Ana Pérez
$ detallado=1
$ rsync -a ${detallado:+--verbose} origen/ destino/   # --verbose solo si detallado tiene valor

En la última línea, la expansión va sin comillas a propósito: si detallado está vacía, no debe quedar un argumento vacío, sino ninguno. Es uno de los pocos casos en que la falta de comillas es deliberada.

3. Los dos puntos: vacía o inexistente

Las cuatro formas existen también sin los dos puntos: ${var-valor}, ${var=valor}, ${var?mensaje} y ${var+valor}. La diferencia es sutil y conviene tenerla clara: con los dos puntos, una variable vacía se trata igual que una inexistente; sin ellos, solo cuenta si la variable existe.

terminal
$ unset inexistente; vacia=""
$ echo "[${inexistente:-def}] [${vacia:-def}]"
[def] [def]
$ echo "[${inexistente-def}] [${vacia-def}]"
[def] []

Casi siempre se quiere la versión con dos puntos: un destino vacío es tan inútil como uno que no existe. La versión sin dos puntos se usa cuando una cadena vacía es un valor legítimo y distinto de «no indicado», por ejemplo un prefijo que el usuario puede querer dejar en blanco a propósito.

Valores por defecto y set -u

Con set -u, leer una variable inexistente detiene el script. Las expansiones de esta sección son la excepción: ${var:-}, con un valor por defecto vacío, permite leer una variable que puede no existir sin provocar el error. Es la forma correcta de consultar, en un script estricto, una variable de entorno opcional: if [[ -n "${DEPURAR:-}" ]].

4. Datos obligatorios: ${var:?}

Cuando un dato es imprescindible, lo mejor es que el script se detenga enseguida si falta, antes de hacer nada. ${var:?mensaje} lo resuelve en una línea: si la variable está vacía o no existe, muestra el mensaje en la salida de errores, precedido del nombre del script, de la línea y de la variable, y termina con código 1.

terminal · un script con origen="${1:?falta el directorio de origen}"
$ ./respaldo
./respaldo: line 8: 1: falta el directorio de origen
$ echo $?
1

El mensaje es algo tosco, por el prefijo que agrega Bash, pero cumple su función, y protege de un caso grave. Compara:

dos maneras de borrar el contenido de un directorio
rm -rf "$directorio"/*                      # si directorio está vacía: rm -rf /*
rm -rf "${directorio:?}"/*                  # si directorio está vacía: el script se detiene

ShellCheck conoce este peligro y avisa con el código SC2115 cuando ve una variable delante de /* en un rm. Para argumentos obligatorios con un mensaje de uso más cuidado, el tema 20 muestra cómo comprobar $# y llamar a una función uso.

5. Longitud: ${#var}

${#var} da la cantidad de caracteres del valor. Sirve para validar datos y para dar formato:

terminal
$ clave="s3cr3t0"
$ echo "${#clave}"
7
$ (( ${#clave} >= 12 )) || echo "La clave debe tener al menos 12 caracteres"
La clave debe tener al menos 12 caracteres

Con letras acentuadas, el resultado depende de la configuración regional. Con una configuración UTF-8, que es lo normal hoy, ${#var} cuenta caracteres: Tamaño tiene 6. Con LC_ALL=C cuenta bytes, y da 7, porque la eñe ocupa dos bytes en UTF-8. Si un script mide textos con acentos, conviene no exportar LC_ALL=C antes de hacerlo.

6. Recortar el principio y el final

Cuatro formas quitan una parte del valor según un patrón: # recorta desde el principio y % desde el final. Duplicado, el símbolo hace que el recorte sea el más largo posible en lugar del más corto.

FormaQuitaCon ruta=/home/ana/docs/informe.final.pdf
${ruta#*/}Del principio, lo más corto que coincida.home/ana/docs/informe.final.pdf
${ruta##*/}Del principio, lo más largo que coincida.informe.final.pdf
${ruta%.*}Del final, lo más corto que coincida./home/ana/docs/informe.final
${ruta%%.*}Del final, lo más largo que coincida./home/ana/docs/informe
Sobre la ruta /home/ana/docs/informe.final.pdf: ${ruta#*/} quita la primera barra; ${ruta##*/} quita /home/ana/docs/; ${ruta%.*} quita .pdf; ${ruta%%.*} quita .final.pdf /home/ana/docs/informe.final.pdf ${ruta#*/} ${ruta##*/} ${ruta%.*} ${ruta%%.*} el prefijo más largo hasta una / la primera / el sufijo más corto desde un . el más largo desde un .
Las barras marcan lo que cada forma elimina. # y ## recortan desde la izquierda; % y %%, desde la derecha.

Para recordar cuál es cuál, una regla mnemotécnica: en un teclado estadounidense, # está a la izquierda de $ y % a su derecha, como el principio y el final de una cadena. Otra: en un número, el # va delante (#1) y el % detrás (50 %).

El patrón no es una expresión regular: es un patrón de comodines, como los de los nombres de archivo. * coincide con cualquier texto, ? con un carácter y […] con uno de un conjunto. Por eso */ significa «cualquier cosa terminada en barra» y .* «un punto seguido de cualquier cosa». Si el patrón no coincide, el valor queda intacto.

terminal
$ version="v2.14.3"
$ echo "${version#v}"                     # quitar un prefijo fijo
2.14.3
$ echo "${version%%.*}"                   # hasta el primer punto
v2
$ linea="ERROR: disco lleno"
$ echo "${linea#*: }"                     # lo que sigue a «: »
disco lleno
$ echo "${linea%%:*}"                     # lo que precede a «:»
ERROR

7. Nombre, directorio y extensión

La aplicación más frecuente de los recortes es descomponer rutas de archivo. Estas son las recetas, que conviene conocer de memoria:

QuieroExpansiónCon /home/ana/fotos/playa.jpgEquivale a
El nombre del archivo${ruta##*/}playa.jpgbasename
El directorio${ruta%/*}/home/ana/fotosdirname
La extensión${ruta##*.}jpg—
Todo menos la extensión${ruta%.*}/home/ana/fotos/playa—
Cambiar la extensión"${ruta%.*}.png"/home/ana/fotos/playa.png—

Las recetas son exactas para los casos normales, pero tienen bordes que conviene conocer, porque basename y dirname los resuelven y la expansión no:

CasoExpansiónResultadoQué conviene
Ruta sin barras: informe.pdf${ruta%/*}informe.pdf, porque el patrón no coincidedirname da .
Archivo sin extensión: LEEME${f##*.}LEEME, el nombre enteroComprobar antes si hay un punto: [[ $f == *.* ]]
Archivo oculto: .bashrc${f%.*}Cadena vacíaTratar aparte los nombres que empiezan con punto.
Doble extensión: datos.tar.gz${f%.*}datos.tar${f%.tar.gz}, si se conoce la extensión.
Barra final: /home/ana/${ruta##*/}Cadena vacíaQuitar antes la barra: ${ruta%/}.

La regla práctica: cuando el script controla de dónde vienen las rutas, como en un bucle sobre "$dir"/*.log, las expansiones son seguras y rápidas. Cuando la ruta viene de un usuario y puede tener cualquier forma, basename y dirname son más robustos, y el costo de un proceso no importa.

8. Subcadenas

${var:inicio:longitud} extrae una parte del valor por posición. Las posiciones empiezan en 0, y si se omite la longitud, se toma hasta el final:

terminal
$ fecha="2026-10-14"
$ echo "${fecha:0:4}"                     # 4 caracteres desde la posición 0
2026
$ echo "${fecha:5:2}"
10
$ echo "${fecha:8}"                       # desde la posición 8 hasta el final
14
$ echo "${fecha: -2}"                     # los 2 últimos: posición negativa
14
El espacio de ${var: -2} es obligatorio

Para contar desde el final se usa una posición negativa, pero hay que separarla de los dos puntos con un espacio o escribirla entre paréntesis: ${fecha: -2} o ${fecha:(-2)}. Sin el espacio, ${fecha:-2} es la forma de valor por defecto de la sección 2, y como fecha tiene valor, devuelve la fecha entera, sin ningún error que delate la confusión.

Las subcadenas sirven cuando los datos tienen un formato de ancho fijo, como fechas, códigos o columnas alineadas. Cuando los campos están separados por un carácter, como en 2026-10-14, suele ser más claro recortar con # y %, o separar los campos con IFS y read, como veremos en el tema 9.

9. Reemplazar texto

${var/patrón/reemplazo} reemplaza la primera coincidencia del patrón; con dos barras al comienzo, ${var//patrón/reemplazo}, reemplaza todas. Si se omite el reemplazo, las coincidencias se borran.

terminal
$ t="uno dos dos tres"
$ echo "${t/dos/2}"                       # la primera
uno 2 dos tres
$ echo "${t//dos/2}"                      # todas
uno 2 2 tres
$ echo "${t// /}"                         # borrar todos los espacios
unodosdostres
$ echo "${t//[aeiou]/}"                   # el patrón puede ser una clase de caracteres
n ds ds trs

Dos variantes anclan el patrón: ${var/#patrón/reemplazo} solo reemplaza si coincide al principio, y ${var/%patrón/reemplazo} solo si coincide al final. La segunda es la forma segura de cambiar una extensión, porque no toca un texto igual que aparezca en medio del nombre:

terminal
$ f="copia.jpeg.de.foto.jpeg"
$ echo "${f/.jpeg/.jpg}"                  # la primera: la del medio
copia.jpg.de.foto.jpeg
$ echo "${f/%.jpeg/.jpg}"                 # solo al final
copia.jpeg.de.foto.jpg
$ echo "${PWD/#$HOME/\~}"                 # mostrar el directorio actual con ~
~/proyectos/web
El & en el reemplazo

Desde Bash 5.2, un & en el texto de reemplazo significa «lo que coincidió con el patrón», como en sed: con x=uno-dos, "${x//-/[&]}" da uno[-]dos. Para un & literal se escribe \&. Las versiones anteriores lo tomaban siempre literalmente, de modo que un script que reemplaza por textos con &, como entidades HTML, puede comportarse distinto según la versión. Lo veremos en el tema 31.

Estas formas cubren los reemplazos simples sobre una variable. Para transformaciones complejas o sobre archivos enteros se sigue usando sed, como veremos en el tema 29; pero aplicar sed a una sola variable, con $(echo "$var" | sed 's/a/b/'), es crear dos procesos para algo que Bash hace solo.

10. Mayúsculas y minúsculas

Cuatro formas cambian el caso de las letras. ^ convierte a mayúsculas y , a minúsculas; simple afecta solo al primer carácter y doble, a todos.

terminal
$ n="ana maría"
$ echo "${n^}"
Ana maría
$ echo "${n^^}"
ANA MARÍA
$ ext="JPG"
$ echo "${ext,,}"
jpg

El uso más común en scripts es normalizar un dato antes de compararlo, para no depender de cómo lo escribió el usuario:

aceptar «s», «S», «si», «Sí» o «SI»
read -rp "¿Continuar? [s/N] " respuesta
respuesta="${respuesta,,}"
[[ "$respuesta" == s* ]] || exit 0

Como con la longitud, la conversión de letras acentuadas depende de la configuración regional: con UTF-8, í pasa a Í; con LC_ALL=C, queda igual. Estas formas existen desde Bash 4; en macOS, que trae un Bash 3 muy antiguo, no funcionan, y es una de las razones para instalar uno actual.

11. Patrones y comillas

En todas las formas que usan un patrón (recortes y reemplazos), el patrón puede venir de una variable. Y ahí aparece una sutileza: si la variable va sin comillas dentro del patrón, sus *, ? y [ funcionan como comodines; entre comillas, se toman literalmente.

terminal
$ nombre='copia[1].txt'
$ quitar='[1].txt'
$ echo "${nombre%$quitar}"                # [1] es una clase: coincide con «1.txt», que no está
copia[1].txt
$ echo "${nombre%"$quitar"}"              # entre comillas: texto literal
copia

La regla: cuando el patrón viene de una variable y quieres quitar ese texto exacto, entrecomíllalo dentro de las llaves. Cuando la variable contiene a propósito un patrón con comodines, déjala sin comillas. Las comillas exteriores, alrededor de toda la expansión, se ponen siempre, como en cualquier lectura de variable.

12. Dos formas más: ${!var} y ${var@Q}

Hay muchas otras formas de expansión, la mayoría de uso raro. Dos merecen conocerse.

La indirección, ${!var}, lee la variable cuyo nombre está guardado en var. Permite, por ejemplo, mostrar el valor de una lista de variables de configuración:

terminal
$ DESTINO=/respaldos; DIAS=30
$ for v in DESTINO DIAS; do printf '%s=%s\n' "$v" "${!v}"; done
DESTINO=/respaldos
DIAS=30

Para asociar nombres con valores de forma general, los arrays asociativos del tema 8 son una herramienta mucho más clara; la indirección conviene reservarla para casos como este.

La transformación ${var@Q} da el valor entrecomillado de forma que Bash pueda volver a leerlo. Es útil en mensajes y registros, porque deja ver exactamente qué contiene una variable, espacios incluidos:

terminal
$ archivo="mi informe 'final'.txt"
$ echo "Borrando ${archivo@Q}"
Borrando 'mi informe '\''final'\''.txt'

13. Un script completo

Los nombres de archivo que llegan de cámaras, teléfonos o de otras personas suelen tener mayúsculas, espacios y extensiones dispares, como Vacaciones Playa 01.JPEG. Este script los normaliza: todo en minúsculas, guiones en lugar de espacios, y .jpeg convertido en .jpg. Con la variable de entorno SIMULAR=1 muestra lo que haría sin hacerlo, la precaución que recomendaba el tema 1.

normalizar-nombres
#!/usr/bin/env bash
#
# normalizar-nombres — renombra los archivos de un directorio: minúsculas,
# guiones en lugar de espacios y extensión .jpeg convertida en .jpg.
#
# Uso:     normalizar-nombres [DIRECTORIO]
# Entorno: SIMULAR=1  muestra los cambios sin hacerlos
#
set -euo pipefail

dir="${1:-.}"
simular="${SIMULAR:-0}"

for ruta in "$dir"/*; do
  [[ -f "$ruta" ]] || continue            # solo archivos comunes

  carpeta="${ruta%/*}"
  nombre="${ruta##*/}"

  nuevo="${nombre,,}"                     # minúsculas
  nuevo="${nuevo// /-}"                   # espacios → guiones
  nuevo="${nuevo/%.jpeg/.jpg}"            # .jpeg al final → .jpg

  [[ "$nuevo" == "$nombre" ]] && continue  # ya estaba normalizado

  if [[ "$simular" == 1 ]]; then
    printf '%s → %s\n' "$nombre" "$nuevo"
  else
    mv -n -- "$ruta" "$carpeta/$nuevo"    # -n: no sobrescribir si ya existe
  fi
done
terminal
$ ls fotos
'Plano Casa.PNG'   recibo.pdf  'Sub Carpeta'  'Vacaciones Playa 01.JPEG'  'Vacaciones Playa 02.JPEG'
$ SIMULAR=1 normalizar-nombres fotos
Plano Casa.PNG → plano-casa.png
Vacaciones Playa 01.JPEG → vacaciones-playa-01.jpg
Vacaciones Playa 02.JPEG → vacaciones-playa-02.jpg
$ normalizar-nombres fotos
$ ls fotos
plano-casa.png  recibo.pdf  'Sub Carpeta'  vacaciones-playa-01.jpg  vacaciones-playa-02.jpg

Tres decisiones del script vale la pena comentar:

14. Tabla de referencia

FormaResultado
${var}El valor.
${var:-valor}valor si var está vacía o no existe.
${var:=valor}Lo mismo, y además lo asigna.
${var:?mensaje}Termina el script con mensaje si var está vacía o no existe.
${var:+valor}valor si var tiene contenido; vacío si no.
${#var}La cantidad de caracteres.
${var#patrón}, ${var##patrón}Sin el prefijo más corto o más largo que coincida.
${var%patrón}, ${var%%patrón}Sin el sufijo más corto o más largo que coincida.
${var:inicio}, ${var:inicio:n}La subcadena desde inicio (desde 0), de n caracteres.
${var: -n}Los últimos n caracteres. El espacio es obligatorio.
${var/patrón/texto}Reemplaza la primera coincidencia.
${var//patrón/texto}Reemplaza todas.
${var/#patrón/texto}, ${var/%patrón/texto}Reemplaza solo al principio, o solo al final.
${var^}, ${var^^}Primera letra, o todas, en mayúsculas.
${var,}, ${var,,}Primera letra, o todas, en minúsculas.
${!var}El valor de la variable cuyo nombre está en var.
${var@Q}El valor entrecomillado para poder releerlo.

Las formas sin los dos puntos (${var-valor}, ${var=valor}, ${var?mensaje}, ${var+valor}) solo consideran si la variable existe, y tratan una variable vacía como un valor válido.

15. Errores frecuentes

EscribisteQué pasaQuerías
${fecha:-2}Devuelve la fecha entera: es un valor por defecto.${fecha: -2}
${var-valor}Con la variable vacía, da la cadena vacía.${var:-valor}
${f%.*} con .bashrcDa una cadena vacía.Tratar aparte los archivos ocultos.
${f##*.} con LEEMEDa el nombre entero como «extensión».Comprobar antes con [[ $f == *.* ]].
${ruta%/*} con informe.pdfDa informe.pdf como directorio.dirname, o asegurar que la ruta tenga barra.
${var/patrón/texto} esperando reemplazar todoSolo cambia la primera coincidencia.${var//patrón/texto}
${nombre%$sufijo} con * o [ en el sufijoEl sufijo se interpreta como patrón.${nombre%"$sufijo"}
${var%[0-9]+}No es una expresión regular: busca un dígito seguido de +.Un patrón de comodines, o =~ (tema 28).
${${a,,}// /-}bad substitution: no se pueden anidar.Dos asignaciones sucesivas.

16. Resumen

17. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Por qué conviene ${ruta##*/} en lugar de $(basename "$ruta") dentro de un bucle?

Porque la expansión se resuelve dentro de Bash, mientras que basename crea un proceso nuevo en cada vuelta. Con miles de archivos, la diferencia de tiempo es grande.

2. ¿Qué diferencia hay entre ${dest:-/tmp} y ${dest:=/tmp}?

Las dos dan /tmp si dest está vacía o no existe. La segunda, además, asigna /tmp a dest, de modo que las lecturas siguientes ya tienen ese valor.

3. Con v="", ¿qué dan ${v:-x} y ${v-x}?

${v:-x} da x, porque con los dos puntos una variable vacía cuenta como ausente. ${v-x} da la cadena vacía, porque la variable existe.

4. ¿Qué hace origen="${1:?falta el origen}" si el script se ejecuta sin argumentos?

Muestra un mensaje de error que incluye falta el origen, en la salida de errores, y termina el script con código 1, sin ejecutar las líneas siguientes.

5. ¿Por qué rm -rf "${dir:?}"/* es más seguro que rm -rf "$dir"/*?

Porque si dir está vacía o no existe, la primera forma detiene el script. La segunda se convertiría en rm -rf /*.

6. Con a=/var/log/app/error.log.1, ¿qué dan ${a##*/}, ${a%/*}, ${a%.*} y ${a%%.*}?

error.log.1, /var/log/app, /var/log/app/error.log y /var/log/app/error.

7. ¿Qué tipo de patrón usan #, % y /?

Patrones de comodines, como los de los nombres de archivo: *, ? y […]. No son expresiones regulares.

8. ¿Qué devuelve ${f##*.} si f=LEEME, y por qué?

LEEME, el nombre entero. El patrón *. no coincide porque no hay ningún punto, y cuando un patrón no coincide el valor queda intacto.

9. Con fecha=2026-10-14, ¿qué dan ${fecha: -2} y ${fecha:-2}?

${fecha: -2} da 14, los dos últimos caracteres. ${fecha:-2} es un valor por defecto, y como fecha tiene valor, da 2026-10-14.

10. ¿Cómo se reemplazan todos los espacios de una variable por guiones bajos?

Con "${var// /_}". Con una sola barra, ${var/ /_}, solo se reemplazaría el primero.

11. ¿Por qué ${f/%.txt/.md} es mejor que ${f/.txt/.md} para cambiar una extensión?

Porque /% solo reemplaza si el patrón coincide al final. / reemplazaría la primera aparición de .txt, que podría estar en medio del nombre.

12. ¿Para qué sirve respuesta="${respuesta,,}" antes de comparar una respuesta del usuario?

Para pasarla a minúsculas, de modo que la comparación funcione igual si el usuario escribió S, s, Si o SI.

13. Con sufijo='[1].txt', ¿por qué ${nombre%$sufijo} y ${nombre%"$sufijo"} pueden dar resultados distintos?

Porque sin comillas el contenido de sufijo se interpreta como patrón, y [1] es una clase que coincide con el carácter 1. Entre comillas se toma literalmente, con los corchetes incluidos.

14. ¿Cómo se aplican dos transformaciones seguidas a una variable, si las expansiones no se pueden anidar?

Con asignaciones sucesivas a una variable intermedia: x="${a,,}" y luego x="${x// /-}".

15. ¿Qué hace ${detallado:+--verbose} sin comillas como argumento de un comando?

Si detallado tiene contenido, agrega el argumento --verbose; si está vacía, no agrega nada, ni siquiera un argumento vacío. Por eso se deja sin comillas a propósito.

Para practicar

  1. Guarda en una variable la ruta /var/log/nginx/access.log.2.gz y obtén con expansiones: el nombre, el directorio, la última extensión, el nombre sin la última extensión y el nombre sin ninguna extensión.
  2. Escribe un script que reciba un nombre de archivo como primer argumento, obligatorio con ${1:?}, y un segundo argumento opcional con la nueva extensión, por defecto bak. Debe mostrar el nombre con la extensión cambiada.
  3. Prueba la diferencia entre ${v:-x} y ${v-x} con v inexistente, vacía y con valor.
  4. A partir de fecha=$(date +%F), muestra la fecha en formato 14/10/2026 usando subcadenas, y luego usando recortes.
  5. Muestra tu directorio actual reemplazando el comienzo $HOME por ~.
  6. Crea un directorio de prueba con archivos llamados Foto 1.JPEG, Mi Documento.TXT y notas.txt. Ejecuta normalizar-nombres primero con SIMULAR=1 y después sin él.
  7. Agrega a normalizar-nombres un paso que reemplace también los guiones bajos por guiones, y otro que quite las tildes de las vocales con varias expansiones //.