Linting de Bash
Integración de Shellcheck y shfmt para garantizar la calidad de scripts bash.
Shellcheck
Instalación
# Debian/Ubuntu
apt install shellcheck
# macOS
brew install shellcheck
# Desde fuente
cabal update && cabal install ShellCheck
Uso básico
# Verificar un solo archivo
shellcheck script.sh
# Verificar múltiples archivos
shellcheck *.sh
# Con dialecto de shell específico
shellcheck --shell=bash script.sh
shellcheck --shell=sh script.sh
# Excluir reglas específicas
shellcheck --exclude=SC2086 script.sh
shellcheck --exclude=SC2086,SC2046 script.sh
# Formatos de salida
shellcheck --format=gcc script.sh # Estilo GCC
shellcheck --format=json script.sh # JSON para herramientas
shellcheck --format=diff script.sh # Diff unificado
Códigos comunes de Shellcheck
| Código | Problema | Solución |
|---|---|---|
| SC2086 | Usar comillas dobles para evitar división | "$var" |
| SC2046 | Poner entre comillas la sustitución de comando | "$(cmd)" |
| SC2006 | Usar $() en lugar de acentos graves | $(cmd) |
| SC2034 | Variable parece no usarse | Eliminar o exportar |
| SC2155 | Declarar y asignar por separado | Separar local var; var=$(...) |
| SC2164 | Usar `cd ... | |
| SC2181 | Verificar estado de salida directamente | if cmd; then |
| SC2129 | Considerar agrupar escrituras | Usar { } > archivo |
| SC1090 | No se puede seguir archivo fuente | Usar # shellcheck source=ruta |
| SC2154 | Variable referenciada pero no asignada | Inicializar o declarar |
Directivas de Shellcheck
# Deshabilitar para la siguiente línea
# shellcheck disable=SC2086
echo $var_sin_comillas
# Deshabilitar para todo el archivo (al inicio)
# shellcheck disable=SC2086,SC2046
# Especificar archivo fuente para sourcing
# shellcheck source=./lib/functions.sh
source "$SCRIPT_DIR/lib/functions.sh"
# Especificar dialecto de shell
# shellcheck shell=bash
# Deshabilitar para bloque (no soportado - usar por línea)
Patrones de directivas inline
# Deshabilitar advertencia específica con explicación
# shellcheck disable=SC2034 # Variable usada por script source
readonly CONFIG_VERSION="1.0"
# Deshabilitar múltiples códigos
# shellcheck disable=SC2086,SC2046
resultado=$(echo $var)
# Directiva source para rutas dinámicas
# shellcheck source=/dev/null
source "${RUTA_DINAMICA}/config.sh"
shfmt
Instalación
# macOS
brew install shfmt
# Instalación con Go
go install mvdan.cc/sh/v3/cmd/shfmt@latest
# Snap
snap install shfmt
# Descarga binaria
# Desde https://github.com/mvdan/sh/releases
Uso básico
# Formatear y mostrar en stdout
shfmt script.sh
# Formatear en el lugar
shfmt -w script.sh
# Verificar formato (código de salida 1 si no está formateado)
shfmt -d script.sh
# Directorio recursivo
shfmt -w .
shfmt -w scripts/
Opciones de formato
# Indentación
shfmt -i 2 script.sh # Indentación de 2 espacios
shfmt -i 4 script.sh # Indentación de 4 espacios
shfmt -i 0 script.sh # Tabuladores (por defecto)
# Operadores binarios al inicio de línea
shfmt -bn script.sh
# Casos switch indentados
shfmt -ci script.sh
# Operadores de redirección seguidos de espacio
shfmt -sr script.sh
# Mantener alineación de columnas
shfmt -kp script.sh
# Llave de apertura de función en línea separada
shfmt -fn script.sh
# Combinado
shfmt -i 4 -ci -bn script.sh
Configuración (.editorconfig)
# .editorconfig
[*.sh]
indent_style = space
indent_size = 4
shell_variant = bash
binary_next_line = true
switch_case_indent = true
space_redirects = true
Ejemplos de transformación
Antes de shfmt:
if [ -f "$archivo" ];then
echo "existe"
fi
for i in 1 2 3;do
procesar $i
done
Después de shfmt -i 4 -ci:
if [ -f "$archivo" ]; then
echo "existe"
fi
for i in 1 2 3; do
procesar $i
done
Integración con Pre-commit
.pre-commit-config.yaml
repos:
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.9.0
hooks:
- id: shellcheck
args: ["--severity=warning"]
- repo: https://github.com/scop/pre-commit-shfmt
rev: v3.7.0-1
hooks:
- id: shfmt
args: ["-i", "4", "-ci", "-w"]
# Alternativa: ganchos locales
- repo: local
hooks:
- id: shellcheck
name: shellcheck
entry: shellcheck
language: system
types: [shell]
args: ["--severity=warning", "-x"]
- id: shfmt
name: shfmt
entry: shfmt
language: system
types: [shell]
args: ["-i", "4", "-ci", "-w"]
Ejecutar Pre-commit
# Instalar ganchos
pre-commit install # o: prek install
# Ejecutar en todos los archivos
pre-commit run --all-files # o: prek run --all-files
# Ejecutar gancho específico
pre-commit run shellcheck --all-files
pre-commit run shfmt --all-files
# Ejecutar en archivos específicos
pre-commit run --files script.sh
Integración con CI/CD
GitHub Actions
name: Lint Shell
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Ejecutar ShellCheck
uses: ludeeus/action-shellcheck@master
with:
severity: warning
- name: Verificar formato con shfmt
uses: mvdan/github-action-shfmt@master
with:
flags: -d -i 4 -ci
GitLab CI
shellcheck:
image: koalaman/shellcheck-alpine:stable
script:
- find . -name "*.sh" -exec shellcheck {} +
shfmt:
image: mvdan/shfmt:latest
script:
- shfmt -d -i 4 -ci .
Solución de problemas comunes
SC2086: Usar comillas para evitar división
# Malo
echo $var
# Bueno
echo "$var"
printf '%s\n' "$var"
SC2155: Declarar y asignar por separado
# Malo - oculta el estado de salida
local var=$(comando)
# Bueno
local var
var=$(comando)
SC2164: Usar cd || exit
# Malo
cd "$dir"
rm -rf *
# Bueno
cd "$dir" || exit 1
rm -rf *
# O con subshell
(cd "$dir" && rm -rf *)
SC2181: Verificar salida directamente
# Malo
comando
if [ $? -eq 0 ]; then
# Bueno
if comando; then
SC1090/SC1091: Problemas de source
# Agregar directiva para source dinámico
# shellcheck source=/dev/null
source "$RUTA_DINAMICA/lib.sh"
# O especificar ruta real
# shellcheck source=./lib/functions.sh
source "$SCRIPT_DIR/lib/functions.sh"
Integración con editores
VS Code
Instalar la extensión "ShellCheck" de Timon Wong.
// settings.json
{
"shellcheck.enable": true,
"shellcheck.run": "onSave",
"shellcheck.executablePath": "shellcheck",
"editor.formatOnSave": true,
"[shellscript]": {
"editor.defaultFormatter": "foxundermoon.shell-format"
}
}
Vim/Neovim
" Con ALE
let g:ale_linters = {'sh': ['shellcheck']}
let g:ale_fixers = {'sh': ['shfmt']}
let g:ale_sh_shfmt_options = '-i 4 -ci'
" Con coc.nvim
" Instalar extensión coc-sh
Buenas prácticas
- Ejecutar shellcheck temprano - integrar en editor y CI
- Corregir problemas, no suprimirlos - solo deshabilitar con buena razón
- Documentar supresiones - explicar por qué se deshabilita la regla
- Usar niveles de severidad -
--severity=warningpara CI - Formateo consistente - usar shfmt en pre-commit
- Versiones fijas de herramientas - fijar versiones en CI/pre-commit