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
- POSIX Shell Command Language Specification — Especificação oficial POSIX para shell scripting, referência definitiva para compatibilidade
- ShellCheck - Shell Script Analysis Tool — Ferramenta de análise estática que suporta perfis sh, bash e zsh para detectar problemas de portabilidade
- Bash Reference Manual - Bash POSIX Mode — Documentação oficial sobre como ativar modo POSIX no Bash para compatibilidade
- Zsh Manual - Compatibility with Bash — Guia oficial do Zsh sobre diferenças e compatibilidade com Bash
- Google Shell Style Guide — Guia de estilo do Google com recomendações práticas para scripts portáteis e manuteníveis
- BusyBox - The Swiss Army Knife of Embedded Linux — Documentação do BusyBox, shell minimalista comum em contêineres Alpine e sistemas embarcados
- Docker Official Images - Alpine — Imagem oficial Alpine Linux, ideal para testar scripts em ambiente com BusyBox sh