Boas práticas de segurança e robustez para scripts Bash
Importante: este artigo assume Bash moderno (versão 4+ quando aplicável). Se trabalhar com shells restritos (dash, sh) verifique compatibilidade.
Sumário
- Introdução e filosofia
- Shebang: qual escolher e por quê
- Citações e expansão de variáveis
- Modo estrito e controle de erros
- Verificação explícita de falhas e tratamento
- Depuração com xtrace e boas práticas de logging
- Parâmetros longos e legibilidade de comandos
- Substituição de comandos moderna ($())
- Valores padrão e aninhamento
- Duplo hífen (–) para separar opções de argumentos
- Variáveis locais em funções e arrays
- Checklist de qualidade e critérios de aceitação
- Padrões e anti-padrões com exemplos
- Segurança, permissões e hardening
- Ferramentas de validação e testes
- Modelos e snippets práticos
- Resumo final
Introdução e filosofia
Bash é onipresente em sistemas UNIX-like. Com ele fazemos automações, instaladores, rotinas de manutenção e integrações. Por outro lado, erros em scripts podem gerar perda de dados, escalonamento de permissões ou downtime. A filosofia aqui é simples:
- Programar defensivamente: assumir que qualquer entrada pode ser malformada.
- Falhar rápido: detectar e interromper quando algo anômalo ocorre.
- Ser explícito: prefira clareza sobre concisão obscura.
- Testar cedo: validar em ambientes não produtivos.
Definições rápidas:
- Shebang: a primeira linha do script que indica o interpretador (por exemplo, #!/usr/bin/env bash).
- Substituição de comando: executar um comando e capturar sua saída (ex.: VAR=$(ls)).
- Set strict (ou modo estrito): combinação de opções do shell que torne o script mais rígido e previsível.
1. Use uma boa linha shebang
A primeira linha do seu script deve declarar o interpretador. Isso evita comportamentos ambíguos quando alguém executar o arquivo diretamente.
Opções comuns:
- Caminho fixo: #!/bin/bash — garante execução com o bash localizado exatamente em /bin/bash.
- Via env: #!/usr/bin/env bash — resolve o bash a partir do PATH do ambiente do usuário.
Comparação rápida:
| Abordagem | Vantagem | Risco/Contras |
|---|
| #!/bin/bash | Determinístico; garante a versão instalada naquele caminho | Menos portátil em sistemas onde bash fica noutro lugar ou é gerenciado por usuários | #!/usr/bin/env bash | Portátil; respeita PATH do usuário | Se PATH for malicioso, executável errado pode ser usado
Quando escolher cada uma:
- Para scripts de uso pessoal ou em ambientes controlados (servidores com configuração fixa): #!/bin/bash pode ser apropriado.
- Para scripts que serão distribuídos e executados em máquinas variadas (colaboradores, contêineres, diferentes distros): #!/usr/bin/env bash tende a ser mais portátil.
Fluxo de decisão (ajuda visual):
flowchart TD
A[Precisa garantir versão exata do bash?] -->|Sim| B[#/bin/bash]
A -->|Não| C[Está distribuindo para diversos ambientes?]
C -->|Sim| D[#/usr/bin/env bash]
C -->|Não| BNotas de segurança:
- Em ambientes restritos (produção sensível), defina a versão esperada no deploy e audite PATH.
- Considere usar hashes de integridade e assinaturas quando distribuir scripts críticos.
2. Sempre coloque variáveis entre aspas
Espaços e quebras de linha em nomes de ficheiros são uma fonte clássica de bugs. Ao expandir variáveis nunca use a expansão não-quotada, a menos que tenha um motivo explícito.
Exemplo perigoso:
#!/bin/bash
FILENAME="docs/Letter to bank.doc"
ls $FILENAMEA expansão se torna: ls docs/Letter to bank.doc — o shell trata isso como três argumentos.
Correção segura:
ls "$FILENAME"Use chaves quando for concatenar com texto literal:
echo "_${FILENAME}_ é um dos meus ficheiros favoritos"Por que as chaves ajudam? Elas delimitam o nome da variável para evitar ambiguidades, por exemplo ${VAR}s vs $VARs.
Edge cases e arrays:
- Para arrays, cite índices e expansões adequadas: “${array[@]}” preserva elementos.
- Nunca use “${array[*]}” sem entender que ele junta os elementos em uma única string.
3. Pare o script em erro: modo estrito
Uma rede de verificações manuais é boa, mas há um atalho poderoso: ativar opções que fazem o shell interromper ao primeiro erro relevante.
Combinações úteis e frequentemente recomendadas (conhecida como “modo estrito”):
set -euo pipefail
IFS=$'\n\t'Significados:
- set -e: sai imediatamente se qualquer comando retornar status != 0 (com nuances; ver notas abaixo).
- -u: trata variáveis não definidas como erro (nounset).
- -o pipefail: em pipelines, retorna o status do primeiro comando que falhar.
- IFS alterado para newline+tab reduz problemas com campos separados por espaços.
Observações importantes sobre set -e:
- Nem sempre funciona como esperado com comandos em subshells, &&/|| e dentro de testes condicionais. Por isso, ainda é necessário validar retornos quando o fluxo exige.
- Use traps para limpeza quando sair inesperadamente (ex.: remover ficheiros temporários).
Exemplo com cleanup:
#!/usr/bin/env bash
set -euo pipefail
trap 'rc=$?; echo "Saindo com $rc"; cleanup; exit $rc' EXIT
tmpfile=$(mktemp)
# ... usar $tmpfile ...4. Trate falhas específicas e propague erro: pague adiante
Falhar por defeito (fail fast) protege, mas você também deve tratar exceções quando faz sentido.
Exemplo tradicional e mais verboso:
cd "$DIR"
if [ $? -ne 0 ]; then
echo "Falha ao entrar em $DIR" >&2
exit 1
fiFormas idiomáticas e concisas:
cd "$DIR" || { echo "Falha ao entrar em $DIR" >&2; exit 1; }Ou com função de erro:
fail() { echo "$*" >&2; exit 1; }
cd "$DIR" || fail "Não foi possível acessar $DIR"Quando capturar saída e ainda assim seguir:
- Capture e valide: out=$(cmd) || fail “cmd falhou”
- Para comandos que podem falhar, documente os casos admitidos claramente.
5. Depure cada comando com xtrace e logging significante
Ative rastreamento para ver o que o script realmente executa:
set -o xtrace # ou set -xIsso imprime cada linha (após expansão) antes de executar. Útil em CI, logs e troubleshooting.
Boas práticas de logging:
- Use printf em vez de echo quando precisar de previsibilidade de escape.
- Logue para stderr (>&2) as mensagens de erro.
- Forneça prefixos e timestamps quando apropriado: printf ‘%s %s\n’ “$(date -u +%Y-%m-%dT%H:%M:%SZ)” “[ERROR] Mensagem” >&2
Exemplo de debug seletivo:
if [ "${DEBUG:-}" = "1" ]; then
set -x
fi
# ... script ...
set +xImagem de exemplo:

6. Prefira parâmetros longos ao chamar comandos
Opções curtas (-r -f) são ambíguas para leitores ocasionalmente. Em scripts, prefira a forma longa quando disponível.
Exemplo legível:
rm --recursive --force filenameObservações:
- Nem todas as ferramentas têm formas longas. Verifique a –help ou man.
- Para interoperabilidade, garanta compatibilidade ao usar opções longas em ambientes antigos.
7. Use a forma moderna de substituição de comando
Prefira $(…) em vez de ... (crase/backticks). A sintaxe moderna lida melhor com aninhamento e legibilidade.
Exemplo:
VAR=$(ls)
VAR2=$(ls -1)Evite usar backticks — eles são legados e mais propensos a erros.
8. Declare valores padrão de forma concisa
Use a expansão com :- para ter valores padrão sem ifs adicionais.
CMD=${PAGER:-more}
DIR=${1:-${HOME:-/home/usuario}}Diferença entre :- e -:
- ${var:-default}: se var estiver vazio ou unset, usa default.
- ${var-default}: se var for unset, usa default; se estiver definido mas vazio, usa o valor vazio.
Escolha consciente conforme a semântica desejada.
9. Use duplo hífen (–) para separar opções
Ficheiros que começam por - confundem comandos que interpretam opções.
Exemplo perigoso:
# Se existir um ficheiro chamado -rf, este comando pode ser catastrófico:
rm *Proteção simples:
rm -- *.mdTambém pode usar caminhos explícitos (./-nome) para indicar arquivo atual:
rm ./-nomeImagem ilustrativa de criação de ficheiro com hífen e problema:

Erro típico:

10. Use variáveis locais em funções e arrays
Por padrão, variáveis em Bash são globais. Dentro de funções, declare locais para evitar efeitos colaterais.
Exemplo inseguro:
#!/bin/bash
function run {
DIR=$(pwd)
echo "fazendo algo..."
}
DIR="/usr/local/bin"
run
echo $DIRSaída pode mostrar DIR modificado. Correção:
function run {
local DIR=$(pwd)
echo "fazendo algo..."
}Trabalhe com arrays quando precisar de coleções de argumentos:
files=("file one.txt" "file two.txt")
for f in "${files[@]}"; do
printf 'Processando: %s\n' "$f"
done11. Checklist de qualidade para um script robusto
Use esta lista como um SOP mínimo antes de colocar um script em produção.
- Shebang apropriado (#/usr/bin/env bash ou /bin/bash conforme necessidade).
- set -euo pipefail (ou uma variante documentada) e trap para cleanup.
- Todas as expansões de variáveis são citadas: “$VAR”.
- Uso de chaves quando concatenar: “${VAR}text”.
- Não há dependência de IFS padrão sem justificativa.
- Entrada de utilizador lida com read -r para preservar barras e espaços.
- Uso de arrays para listas de ficheiros/argumentos.
- Evitar parsing de ls; usar find, stat ou arrays.
- Testes unitários e de integração (ex.: bats) para caminhos críticos.
- Revisão com shellcheck e uma revisão por pares.
- Logging informativo e mensagens de erro para stderr.
Critérios de aceitação — o script é robusto se atender aos itens acima e passar os testes automatizados.
12. Padrões e anti-padrões (com exemplos)
Anti-padrão: iterar sobre ls para obter ficheiros
for f in $(ls *.txt); do
echo "$f"
doneProblema: espaços em nomes quebram o loop. Correto:
shopt -s nullglob
for f in *.txt; do
printf '%s\n' "$f"
doneOu usando arrays:
mapfile -t files < <(printf '%s\0' *.txt)
# melhor: usar find -print0 + read -d '\0' quando recursivoAnti-padrão: usar echo para strings arbitrárias (problemas com escapes)
Use printf:
printf '%s\n' "Variável com\nquebras"13. Segurança e hardening (práticas essenciais)
- Minimize privilégios: não execute scripts como root a menos que estritamente necessário.
- Evite confiar no PATH — use caminhos absolutos para binários críticos quando segurança for vital.
- Sanitize entradas que acabarão em comandos (onde possível, evite construções via eval ou concatenação insegura).
- Use mktemp para ficheiros temporários e garanta permissões restritas: mktemp -p /tmp myscript.XXXXXX
- Bloqueie o uso de IFS e variáveis externas antes de parsear argumentos.
Exemplo de proteção contra PATH malicioso:
PATH="/bin:/usr/bin"
export PATHIsso reduz risco de execução de binários inesperados durante o deploy.
14. Ferramentas de validação e testes
- shellcheck: análise estática que detecta muitos anti-padrões e bugs comuns.
- bats-core: framework de testes unitários para scripts Bash.
- shfmt: formata código shell para consistência.
- set -x, env DEBUG=1, e logs para debugging em CI.
Teste recomendado antes de merge/produção:
- Teste por pares com shellcheck.
- Execução em ambiente de staging replicando variáveis de ambiente.
- Testes de tolerância a entradas malformadas.
15. Snippets e cheatsheet úteis
Cheat: modo estrito e trap básico
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
trap 'rc=$?; echo "Saindo com $rc" >&2; cleanup || true; exit $rc' EXITLoop robusto para ler linhas (preserva espaços e barras):
while IFS= read -r line; do
printf '%s\n' "$line"
done < input.txtExecução de comando com fallback e logging:
cmd_output=$(somecommand 2>&1) || { echo "somecommand falhou: $cmd_output" >&2; exit 1; }Criar ficheiro temp e limpar automaticamente:
tmpfile=$(mktemp) || exit 1
trap 'rm -f "$tmpfile"' EXITArgument parsing simples com getopts:
while getopts ":f:o:" opt; do
case $opt in
f) file=$OPTARG ;;
o) out=$OPTARG ;;
\?) echo "Opção inválida: -$OPTARG" >&2; exit 1 ;;
esac
done
shift $((OPTIND -1))16. Alternativas e quando Bash NÃO é a melhor escolha
Bash é ótimo para gluing, manipulação de ficheiros e automações simples. No entanto, para tarefas complexas considere:
- Python: melhor para lógica pesada, bibliotecas, manuseio de JSON e testes unitários.
- Go/Rust: para binários com alto desempenho e distribuição independente.
- Make/Ansible: para orquestração e repeatability em infra.
Se o script está crescendo, considere portar para uma linguagem com melhor ergonomia para testes e estruturas.
17. Maturidade e modelo de evolução de scripts
Pequeno roteiro para amadurecimento de um script desde protótipo até produção:
- Protótipo: funcional, mínimo, sem hardening.
- Revisão: adicionar shebang, citações, set -e, comentários.
- Harden: traps, validações, logs, permissões.
- Testes: adicionar testes unitários e integração.
- Produção: empacotar, adicionar CI, policy de deploy.
18. Critérios de aceitação
Um script pode ser considerado pronto para produção se:
- Passa todos os testes automatizados.
- Não gera warnings do shellcheck (nível mais alto quando aplicável).
- Tem documentação básica (uso, flags, pré-requisitos).
- Não executa comandos com privilégios elevados sem confirmação documentada.
- Tem limpeza garantida de recursos temporários.
19. Exemplos de casos reais e contraexemplos quando falha
Caso: parsing de nomes com espaços
- Falha comum: for f in $(ls *.txt); do …; done
- Correção: for f in *.txt; do printf ‘%s\n’ “$f”; done
Caso: uso de set -e com comando dentro de if
- Às vezes set -e pode não interromper o script quando o comando falha dentro de um teste condicional. Portanto, trate explicitamente verificações críticas.
Caso: harness de CI
- Teste seus scripts em múltiplas imagens (alpine vs ubuntu) para capturar diferenças de sed, grep e utilitários coreutils. Em Alpine, BusyBox pode não suportar as mesmas opções; prefira POSIX ou verifique disponibilidade.
20. Compatibilidade e migração
- Para máxima compatibilidade entre shells, mantenha-se dentro de POSIX sh quando possível.
- Recursos do Bash moderno (arrays, process substitution <(), extglob) não funcionam em dash/sh.
- Se precisar de compatibilidade, documente claramente a versão mínima requerida do Bash.
Resumo
- Use shebangs conscientes (#/usr/bin/env bash para portabilidade; /bin/bash para determinismo).
- Cite variáveis sempre: “$VAR”. Use arrays quando necessário.
- Adote set -euo pipefail com traps para limpeza.
- Prefira $(…) à crase, e opções longas para legibilidade.
- Valide com shellcheck e escreva testes; migre para outra linguagem se o script crescer demais.
Resumo das principais ações rápidas:
- Ative modo estrito e traps.
- Use aspas e arrays.
- Evite parsing de ls para listas de ficheiros.
- Teste em imagens diferentes e use ferramentas de lint.
Obrigado por seguir estas práticas. Scripts mais previsíveis são mais seguros e mais fáceis de manter.
Materiais semelhantes
Instalar e usar Podman no Debian 11
Apt‑pinning no Debian: guia prático
Injete FSR 4 com OptiScaler em qualquer jogo
DansGuardian e Squid com NTLM no Debian Etch
Corrigir erro de instalação no Android