0%
Bash Scripting: Escrevendo Scripts Robustos e Fáceis de Manter

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.

I

InSkillCoach

· min

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çãoEfeitoSem ela
set -eInterrompe o script no primeiro comando que falhaO script continua após um erro e agrava os danos
set -uErro se uma variável não definida for usadaUm erro de digitação resulta em uma string vazia silenciosa
set -o pipefailUm pipeline falha se algum de seus comandos falharcommande_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 pipefail e justifique cada exceção com || true.
  • Use aspas sistematicamente em suas variáveis: "$var", "${tableau[@]}", "$@".
  • Declare as variáveis de função com local e as constantes com readonly.
  • Escreva os logs no stderr para manter o stdout disponível para os dados.
  • Use mktemp para arquivos temporários e um trap EXIT para 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.

InSkillCoach

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+
1.5k
155

Comentários

Os comentários são alimentados pelo GitHub Discussions

Conecte-se com o GitHub para participar da discussão

Link copiado!