1. Un script se lee más de lo que se escribe
Un script se escribe una vez, en una tarde. Después se lee muchas veces: cuando falla, cuando hay que cambiarle algo, cuando otra persona lo hereda, cuando tú mismo lo abres un año más tarde y no recuerdas qué hacía. Cada una de esas lecturas cuesta más o menos según cómo esté escrito. Compara estas dos versiones, que hacen el mismo trabajo: copiar los archivos de texto de un directorio a otro.
#!/bin/bash
D=/home/ana/respaldos
cd $1
for F in `ls *.txt`
do
cp $F $D
done
N=`ls $D | wc -l`
echo Hay $N archivos en $D
#!/usr/bin/env bash
#
# copiar-textos — copia los archivos .txt de un directorio al de respaldos.
# Uso: copiar-textos DIRECTORIO
#
set -euo pipefail
readonly DESTINO="$HOME/respaldos"
origen="${1:?Uso: copiar-textos DIRECTORIO}"
cd "$origen"
for archivo in *.txt; do
cp -- "$archivo" "$DESTINO/"
done
La versión B es más larga, pero se entiende de un vistazo: qué hace, cómo se usa, de dónde viene cada dato. Y no es solo cuestión de aspecto: la versión A tiene al menos tres errores reales, que en la B no existen. Si el directorio no existe, cd falla y la copia ocurre en otro lugar; los archivos con espacios se parten en pedazos; y si falta el argumento, cd sin argumentos va al directorio personal y copia desde allí. Un estilo cuidado no es una cuestión estética: un código claro esconde menos errores, y los que quedan se ven.
2. La cabecera
Las primeras líneas de un script, después de la línea shebang, deberían responder las preguntas de quien lo abre por primera vez. Para un script corto alcanza con dos: qué hace y cómo se usa. Para uno que van a usar otras personas, conviene una cabecera más completa:
#!/usr/bin/env bash
#
# respaldo-web — respalda los archivos y la base de datos del sitio web.
#
# Uso: respaldo-web [-n] [-d DÍAS] DESTINO
# Opciones: -n muestra lo que haría, sin hacerlo
# -d DÍAS conserva los respaldos de los últimos DÍAS días (30)
# Ejemplo: respaldo-web -d 7 /mnt/respaldos
#
# Requiere: tar, gzip, mysqldump, y permiso de lectura sobre /var/www.
# Salida: 0 si todo salió bien; 1 si falló el respaldo; 2 si el uso es incorrecto.
#
# Se ejecuta cada noche desde la tabla de cron de root.
| Parte | Para qué sirve |
|---|---|
| Nombre y propósito | Una línea que diga qué hace, no cómo. Si no puedes escribirla, quizá el script hace demasiadas cosas. |
| Uso y opciones | La forma de invocarlo. Es la misma información que mostrará el mensaje de ayuda (temas 20 y 21). |
| Ejemplo | Una invocación real. Vale más que cualquier descripción. |
| Requisitos | Los programas y permisos que necesita. Ahorra mucho tiempo a quien lo instala en otra máquina. |
| Códigos de salida | Qué significa cada uno, para quien lo llama desde otro script (tema 10). |
| Contexto | Dónde y cuándo se ejecuta. Es lo que más se olvida y lo primero que se necesita cuando algo falla. |
Lo que no hace falta poner es el autor, la fecha y un historial de cambios: eso lo registra mucho mejor un control de versiones como Git, que además no se desactualiza.
3. Comentarios que valen la pena
Un comentario repite lo que el código ya dice, o dice lo que el código no puede decir. Los primeros sobran: hay que leerlos, ocupan espacio, y cuando el código cambia quedan mintiendo. Los segundos son valiosos, porque el código explica qué se hace, pero rara vez por qué.
| Comentario inútil | Comentario útil |
|---|---|
# incrementa el contadorn=$(( n + 1 )) | # Los domingos no hay registro: no contarlos como faltantes. |
# copia el archivocp "$a" "$b" | # cp y no mv: el original lo sigue leyendo el servicio web. |
# espera 5 segundossleep 5 | # El servidor tarda unos segundos en liberar el puerto tras reiniciarse. |
# borra los archivos viejosfind … -mtime +30 -delete | # 30 días: el plazo que exige la política de retención (ver ticket 412). |
Algunas pautas más:
- Comenta lo que sorprende. Una opción rara, un orden que parece arbitrario, un caso especial, un truco para esquivar un error de otro programa. Si al leerlo dentro de un año pensarías «¿por qué hice esto?», necesita un comentario.
- Usa comentarios para separar secciones en scripts largos:
# --- Comprobaciones previas ---. Ayudan a orientarse. - Marca lo pendiente de forma reconocible:
# TODO: admitir varios directorios. Se encuentra luego congrep -n TODO. - No dejes código comentado. Un bloque desactivado con
#confunde: nadie sabe si está ahí por error, como reserva o porque «por las dudas». Si ya no hace falta, bórralo; el control de versiones lo guarda. - Prefiere un buen nombre a un comentario.
dias_de_retencion=30no necesita explicación;d=30 # días que se conservan, sí.
4. Nombres de variables
Bash no impone ninguna convención, pero hay una muy extendida que evita un problema serio:
| Tipo de variable | Convención | Ejemplos |
|---|---|---|
| Variables de entorno y de la shell | MAYÚSCULAS | PATH, HOME, USER, LANG, IFS |
| Constantes y configuración del script | MAYÚSCULAS, a menudo con readonly | DESTINO, DIAS_RETENCION |
| Variables de trabajo del script | minúsculas, con guion bajo | archivo, total_copiados, fecha_inicio |
La razón de usar minúsculas para tus variables es evitar que choquen con las del sistema, que están en mayúsculas. Bash usa decenas de variables con nombres comunes, y asignarles algo sin saberlo rompe el script de formas extrañas. El caso más dramático:
$ cat listar.sh
#!/usr/bin/env bash
PATH="$1" # «la ruta que me pasaron»
ls -l "$PATH"
$ ./listar.sh /tmp
./listar.sh: line 3: ls: command not found
Al reemplazar PATH, el script perdió la lista de directorios donde buscar los comandos, y ya no encuentra ls. Con ruta="$1" no habría pasado nada. Otros nombres que conviene no usar como variables propias: HOME, USER, PWD, IFS, RANDOM, SECONDS, LINENO, HOSTNAME, SHELL, UID, TMPDIR. Con minúsculas, el riesgo desaparece.
Además del caso, el nombre debe decir qué contiene. archivo_origen es mejor que ao o f; la excepción son las variables de un bucle muy corto, donde f o i se entienden por el contexto. Y cuando una variable va seguida de letras, números o guiones bajos, hay que delimitarla con llaves, porque esos caracteres forman parte del nombre:
$ nombre="ventas"
$ echo "$nombre_2026.csv" # busca una variable llamada nombre_2026
.csv
$ echo "${nombre}_2026.csv"
ventas_2026.csv
5. Nombres de funciones y de scripts
Las funciones, que estudiaremos en el bloque 5, siguen la misma convención que las variables de trabajo: minúsculas con guion bajo. Como representan acciones, conviene que su nombre empiece con un verbo: comprobar_espacio, crear_respaldo, enviar_aviso. Una función llamada respaldo no dice si lo crea, lo verifica o lo borra.
Para los scripts, en cambio, la convención habitual son minúsculas con guiones, como los comandos del sistema: limpiar-registros, respaldo-web. Como vimos en el tema 2, la extensión .sh se usa mientras el script está en desarrollo y se quita al instalarlo, y antes de elegir un nombre conviene comprobar con type que esté libre.
6. Sangría, espacios y líneas largas
La sangría muestra la estructura: qué está dentro de un if, de un bucle o de una función. Bash la ignora por completo, de modo que su único propósito es la lectura, y para eso tiene que ser consistente. Las pautas de este curso:
- Dos espacios por nivel, sin tabuladores. Los tabuladores se ven de distinto ancho en cada editor, y mezclados con espacios desordenan todo.
thenydoen la misma línea que suif,forowhile, separados por;. Así, la palabra que cierra el bloque (fi,done) queda alineada con la que lo abre.- Un comando por línea. El
;para juntar varios comandos en una línea ahorra espacio y cuesta claridad. - Líneas en blanco para separar partes lógicas, como los párrafos de un texto.
- Líneas de hasta unos 80 caracteres. Más allá, hay que desplazarse para leerlas, y en una terminal angosta se cortan.
for archivo in "$dir"/*.log; do
if [[ -s "$archivo" ]]; then
gzip "$archivo"
else
rm -- "$archivo" # registro vacío: no vale la pena guardarlo
fi
done
Para partir una línea larga se termina con una barra invertida, \, que le indica a Bash que el comando sigue en la línea siguiente. Lo habitual es poner una opción por línea, con sangría:
rsync --archive --delete \
--exclude '*.tmp' \
--exclude '.cache/' \
"$origen/" "$destino/"
La barra invertida tiene que ser el último carácter de la línea. Un espacio invisible después de ella hace que proteja ese espacio en lugar del salto de línea, y la línea siguiente se ejecuta como un comando aparte. Muchos editores permiten mostrar los espacios finales, o eliminarlos al guardar: conviene activarlo. Las tuberías y los operadores && y || no necesitan barra: una línea que termina en | continúa sola en la siguiente.
7. Las construcciones preferibles
Bash arrastra cincuenta años de historia, y para casi todo hay una forma antigua y una moderna. Los scripts viejos que encuentres usarán las antiguas; los que escribas tú deberían usar las nuevas, que son más seguras y más legibles.
| En lugar de | Usa | Por qué | Tema |
|---|---|---|---|
`comando` | $(comando) | Se anida sin problemas, se distingue de las comillas simples y no requiere escapar barras. | 5 |
[ … ] | [[ … ]] | No divide palabras ni expande comodines, y admite &&, || y patrones. | 11 |
expr $a + 1, let | $(( a + 1 )), (( … )) | Es parte de Bash, no un programa externo, y se escribe como una expresión normal. | 7 |
echo para datos | printf | Se comporta igual con cualquier contenido y en cualquier sistema. | 8 de este tema |
$*, $@ sin comillas | "$@" | Conserva cada argumento intacto, con sus espacios. | 20 |
for f in $(ls) | for f in * | Los nombres con espacios no se parten. | 14 |
cat archivo | comando | comando < archivo | Un proceso menos, y el comando puede saber que lee de un archivo. | — |
function nombre { } | nombre() { } | Es la forma estándar, válida también en sh. | 18 |
La regla general detrás de la tabla: dentro de un script de Bash, usa lo que Bash ofrece. Recurrir a expr, basename o cut para operaciones que Bash resuelve solo crea procesos innecesarios, y en un bucle de miles de vueltas la diferencia se nota.
8. printf en lugar de echo
echo parece el comando más simple del mundo, pero tiene un problema: interpreta como opciones algunos argumentos que empiezan con guion, y distintas versiones de echo tratan de forma distinta las barras invertidas. Con texto fijo no importa; con datos que no controlas, sí:
$ opcion="-n"
$ echo "$opcion" # echo lo toma como su opción -n y no muestra nada
$ printf '%s\n' "$opcion"
-n
$ echo 'columna1\tcolumna2' # en Bash, la \t queda tal cual
columna1\tcolumna2
$ printf 'columna1\tcolumna2\n'
columna1 columna2
printf recibe primero un formato y luego los datos. En el formato, %s marca dónde va una cadena, %d un número entero, y \n el salto de línea, que printf no agrega por su cuenta. Los datos nunca se interpretan como opciones ni como formato, sea cual sea su contenido.
$ printf '%s copió %d archivos a %s\n' "$USER" 12 "/mnt/respaldos"
ana copió 12 archivos a /mnt/respaldos
$ printf '%-12s %6s\n' Archivo Tamaño informe.txt 48K notas.txt 3K
Archivo Tamaño
informe.txt 48K
notas.txt 3K
El segundo ejemplo muestra otra virtud: si hay más datos que marcas, printf repite el formato, y los números entre % y la letra fijan anchos de columna. Lo aprovecharemos para generar informes en el tema 31. La pauta del curso: echo para mensajes fijos o simples, printf cuando se muestran datos.
9. Mensajes y errores
Lo que un script escribe en pantalla también es parte de su estilo, y de su utilidad. Tres pautas:
- Los errores van a la salida de errores, con
>&2. Así no se mezclan con los datos si alguien redirige la salida del script a un archivo o a otro comando. - Un mensaje de error dice quién habla y qué pasó, con los datos concretos:
respaldo: error: no existe el directorio /mnt/respaldos, noError!niFalló. - Silencio en el éxito. Un script que se ejecuta desde
cronno necesita anunciar cada paso. Si hace falta saber qué hizo, eso va a un registro, o se muestra solo con una opción como-v.
Como todos los scripts necesitan mostrar errores, conviene escribirlo una vez, como una pequeña función:
error() {
printf '%s: error: %s\n' "${0##*/}" "$*" >&2
}
[[ -d "$destino" ]] || { error "no existe el directorio $destino"; exit 1; }
$ respaldo /mnt/respaldos
respaldo: error: no existe el directorio /mnt/respaldos
El tema 25 amplía esta idea con funciones para distintos niveles de mensaje y para dejar registro en un archivo.
10. Revisar la sintaxis: bash -n
Pasemos a las herramientas. La primera viene con Bash: la opción -n lee el script completo y comprueba que su sintaxis sea correcta, sin ejecutar nada. Detecta un fi o un done que falta, unas comillas sin cerrar o un paréntesis de más:
$ bash -n limpiar-registros.sh # sin salida: la sintaxis es correcta
$ bash -n roto.sh
roto.sh: line 14: syntax error: unexpected end of file
«Fin de archivo inesperado» significa que Bash llegó al final esperando todavía algo que cerrara un bloque: un fi, un done, una llave o una comilla. El número de línea es el del final del archivo, no el del bloque sin cerrar, que habrá que buscar más arriba. Es útil sobre todo antes de ejecutar un script que hace cambios, pero tiene un límite: solo revisa la gramática, y un script sintácticamente perfecto puede estar lleno de errores. Para esos está ShellCheck.
11. ShellCheck
ShellCheck es un analizador estático de scripts de shell: lee el código sin ejecutarlo y señala errores comunes, construcciones frágiles y problemas de portabilidad, con una explicación y, a menudo, la corrección.
Es la herramienta más útil que puedes sumar a tu forma de trabajar con Bash. Detecta casi todos los errores de la tabla del tema 3 y muchos más, y cada aviso enseña algo. Se instala desde el gestor de paquetes:
$ sudo apt install shellcheck # Debian, Ubuntu
$ sudo dnf install ShellCheck # Fedora
$ shellcheck --version | head -2
ShellCheck - shell script analysis tool
version: 0.10.0
Si no puedes instalarlo, existe una versión en línea en shellcheck.net, donde se pega el script y se ven los avisos. Veamos qué dice de un fragmento de la versión A de la sección 1:
$ shellcheck antes.sh
In antes.sh line 3:
cd $1
^---^ SC2164 (warning): Use 'cd ... || exit' or 'cd ... || return' in case cd fails.
^-- SC2086 (info): Double quote to prevent globbing and word splitting.
…
For more information:
https://www.shellcheck.net/wiki/SC2045 -- Iterating over ls output is fragile. Use globs.
https://www.shellcheck.net/wiki/SC2164 -- Use 'cd ... || exit' or 'cd ... |...
https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...
Cada aviso tiene cuatro partes: la línea afectada, una marca que señala la parte exacta, un código (como SC2086) con su nivel de gravedad, y una explicación breve. Para cada código existe una página en shellcheck.net/wiki/ que explica el problema con ejemplos, cuándo es grave y cómo se corrige. Leerlas es una de las mejores formas de aprender Bash.
Para revisar un script completo, el formato compacto -f gcc muestra un aviso por línea, que se lee mejor cuando son muchos:
$ shellcheck -f gcc antes.sh
antes.sh:3:1: warning: Use 'cd ... || exit' or 'cd ... || return' in case cd fails. [SC2164]
antes.sh:3:4: note: Double quote to prevent globbing and word splitting. [SC2086]
antes.sh:4:10: warning: Iterating over ls output is fragile. Use globs. [SC2045]
antes.sh:4:10: note: Use $(...) notation instead of legacy backticks `...`. [SC2006]
antes.sh:6:4: note: Double quote to prevent globbing and word splitting. [SC2086]
antes.sh:6:7: note: Double quote to prevent globbing and word splitting. [SC2086]
antes.sh:8:3: note: Use $(...) notation instead of legacy backticks `...`. [SC2006]
antes.sh:8:7: note: Double quote to prevent globbing and word splitting. [SC2086]
antes.sh:9:10: note: Double quote to prevent globbing and word splitting. [SC2086]
antes.sh:9:25: note: Double quote to prevent globbing and word splitting. [SC2086]
Una decena de avisos en nueve líneas. Los niveles indican la gravedad: error para lo que seguro está mal, warning para lo que muy probablemente cause problemas, info (que el formato compacto muestra como note) para lo que puede causarlos según los datos, y style para lo que funciona pero se escribiría mejor. Que un aviso sea de nivel info no lo vuelve menor: los SC2086 de este script son los que harían fallar la copia con un nombre de archivo con espacios.
Los editores más usados pueden ejecutar ShellCheck mientras escribes y subrayar cada problema en el momento. En Visual Studio Code basta con instalar la extensión ShellCheck. Es la forma más cómoda de usarlo: los errores se corrigen antes de guardar el archivo.
12. Los avisos más frecuentes
Hay cientos de códigos, pero unos pocos concentran la mayoría de los avisos. Conviene reconocerlos:
| Código | Qué señala | Corrección habitual |
|---|---|---|
SC2086 | Una variable sin comillas, que puede dividirse o expandirse como comodín. | "$var" |
SC2046 | Lo mismo, con una sustitución de comandos. | "$(cmd)" |
SC2164 | Un cd que puede fallar sin que el script se entere. | cd "$dir" || exit 1 |
SC2045 | Recorrer la salida de ls. | for f in * |
SC2006 | Comillas invertidas para la sustitución de comandos. | $( ) |
SC2034 | Una variable que se asigna pero nunca se usa. Suele delatar un nombre mal escrito. | Revisar el nombre, o borrarla. |
SC2154 | Una variable que se usa pero nunca se asigna. El caso inverso, y el mismo origen. | Revisar el nombre. |
SC2162 | read sin -r, que altera las barras invertidas. | read -r (tema 9) |
SC2155 | local x=$(cmd): el código de salida de cmd se pierde. | Declarar y asignar en dos líneas (tema 18). |
SC2002 | Un cat innecesario delante de una tubería. | cmd < archivo |
SC2148 | Falta la línea shebang, y ShellCheck no sabe qué shell revisar. | #!/usr/bin/env bash |
SC1017 | Finales de línea de Windows (CRLF). | sed -i 's/\r$//' (tema 2) |
Los avisos SC2034 y SC2154 merecen una mención especial, porque detectan un error que Bash nunca señala: una variable mal escrita. Si asignas destino=… y luego usas $destion, Bash la expande como cadena vacía, sin quejarse. ShellCheck sí lo nota.
13. Cuando ShellCheck se equivoca
A veces un aviso no corresponde: la falta de comillas es deliberada, o ShellCheck no puede ver algo que ocurre fuera del archivo. Para esos casos se puede desactivar un aviso concreto con una directiva en un comentario, justo antes de la línea afectada. Conviene acompañarla siempre de la razón:
# Las opciones deben dividirse en palabras: vienen del archivo de configuración
# como "-a -z --delete".
# shellcheck disable=SC2086
rsync $OPCIONES_RSYNC "$origen/" "$destino/"
Hay dos directivas más que vas a necesitar cuando los scripts empiecen a cargar otros archivos con source. ShellCheck no los sigue por defecto, y avisa con SC1091; se le indica dónde están con -x en la línea de comandos, o con un comentario source=:
# shellcheck source=lib/registro.sh
source "$(dirname "$0")/lib/registro.sh"
La tentación, al principio, es silenciar todo lo que molesta. Resístela: casi siempre que ShellCheck parece equivocarse, tiene razón y la situación que imagina es más rara de lo que crees, pero no imposible. Una directiva sin explicación es una deuda; una con explicación es una decisión.
14. Dar formato automático: shfmt
La sangría y los espacios no hace falta cuidarlos a mano. shfmt es un programa que reformatea un script según reglas fijas, del mismo modo que otros lenguajes tienen formateadores automáticos. Se instala con sudo apt install shfmt en las versiones recientes de Debian y Ubuntu.
$ shfmt -i 2 -d desordenado.sh # -d: muestra qué cambiaría, sin tocar el archivo
--- desordenado.sh.orig
+++ desordenado.sh
@@ -1,6 +1,6 @@
for f in *.log
do
-gzip "$f"
- echo "comprimido $f"
+ gzip "$f"
+ echo "comprimido $f"
done
$ shfmt -i 2 -w desordenado.sh # -w: escribe los cambios en el archivo
La opción -i 2 indica sangría de dos espacios; sin ella, shfmt usa tabuladores. Las dos herramientas se complementan: shfmt se ocupa de la forma y ShellCheck del contenido. Ninguna de las dos cambia lo que hace el script.
15. Antes y después
Apliquemos todo a la versión A de la sección 1. Esta es la versión final, que además de corregir los avisos de ShellCheck incorpora el resto de las pautas del tema:
#!/usr/bin/env bash
#
# copiar-textos — copia los archivos .txt de un directorio al de respaldos.
#
# Uso: copiar-textos DIRECTORIO
# Ejemplo: copiar-textos ~/notas
# Salida: 0 si se copió todo; 1 si hubo un error.
#
set -euo pipefail
readonly DESTINO="$HOME/respaldos"
error() {
printf '%s: error: %s\n' "${0##*/}" "$*" >&2
}
origen="${1:?Uso: copiar-textos DIRECTORIO}"
[[ -d "$origen" ]] || { error "no existe el directorio $origen"; exit 1; }
mkdir -p "$DESTINO"
copiados=0
for archivo in "$origen"/*.txt; do
[[ -e "$archivo" ]] || continue # el patrón no coincidió con nada
cp -- "$archivo" "$DESTINO/"
copiados=$(( copiados + 1 ))
done
printf 'Copiados %d archivos a %s\n' "$copiados" "$DESTINO"
| Cambio | Qué resuelve |
|---|---|
| Cabecera con uso, ejemplo y códigos de salida | Quien lo abre sabe qué hace y cómo llamarlo. |
DESTINO en mayúsculas, con readonly y $HOME | Configuración visible arriba, que no puede modificarse por error ni depende de un usuario concreto. |
${1:?…} y la comprobación con [[ -d ]] | Sin argumento, o con un directorio inexistente, termina con un mensaje claro en lugar de copiar desde otro lugar. |
"$origen"/*.txt en lugar de cd y ls | Desaparece el cd que podía fallar, y los nombres con espacios no se parten. |
[[ -e ]] || continue | Si no hay archivos .txt, no intenta copiar uno llamado *.txt. |
cp -- | Un archivo cuyo nombre empiece con guion no se confunde con una opción. |
mkdir -p | El destino existe aunque sea la primera ejecución. |
printf y nombres descriptivos | Mensaje final fiable y código que se lee sin esfuerzo. |
El mensaje final cambió de significado: la versión A contaba los archivos que había en el destino, incluidos los de copias anteriores; esta cuenta los que copió en esta ejecución, que es lo que realmente informa sobre lo que hizo. Y shellcheck copiar-textos ya no tiene nada que decir.
16. La guía de estilo del curso
Estas son las pautas que siguen todos los scripts de aquí en adelante. Puedes adoptarlas tal cual o usarlas como punto de partida para la tuya; lo importante es tener una y aplicarla siempre. Si trabajas en equipo, la guía debe ser común. Como referencia más amplia, la Shell Style Guide de Google es una de las más citadas.
| Aspecto | Pauta |
|---|---|
| Primera línea | #!/usr/bin/env bash |
| Cabecera | Propósito y uso, como mínimo. Ejemplo, requisitos y códigos de salida si otros lo usarán. |
| Opciones | set -euo pipefail en la segunda línea de código (tema 23). |
| Estructura | Configuración, funciones, programa principal, en ese orden (tema 2). |
| Variables | Minúsculas con guion bajo; mayúsculas solo para constantes y variables de entorno. |
| Funciones | verbo_objeto() { … }, en minúsculas. |
| Comillas | Toda expansión entre comillas dobles, salvo excepción comentada. |
| Sangría | Dos espacios. then y do en la línea del if o del bucle. |
| Líneas | Hasta 80 caracteres; partir con \ al final. |
| Construcciones | $( ), [[ ]], (( )), printf para datos. |
| Errores | A la salida de errores, con el nombre del script y los datos concretos. Código de salida distinto de cero. |
| Comentarios | El porqué, no el qué. Sin código comentado. |
| Revisión | Sin avisos de ShellCheck antes de dar el script por terminado. |
17. Resumen
- Un script se lee muchas más veces de las que se escribe. Un código claro esconde menos errores, y los que quedan se ven.
- La cabecera dice qué hace el script y cómo se usa; si otros lo usarán, también un ejemplo, los requisitos y los códigos de salida.
- Los comentarios útiles explican el porqué. Los que repiten el código sobran, y el código comentado se borra.
- Las variables propias van en minúsculas, para no chocar con las del sistema; asignar
PATHpor error deja al script sin comandos. - Una variable seguida de letras, números o guiones bajos se delimita con llaves:
"${nombre}_2026". - Las funciones se nombran con un verbo; los scripts, en minúsculas con guiones.
- Sangría de dos espacios, un comando por línea, líneas de unos 80 caracteres, y
\como último carácter para partirlas. - Se prefieren
$( ),[[ ]],$(( ))y"$@"a sus equivalentes antiguos. printf '%s\n' "$dato"muestra cualquier contenido de forma fiable;echoqueda para mensajes fijos.- Los errores van a la salida de errores, dicen quién habla y qué pasó, y el script termina con código distinto de cero.
bash -nrevisa la sintaxis sin ejecutar nada.- ShellCheck detecta los errores comunes antes de ejecutar. Cada aviso tiene un código con una página explicativa, y se puede desactivar con una directiva justificada.
shfmtda formato automático; ShellCheck revisa el contenido. Ninguno cambia lo que hace el script.
18. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Por qué el estilo de un script no es solo una cuestión estética?
Porque un código claro deja ver sus errores, y un código desordenado los esconde. Además, el script se lee muchas veces para corregirlo o modificarlo, y cada lectura cuesta más si está mal escrito.
2. ¿Qué debería contener, como mínimo, la cabecera de un script?
Una línea con su propósito y la forma de usarlo. Si lo usarán otras personas, también un ejemplo, los requisitos y el significado de sus códigos de salida.
3. ¿Por qué # incrementa el contador es un mal comentario?
Porque repite lo que el código ya dice. No aporta información, hay que leerlo, y si el código cambia puede quedar desactualizado. Un buen comentario explica por qué se hace algo, cuando eso no es evidente.
4. ¿Qué ocurre si un script asigna PATH="$1"?
Reemplaza la lista de directorios donde Bash busca los comandos. A partir de ahí, comandos como ls o cp dan command not found. Por eso las variables propias se escriben en minúsculas.
5. Con nombre=ventas, ¿qué muestra echo "$nombre_2026.csv", y cómo se corrige?
Muestra .csv, porque Bash busca una variable llamada nombre_2026, que no existe. Se corrige delimitando el nombre con llaves: "${nombre}_2026.csv".
6. ¿Qué pasa si después de la \ que parte una línea queda un espacio?
La barra protege ese espacio en lugar del salto de línea. El comando termina en esa línea, y la siguiente se ejecuta como un comando separado, normalmente con un error.
7. ¿Por qué se prefiere $(comando) a `comando`?
Porque se puede anidar sin escapar nada, se distingue fácilmente de las comillas simples, y su contenido se escribe igual que fuera de ella.
8. ¿Qué muestra echo "$v" si v contiene -n? ¿Cómo se muestra de forma fiable?
No muestra nada, porque echo toma -n como su opción para no agregar el salto de línea. Con printf '%s\n' "$v" se muestra siempre el contenido, sea cual sea.
9. ¿Por qué los mensajes de error deben ir a la salida de errores?
Para que no se mezclen con los datos cuando la salida del script se redirige a un archivo o a otro comando. Así, quien procesa la salida recibe solo datos, y los errores siguen viéndose en pantalla o en el registro.
10. ¿Qué hace bash -n script.sh, y qué no puede detectar?
Comprueba la sintaxis del script sin ejecutarlo: bloques sin cerrar, comillas sin pareja, palabras fuera de lugar. No detecta errores de lógica ni construcciones frágiles, como variables sin comillas, porque esas líneas son sintácticamente correctas.
11. ¿Qué significa el aviso SC2086, y cómo se corrige?
Que una expansión de variable está sin comillas y puede dividirse en palabras o expandirse como comodín. Se corrige poniéndola entre comillas dobles: "$var".
12. ¿Qué error de programación delatan los avisos SC2034 y SC2154?
Casi siempre, un nombre de variable mal escrito: se asigna una y se usa otra parecida. Bash no avisa, porque una variable inexistente se expande como cadena vacía; ShellCheck sí.
13. ¿Cómo se desactiva un aviso de ShellCheck en una línea concreta, y qué conviene agregar?
Con un comentario # shellcheck disable=SC2086 (con el código correspondiente) justo antes de la línea. Conviene agregar siempre un comentario que explique por qué la excepción es correcta.
14. ¿En qué se diferencian shfmt y ShellCheck?
shfmt solo cambia la forma: sangría, espacios, saltos de línea. ShellCheck analiza el contenido y señala errores y construcciones frágiles. Ninguno de los dos cambia lo que hace el script.
15. ¿Para qué sirve el -- en cp -- "$archivo" "$destino/"?
Indica a cp que lo que sigue ya no son opciones. Así, un archivo cuyo nombre empiece con guion, como -r.txt, se trata como archivo y no como opción.
Para practicar
- Instala ShellCheck. Si no puedes, usa la versión en línea de
shellcheck.net. - Guarda la versión A de la sección 1 como
antes.shy pásala porshellcheck. Abre la página de al menos tres de los códigos que aparezcan y léela. - Pasa por
shellchecklos scripts que escribiste en los temas 2 y 3. Corrige todos los avisos. - Escribe una variable mal a propósito en un script (asigna
destinoy usa$destion). Ejecuta el script y luegoshellcheck. ¿Quién lo detecta? - Borra el
fide algún script y ejecutabash -n. ¿Qué número de línea informa? - Compara
echo "$x"yprintf '%s\n' "$x"conxigual a-e,-nyhola\tmundo. - Agrega la función
errorde la sección 9 al scriptnuevo-scriptdel tema 2, para que la incluya en cada script que genere. - Si usas Visual Studio Code u otro editor, instala la extensión de ShellCheck y comprueba que subraya los avisos mientras escribes.