Portabilidade: bash vs POSIX sh
Esta aula aborda as diferenças entre Bash e POSIX sh, destacando os chamados 'bashisms' (extensões do Bash não presentes no POSIX), quando é apropriado escrever scripts POSIX, a importância da linha shebang (#!) e estratégias para garantir compatibilidade entre diferentes shells. O aluno aprenderá a identificar construções não portáveis e a adaptar seus scripts para máxima compatibilidade.
Nesta aula, vamos explorar um dos temas mais importantes para quem escreve scripts no Linux: a portabilidade entre o Bash e o POSIX sh. O Bash é o shell padrão na maioria das distribuições Linux, mas nem sempre está disponível em sistemas embarcados, contêineres mínimos ou sistemas Unix tradicionais. Nesses ambientes, o shell padrão costuma ser o POSIX sh (como o Dash no Debian/Ubuntu). Entender as diferenças entre eles é essencial para criar scripts que funcionem em qualquer lugar.
Vamos mergulhar nos conceitos de 'bashisms' (extensões do Bash que não fazem parte do padrão POSIX), discutir quando vale a pena escrever scripts POSIX, analisar o papel da linha shebang (#!) e, por fim, apresentar boas práticas para garantir compatibilidade. Ao final, você terá um guia prático para decidir quando usar cada abordagem e como evitar armadilhas comuns.
Bashisms
Bashisms são recursos do Bash que não estão definidos no padrão POSIX. Eles incluem desde simples atalhos de sintaxe até funcionalidades avançadas como arrays, substituição de processo e expressões regulares estendidas. Embora esses recursos sejam extremamente úteis, eles tornam o script dependente do Bash, quebrando a portabilidade para outros shells.
Um exemplo clássico é o uso de [[ ]] para testes condicionais. No POSIX sh, usa-se [ ] (ou test). O [[ ]] oferece recursos como comparação de padrões e expressões regulares, mas não é suportado pelo Dash. Outro exemplo é o operador =~ para regex, que também é um bashism. Arrays são outro recurso poderoso do Bash, mas não fazem parte do POSIX. Vamos ver alguns exemplos:
# Bashism: usando [[ ]]
if [[ $1 == "start" ]]; then
echo "Iniciando..."
fi
# Alternativa POSIX
if [ "$1" = "start" ]; then
echo "Iniciando..."
fi
# Bashism: arrays
frutas=("maçã" "banana" "laranja")
echo "${frutas[0]}"
# POSIX: não há arrays, use variáveis separadas ou listas
fruta1="maçã"
fruta2="banana"
fruta3="laranja"
echo "$fruta1"Outros bashisms comuns incluem o uso de > para redirecionar tanto stdout quanto stderr (no POSIX, é 2>&1), o operador de expansão ${var:-default} (que é POSIX, mas cuidado com a sintaxe), e a função source (no POSIX, usa-se .). É importante conhecer essas diferenças para evitar surpresas.
Quando escrever POSIX
Escrever scripts puramente POSIX é uma escolha estratégica. Se você precisa que o script funcione em qualquer sistema Unix-like, incluindo sistemas embarcados, contêineres mínimos (como Alpine Linux, que usa BusyBox ash) ou sistemas BSD, então optar por POSIX é a decisão mais segura. Isso garante que o script será executado com o shell padrão do sistema, sem dependências adicionais.
Por outro lado, se você está escrevendo scripts para um ambiente controlado, como servidores que você administra e que têm o Bash instalado, pode aproveitar os recursos avançados do Bash para escrever código mais conciso e eficiente. A decisão depende do contexto: scripts de sistema que precisam ser confiáveis em qualquer lugar devem ser POSIX; scripts de automação para um ambiente específico podem usar Bash.
# Exemplo de script POSIX que funciona em qualquer shell
#!/bin/sh
# Verifica se o usuário passou um argumento
if [ $# -eq 0 ]; then
echo "Uso: $0 <nome>" >&2
exit 1
fi
echo "Olá, $1!"Uma boa prática é começar com POSIX e, se necessário, adicionar extensões do Bash de forma condicional, usando verificações para detectar se o shell é Bash. Isso permite que o script aproveite recursos avançados quando disponíveis, mas ainda funcione em ambientes restritos.
#!/bin/sh
A linha shebang (#!) no início de um script indica qual interpretador deve ser usado. Quando você escreve #!/bin/sh, está pedindo ao sistema para usar o shell POSIX padrão. Em muitas distribuições, /bin/sh é um link simbólico para o Bash (quando invocado como sh, o Bash se comporta de forma mais compatível com POSIX), mas em outras, como Debian e Ubuntu, é o Dash, que é muito mais rápido e estritamente POSIX.
Usar #!/bin/sh é uma declaração de intenção: seu script deve ser compatível com POSIX. Isso é importante porque, se você usar #!/bin/sh e depois usar um bashism, o script pode quebrar em sistemas onde /bin/sh não é Bash. Por isso, ao escolher essa shebang, você deve garantir que o código segue o padrão POSIX.
#!/bin/sh
# Exemplo de script POSIX
# Este script funciona em qualquer shell POSIX
echo "Iniciando script..."
# Uso de case (POSIX)
case "$1" in
start)
echo "Iniciando serviço"
;;
stop)
echo "Parando serviço"
;;
*)
echo "Uso: $0 {start|stop}" >&2
exit 1
;;
esacSe você precisa de recursos do Bash, use #!/bin/bash explicitamente. Isso deixa claro que o script requer Bash e evita que o sistema tente executá-lo com outro shell. No entanto, lembre-se de que /bin/bash pode não estar presente em todos os sistemas (embora seja comum). Para máxima portabilidade, prefira #!/bin/sh com código POSIX.
Compatibilidade
Garantir compatibilidade entre shells envolve mais do que evitar bashisms. É preciso considerar diferenças em comandos externos, opções de utilitários e comportamento de expansão. Por exemplo, o comando echo tem variações: no Bash, echo -e interpreta escapes, mas no POSIX, o comportamento não é especificado. É melhor usar printf para saída formatada, pois é mais previsível.
Outra questão é o uso de local para variáveis locais em funções: é suportado no Bash e no Dash, mas não é POSIX. Se você precisa de portabilidade estrita, evite local e use variáveis globais com cuidado, ou use subshells para isolar escopo.
# Exemplo de uso de printf (mais portável que echo)
printf "%s\n" "Olá, mundo!"
# Evite echo -e em scripts POSIX
# echo -e "Linha1\nLinha2" # não portável
# Use printf para múltiplas linhas
printf "Linha1\nLinha2\n"Além disso, ao usar comandos externos, prefira opções simples e amplamente suportadas. Por exemplo, grep -E (ou egrep) é mais portável que grep -P (que usa PCRE e não é POSIX). Da mesma forma, sed -r é uma extensão; use sed -E para ERE (embora -E também não seja POSIX, mas é amplamente suportado). Para máxima compatibilidade, limite-se a BRE (basic regex) quando possível.
Boas Práticas e Observações Finais
Para garantir que seus scripts sejam portáveis, siga estas recomendações: teste seus scripts em múltiplos shells (Dash, Bash, ksh, etc.) usando ferramentas como shellcheck e checkbashisms. Essas ferramentas identificam construções não portáveis e sugerem alternativas. Além disso, documente a compatibilidade esperada do script no cabeçalho, informando se ele é POSIX ou Bash.
Outra prática é usar set -u (ou set -o nounset) para evitar variáveis não definidas, e set -e para sair em caso de erro, mas lembre-se de que essas opções são POSIX (exceto set -o pipefail, que é uma extensão). Use-as com cuidado.
Por fim, sempre que possível, evite depender de recursos específicos de um shell. A portabilidade é uma forma de garantir que seus scripts continuarão funcionando no futuro, independentemente das mudanças no ambiente.
Referências
- POSIX Shell Command Language (The Open Group)
- Bash Reference Manual (GNU)
- Dash as /bin/sh (Ubuntu Wiki)
- ShellCheck - Shell Script Analysis Tool
- checkbashisms man page (Debian)
- Bashism (GreyCat's Wiki)
Exercícios
- Identifique e corrija os bashisms no seguinte trecho de script:
#!/bin/bash if [[ $1 == "-v" ]]; then echo "Versão 1.0" fi✓ Resposta: O bashism é o uso de[[ ]]. Para corrigir, troque por[ ]e use aspas duplas nas variáveis. Além disso, se você quiser que o script seja POSIX, mude a shebang para#!/bin/sh. O código corrigido fica:#!/bin/sh if [ "$1" = "-v" ]; then echo "Versão 1.0" fi - Escreva um script POSIX que aceite um número como argumento e imprima se ele é par ou ímpar. Use apenas construções POSIX.
#!/bin/sh
if [ $# -ne 1 ]; then
echo "Uso: $0 <número>" >&2
exit 1
fi
n=$1
resto=$((n % 2))
if [ "$resto" -eq 0 ]; then
echo "Par"
else
echo "Ímpar"
fi
#!/bin/sh em um script que contém arrays do Bash é problemático e o que pode acontecer.#!/bin/sh indica que o script deve ser executado pelo shell POSIX padrão do sistema. Se o script usar arrays (um bashism), o shell POSIX (como Dash) não entenderá a sintaxe e emitirá um erro de sintaxe, interrompendo a execução. Isso pode causar falhas inesperadas, especialmente em sistemas onde /bin/sh não é Bash.#!/bin/bash
names=("Ana" "Bruno" "Carla")
for name in "${names[@]}"; do
echo "Olá, $name!"
done
#!/bin/sh
names="Ana Bruno Carla"
for name in $names; do
echo "Olá, $name!"
done
Ou, se você quiser aceitar nomes com espaços, pode usar argumentos do script:#!/bin/sh
for name in "$@"; do
echo "Olá, $name!"
done
dash (ou outro shell POSIX) para ver se há erros. Essas ferramentas ajudam a garantir que o script funcionará em ambientes restritos.