Sed salva o dia novamente

Já perdi a conta de quantas vezes o sed me salvou.

Ele é meu comando preferido do Unix. É simples, direto, minimalista, poderoso, enigmático. E claro, faz uso intensivo de expressões regulares, que é minha outra paixão.

Pórem, esta aparente simplicidade esconde um programa muito poderoso, que pode fazer as mais complicadas edições em textos, de maneira automatizada. Precisou substituir, apagar, inserir, juntar, quebrar, inverter, duplicar um texto? Sed nele!

Também dá pra fazer jogos com o sed, se você for desocupado o suficiente ;)

Neste sábado, precisei do sed mais uma vez.

A 5ª edição do livro Expressões Regulares será lançada daqui alguns dias, e a grande novidade desta vez é que além da versão impressa, também terá a versão digital (ebook). Finalmente! 🎉

Porém, quando fui testar o ebook que a editora gerou, todos os códigos-fonte listados no livro apareceram sem alinhamento (indent), com todas as linhas começando no canto esquerdo :(

Os fontes originais do livro estão no InDesign, que é o programa que a editora usa para a diagramação, e foram exportados para o formato EPUB para gerar o ebook. Felizmente, o formato EPUB é editável, pois nada mais é do que um arquivo ZIP com arquivos XHTML e CSS dentro, então o descompactei e fui fuçá-lo.

Aqui está o trecho XHTML que gera o código da foto anterior:

<p class="_CodigoFontePrimLin1"># Cada domínio está numa subpasta em /sites</p>
<p class="_CodigoFonte1">server {</p>
<p class="_CodigoFonte1">    server_name ~ "^(www\.)?(?&lt;dominio&gt;.+)$";
</p>
<p class="_CodigoFonte1">    location / {</p>
<p class="_CodigoFonte1">        root /sites/$dominio;</p>
<p class="_CodigoFonte1">    }</p>
<p class="_CodigoFonte1">}</p>

Como deveria ser:

<pre>
# Cada domínio está numa subpasta em /sites
server {
    server_name ~ "^(www\.)?(?&lt;dominio&gt;.+)$";
    location / {
        root /sites/$dominio;
    }
}
</pre>

Por algum motivo sobrenatural, cada linha do código-fonte virou um parágrafo diferente (tag <p>…</p>), em vez de tudo ser um único bloco com a tag <pre>…</pre>.

Se fosse poucos casos eu até arrumaria na mão mesmo, mas contei aqui (na verdade, o grep | wc -l contou) e são 340 blocos de código a serem arrumados no total. É, vai ter que rolar um sed macho...

Vamos analisar o formato geral dos blocos de código:

qualquer coisa antes
<p class="_CodigoFontePrimLin1">primeira linha do bloco</p>
<p class="_CodigoFonte1">segunda linha do bloco</p>
<p class="_CodigoFonte1">terceira linha do bloco</p>
...
<p class="_CodigoFonte1">última linha do bloco</p>
qualquer coisa depois

O maior problema aqui é identificar o início e o fim dos blocos. O início tudo bem, está identificado pela classe _CodigoFontePrimLin1. Já o final, não possui uma marcação especial. Eu só sei que terminou o bloco de código quando aparece uma linha que não tem a classe _CodigoFonte1.

Se tivesse uma marcação clara de início e fim, seria bem fácil. O sed já suporta nativamente esta notação, basta colocar os padrões entre barras, antes dos comandos, assim:

sed '/início/,/fim/ { comandos; }'

Como não posso usar isso, pois não tenho um padrão claro de término do bloco, terei que usar outra tática: identificar o início do bloco, entrar num loop que vai lendo as próximas linhas e só sair do loop quando encontrar uma linha que não faça parte do bloco. Esse é o esqueleto do comando:

sed '
  /início/ {

    # insere a tag <pre> no início

    # inicia o loop
      # remove as tags <p> e </p>
      # lê a próxima linha
      # continua no loop se for uma linha do bloco

    # insere a tag </pre> no final
  }
'

O primeiro passo é fazer o padrão que identifica o ínicio dos blocos. Já vimos que a classe _CodigoFontePrimLin1 foi usada no início deles. Porém, vai que algum bloco está marcado errado e não usou essa classe? Bem, eu sei com certeza que todas as linhas de código possuem uma classe cujo nome começa com _CodigoFonte, então vou preferir usar esse padrão mais abrangente para não deixar de fora nenhum bloco.

/_CodigoFonte/ {

  # insere a tag <pre> no início

  # inicia o loop
    # remove as tags <p> e </p>
    # lê a próxima linha
    # continua no loop se for uma linha do bloco

  # insere a tag </pre> no final
}

Agora vamos para o loop. Além de montar a estrutura do loop, o que eu tenho que fazer em todas as linhas do bloco, é apagar as tags <p> e </p> feiosas. Farei isso em dois passos, com dois s/// para ficar mais simples a expressão regular.

/_CodigoFonte/ {

  # insere a tag <pre> no início

  # inicia o loop
  :meuloop

    # remove as tags <p> e </p>
    s/^[[:blank:]]*<p [^>]*>//
    s/[[:blank:]]*<\/p>$//

    # lê a próxima linha
    n

    # continua no loop se for uma linha do bloco
    /_CodigoFonte/ b meuloop

  # insere a tag </pre> no final
}

O loop em sed é feito marcando um ponto qualquer no script com o comando : seguido de um nome (no exemplo usei :meuloop) e depois você usa o comando b (de Branch), para pular para o ponto marcado (sim, é um GOTO). Só perceba que meu comando b não é incondicional, ele só é aplicado se a linha atual tiver o padrão _CodigoFonte. Esse é o jeitão do sed de fazer um if.

Então beleza, nesse ponto meu script já identifica os blocos de código-fonte e apaga todas as tags <p> dentro deles. As outras linhas do livro permanecem intactas. O que está faltando agora é inserir a tag <pre> no início e a </pre> no fim do bloco. Isso é fácil com o comando i (de Insert). A sintaxe dele é estranha, exigindo uma quebra de linha escapada, mas funciona.

/_CodigoFonte/ {

  # insere a tag <pre> no início
  i \
<pre>

  # inicia o loop
  :meuloop

    # remove as tags <p> e </p>
    s/^[[:blank:]]*<p [^>]*>//
    s/[[:blank:]]*<\/p>$//

    # lê a próxima linha
    n

    # continua no loop se for uma linha do bloco
    /_CodigoFonte/ b meuloop

  # insere a tag </pre> no final
  i \
</pre>

}

E pronto!

O sed é extremamente rápido. Num piscar de olhos ele vai editar o livro todo, arrumando todos os blocos. Aí é só compactar tudo de volta e o arquivo EPUB estará do jeito que Jesus gosta: com todos os códigos-fonte bem alinhados :)

Claro que a “vida real” é mais dura do que isso, e tive que fazer mais alguns sed para lidar com falsos-positivos, trocar tabs por espaços, remover tags vazias de índice remissivo e fazer ajustes no arquivo CSS.

Veja como ficou o script completo:

— EOF —

Gostou desse texto? Aqui tem mais.