Bash Scripting: Escrevendo Scripts Robustos e Fáceis de Manter
Guia completo de scripting Bash: variáveis, quoting, set -euo pipefail, funções, getopts, trap, arrays e shellcheck para scripts confiáveis.
InSkillCoach
Bash Scripting: Escrevendo Scripts Robustos e Fáceis de Manter
Qualquer pessoa consegue escrever um script Bash que funciona uma vez, na sua máquina, num dia de sol. Escrever um script que sobrevive a espaços em nomes de arquivos, a comandos que falham silenciosamente e a interrupções no meio de um processamento é outra história. Neste guia, revisamos as técnicas que separam um script descartável de um script de produção: o modo estrito, o quoting rigoroso, as funções, o tratamento de argumentos, a limpeza com trap e a validação com shellcheck. Tudo isso termina com um script de backup completo e comentado.
1. Shebang e Execução
A primeira linha importa
O shebang indica ao sistema qual interpretador usar. Duas formas coexistem:
#!/bin/bash
# Caminho direto : simples, funciona em quase todas as distribuições
#!/usr/bin/env bash
# Procura o bash no PATH : preferível para portabilidade (macOS, Nix, etc.)
Não use #!/bin/sh se o seu script contém construções específicas do Bash ([[ ]], arrays, local): no Debian e no Ubuntu, /bin/sh aponta para o dash, que não as entende.
Tornar o script executável
# Dar permissão de execução
chmod +x mon_script.sh
# Executar com o caminho relativo
./mon_script.sh
# Executar sem permissão de execução (passando pelo interpretador)
bash mon_script.sh
# Verificar a sintaxe sem executar
bash -n mon_script.sh
# Executar em modo trace para depurar (exibe cada comando)
bash -x mon_script.sh
2. O Modo Estrito: set -euo pipefail
Esta é a primeira linha a escrever depois do shebang de qualquer script sério:
#!/usr/bin/env bash
set -euo pipefail
Detalhe de cada opção:
| Opção | Efeito | Sem ela |
|---|---|---|
set -e | Interrompe o script no primeiro comando que falha | O script continua após um erro e agrava os danos |
set -u | Erro se uma variável não definida for usada | Um erro de digitação resulta em uma string vazia silenciosa |
set -o pipefail | Um pipeline falha se algum de seus comandos falhar | commande_cassee | grep x tem sucesso se o grep tiver sucesso |
Exemplo concreto do perigo sem set -u:
# CATÁSTROFE potencial : se REPERTOIRE não estiver definido,
# esta linha vira "rm -rf /" com certos valores
rm -rf "${REPERTOIRE}/cache"
# Com set -u, o script para imediatamente com :
# linha N: REPERTOIRE: variável não associada
Quando um comando tem permissão para falhar, diga isso explicitamente:
# Permitir a falha de um comando específico apesar do set -e
grep "motif" fichier.log || true
# Ou capturar o código de retorno
if ! systemctl is-active --quiet nginx; then
echo "nginx est arrêté"
fi
3. Variáveis e Quoting
Declarar e usar variáveis
# Atribuição : nenhum espaço ao redor do sinal de igual
nom="serveur-web-01"
# Sempre coloque as variáveis entre aspas duplas
echo "Machine : ${nom}"
# Substituição de comando : prefira $() aos backticks
date_jour=$(date +%Y-%m-%d)
# Valor padrão se a variável estiver vazia ou ausente
port="${PORT:-8080}"
# Erro explícito se a variável estiver ausente
fichier="${1:?Usage : $0 <fichier>}"
# Variáveis somente leitura para constantes
readonly VERSION="2.1.0"
O quoting: a fonte do bug número um
Uma variável sem aspas sofre word splitting e expansão de curingas (globbing). É a causa da maioria dos bugs em Bash.
fichier="rapport final.pdf"
# ERRADO : o Bash enxerga dois argumentos, "rapport" e "final.pdf"
cp $fichier /backup/
# CORRETO : um único argumento, qualquer que seja o conteúdo
cp "$fichier" /backup/
# ERRADO : se o diretório contiver *.txt, o curinga é expandido
message="Fichiers *.txt trouvés"
echo $message
# CORRETO
echo "$message"
Regra simples: coloque sempre suas variáveis entre aspas ("$var"), a menos que você saiba exatamente por que não está fazendo isso.
4. Testes com [[ ]]
O par duplo de colchetes [[ ]] é específico do Bash e mais seguro que [ ]: sem word splitting em seu interior, operadores lógicos legíveis e correspondência de padrões integrada.
# Testes em strings
if [[ -z "$variable" ]]; then echo "vide"; fi
if [[ -n "$variable" ]]; then echo "non vide"; fi
if [[ "$reponse" == "oui" ]]; then echo "confirmé"; fi
# Correspondência de padrão (glob)
if [[ "$fichier" == *.log ]]; then echo "fichier de log"; fi
# Expressão regular
if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "ressemble à une adresse IPv4"
fi
# Testes em arquivos
if [[ -f "/etc/nginx/nginx.conf" ]]; then echo "fichier existe"; fi
if [[ -d "/var/www" ]]; then echo "répertoire existe"; fi
if [[ -r "$fichier" && -w "$fichier" ]]; then echo "lecture et écriture"; fi
# Comparações numéricas (ou use (( )) para aritmética)
if [[ "$nombre" -gt 100 ]]; then echo "supérieur à 100"; fi
if (( nombre > 100 )); then echo "idem, syntaxe arithmétique"; fi
5. Laços
# Laço sobre uma lista explícita
for service in nginx postgresql redis; do
systemctl status "$service" --no-pager
done
# Laço sobre arquivos : o glob lida corretamente com espaços
for fichier in /var/log/*.log; do
echo "Traitement de $fichier"
done
# Laço numérico estilo C
for (( i = 1; i <= 5; i++ )); do
echo "Tentative $i"
done
# Laço while com contador
tentatives=0
while (( tentatives < 3 )); do
(( tentatives++ )) || true
echo "Essai numéro $tentatives"
done
# Laço until : esperar até que um serviço responda
until curl -sf http://localhost:8080/health > /dev/null; do
echo "En attente du service..."
sleep 2
done
Nunca itere sobre a saída de ls: use globs ou find com -print0.
6. Funções
# Definição : variáveis locais obrigatórias para evitar efeitos colaterais
log() {
local niveau="$1"
local message="$2"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${niveau}] ${message}" >&2
}
# Função com valor de retorno via stdout
espace_libre_mo() {
local point_montage="$1"
df --output=avail -m "$point_montage" | tail -1 | tr -d ' '
}
# Utilização
log "INFO" "Démarrage du traitement"
libre=$(espace_libre_mo /backup)
if (( libre < 1024 )); then
log "ERREUR" "Moins de 1 Go disponible sur /backup"
exit 1
fi
Duas regras de ouro: declare todas as variáveis de função com local e escreva as mensagens de diagnóstico no stderr (>&2) para não poluir os valores retornados no stdout.
7. Argumentos e getopts
As variáveis especiais
echo "Nom du script : $0"
echo "Premier argument : $1"
echo "Nombre d'arguments : $#"
echo "Tous les arguments : $*"
# "$@" preserva os argumentos individuais (sempre prefira esta forma)
for arg in "$@"; do
echo "Argument : $arg"
done
Analisar opções com getopts
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage : $0 [-v] [-o fichier_sortie] -s source" >&2
exit 1
}
verbeux=false
sortie="/tmp/resultat.txt"
source_dir=""
while getopts ":vo:s:" option; do
case "$option" in
v) verbeux=true ;;
o) sortie="$OPTARG" ;;
s) source_dir="$OPTARG" ;;
:) echo "L'option -$OPTARG requiert un argument" >&2; usage ;;
\?) echo "Option inconnue : -$OPTARG" >&2; usage ;;
esac
done
shift $((OPTIND - 1))
# Validar os argumentos obrigatórios
[[ -n "$source_dir" ]] || usage
"$verbeux" && echo "Source : $source_dir, sortie : $sortie"
8. trap e Limpeza
Um script pode ser interrompido a qualquer momento (Ctrl+C, kill, erro com set -e). O trap garante que a limpeza aconteça aconteça o que acontecer.
#!/usr/bin/env bash
set -euo pipefail
# Criar um diretório de trabalho temporário
travail=$(mktemp -d)
# A função de limpeza é executada na saída do script,
# seja ela normal, causada por um erro ou por uma interrupção
nettoyage() {
local code=$?
rm -rf "$travail"
echo "Nettoyage effectué (code de sortie : $code)" >&2
}
trap nettoyage EXIT
# Exibir a linha com falha em caso de erro : valioso para depuração
trap 'echo "Erreur à la ligne $LINENO" >&2' ERR
# O restante do script pode usar $travail com tranquilidade
cp /etc/hosts "$travail/"
O trap em EXIT cobre quase todos os casos: saída normal, exit explícito, erro com set -e e sinais SIGINT/SIGTERM (o Bash executa o trap EXIT após receber o sinal).
9. Arrays
# Array indexado
serveurs=("web-01" "web-02" "db-01")
# Adicionar um elemento
serveurs+=("cache-01")
# Número de elementos
echo "Nombre de serveurs : ${#serveurs[@]}"
# Iterar : sempre "${tableau[@]}" com aspas
for serveur in "${serveurs[@]}"; do
echo "Vérification de $serveur"
done
# Array associativo (Bash 4 e superior)
declare -A ports
ports[http]=80
ports[https]=443
ports[ssh]=22
for service in "${!ports[@]}"; do
echo "$service écoute sur le port ${ports[$service]}"
done
10. Leitura de Arquivos
O método canônico para ler um arquivo linha por linha:
# IFS= preserva os espaços no início/fim da linha
# -r impede a interpretação das barras invertidas
while IFS= read -r ligne; do
echo "Lu : $ligne"
done < /etc/hostname
# Ler um arquivo CSV separando os campos
while IFS=, read -r nom ip role; do
echo "Serveur $nom ($ip) : rôle $role"
done < serveurs.csv
# Carregar um arquivo inteiro em um array
mapfile -t lignes < /etc/hosts
echo "Le fichier contient ${#lignes[@]} lignes"
Evite a armadilha clássica cat fichier | while read: o pipe cria um subshell e as variáveis modificadas dentro do laço são perdidas ao sair dele.
11. shellcheck: seu revisor automático
O shellcheck analisa seus scripts e detecta os erros clássicos: variáveis sem aspas, comparações duvidosas, construções não portáveis.
# Instalação
sudo apt install shellcheck # Debian/Ubuntu
sudo dnf install ShellCheck # Fedora/Rocky
# Analisar um script
shellcheck mon_script.sh
# Ignorar uma regra específica em uma linha (com justificativa em comentário)
# shellcheck disable=SC2086 # word splitting intencional das opções
commande $options
Integre o shellcheck à sua CI: um script que não passa no shellcheck não deveria ser mesclado.
12. Exemplo Completo: Script de Backup
Este script reúne todas as técnicas vistas anteriormente.
#!/usr/bin/env bash
#
# backup.sh — Backup compactado de um diretório com rotação.
# Uso : backup.sh [-k numero_de_copias] -s /diretorio/origem -d /diretorio/destino
#
set -euo pipefail
readonly SCRIPT_NAME="${0##*/}"
# --- Funções ---------------------------------------------------------------
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
}
usage() {
echo "Usage : $SCRIPT_NAME [-k copies] -s source -d destination" >&2
exit 1
}
nettoyage() {
# Remover o arquivo parcial se o script for interrompido
if [[ -n "${archive_temp:-}" && -f "$archive_temp" ]]; then
rm -f "$archive_temp"
log "Archive partielle supprimée : $archive_temp"
fi
}
trap nettoyage EXIT
# --- Análise dos argumentos ------------------------------------------------
copies=7
source_dir=""
dest_dir=""
while getopts ":k:s:d:" option; do
case "$option" in
k) copies="$OPTARG" ;;
s) source_dir="$OPTARG" ;;
d) dest_dir="$OPTARG" ;;
*) usage ;;
esac
done
[[ -n "$source_dir" && -n "$dest_dir" ]] || usage
[[ -d "$source_dir" ]] || { log "ERREUR : source introuvable : $source_dir"; exit 1; }
mkdir -p "$dest_dir"
# --- Backup ----------------------------------------------------------------
horodatage=$(date +%Y%m%d-%H%M%S)
archive="${dest_dir}/backup-${horodatage}.tar.gz"
archive_temp="${archive}.part"
log "Sauvegarde de $source_dir vers $archive"
# Escrever em um arquivo temporário e depois renomear : o arquivo final
# só existe se estiver completo
tar -czf "$archive_temp" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"
mv "$archive_temp" "$archive"
archive_temp=""
log "Archive créée : $(du -h "$archive" | cut -f1)"
# --- Rotação : manter os N arquivos mais recentes ---------------------------
mapfile -t anciennes < <(ls -1t "${dest_dir}"/backup-*.tar.gz 2>/dev/null | tail -n +"$((copies + 1))")
for ancienne in "${anciennes[@]}"; do
rm -f "$ancienne"
log "Ancienne archive supprimée : $ancienne"
done
log "Terminé : $(ls -1 "${dest_dir}"/backup-*.tar.gz | wc -l) archive(s) conservée(s)"
Boas práticas
- Comece cada script com
set -euo pipefaile justifique cada exceção com|| true. - Use aspas sistematicamente em suas variáveis:
"$var","${tableau[@]}","$@". - Declare as variáveis de função com
locale as constantes comreadonly. - Escreva os logs no stderr para manter o stdout disponível para os dados.
- Use
mktemppara arquivos temporários e umtrap EXITpara limpá-los. - Escreva em um arquivo temporário e depois renomeie com
mv: uma operação atômica evita arquivos escritos pela metade. - Passe cada script pelo shellcheck antes de fazer o deploy.
- Se um script passar de algumas centenas de linhas ou manipular estruturas de dados complexas, considere Python: o Bash se destaca na orquestração de comandos, não na lógica de negócio.
Conclusão
A robustez de um script Bash não vem do seu tamanho, mas da sua disciplina: o modo estrito que transforma erros silenciosos em paradas claras, o quoting que neutraliza nomes de arquivos problemáticos, o trap que garante a limpeza e o shellcheck que revisa cada linha no seu lugar. Esses hábitos custam alguns segundos na escrita e economizam horas de depuração em produção.
Reaproveite o script de backup da seção 12 como esqueleto para suas próprias ferramentas: a estrutura de logging, argumentos, trap e operações atômicas se adapta a quase todos os scripts de administração.
Sobre InSkillCoach
Especialista em treinamento e tecnologias
Coach especializado em tecnologias avançadas e IA, apoiado pela GNeurone Inc.
Certificações:
- AWS Certified Solutions Architect – Professional
- Certificações Google Cloud
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
Comentários
Os comentários são alimentados pelo GitHub Discussions
Conecte-se com o GitHub para participar da discussão