O Bash é como um canivete suíço: simples, omnipresente e perigoso se não souberes o que estás a fazer. Já perdi horas a debuggar scripts que correram em produção com variáveis vazias. Depois de set -euo pipefail, nunca mais.

Durante anos, os meus scripts Bash eram um monstro: funcionavam no meu Mac, partiam no servidor, e quando algo corria mal, o script continuava feliz como se nada tivesse acontecido. Até que um dia um rm -rf com uma variável vazia quase me custou um servidor inteiro.

Foi aí que percebi que escrever Bash não é só colar comandos. É uma arte de disciplina: error handling, quoting consistente, funções bem definidas, e ferramentas como o shellcheck que te salvam de ti próprio.

Vou mostrar-te:

  • Error handling a sério com set -euo pipefail e trap
  • Debugging como um profissional
  • Subshells, funções e argument parsing
  • Boas práticas: quoting, [[ ]], variáveis com fallback
  • O essencial do shellcheck e profiles

Bash Scripting no terminal

⬆ Script Bash a correr com debugging ativo (set -x) e cores de output


Error handling: o teu salva-vidas

Por omissão, o Bash não para quando um comando falha. Isto é aterrador. Se fizeres cd /diretorio-que-nao-existe && rm -rf ., o Bash queixa-se do cd mas faz o rm na mesma no diretório atual. Já viste o perigo?

#!/usr/bin/env bash

# Liga o modo "sai à primeira" — o teu seguro de vida
set -euo pipefail
Copy

Cada letra tem um propósito:

FlagO que fazExemplo
-eSai logo se um comando falhar (exit code ≠ 0)cd /x || exit 1
-uErro ao usar variável não definidaecho $NAO_EXISTO → erro
-o pipefailSe um comando no pipe falhar, o pipe todo falhagrep foo /x | sort → falha se grep falhar

Mas há cenários onde queres ignorar erros específicos. Usa || true para isso:

rm /tmp/lockfile 2>/dev/null || true
# Se o ficheiro não existir, o script continua
Copy
trap: o paraquedas de emergência

O trap permite-te executar código quando o script recebe sinais ou termina. Uso essencial: limpar ficheiros temporários:

cleanup() {
    echo "🧹 A limpar..."
    rm -f /tmp/meu_script_*
}
TEMP_FILE=$(mktemp)
trap cleanup EXIT ERR

# Se algo falhar, a trap corre antes de sair
curl -o "$TEMP_FILE" https://exemplo.com/dados.csv
Copy

Dica: Usa trap 'comando' EXIT para garantires que a limpeza corre sempre, mesmo que o script rebente a meio. O trap ERR é utilíssimo para logging de erros.


Debugging: set -x e amigos

Quando um script não faz o que queres, o set -x é o teu melhor amigo. Ele mostra cada comando antes de executar, com expansão de variáveis:

#!/usr/bin/env bash
set -euo pipefail

# Liga debugging apenas numa secção
set -x
USER="joao"
echo "Olá, $USER"
set +x

# Output:
# + USER=joao
# + echo 'Olá, joao'
# Olá, joao
Copy

Outras técnicas de debugging:

  • bash -n script.sh: verifica sintaxe sem executar (linter básico)
  • export SHELLOPTS: passa opções para sub-shells
  • PS4='+ $BASH_SOURCE:$LINENO ': personaliza o output do set -x com ficheiro e linha
# Debugging avançado com contexto
export PS4='+ ${BASH_SOURCE}:${LINENO}: ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x

# Agora cada debug line mostra ficheiro:linha:função.
Copy

Subshells: $() vs ( ) vs { }

Esta é uma das maiores fontes de confusão no Bash. Vamos clarificar:

SintaxeTipoComportamento
$(comando)Substituição de comandoCorre o comando e devolve o output. Não cria subshell pai
(comandos)SubshellCorre comandos num shell filho. Variáveis não afetam o pai
{ comandos; }GroupingCorre no mesmo shell. Mais eficiente. Espaços obrigatórios

Exemplo prático para perceberes a diferença:

x=1

# Subshell — x continua 1 lá fora
(
    x=2
    echo "Dentro do subshell: x=$x"
)
echo "Fora do subshell: x=$x"

# Grouping — x muda
{
    x=3
    echo "Dentro do group: x=$x"
}
echo "Fora do group: x=$x"

# Output:
# Dentro do subshell: x=2
# Fora do subshell: x=1
# Dentro do group: x=3
# Fora do group: x=3
Copy

Atenção: Usa { } em vez de ( ) sempre que não precisares de isolamento. É mais rápido e consome menos memória. Em loops com milhares de iterações, a diferença é notória.


Funções: local, return e argumentos

Funções em Bash são o equivalente a organização. Sem elas, os teus scripts são uma sopa de comandos. Com funções bem desenhadas, são programas.

# Declaração — usa sempre a sintaxe POSIX
log_info() {
    local msg="$1"
    echo "[INFO] $(date '+%H:%M:%S')$msg"
}

validate_env() {
    local var_name="$1"
    if [ -z "${!var_name}" ]; then
        log_info "ERRO: $var_name não está definida"
        return 1
    fi
}

# Return values: 0 = sucesso, 1-255 = erro
if validate_env "DATABASE_URL"; then
    log_info "Ambiente válido"
else
    log_info "A configurar variáveis..."
fi
Copy

Regras de ouro para funções:

  • Sempre local nas variáveis: não poluas o scope global
  • return apenas para código de erro (0-255). Para devolver dados, usa echo ou variáveis globais
  • Argumentos: $1, $2, ... $@ para todos, $# para contagem
  • Valida sempre os argumentos no início da função

Boas práticas essenciais

Ao longo dos anos, cristalizei um conjunto de regras que todo o script Bash devia seguir:

Quoting: sempre que há espaços
# Mau — parte se o filename tiver espaços
for f in $(ls *.jpg); do ... done

# Bom — quoting e glob do shell
for f in *.jpg; do
    echo "A processar: '$f'"
done

# Sempre que usares variáveis: "$VAR" — SEMPRE
rm -rf "$DIRETORIO"
Copy
[[ ]] vs [ ]: a batalha dos tests
[ ] (POSIX)[[ ]] (Bash)
Disponível em qualquer shell POSIXApenas Bash/Zsh/Ksh
Não suporta &&, || dentroSuporta &&, ||, ! naturalmente
Precisa de quoting para variáveis vaziasLida com strings vazias sem problemas
Sem suporte para regex=~ para regex nativo
# [[ ]] é mais seguro e legível
if [ "$USER" = "root" ]; then
if [ "$VAR" != "" ]; then

# vs [[ ]] — mais limpo e com regex
if [[ $USER == root ]]; then
if [[ -n $VAR ]]; then
if [[ $EMAIL =~ ^.+@.+\..+$ ]]; then
Copy
Variáveis com valores por omissão
# Se PORT não estiver definida, usa 8080
PORT="${PORT:-8080}"

# Se PORT não estiver definida, define e usa
PORT="${PORT:=8080}"

# Erro se variável não estiver definida
PORT="${PORT:?ERRO: PORT não definida}"
Copy
Variáveis readonly e constantes
readonly SCRIPT_NAME="$(basename "$0")"
readonly BACKUP_DIR="/var/backups/app"
readonly CONFIG_FILE="/etc/app/config.yml"

# Isto dá erro (bom!)
BACKUP_DIR="/outro/sitio"  # ← Bash reclama
Copy

Ferramentas que te salvam a vida

FerramentaO que faz
shellcheckLinter para Bash: deteta quoting errors, variáveis não usadas, problemas de sintaxe. Instala com brew install shellcheck ou apt install shellcheck
bash -nVerifica sintaxe sem executar: rápido para CI/CD
shfmtFormatador automático de Bash: indenta, alinha, normaliza
# Exemplo de shellcheck em ação
shellcheck meu_script.sh

# Se tiveres erros do tipo:
# In meu_script.sh line 6:
#     rm -rf $DIR
#            ^-- SC2086: Double quote to prevent globbing
#            ^-- SC2115: Use "${DIR:?}" to ensure it never expands to /
Copy

shellcheck a analisar script Bash

⬆ Output do shellcheck a identificar problemas num script: repara nos números de linha e sugestões


.bashrc vs .bash_profile vs .profile

A confusão entre estes ficheiros é lendária. Vamos simplificar:

FicheiroQuando é lido
~/.bash_profileLogin shells (ssh, terminal ao abrir)
~/.bashrcShells interativos não-login (terminal dentro de terminal)
~/.profileFallback se ~/.bash_profile não existir

A dica de ouro: source um pelo outro para não teres duplicação:

# ~/.bash_profile — só carrega o .bashrc
if [ -f ~/.bashrc ]; then
    source ~/.bashrc
fi
Copy

Exemplo completo: script de backup com boas práticas

#!/usr/bin/env bash
# backup.sh — faz backup da base de dados e envia para S3
set -euo pipefail

readonly SCRIPT_NAME="$(basename "$0")"
readonly TIMESTAMP="$(date '+%Y%m%d_%H%M%S')"
readonly BACKUP_DIR="${BACKUP_DIR:-/tmp/backups}"

# ── Logging ──
log_info()  { echo "[$(date '+%H:%M:%S')] INFO:  $*"; }
log_error() { echo "[$(date '+%H:%M:%S')] ERROR: $*" >&2; }

# ── Cleanup trap ──
cleanup() {
    log_info "Limpeza de ficheiros temporários..."
    rm -f "$BACKUP_DIR"/backup_"$TIMESTAMP".sql.gz
}
trap cleanup EXIT

# ── Main ──
main() {
    local db_name="${1:?USO: $SCRIPT_NAME <nome_db>}"

    log_info "A fazer dump da base de dados '$db_name'..."
    mkdir -p "$BACKUP_DIR"

    pg_dump "$db_name" | gzip > "$BACKUP_DIR/backup_$TIMESTAMP.sql.gz"

    log_info "Backup concluído: backup_$TIMESTAMP.sql.gz"
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
    main "$@"
fi
Copy

O shebang correto

Usa sempre #!/usr/bin/env bash em vez de #!/bin/bash. Porquê?

  • env procura o Bash no $PATH do utilizador
  • Funciona em macOS (onde /bin/bash é versão antiga) e em NixOS (onde o Bash não está em /bin)
  • Mais portável entre sistemas

Dica final: O teu script devia começar sempre com o bloco: #!/usr/bin/env bash, set -euo pipefail, e a declaração de readonly constants. O shellcheck devia passar sem warnings. E cada função devia caber no ecrã sem scroll. Segue estas regras e os teus scripts vão durar anos.


Recapitulando

  • set -euo pipefail em todo o lado: error handling não é opcional
  • trap para limpeza e logging de erros
  • $() para substituição, () para subshells, {} para grouping
  • Funções com local, validação de argumentos e return consistente
  • Quoting SEMPRE nas variáveis, [[ ]] em vez de [ ]
  • ${VAR:-default} para valores por omissão
  • shellcheck antes de cada deploy

O Bash é uma linguagem traiçoeira. Mas com disciplina, as ferramentas certas e estas boas práticas, os teus scripts vão ser robustos, legíveis e portáveis.

Experimenta executar o shellcheck num dos teus scripts antigos. Aposto que encontras pelo menos 5 coisas para corrigir.

Recursos adicionais

Comentários (0)

Nenhum comentário ainda. Seja o primeiro!

Deixar comentário