Conheça o ‘printf’ (mas não abandone o ‘echo’!)

Conheça o 'printf' (mas não abandone o 'echo'!)

Apesar de toda popularidade do comando interno echo, nem sempre ele é a opção mais adequada para produzir saídas de texto a partir dos argumentos que recebe. Menos conhecido, o builtin printf faz a mesma coisa e muito mais, mas isso não é motivo para, como virou moda dizer, "parar de usar o echo".

Em tempo, enquanto estiver lendo este artigo, evite pensar em termos de "melhor ou pior": nosso objetivo sempre deve ser encontrar a ferramenta certa para cada situação: é o contexto que manda!

O 'echo' POSIX e o 'echo' do Bash

Quando utilizamos o Bash como shell padrão, é comum pensarmos que seus comandos internos (builtins) e opções são universais. Porém, este nem sempre é o caso e, dependendo do shell que interpretará nossos comandos, é possível que tenhamos que encarar diferenças relevantes até nos comandos mais simples, como o echo, por exemplo.

O Bash é um shell compatível com o Bourne Shell (o shell sh de fato) e implementa as normas POSIX: o que também faz dele um shell POSIX. Entretanto, as normas POSIX só estabelecem um conjunto mínimo de comandos e comportamentos esperados de shells compatíveis com o Bourne Shell.

Alguns shells, como o ash e o dash, limitam-se quase exclusivamente às normas POSIX por dois motivos: reduzir o tamanho (em bytes) de seus binários e proporcionar uma compatibilidade relativamente maior com scripts criados com vistas à portabilidade. Outros shells, como o bash e o ksh, priorizando as funcionalidades, vão além do esperado e implementam alguns comandos e comportamentos que podem exceder as normas POSIX.

Geralmente, as diferenças não são um grande problema, porque nós podemos especificar o shell que executará o nosso código na shebang. Porém, em sistemas GNU/Linux, quando a shebang é escrita especificando o caminho /bin/sh, nós não podemos dizer, com certeza, qual shell será executado. Isso porque, no GNU/Linux, o caminho /bin/sh é um link simbólico para o shell que a distribuição elegeu para a finalidade de ser compatível com as normas POSIX.

Por exemplo:

Distribuição Ligação com /bin/sh
Debian, Void, Trisquel, Ubuntu dash
Fedora, Parabola, Gentoo, Arch bash

É nesse contexto que entra a ponderação que faremos sobre o uso dos comandos echo e printf, especialmente por conta de um fato muito interessante: aquilo que não pudermos fazer de forma portável com o echo, poderá ser feito tranquilamente com o printf.

Implementações e portabilidade

Em termos de portabilidade, a principal preocupação com o echo está nas suas opções. Pelas normas POSIX, as implementações do echo "não deverão suportar quaisquer opções". Mesmo assim, tanto o ash quanto o dash, amplamente utilizados como shells sh, suportam a opção -n para inibir a inclusão de uma quebra de linha na saída produzida. O bash e o ksh, por sua vez, vão além e oferecem três opções:

Opção Bash/KornShell Dash/Ash
-n Não inclui uma quebra de linha na saída. Mesmo comportamento.
-e Habilita a interpretação de sequências de escape. Não existe, porque as sequências de escape estão habilitadas por padrão, como ditam as normas POSIX.
-E Inibe explicitamente a interpretação de sequências de escape. Não existe.

Repare que as opções -e e -E implementam outra especificação POSIX do comando echo: a interpretação de sequências de escape. Neste aspecto, os shells ash e dash seguem a norma, ou seja: eles interpretam sequências de escape por padrão e não oferecem opções para inibir este comportamento.

É justamente por essa variabilidade nas implementações que, quando a portabilidade for um requisito, o uso do echo deve ser limitado apenas aos casos onde se quer produzir uma saída de texto seguida de uma quebra de linha. Se precisarmos remover a quebra de linha ou da interpretação de sequências de escape, a nossa ferramenta portável será o comando printf.

O quase desconhecido 'printf'

No extremo oposto da popularidade do echo, encontra-se um dos comandos internos mais poderosos do shell: o printf. Ambos imprimem argumentos na saída padrão, mas o printf permite que a saída seja formatada e até convertida de muitas formas.

Sintaxe geral no Bash

A implementação do comando no Bash tem a seguinte sintaxe:

printf [-v VAR] FORMATO [ARGUMENTOS]

Nas implementações POSIX:

printf FORMATO [ARGUMENTOS]

Entenda os elementos da sintaxe:

Elemento Descrição
-v VAR Embora seja extremamente útil, a opção -v VAR, que permite enviar a saída para uma variável em vez de exibi-la na saída padrão, é um "mimo" implementado em shells como o Bash e o KornShell, mas não deve ser utilizada em scripts que pretendem ser portáveis.
FORMATO Em relação ao echo, o grande diferencial do printf está na string de formato: um primeiro argumento que descreve as conversões e os formatos que serão aplicados aos demais argumentos.
ARGUMENTOS Nós podemos imprimir diretamente a string de formato, mas o verdadeiro poder do printf está na possibilidade de aplicar formatações e conversões a uma lista de argumentos.

Vejamos alguns exemplos:

Por padrão, o 'printf' não inclui a quebra de linha final

:~$ printf banana
banana:~$

Logo, é uma alternativa portável ao 'echo -n'

:~$ echo -n zebra
zebra:~$

:~$ printf zebra
zebra:~$

Incluindo a quebra de linha final (\n)

:~$ printf 'banana\n'
banana
:~$

Observe que \n é uma sequência de escape que está sendo interpretada por padrão no primeiro argumento.

Se não escaparmos, de algum modo, a contra-barra (\), o shell entenderá que queremos tornar literal o caractere que vier em seguida:

:~$ printf banana\n
bananan:~$

Escapando...

:~$ printf 'banana\n'
banana
:~$ printf "banana\n"
banana
:~$ printf banana\\n
banana

Imprimindo strings separadas por quebras de linha (\n)

:~$ printf 'banana\nlaranja\n'
banana
laranja
:~$

Definindo onde os argumentos serão inseridos

:~$ printf '%s\n%s\n' abacate pitanga
abacate
pitanga
:~$

O caractere s precedido do símbolo % é um dos vários especificadores de formato, que servem, tanto para guardar o lugar dos argumentos na string de formato, quanto para especificar como os dados nos argumentos deverão ser tratados.

O primeiro argumento sempre será tratado como uma string de formato: se não contiver especificadores de formato, apenas ele será impresso:

:~$ printf abacate pitanga
abacate:~$

Todo espaçamento entre argumentos deve constar na string de formato

:~$ printf '%s%s\n' abacate pitanga
abacatepitanga
:~$

:~$ printf '%s %s\n' abacate pitanga
abacate pitanga
:~$

:~$ printf '%s    %s\n' abacate pitanga
abacate    pitanga
:~$

Imprimindo sequências de escape

As sequências de escape, por padrão, são interpretadas apenas na string de formato:

:~$ printf '%s\n%s\n' zebra jacaré
zebra
jacaré
:~$

:~$ printf '%s%s' 'zebra\n' 'jacaré\n'
zebra\njacaré\n:~$

Com o especificador %b, as sequências de escape em argumentos serão interpretadas:

:~$ printf '%b%b' 'zebra\n' 'jacaré\n'
zebra
jacaré
:~$

Nota: pessoalmente, eu adoto o princípio da separação entre dados e formatação: para mim, implícito na própria razão de existir um comando printf. Assim, eu raramente utilizo o especificador de formato %b, dando preferência ao uso de sequências de escape apenas na string de formato.

Equivalentes portáveis

Como pudemos observar nos exemplos, o printf pode substituir perfeitamente o echo quando a portabilidade for importante:

echo -n banana laranja
printf 'banana laranja'

echo -e 'banana\nlaranja'
printf '%s\n%s\n' banana laranja
printf '%b' 'banana\nlaranja\n'

Especificadores de formato

Relembrando a sintaxe do printf:

# Bash e KornShell
printf [-v VAR] FORMATO [ARGUMENTOS]

# Implementações POSIX
printf FORMATO [ARGUMENTOS]

Tendo em conta que a opção -v VAR não é portável, vamos continuar nos referindo à string FORMATO como primeiro argumento do printf.

O primeiro argumento do printf é uma string que pode conter caracteres literais, sequências de escape e especificadores de formato. Os especificadores de formato são sequências de caracteres iniciadas pelo sinal de percentual (%) seguido, opcionalmente, de um modificador e de um caractere alfabético que determinará o tipo de formatação ou conversão a ser aplicada ao argumento a que estiver associado.

Na tabela abaixo, nós temos os especificadores de formato interpretados pelo Bash e suas respectivas descrições.

Especificador Descrição
%s Interpreta o argumento associado literalmente, como uma string.
%b Imprime o argumento associado interpretando as sequências de escape que existirem nele.
%q Imprime o argumento associado citando os caracteres especiais do shell.
%d ou %i Imprime o argumento associado como um número decimal com sinal.
%u Imprime o argumento associado como um número decimal sem sinal.
%o Imprime o argumento associado como um número octal sem sinal.
%x Imprime o argumento associado como um número hexadecimal sem sinal e dígitos minúsculos (a-f).
%X O mesmo que %x, mas com dígitos maiúsculos (A-F).
%f Interpreta e imprime o argumento associado como um número de ponto flutuante (o caractere separador de casas decimais depende da localização do sistema).
%e Interpreta o argumento associado como um número de dupla precisão (double) e o imprimie com notação exponencial (N±eN).
%E O mesmo que %e, mas com o E maiúsculo.
%g Interpreta o argumento associado como um número de dupla precisão (double) e o imprime como %f e %e.
%G O mesmo que %g, mas com E maiúsculo.
%c Imprime o primeiro caractere do argumento associado.
%n Atribui o número de caracteres impressos até o momento a uma variável com o nome do argumento associado.
%a Interpreta o argumento associado como um número de dupla precisão (double) e o imprime no formato de constantes hexadecimais com ponto flutuante (C99).
%A O mesmo que %a, mas com P e dígitos hexadecimais maiúsculos.
%(FORMATO)T Imprime a data e hora desde o início da era Unix resultantes do valor em segundos no argumento associado com a formatação definida em FORMATO. Se o argumento for -1 ou não houver argumentos, serão formatadas a data e a hora correntes. Se o argumento for -2, serão formatadas a data e a hora do início da sessão do shell.
%% Imprime o caractere %.

Portabilidade dos especificadores de formato

Nem todos os especificadores de formato acima são implementados em todos os shells compatíveis com o Bourne Shell. Neste caso, as normas POSIX não limitam a implementação de especificadores, elas apenas dizem o que não precisa ser implementado, quais são esperados e como devem ser seus comportamentos.

Nos meus testes com o dash e o ash, somente os especificadores %q e %(FORMATO)T falharam, mas nós sempre podemos testar no terminal antes de contarmos com eles.

Modificadores

Sempre de acordo com o tipo de especificador de formato, nós podemos modificar a forma pela qual os argumentos serão impressos. Os modificadores, a seguir, são utilizados entre o % e o caractere da especificação do formato.

Modificador Descrição
N Um número inteiro informando a largura mínima de impressão, que é a quantidade total de colunas de terminal utilizadas pela impressão do argumento. Preenche com espaços as colunas não utilizadas, mas imprime todos os caracteres do argumento, caso ele seja mais longo do que a largura mínima.
. Incluindo um ponto seguido de um número inteiro, a largura informada torna-se a quantidade máxima de colunas que a impressão do argumento pode ocupar, fazendo com que a impressão do argumento seja truncada se exceder a definição. Se o número não for informado (%.s) ou se o número for zero (%.0s), forçará a largura da impressão para zero colunas de terminal, o que resulta na ocultação da impressão.
* Recebe largura da impressão como um argumento numérico antes do argumento que será impresso.
# Aplica notações de bases numéricas na impressão de números octais (prefixo 0) e hexadecimais (prefixo 0x).
- Alinha a impressão do argumento à esquerda da largura mínima especificada (o padrão é alinhar à direita).
0 Em argumentos numéricos, preenche com zeros os espaços não utilizados de uma largura mínima de impressão.
ESPAÇO Inclui um espaço antes de números positivos para que fiquem alinhados com números negativos.
+ Força a exibição de todos os números decimais com os sinais - (números negativos) ou + (números positivos).
' Imprime números decimais com separadores de milhares na parte inteira segundo a definição da variável LC_NUMERIC.

Largura mínima e alinhamento

Exemplos: %10s %5d %#5o

:~$ printf '%10s%5d%#5o\n' banana 17 85
    banana   17 0125
         ↑    ↑    ↑
····|····|····|····|
colunas 10   15   20

Por padrão, o alinhamento na largura é feito à direita, mas nós podemos alinhar à esquerda incluindo um traço (-) antes da quantidade de espaços:

:~$ printf '%-10s%5d%#5o\n' banana 17 85
banana       17 0125
         ↑    ↑    ↑
····|····|····|····|
colunas 10   15   20

Largura máxima de impressão

Exemplos: %.10s %.10d

:~$ printf '%10s%10d\n' 'linha de pesca' 12345
linha de pesca     12345
         ↑    ↑    ↑
····|····|····|····|
colunas 10   15   20

Aqui, linha de pesca tem 14 caracteres, o que excede a quantidade definida de 10 colunas e desloca todo o restante da impressão. Com o modificador ponto (.), os caracteres excedentes serão truncados:

:~$ printf '%.10s%10d\n' 'linha de pesca' 12345
linha de p     12345
         ↑    ↑    ↑
····|····|····|····|
colunas 10   15   20

Informando largura mínima como argumento

Exemplos: %*s %*d %#*o

:~$ printf '%*s%*d%#*o\n' 10 banana 5 17 5 85
    banana   17 0125
         ↑    ↑    ↑
····|····|····|····|
colunas 10   15   20

Para tornar o número uma largura máxima, o ponto (.) ainda deve ser informado na string de formato:

:~$ printf '%.*s%*d\n' 10 'linha de pesca' 10 12345
linha de p     12345
         ↑    ↑    ↑
····|····|····|····|
colunas 10   15   20

Notações de números octais e hexadecimais

Observe que os argumentos numéricos podem ser convertidos para as bases 8, 10 e 16. Contudo, os valores impressos podem ser confusos:

:~$ printf 'Decimal: %d\nOctal: %o\nHexa: %x\n' 32 32 32
Decimal: 32
Octal: 40
Hexa: 20

Para que a impressão evidencie as bases de numeração, vamos utilizar o modificador #:

:~$ printf 'Decimal: %#d\nOctal: %#o\nHexa: %#x\n' 32 32 32
Decimal: 32
Octal: 040
Hexa: 0x20

Alinhamento de números com sinais

Imprimindo uma coluna de argumentos numéricos decimais com e sem sinais, eles podem acabar desalinhados:

:~$ printf '%d\n%d\n%d\n' 10 -10 100
10
-10
100

Incluindo um espaço entre % e d, todos os números sem sinal receberão um espaço à esquerda:

:~$ printf '% d\n% d\n% d\n' 10 -10 100
 10
-10
 100

Todos com sinais

Em vez do espaço, podemos forçar a exibição de todos os decimais inteiros com seus respectivos sinais:

:~$ printf '%+d\n%+d\n%+d\n' 10 -10 100
+10
-10
+100

Zeros à esquerda

Para argumentos numéricos, nós podemos preencher as colunas de terminal não utilizadas pela impressão do argumento com zeros:

:~$ printf '%010d\n' 12345
0000012345
         ↑
····|····|
colunas 10

:~$ printf '%#010X\n' 012345
0X000014E5
         ↑
····|····|
colunas 10

Localização de decimais

A parte inteira de números decimais pode ser agrupada em milhares de acordo com a definição na variável LC_NUMERIC. No caso da minha localidade (pt_BR.UTF-8), a separação é feita com pontos:

:~$ locale | grep LC_NUMERIC
LC_NUMERIC="pt_BR.UTF-8"

:~$ printf "%'d\n" 12345678
12.345.678

Formatação de moeda

A localização de números decimais pode ser útil para a formatação de moeda se utilizanda em conjunto com a formatação de números com ponto flutuante (%f) especificando a quantidade das casas decimais:

:~$ printf "R$ %'.2f\n" 12345678,75
R$ 12.345.678,75

Para ser válido, um número válido, o argumento deve informar a separação de casas decimais igualmente definida para a localidade (no meu caso, a vírgula). Se tentarmos utilizar o ponto, haverá um erro e as casas decimais serão ignoradas:

:~$ printf "R$ %'.2f\n" 12345678.75
bash: printf: 12345678.75: número inválido
R$ 12.345.678,00
              ↑
    casas decimais ignoradas

Observe também que %.2f é a mesma definição de uma largura (2) antecedida de um ponto: ou seja, é uma largura mínima que será aplicada apenas ao número que vier depois do separador de casas decimais. Neste caso, o alinhamento da parte decimal será à esquerda e os zeros serão incluídos para preencher as colunas de terminal não ocupadas à direita:

:~$ printf '%.4f\n' 123,45
123,4500

Resumo da ópera

Apesar de todo seu poder, o printf só será um substituto para o echo onde, em havendo o requisito da portabilidade, o echo precisar ser utilizado com opções as -e ou -n. Fora isso, não se trata de um comando ser melhor do que o outro, mas de comandos diferentes com aplicações diferentes.

Isso reforça o que eu tenho defendido sempre que tenho oportunidade: não se aprende nada, muito menos o shell, a partir de regras sem argumentação, tabelas de comandos ou simplificações. Você não precisa saber de cor todas as capacidades de todos os comandos e programas disponíveis para uso no shell (ninguém é capaz disso). Porém, encarando os problemas com método, buscando técnicas em vez de comandos, é possível construir uma base sólida para encontrar as soluções e transformar problemas em conhecimento.

Músico, programador, designer e apaixonado pelos ideais do Software Livre.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Post comment