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!
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
.
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
.
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.
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:
:~$ printf banana
banana:~$
Logo, é uma alternativa portável ao 'echo -n'
:~$ echo -n zebra
zebra:~$
:~$ printf zebra
zebra:~$
:~$ 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
:~$ printf 'banana\nlaranja\n'
banana
laranja
:~$
:~$ 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:~$
:~$ printf '%s%s\n' abacate pitanga
abacatepitanga
:~$
:~$ printf '%s %s\n' abacate pitanga
abacate pitanga
:~$
:~$ printf '%s %s\n' abacate pitanga
abacate pitanga
:~$
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.
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'
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 à stringFORMATO
como primeiro argumento doprintf
.
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 % . |
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 oash
, somente os especificadores%q
e%(FORMATO)T
falharam, mas nós sempre podemos testar no terminal antes de contarmos com eles.
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 . |
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
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
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
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
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
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
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
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
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
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.