Cross-platform scripting: compatibilidade Bash/Zsh/sh

1. Introdução ao ecossistema de shells Unix-like

O ambiente de shells Unix-like é diverso e fragmentado. Bash (Bourne Again SHell) é o shell padrão na maioria das distribuições Linux, Zsh (Z Shell) domina no macOS desde Catalina e é popular entre desenvolvedores, enquanto sh (geralmente um link simbólico para Dash no Debian/Ubuntu ou BusyBox em sistemas embarcados) representa o mínimo denominador comum POSIX.

As diferenças fundamentais entre esses shells vão muito além de sintaxe cosmética:

  • Bash: Suporte a arrays indexados e associativos, [[ ]], expressões regulares com =~, expansão de chaves avançada
  • Zsh: Arrays com índices baseados em 1 (não 0), globs poderosos com **, operador ~ para regex, correção ortográfica automática
  • sh (Dash/POSIX): Apenas construções POSIX.1-2008, sem arrays, sem [[ ]], sem expansão de chaves aninhada

A compatibilidade cross-platform é crítica porque scripts frequentemente precisam rodar em ambientes heterogêneos: servidores Linux com Bash, contêineres Alpine com BusyBox sh, macOS com Zsh, e pipelines CI/CD que podem usar qualquer shell disponível.

2. Shebang e detecção do interpretador correto

A escolha do shebang é a primeira decisão de portabilidade:

#!/bin/sh          # POSIX puro - máxima portabilidade
#!/bin/bash        # Exige Bash explicitamente
#!/usr/bin/env bash # Mais flexível (respeita PATH do usuário)

Para scripts que precisam de recursos avançados mas devem cair graciosamente, use:

#!/bin/sh
# Script com fallback inteligente

if [ -n "$BASH_VERSION" ] || [ -n "$ZSH_VERSION" ]; then
    echo "Shell moderno detectado"
else
    echo "Modo POSIX estrito"
    set -o posix 2>/dev/null || true
fi

O comando set -o posix no Bash força comportamento compatível com POSIX, desativando extensões como [[ ]] e expansão de arrays.

Para verificar qual shell está executando o script:

current_shell=$(basename "$(ps -p $$ -o comm= 2>/dev/null || echo "sh")")
echo "Interpretador atual: $current_shell"

3. Construções sintáticas compatíveis (o "safe subset")

Expansão de variáveis

Use formas POSIX universais:

# Compatível com Bash, Zsh e sh
nome="${1:-default}"       # Valor padrão se variável não existe
nome="${1:=default}"       # Atribui e retorna (cuidado com readonly)
mensagem="${var:+presente}" # Retorna string se variável está definida

Substituição de comando

Sempre use $(cmd) em vez de backticks:

# ✅ POSIX e universal
data=$(date +%Y-%m-%d)

# ❌ Obsoleto e problemático com aninhamento
data=`date +%Y-%m-%d`

Testes condicionais

Prefira [ ] (POSIX) em vez de [[ ]]:

# ✅ Compatível com todos os shells
if [ "$var" = "valor" ] && [ -f "/etc/config" ]; then
    echo "OK"
fi

# ❌ Apenas Bash/Zsh (não funciona em sh)
if [[ "$var" == "valor" && -f /etc/config ]]; then
    echo "OK"
fi

Loops e case

# Loop for POSIX (sem chaves de expansão)
for i in 1 2 3; do
    echo "$i"
done

# Case com sintaxe POSIX
case "$1" in
    start) echo "Iniciando..." ;;
    stop)  echo "Parando..." ;;
    *)     echo "Uso: $0 start|stop" ;;
esac

4. Armadilhas comuns entre Bash e Zsh

Arrays indexados

# Bash (índices baseados em 0)
frutas=("maçã" "banana" "cereja")
echo "${frutas[0]}"  # maçã

# Zsh (índices baseados em 1 por padrão)
frutas=("maçã" "banana" "cereja")
echo "${frutas[1]}"  # maçã (no Zsh)

# Solução portátil: evitar arrays indexados em scripts cross-platform
# Use strings delimitadas ou arquivos temporários

Expressões regulares

# Bash: usa =~
if [[ "$email" =~ ^[a-z]+@[a-z]+\.[a-z]{2,}$ ]]; then
    echo "válido"
fi

# Zsh: usa ~ ou =~ (compatível com Bash recente)
if [[ "$email" ~ ^[a-z]+@[a-z]+\.[a-z]{2,}$ ]]; then
    echo "válido"
fi

# Solução portátil: grep POSIX
if echo "$email" | grep -Eq '^[a-z]+@[a-z]+\.[a-z]{2,}$'; then
    echo "válido"
fi

Source vs ponto

Ambos source e . são equivalentes em Bash/Zsh, mas apenas . é POSIX:

# ✅ POSIX (funciona em todos)
. ./biblioteca.sh

# ❌ Não POSIX (falha em sh)
source ./biblioteca.sh

5. Lidando com comandos e utilitários ausentes

Verificação de disponibilidade

Use command -v (POSIX) em vez de which:

if command -v docker >/dev/null 2>&1; then
    echo "Docker disponível"
else
    echo "Docker não encontrado"
fi

Substituição por alternativas

# readlink -f não é POSIX (falta no macOS)
# Alternativa portátil:
caminho_absoluto() {
    case "$1" in
        /*) echo "$1" ;;
        *)  echo "$(cd "$(dirname "$1")" && pwd -P)/$(basename "$1")" ;;
    esac
}

printf vs echo

# ✅ Portátil e previsível
printf "%s\n" "Olá, mundo!"

# ❌ Comportamento varia entre shells
echo "Olá, mundo!"      # Pode interpretar escapes
echo -e "Olá\nmundo!"   # -e não é POSIX

Cuidados com flags não-POSIX

# sed -i: incompatível entre GNU sed (Linux) e BSD sed (macOS)
# Solução: usar arquivo temporário
sed 's/foo/bar/g' arquivo.txt > arquivo.tmp && mv arquivo.tmp arquivo.txt

# grep -P (Perl regex): não POSIX
# Use grep -E para ERE (Extended Regular Expressions)

# find -printf: específico do GNU find
# Use stat ou ls como alternativa

6. Estratégias de teste e validação cross-platform

ShellCheck com perfis específicos

# Verificar compatibilidade POSIX
shellcheck -s sh script.sh

# Verificar como Bash
shellcheck -s bash script.sh

# Verificar como Zsh
shellcheck -s zsh script.sh

Testes em múltiplos shells com Docker

#!/bin/sh
# Script de teste cross-platform

testar_em_shell() {
    shell="$1"
    script="$2"
    echo "Testando com $shell..."
    docker run --rm -v "$(pwd):/script" alpine sh -c "
        apk add --no-cache $shell >/dev/null 2>&1
        $shell /script/$script
    " || echo "Falha em $shell"
}

testar_em_shell "bash" "meu_script.sh"
testar_em_shell "dash" "meu_script.sh"
testar_em_shell "zsh"  "meu_script.sh"

Função de adaptação condicional

is_bash_or_zsh() {
    [ -n "$BASH_VERSION" ] || [ -n "$ZSH_VERSION" ]
}

# Uso em script portátil
if is_bash_or_zsh; then
    # Usar recursos avançados com segurança
    tipos=("texto" "json" "xml")
else
    # Fallback POSIX
    tipos="texto json xml"
fi

7. Boas práticas para scripts verdadeiramente portáteis

Uso de set -euo pipefail e limitações

#!/bin/sh
# Configurações de segurança (compatíveis com POSIX básico)
set -e     # Sair ao primeiro erro
set -u     # Erro ao usar variável não definida

# pipefail não é POSIX (funciona em Bash/Zsh, não em Dash)
# Alternativa manual:
ultimo_comando() {
    error=$?
    if [ $error -ne 0 ]; then
        exit $error
    fi
}
cmd1 | cmd2; ultimo_comando

Documentação de dependências

#!/bin/sh
#
# Script: deploy.sh
# Descrição: Deploy automatizado cross-platform
# Dependências: curl, jq, openssl
# Testado em: Bash 5+, Zsh 5.8+, Dash 0.5.10+
#
# Uso: ./deploy.sh [ambiente]

Checklist final de verificação

# Verificações de portabilidade antes do deploy
check_portability() {
    echo "=== Verificação Cross-Platform ==="

    # 1. Shebang correto
    head -1 "$1" | grep -q '^#!/bin/sh' || echo "⚠️  Shebang não é sh"

    # 2. Sem construções não-POSIX
    grep -q '\[\[\s' "$1" && echo "⚠️  Usa [[ ]] (não POSIX)"
    grep -q 'source\s' "$1" && echo "⚠️  Usa source (não POSIX)"

    # 3. Verificar com ShellCheck
    if command -v shellcheck >/dev/null 2>&1; then
        shellcheck -s sh "$1" || echo "⚠️  Falha no ShellCheck"
    fi

    echo "=== Verificação concluída ==="
}

check_portability "$1"

Referências