Portabilidade

itamarnet edited this page Jan 15, 2018 · 37 revisions

De que adianta se empenhar em fazer uma função se ela funciona somente em sua máquina?

Tenha sempre em mente que há uma diversidade de sistemas e configurações, e quanto mais você programar de maneira conservadora, maiores as chances da sua função funcionar em vários sistemas: Linux, BSD, AIX, Mac OS X, Cygwin e tantos outros sabores de Unix.

Não use novidades, não use comandos ou opções recém-criados. Fique no arroz com feijão, mesmo que isso signifique usar comandos e pipes adicionais. A portabilidade é mais importante que elegância e performance. Aliás, performance e bash não combinam, nem perca tempo querendo "economizar" pipes, processos, memória, etc.

Evite as opções GNU

Um erro comum é usar opções específicas da versão GNU nos comandos sed, grep, awk, find e amigos. Elas facilitam a sua vida, mas tornarão sua função incompatível com outros Unixes. Evite-as a todo custo.

Na dúvida se uma opção é específica da versão GNU, leia as man pages de outros Unixes. Por exemplo, procure na internet por bsd man sed.

bash1, bash2, bash3, bashN

Há servidores por aí rodando versões bem antigas do bash, então não use aquela funcionalidade modernosa recém-implementada do bash4. Quanto menor a versão do bash que sua função funcionar, melhor.

Para manter compatibilidade com o bash1, você não pode usar: arrays, ==, i++, [[, ${!var}, ${var/isso/aquilo}, ${var:1:3}, $FUNCNAME, for(;;), echo $'…', comando < <(comando), entre outros.

Outra vantagem é que quanto mais conservador seu script, mais chances de ele funcionar em outros shells sem alteração. O Ubuntu por exemplo, instituiu o dash como shell padrão a partir da versão 8.04 LTS. Sua função funcionará tanto no bash quanto no dash se você evitar as funcionalidades mais modernas.

Veja aqui alguns exemplos do que evitar, para que sua função seja mais portável:

Não use comandos específicos da plataforma

Evite ao máximo usar comandos específicos de um sistema operacional. Lembre-se que as funções rodam em Linux, Unix, Mac e até no Windows (via Cygwin).

Use os comandos padrão (sed, grep, find) que ela rodará em qualquer máquina. Porém atente que, mesmo nos comandos padrão, podem haver opções (geralmente GNU) que só funcionam no Linux mas não nos Unix. Seja conservador, usar a mais-nova-opção-bacanosa não é bom para a portabilidade.

Não use shift N

Usar um valor numérico no shift não é portável. Então em vez disso:

shift 2

faça isso:

shift
shift

Maneira portável de inserir um Tab

Se você precisar inserir um Tab literal em uma variável, ou em um comando que não aceita o \t nativamente (como o sed do BSD), use o printf para guardá-lo numa variável:

local tab=$(printf '\t')

sed "s/ /$tab/g"  # troque todos os espaços por tabs

Mas lembre-se que outros comandos, como o tr por exemplo, já aceitam o \t nativamente:

tr ' ' '\t'       # troque todos os espaços por tabs

Comando paste e STDIN

O paste do BSD precisa do - no final para indicar que os dados serão lidos da STDIN:

zzseq 5 | paste -s -d : -

No Linux o hífen final não é necessário, mas no BSD, é.

Comando echo

As opções -e e -n não são portáveis. Para piorar, em alguns sistemas, a opção -e é implícita, ou seja, os escapes sempre são expandidos mesmo ao chamar o echo sem opções. Prefira o comando printf em seu lugar.

Mas primeiro você precisa entender uma diferença crucial entre os dois comandos: a quebra de linha no final. O echo normal sempre quebra a linha no final, a não ser que use a opção -n. Já o printf nunca quebra a linha, você deve informar o \n no final do primeiro argumento se quiser a quebra.

# Com quebra de linha
echo 'foo'
printf 'foo\n'

# Sem quebra de linha
echo -n 'foo'
printf 'foo'

Entendida esta diferença sobre a quebra de linha, estes são os comandos equivalentes:

echo -n 'foo bar'           # ERRADO
printf %s 'foo bar'         # CERTO

echo -e 'foo\tbar'          # ERRADO
printf '%b\n' 'foo\tbar'    # CERTO

echo -n -e 'foo\tbar'       # ERRADO
printf %b 'foo\tbar'        # CERTO

Note que usa-se %s quando é echo -n, pois ele não faz nenhum tipo de expansão. Já quando é pra substituir o echo -e, usa-se o %b que expande os backslashes: \t, \n, e amigos.

Para memorizar:

    %s    string normal
    %b    string com backslashes

Muito cuidado ao usar o printf diretamente, sem %s nem %b. Tenha em mente que desta forma ele sempre vai expandir os backslashes e também os escapes no formato %char (%s, %d, %x, %%, …). E se o texto em questão tiver o caractere %, vai dar problema.

$ echo -e '%%%%'
%%%%
$ printf '%%%%'
%%
$ printf '10%'
-bash: printf: `%': missing format character
10

Então evite o printf puro, sem %s ou %b. Caso o faça, assegure-se que a string é segura, sem escapes nem %.

Comando grep

As opções -q e -s não são portáveis. Em vez disso, redirecione para /dev/null:

grep -q "foo" arquivo.txt                  # ERRADO
grep "foo" arquivo.txt > /dev/null         # CERTO

grep -s "foo" arquivo.txt                  # ERRADO
grep "foo" arquivo.txt 2> /dev/null        # CERTO

grep -qs "foo" arquivo.txt                 # ERRADO
grep "foo" arquivo.txt > /dev/null 2>&1    # CERTO
if grep -q "foo" arquivo.txt           # ERRADO
then
	echo "encontrei"
fi

if grep "foo" arquivo.txt > /dev/null  # CERTO
then
	echo "encontrei"
fi

Comando fmt

Evite usá-lo em textos coloridos (com caracteres de controle), pois no BSD ele remove o caractere Esc. Veja um exemplo:

$ zzecho -f azul foo | fmt
[44mfoo[m

Comando awk

Arrays em awk:

  • Arrays multi-dimensionais do tipo array[a][b] é uma extensão GNU, prefira usar a forma array[a, b].
  • Para apagar um array a forma usual seria o comando delete array, apesar de ser um padrão POSIX ainda não é implementado em todas as versões, prefira usar a função split("", array).
  • Não use length(array) para contar os elementos de um array. Se o array é resultante da divisão de uma string a função split(string, array, [separador]) já fornece esse resultado, e em outras situações faça um laço com um contador: for (i in array) { contador++ }.

O comando switch não é portável. Em seu lugar, use vários if.

O comando close aceita apenas um argumento. Usar "to" e "from" como argumento adicional é uma extensão GNU, da mesma forma que o uso de pipe e getline nesse formato: instrução |& getline também não é portável.

As funções asort, asorti, gensub, patsplit, strtonum e isarray, funções com bits e de tempo não são portáveis, evite-os.

A função length, além de não ser portável no uso com arrays, não deve ser declado sem os parenteses para determinar o tamanho da linha inteira, para esse caso use length() ou length($0).

Na função match não use o terceiro argumento, e nem o quarto argumento na função split, são extensões GNU.
O terceiro argumento das funções gsub e sub se for omitido aplica a substituição na linha corrente, mas se for declarado deve ser apenas um array, uma variável ou um campo da linha; outras opções nesse argumento não são portáveis.

As funções toupper e tolower funcionam bem apenas em caracteres ascii, escopo além disso os resultados são variáveis em função da versão e locale do sistema usado.

Não use as variáveis de ambiente e built-in: ARGIND, BINMODE, ERRNO, FIELDWIDTHS, FUNCTAB, FPAT, IGNORECASE, LINT, PREC, PROCINFO, RT, SYMTAB, TEXTDOMAIN.

Nota especial sobre o mawk:

  • Não suporta o uso de classe POSIX nas expressões regulares, ao invés de usar [[:digit:]] use [0-9]
  • Não suporta o uso de chaves como quantificador nas expressões regulares, então use outras alternativas por exemplo:
 [0-9]{3}    =>  [0-9][0-9][0-9]
 [a-z]{1,3}  =>  [a-z][a-z]?[a-z]?
 [0-9]{0,1}  =>  [0-9]?
 [a-z]{1,}   =>  [a-z]+
 [a-z]{0,}   =>  [a-z]*

Comando sed

  • Opção -u é uma extensão do GNU sed, não presente em outros seds. Não use.
  • Opção -i não está presente em todos os seds. Não use.
  • As opções -r (GNU) e -E (BSD) não são portáveis. Não use.
  • O metacaractere \? não funciona no BSD sed. Use \{0,1\}.
  • O metacaractere \+ não funciona no BSD sed. Use \{1,\}.
  • O metacaractere \| não funciona no BSD sed. Não use.
  • O intervalo aberto de entrada {,1} não é portável, use {0,1}.
  • Não use \t, insira um Tab literal.
  • Não use \n para casar múltiplas linhas. Ele só é portável se antes você usou os comandos N ou G para criar múltiplas linhas dentro do PATTERN SPACE (raro).
  • Se usar o comando t label, sempre deixe-o sozinho na linha, ou dentro de um -e isolado. Se colocar algo após, como em t label; } vai dar erro no BSD.
  • Se usar blocos { … } em uma única linha, sempre feche o último comando do bloco com ;, exemplos:
sed '/foo/{ s/x/y/ }'     # ERRO no BSD
sed '/foo/{ s/x/y/; }'    # OK no BSD e GNU
  • Não use - para significar STDIN, não funciona no BSD sed. Deixe em branco ou use /dev/stdin.
echo foo | sed 's/f/./' -             # ERRO no BSD
echo foo | sed 's/f/./' /dev/stdin    # OK no BSD e GNU
echo foo | sed 's/f/./'               # OK no BSD e GNU

echo 's/:/@/g' | sed -f - /etc/passwd             # ERRO no BSD
echo 's/:/@/g' | sed -f /dev/stdin /etc/passwd    # OK no BSD e GNU
  • Não use \n na segunda parte do s/// para inserir uma quebra de linha. Use uma quebra de linha literal, ou o comando G.
sed '
    # Pra colocar ============ embaixo da linha "título"

    # Usando \n, não é portável
    /título/ s/$/\n===============/

    # Usando quebra de linha literal, está OK
    /título/ s/$/\
===============/

    # Usando o comando a, está OK
    /título/ a \
===============

    # Usando o comando G para inserir uma linha em branco, está OK
    /título/ G
'
  • Em substituições não use a flag "g" junto com um "número" da ocorrência. Para contornar isso use desvios com ajuda de "labels" e em scripts multi-linhas, inline não são portáveis. As flags "g" e "p" são permitidas juntas, assim como "número" com "p".
echo foooo | sed 's/o/a/2g'         # ERRO no BSD
echo foooo | sed ':a; s/o/a/2; ta'  # ERRO no BSD
echo foooo | sed '
	:a
	s/o/a/2
	ta
'                                   # OK no BSD e GNU
  • Dentro de um script sed, somente os comentários de linha inteira são portáveis. O de meia linha, não.
sed '

    # CERTO: Isso é um comentário portável
    s/isso/aquilo/

    s/aquilo/outro/  # ERRADO: comentário não-portável
'
  • Caso o texto a ser editado não possua uma quebra de linha \n no final, o GNU sed não a adiciona, enquanto o BSD sed sempre adiciona a quebra no final. Portanto, evite o usar o sed em textos sem a quebra de linha, sempre adicione a quebra antes de passar pelo sed, para garantir um comportamento igual.
prinf foo | gnu-sed 's/foo/bar/'            # resultado: bar
prinf foo | bsd-sed 's/foo/bar/'            # resultado: bar\n

(prinf foo; echo) | gnu-sed 's/foo/bar/'    # resultado: bar\n
(prinf foo; echo) | bsd-sed 's/foo/bar/'    # resultado: bar\n
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.