↓Pular para o conteúdo principal
  1. blog/
ARTIGO TÉCNICO

Quando arquivos de override redefinem funções silenciosamente: bug difícil de encontrar em toolkits Shell

Como a ordem de source pode redefinir silenciosamente funções Bash, fazer menus perderem itens e criar bugs que só aparecem no plantão.

·6 minutos

Em Bash, redefinir uma função não gera warning.

Se dois arquivos carregados pelo shell possuem:

1
2
3
minha_funcao() {
  ...
}

a última definição carregada vence.

Isso parece óbvio quando o exemplo tem três linhas. Em um toolkit operacional dividido em vários módulos e overrides, pode virar um bug difícil de perceber.

Em uma auditoria real do meu toolkit de operação, encontrei funções críticas definidas várias vezes em arquivos diferentes. O caso mais visível era um menu de plantão com versões contendo quantidades diferentes de itens.

Se a ordem de carregamento mudasse, o shell continuaria abrindo normalmente — só que o operador receberia uma versão incompleta do menu.

Nenhum crash. Nenhum erro de sintaxe. Apenas comportamento diferente.

O Bash aceita a redefinição sem reclamar #

Considere:

1
2
3
4
5
6
7
menu() {
  echo "versão A"
}

menu() {
  echo "versão B"
}

Ao executar:

1
menu

o resultado é:

1
versão B

Para verificar a definição efetiva:

1
declare -f menu

O problema aparece quando as duas definições estão em arquivos separados:

1
2
3
4
~/.bashrc
  ├── source 20-base.sh
  ├── source 40-ui.sh
  └── source zz-overrides.sh

Agora a função efetiva depende da ordem de source.

A auditoria encontrou duplicação real #

No toolkit analisado, várias funções tinham mais de uma definição.

Entre elas estavam funções equivalentes a:

1
2
3
4
5
6
7
plantao-menu
aws-menu
diag-host
tools-menu
troubleshooting-menu
banner principal
menu específico de operações

Algumas apareciam duas vezes. Outras, três ou mais.

O caso mais crítico era o menu principal de plantão: foram encontradas versões com 5, 6 e 7 itens.

Isso significava que uma mudança aparentemente inocente na ordem de carregamento poderia retirar funcionalidades do menu sem produzir qualquer erro técnico.

O último carregado vira a verdade #

Uma forma simples de reproduzir:

10-base.sh #

1
2
3
4
5
6
7
plantao-menu() {
  printf '%s\n' \
    "Alarmes" \
    "SSH" \
    "Docker" \
    "Cloud"
}

90-override.sh #

1
2
3
4
5
plantao-menu() {
  printf '%s\n' \
    "Alarmes" \
    "SSH"
}

Se .bashrc fizer:

1
2
source 10-base.sh
source 90-override.sh

o menu final terá dois itens.

Inverter a ordem muda o resultado.

Isso é exatamente o tipo de bug que pode passar por revisão porque cada arquivo isoladamente parece válido.

Descobrir a definição ativa #

Para função já carregada:

1
type plantao-menu

ou:

1
declare -f plantao-menu

Também é útil listar todas as ocorrências no código:

1
grep -RnsE '^plantao-menu\(\)[[:space:]]*\{' ~/.bashrc.d

Para procurar várias funções:

1
grep -RnsE '^(plantao-menu|aws-menu|diag-host)\(\)' ~/.bashrc.d

O objetivo não é apenas saber qual código está rodando, mas também quantas definições concorrentes existem.

Ordem de carregamento precisa ser explícita #

Depois da auditoria, o toolkit foi organizado com prefixos numéricos.

Conceitualmente:

1
2
3
4
5
6
7
8
00-platform.sh
10-menus.sh
20-base.sh
30-clientes.sh
40-backup.sh
50-diagnosticos.sh
60-docker.sh
zz-overrides.sh

Isso transforma uma ordem implícita em contrato visível.

O zz- final continua sendo override, mas agora sua função é deliberada.

Se uma função existir ali, fica claro que ela pretende ter a última palavra.

Melhor ainda: reduzir overrides #

Ordenar os arquivos resolve previsibilidade, mas não elimina dívida técnica.

Se uma função aparece em quatro lugares, qualquer manutenção continua arriscada.

O melhor desenho tende a ser:

1
2
3
4
5
uma implementação canônica
+
helpers reutilizáveis
+
configuração/dados variáveis

em vez de:

1
2
3
4
função base
função corrigida
função override
função override do override

Overrides devem ser exceção, não mecanismo principal de versionamento.

Outro bug parecido: guards com nomes diferentes #

A mesma auditoria encontrou duas versões de um banner usando variáveis de guarda distintas.

Conceitualmente:

1
2
[[ "${BANNER_SHOWN:-0}" == "1" ]] && return
export BANNER_SHOWN=1

em um arquivo, e:

1
2
[[ "${MAIN_BANNER_SHOWN:-0}" == "1" ]] && return
export MAIN_BANNER_SHOWN=1

em outro.

As duas funções tentavam impedir execução duplicada, mas os guards não compartilhavam estado.

Resultado possível: o banner aparecer duas vezes dependendo da combinação de funções carregadas.

A correção é definir uma única variável de contrato.

source também pode ter efeitos colaterais #

Outro achado da auditoria era código que criava/sobrescrevia arquivo no nível principal de um script carregado pelo shell.

Algo assim:

1
2
3
cat > "$HOME/bin/ferramenta" <<'EOF'
...
EOF

Se o arquivo é source-ado toda vez que abre o terminal, essa escrita acontece novamente a cada shell.

Isso é perigoso porque:

  • sobrescreve edição manual;
  • adiciona efeito colateral ao login;
  • torna debugging mais difícil;
  • mistura carregamento de funções com instalação.

A solução é mover para uma função explícita:

1
2
3
4
5
6
toolkit-install-helper() {
  cat > "$HOME/bin/ferramenta" <<'EOF'
...
EOF
  chmod +x "$HOME/bin/ferramenta"
}

E chamá-la somente no fluxo de instalação/upgrade.

Como auditar um toolkit modular #

Eu faria pelo menos quatro verificações.

1. Funções duplicadas #

Uma aproximação:

1
2
3
4
5
grep -RhoE '^[a-zA-Z_][a-zA-Z0-9_-]*\(\)[[:space:]]*\{' ~/.bashrc.d \
  | sed 's/().*//' \
  | sort \
  | uniq -cd \
  | sort -nr

Depois investigar as ocorrências reais com grep -Rn.

2. Ordem de source #

1
grep -nE '(^|[[:space:]])(source|\.)[[:space:]]' ~/.bashrc

3. Definição efetiva #

1
declare -f nome-da-funcao

4. Efeitos colaterais no top-level #

Procurar comandos de escrita, instalação ou chamadas executadas fora de funções.

Um teste simples para impedir regressão #

Depois de modularizar, vale criar um validador que carregue o toolkit e confirme a presença das funções esperadas.

Exemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
bash --noprofile --norc <<'EOF'
source "$HOME/.bashrc.d/00-platform.sh"
source "$HOME/.bashrc.d/10-menus.sh"
source "$HOME/.bashrc.d/20-base.sh"

for fn in plantao-menu aws-menu diag-host; do
  declare -F "$fn" >/dev/null || {
    echo "FALTA: $fn"
    exit 1
  }
done
EOF

Um passo além é calcular hash ou comparar a origem esperada das funções críticas.

O que a fonte deste artigo comprova #

A auditoria técnica recuperada registra:

  • múltiplas definições reais de funções críticas;
  • menu de plantão com versões contendo 5, 6 e 7 opções;
  • última definição carregada como comportamento efetivo;
  • guards inconsistentes para banner;
  • side-effect de geração de arquivo durante source;
  • refatoração posterior para uma ordem de módulos numerados e override final explícito.

Os nomes de clientes, hosts, FQDNs, IPs e demais detalhes de infraestrutura encontrados na auditoria não são necessários para explicar o problema e ficaram fora deste artigo.

Checklist #

1
2
3
4
5
6
7
8
[ ] listar funções duplicadas
[ ] documentar ordem de source
[ ] definir implementação canônica
[ ] limitar overrides deliberados
[ ] padronizar guards
[ ] retirar escrita/instalação do top-level
[ ] validar funções críticas após carregar toolkit
[ ] preservar backup antes da refatoração

O principal aprendizado #

Bash permite que a versão errada de uma função rode perfeitamente.

Por isso esse tipo de defeito é tão traiçoeiro: o shell não quebra; a sua operação muda silenciosamente.

Em toolkits usados durante incidente, ordem de carregamento e unicidade das funções precisam ser tratadas como parte da arquitetura, não como detalhe do .bashrc.

TEM UM CENÁRIO PARECIDO?

Me chama.

Manda o contexto, os sintomas e o que já foi testado. Bora organizar as evidências antes de sair mexendo.

Falar com Castro →
Sem enrolação. Com evidência.