você está aqui: Home  → Arquivo de Mensagens Programe sua mente para aprender inglês - ebook gratuito

Uma longa discussão sobre o printf

Colaboração: Julio Cezar Neves

Data de Publicação: 11 de dezembro de 2018

Essa é uma dica longa porque foi uma thread que começou com uma dúvida e durou diversos dias em uma lista de discussão de Shell. O noso colega de lista queria fazer preenchimento à esquerda com o símbolo # para fazer bloqueio de um campo. Para tal, ele fez um for cheio de álgebra, para calcular quantas voltas daria o loop, já que o tamanho total do campo era 15, mas a quantidade de algarismos de $Valor era variável. A minha proposta foi:

$ Valor=123,45
$ Valor=$(printf "%15s\n" $Valor) 
$ tr ' ' '#' <<< "$Valor" 
#########123,45

Ou seja, o printf preencheu com espaços completando as 15 posições predeterminadas e o tr trocou os espaços pelos caracteres de bloqueio (#). Fique atento: se você não proteger com aspas a variável $Valor, os espaços em branco inseridos pelo printf serão perdidos durante a execução do tr.

Mas o printf possui a opção -v, com a qual podemos indicar o nome de uma variável que receberá a saída do comando. Assim sendo, obteríamos o mesmo resultado fazendo:

$ Valor=123,45
$ printf -v Valor "%15s\n" $Valor
$ printf '%s\n' ${Valor// /#}
#########123,45

Isso por si só, já aceleraria bastante, pois evitaria o fork produzido pela substituição de comandos ($(..)), mas também usamos uma Expansão de Parâmetros, onde ${Valor// /#} que atua como um comando tr só que muito mais veloz, ela serve para trocar todos os espaços em branco por #.

Já que falamos nessa Expansão de Parâmetros, olha uma forma legal de se corrigir a máscara de um endereço de hardware (mac address) em maiúsculas ou minúsculas, a seu gosto:

$ Mac=0:13:ce:7:7a:ad
$ printf -v Mac "%02x:%02x:%02x:%02x:%02x:%02x" 0x${Mac//:/ 0x}
$ echo $Mac
00:13:ce:07:7a:ad
$ printf -v Mac "%02X:%02X:%02X:%02X:%02X:%02X" 0x${Mac//:/ 0x}
$ echo $Mac
00:13:CE:07:7A:AD

Nesse exemplo, repare o que a Expansão de Parâmetros fez:

$ Mac=0:13:ce:7:7a:ad
$ echo 0x${mac//:/ 0x}
0x0 0x13 0xce 0x7 0x7a 0xad

Agora que todos viraram hexadecimais, foi só meter um printf para formatar.

Seguindo a discussão da lista de Shell da qual falávamos antes da Expansão de Parâmetros interromper o nosso papo, alguém começou a falar em fazer uma linha tracejada com 20 caracteres traço (-) e aí apareceram as seguintes propostas:

  • O processo caretão, estilo "NPF" (Não Pense, Faça!):

    $ printf '%s\n' --------------------

  • Da forma interativa e lenta:

    Basta um loop (feito com o for) para imprimir um hífen (com printf) vinte vezes e no final saltar linha (echo).

    $ for ((x = 0; x < 20; x++)); do
    >     printf %s -
    > done; echo
    

  • Usando o processo que acabamos de ver no exemplo de bloqueio de valores, só que agora não usaremos o #, e sim o -:

    $ printf -v Espacos %20s
    $ echo "${Espacos// /-}"
    --------------------
    

Assim procedendo, a opção -v jogou a saída do primeiro printf (vinte espaços em branco) na variável $Espacos e a Expansão de Parâmetros substituiu os espaços em branco por hifens, gerando o resultado desejado.

  • Para passar uma linha por todo o terminal (Dica 1)

    $ printf "%$(tput cols)s\n" ' ' | tr ' ' -

Nesse caso o tput cols devolveu a quantidade de colunas da janela e o printf, juntamente com o tr, executou o que desejávamos, como já vimos nos outros exemplos.

  • Para passar uma linha por todo o terminal (Dica 2):

    Não é tão boa como a primeira dica, mas serve porque é didática.

    $ Tracos="--------------------------------------\
    —----------------------------------------------\
    —----------------------------------------------\
    —----------------------------------------------"
    $ printf '%s\n' "${Tracos:0:$(tput cols)}"
    
    Primeiramente criei uma linha com muitos hifens (mais do que suficientes para fazer o tracejado que desejamos), em seguida usei uma Expansão de Parâmetros que significa "extraia da variável $Tracos uma subcadeia que começa na posição 0 (início) e tem tput cols (quantidade de colunas da sessão corrente) caracteres".

    Poderíamos evitar a Expansão de Parâmetros usando o comando cut, porém ficaria mais lenta e mais complicada:
    $ printf '%s\n' $(cut -c 1-$(tput cols) <<< $Tracos)
    Nesse caso o cut cortou Tracos desde o 10 caractere (-c 1) até (-) o caractere na posição do tamanho da tela ($(tput cols)).

    De repente um colega mandou a seguinte dica para a nossa lista:

    "Só compartilhando uma funçãozinha que fiz aqui para desenhar caixas de mensagem (só funciona para mensagens com uma linha. Se alguém quiser alterar, fique à vontade)"

    E nos brindou com esse código:
    function DrawBox
    {
        string="$*";
        tamanho=${#string}
        tput setaf 4; printf "\e(0\x6c\e(B"
        for i in $(seq $tamanho)
            do printf "\e(0\x71\e(B"
        done
        printf "\e(0\x6b\e(B\n"; tput sgr0;
        tput setaf 4; printf "\e(0\x78\e(B"
        tput setaf 1; tput bold; echo -n $string; tput sgr0
        tput setaf 4; printf "\e(0\x78\e(B\n"; tput sgr0;
        tput setaf 4; printf "\e(0\x6d\e(B"
        for i in $(seq $tamanho)
            do printf "\e(0\x71\e(B"
        done
        printf "\e(0\x6a\e(B\n"; tput sgr0;
    }
    Seu uso seria da seguinte forma:

    $ DrawBox Qualquer frase que caiba no terminal

Qualquer frase que caiba no terminal

Só que essa caixa é azul (tput setaf 4) e as letras são vermelhas, com ênfase (tput setaf 1; tput bold).

Mas observe a tabela a seguir. Ela explica os códigos gerados por esse monte de printf estranho:

O printf Produz
\e(0\x6c\e(B +
\e(0\x71\e(B -
\e(0\x6b\e(B +
\e(0\x78\e(B |
\e(0\x6d\e(B +
\e(0\x6a\e(B +

Como eu tinha acabado de escrever os exemplos que vimos antes e ainda estava com eles na cabeça, sugeri que o for que ele usou para fazer as linhas horizontais fosse trocado; assim, substituiríamos:

for i in $(seq $tamanho)
    do printf "\e(0\x71\e(B"
done
Por:
printf -v linha "%${tamanho}s" ' '
printf -v traco "\e(0\x71\e(B"
echo -n ${linha// /$traco}
Tudo Shell puro, pois o for, o printf e o echo são built-ins, mas como o printf dentro do for seria executado tantas vezes quantos traços horizontais houvessem, imaginei que a minha solução fosse um pouco mais veloz e pedi-lhe para testar os tempos de execução, não sem antes apostar um chope. Ele criou dois scripts, um que executava mil vezes a função que ele havia proposto e outro que fazia o mesmo com a minha solução. Não vou dizer qual foi a forma mais veloz, porém adianto que o colega ainda está me devendo um chope... ;)

O código otimizado ficaria assim:

function DrawBox
{
    string="$*";
    tamanho=${#string}
    tput setaf 4; printf "\e(0\x6c\e(B"
    printf -v linha "%${tamanho}s" ' '
    printf -v traco "\e(0\x71\e(B"
    echo -n ${linha// /$traco}
    printf "\e(0\x6b\e(B\n"; tput sgr0;
    tput setaf 4; printf "\e(0\x78\e(B"
    tput setaf 1; tput bold; echo -n $string; tput sgr0
    tput setaf 4; printf "\e(0\x78\e(B\n"; tput sgr0;
    tput setaf 4; printf "\e(0\x6d\e(B"
    printf -v linha "%${tamanho}s" ' '
    printf -v traco "\e(0\x71\e(B"
    echo -n ${linha// /$traco}
    printf "\e(0\x6a\e(B\n"; tput sgr0;
}
Então agora, voltando aos nossos exemplos de passar uma linha por todo o terminal, posso sugerir uma outra (e mais bonita) solução:

  • Para passar uma linha por todo o terminal (Dica 3)
    printf -v linha "%$(tput cols)s" ' '
    printf -v traco "\e(0\x71\e(B"
    echo ${linha// /$traco}


Veja a relação completa dos artigos de Julio Cezar Neves