Armadilhas Comuns de Scripting em Bash e Como Evitá-las

Evite bugs comuns em scripts Bash com tratamento de erros mais seguro, uso de aspas, arrays, traps e análise de argumentos.

Armadilhas Comuns em Scripts Bash e Como Evitá-las

Armadilhas em scripts Bash geralmente aparecem quando seu script encontra nomes de arquivos reais, variáveis ausentes, comandos falhos ou entrada inesperada. Um script que funciona no seu laptop pode quebrar em CI ou produção se depender de padrões frágeis.

Você não precisa tornar cada script de shell complicado. Você precisa usar aspas em expansões, verificar falhas intencionalmente e testar com nomes que contenham espaços.

Defina Padrões Mais Seguros com Cuidado

Muitos scripts começam com:

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

Isso é uma boa base para muitos scripts de automação, mas cada opção tem arestas cortantes:

  • set -e sai quando um comando simples falha, exceto em lugares como testes if, partes de listas && e ||, e algumas substituições de comando.
  • set -u sai quando você expande uma variável não definida.
  • set -o pipefail faz um pipeline falhar se qualquer comando no pipeline falhar, não apenas o último comando.

Use essas opções quando a falha precoce for mais segura do que continuar. Para comandos onde a falha é esperada, trate o status explicitamente.

if ! grep -q "ready" status.txt; then
  echo "o serviço ainda não está pronto"
  exit 1
fi

Use Aspas em Expansões de Variáveis

Variáveis sem aspas são o bug mais comum em Bash. O Bash realiza divisão de palavras e expansão de glob em expansões sem aspas, então um caminho como release notes/*.txt pode se tornar vários argumentos ou corresponder a arquivos que você não pretendia.

file="release notes.txt"

# Ruim: quebra porque o valor é dividido em duas palavras.
rm $file

# Bom: passa um argumento exato.
rm -- "$file"

Use -- antes de nomes de arquivos controlados pelo usuário quando um comando suportar isso. Isso impede que um nome de arquivo como -rf seja interpretado como uma opção.

Use Arrays para Listas de Argumentos

Não armazene um comando com argumentos em uma única string e depois execute-o. O uso de aspas se torna frágil rapidamente.

# Ruim
flags="-a --exclude node_modules"
rsync $flags "$src" "$dest"

# Bom
flags=(-a --exclude "node_modules")
rsync "${flags[@]}" "$src" "$dest"

Arrays preservam os limites dos argumentos. Isso é importante quando um argumento contém espaços, caracteres curinga ou valores que começam com um traço.

Prefira $(...) em Vez de Crases

Crases são difíceis de aninhar e fáceis de ler errado. Use $(...) para substituição de comando.

current_branch="$(git rev-parse --abbrev-ref HEAD)"
echo "construindo branch: $current_branch"

Mantenha as substituições de comando entre aspas, a menos que você queira deliberadamente a divisão de palavras.

Leia Arquivos Sem Perder Dados

Este padrão parece inofensivo, mas quebra em espaços e pode distorcer barras invertidas:

for line in $(cat hosts.txt); do
  echo "$line"
done

Leia arquivos com while IFS= read -r.

while IFS= read -r host; do
  echo "verificando $host"
done < hosts.txt

IFS= preserva espaços em branco no início e no final. -r impede que escapes de barra invertida sejam interpretados.

Lide com Arquivos Temporários Usando mktemp e trap

Caminhos temporários codificados podem colidir com outro processo ou deixar arquivos obsoletos para trás. Crie um caminho único e limpe-o ao sair.

tmp_file="$(mktemp)"
cleanup() {
  rm -f "$tmp_file"
}
trap cleanup EXIT

printf '%s\n' "dados de trabalho" > "$tmp_file"

Para diretórios, use mktemp -d e remova o diretório na sua função de limpeza.

Analise Opções com getopts

A análise manual de argumentos frequentemente perde casos extremos. Para opções curtas, o getopts embutido do Bash geralmente é suficiente.

verbose=false
output=""

while getopts ":vo:" opt; do
  case "$opt" in
    v) verbose=true ;;
    o) output="$OPTARG" ;;
    :)
      echo "A opção -$OPTARG requer um argumento" >&2
      exit 2
      ;;
    \?)
      echo "Opção desconhecida: -$OPTARG" >&2
      exit 2
      ;;
  esac
done
shift "$((OPTIND - 1))"

getopts lida com flags curtas como -v e -o arquivo. Se seu script precisar de opções longas como --output, escreva um analisador cuidadoso ou use uma linguagem com uma biblioteca de análise de argumentos mais robusta.

Verifique Comandos que Podem Falhar

Não presuma que um comando funcionou porque ele imprimiu algo. Verifique operações importantes antes de usar sua saída.

if ! archive="$(tar -czf app.tar.gz app 2>&1)"; then
  echo "falha no arquivo: $archive" >&2
  exit 1
fi

Para pipelines, ative pipefail quando uma falha no meio deve falhar todo o pipeline.

set -o pipefail
journalctl -u api.service | grep -i "error"

Sem pipefail, o status do pipeline normalmente vem do último comando.

Evite Bash Quando a Portabilidade For Importante

Se seu script usa arrays, [[ ... ]], mapfile ou pipefail, é um script Bash. Comece com:

#!/usr/bin/env bash

Se você precisar de portabilidade POSIX sh, evite recursos exclusivos do Bash e teste com o shell que seu sistema alvo usa. Não escreva um script Bash com #!/bin/sh e espere que ele se comporte da mesma forma em todos os lugares.

Conclusão

A maneira mais rápida de melhorar seus scripts Bash é testá-los com entrada bagunçada: espaços em nomes de arquivos, variáveis ausentes, arquivos vazios e comandos falhos. Use aspas em expansões, use arrays para listas de argumentos, limpe arquivos temporários com trap e torne os caminhos de falha explícitos. Seu eu futuro gastará menos tempo depurando scripts que só funcionavam com entrada perfeita.