Garantindo a Portabilidade de Scripts Bash em Diferentes Sistemas

Escreva scripts Bash portáveis que lidam com diferenças entre GNU, BSD e BusyBox em ambientes Linux, macOS e CI.

Garantindo a Portabilidade de Scripts Bash em Diferentes Sistemas

Escrever scripts Bash que funcionam no seu laptop, em um servidor Linux e em um executor de CI é mais difícil do que parece. A portabilidade de scripts Bash geralmente quebra em pequenas diferenças: uma flag sed -i que funciona no Linux, mas falha no macOS, uma opção date que existe apenas no GNU coreutils, ou um script que assume que /bin/bash é a versão que você testou.

A dificuldade central é que o Bash é apenas parte do ambiente. O Linux normalmente vem com utilitários GNU. O macOS vem com utilitários do tipo BSD. Contêineres baseados em BusyBox podem fornecer implementações menores com menos opções. Seu script precisa ser claro sobre o que ele requer.

Este guia foca em scripts Bash, não estritamente em scripts POSIX sh. Se você precisar de verdadeira portabilidade /bin/sh, evite completamente a sintaxe exclusiva do Bash e teste com shells como dash.

Comece com um Contrato de Shell Claro

Use um shebang que corresponda à sua intenção. Se o script requer Bash, diga:

#!/usr/bin/env bash

/usr/bin/env localiza o Bash através do $PATH, o que é útil quando os usuários instalam um Bash mais novo fora do /bin. Se seus hosts de produção exigem um caminho de interpretador fixo, documente e aplique esse caminho.

O modo estrito captura muitos erros cedo, mas não é mágico:

#!/usr/bin/env bash

set -euo pipefail
IFS=$'\n\t'

Essas opções ajudam, com ressalvas:

  • -e: Sai quando muitos comandos simples retornam um status diferente de zero.
  • -u: Trata variáveis não definidas como erros.
  • pipefail: Faz um pipeline falhar se qualquer comando no pipeline falhar.

Lide com falhas esperadas explicitamente:

if ! grep -q "ready" "$log_file"; then
    echo "O serviço ainda não está pronto"
fi

Conheça Sua Versão do Bash

Não dependa acidentalmente de um recurso do Bash que seus sistemas alvo não possuem. O macOS historicamente vem com um Bash mais antigo em /bin/bash, enquanto muitas distribuições Linux vêm com versões mais novas.

Recursos a serem usados com cuidado incluem:

  • Arrays associativos.
  • Globbing avançado como **.
  • Substituição de processo como <(command).
  • Comportamento mais novo de expansão de parâmetros.

Se você precisar de uma versão mínima do Bash, verifique perto do topo:

if (( BASH_VERSINFO[0] < 4 )); then
    echo "Este script requer Bash 4 ou mais novo." >&2
    exit 1
fi

Lidando com Diferenças entre GNU, BSD e BusyBox

Os maiores problemas de portabilidade geralmente vêm de comandos externos, não do próprio Bash.

sed -i

O GNU sed aceita -i sem uma extensão de backup. O BSD sed no macOS requer um argumento de extensão após -i, mesmo que essa extensão seja uma string vazia.

file="data.txt"
pattern="s/error/success/g"

case "$(uname -s)" in
    Darwin)
        sed -i '' "$pattern" "$file"
        ;;
    *)
        sed -i "$pattern" "$file"
        ;;
esac

Para scripts críticos, um padrão mais seguro é escrever em um arquivo temporário e depois movê-lo para o lugar. Isso evita depender do comportamento de edição in-place.

date

A matemática de datas é diferente entre os sistemas:

Objetivo GNU date BSD date no macOS
30 dias atrás date -d "30 days ago" +%Y%m%d date -v-30d +%Y%m%d

Se seu script precisa de matemática de datas complexa, use uma dependência consistente como Python, ou exija GNU coreutils no macOS e chame gdate explicitamente. Não assuma silenciosamente que date -d existe.

grep, find e xargs

Use opções amplamente suportadas quando possível:

  • Use grep -E em vez de depender de egrep.
  • Evite grep -P a menos que você verifique se o GNU grep tem suporte a PCRE.
  • Tenha cuidado com predicados find que diferem entre implementações GNU e BSD.
  • Prefira pipelines delimitados por nulo para nomes de arquivos quando suportado:
find "$root" -type f -name '*.log' -print0 | xargs -0 rm -f

Gerencie Dependências e Caminhos

Use $PATH para a busca normal de comandos, mas verifique as ferramentas necessárias antes de fazer o trabalho:

check_dependency() {
    if ! command -v "$1" >/dev/null 2>&1; then
        echo "Erro: comando necessário '$1' não encontrado." >&2
        exit 1
    fi
}

check_dependency jq
check_dependency curl

Prefira command -v em vez de which porque é um built-in do shell no Bash e se comporta de forma mais previsível em scripts.

Coloque variáveis entre aspas, a menos que você queira intencionalmente a divisão de palavras:

cp "$source_file" "$target_dir/"

Isso é importante para caminhos como Project Files/report.txt, e também protege contra a expansão de curingas em entradas inesperadas.

Use Arquivos Temporários com Segurança

Use mktemp para trabalho temporário. Um padrão simples e portátil é criar um diretório temporário e colocar arquivos dentro dele:

tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT

tmp_file="$tmp_dir/output.txt"
some_command > "$tmp_file"

O trap com aspas simples mantém $tmp_dir de ser expandido até que o trap seja executado. Como a variável ainda está no escopo, a limpeza remove o diretório correto.

Cuidado com Finais de Linha e Case do Sistema de Arquivos

Scripts editados no Windows podem usar finais de linha CRLF. Um sintoma comum é:

/usr/bin/env: bash\r: No such file or directory

Configure seu editor para salvar scripts de shell com finais de linha LF, ou execute dos2unix no seu processo de build.

Lembre-se também que a maioria dos sistemas de arquivos Linux é sensível a maiúsculas/minúsculas por padrão, enquanto as configurações padrão do APFS no macOS geralmente são insensíveis a maiúsculas/minúsculas. Se seu script escreve Config.yml e depois lê config.yml, pode funcionar no seu Mac e falhar no Linux.

Teste nos Sistemas que Você Suporta

A melhor verificação de portabilidade é uma pequena matriz de teste:

  • Linux com utilitários GNU.
  • macOS com utilitários BSD.
  • Contêineres mínimos se seu script for executado em ambientes Alpine ou BusyBox.

Execute o ShellCheck também. Ele não vai pegar todos os problemas de plataforma, mas pega muitos padrões de citação, variáveis não definidas e comandos frágeis antes que seus usuários o façam.

Conclusão

A portabilidade de scripts Bash vem de tornar suas suposições explícitas. Escolha o shell, verifique dependências, coloque variáveis entre aspas, evite flags exclusivas do GNU a menos que você as exija, e teste nos mesmos sistemas operacionais que seus usuários executam. Uma pequena matriz de CI com Linux e macOS pega a maioria dos bugs de portabilidade antes que sua automação chegue à produção.