Branches no sed

Thobias Salazar Trevisan
13/06/2003

Sed é quase uma linguagem de programação. Ela nos oferece desvios (branches), ou seja, nos permite pular de um ponto do script para outro, como se fosse o comando GOTO de algumas linguagens. O sed oferece dois tipos de desvio:

  1. t, desvio condicional, ou seja, pula para um determinado ponto do script se a última substituição (s///) foi executada com sucesso.
  2. b, desvio incondicional, ou seja, chegou no desvio sempre pula para um determinado ponto do script.
Nos dois casos, se nenhum ponto for especificado, pula para o fim do script.

Já sabemos fazer os desvios, mas como determinar para qual ponto do script devemos pular ?

Simples, o sed nos possibilita a utilização de label. Um label começa com o sinal de dois-pontos (:) e marca uma posição no script.

Vamos praticar!! As vezes precisamos executar algum comando sed N vezes, mas não sabemos o valor de N. O exemplo clássico é quando precisamos trocar um caractere por outro até encontrarmos um determinado caractere. Exemplo: trocar 1 por 0 até encontrarmos a primeira vírgula ','. Então se tivessemos como entrada o número 121,213, gostariamos de transformá-lo em 020,213. Nossas primeiras tentativas seriam:

  prompt> echo 121,213 | sed 's/1/0/'
  021,213
  
  prompt> echo 121,213 | sed 's/1/0/g'
  020,203
  
  prompt> echo 121,213 | sed 's/^\([^,]*\)1/\10/'
  120,213
  
  prompt> echo 121,213 | sed 's/^\([^,]*\)1/\10/g'
  120,213

Todas falhariam. Na primeira tentativa, trocamos somente a primeira ocorrência de 1 por 0. Na segunda, utilizando a opção g da substituição, trocamos todos os 1s por 0s sem respeitar se eles estão antes ou depois da nossa tag (a vírgula). Na terceira tentativa, nós especificamos que queremos o caractere 1 antes de uma vírgula, mas neste caso, como o sed é guloso ele trocaria somente uma ocorrência que satisfaça o que especificamos, ou seja, um caractere 1 antes de uma vírgula. Na última opção, adicionamos o g, para trocarmos todos os 1 por 0. Mas também falhou, pois como o sed lê a linha e executa o comando uma única vez, ele irá procurar satisfazer o que especificamos, ou seja, um caractere 1 antes de uma vírgula.

Para resolver este problema, precisamos usar o desvio. Relembrando, queremos trocar todas as ocorrências do caractere 1 antes de uma vírgula por 0. O comando para solucionar o problema é:

  prompt> echo 121,213 | sed ':a;s/^\([^,]*\)1/\10/;ta'
  020,213

No início marcamos um label (:a), após realizamos a substituição do caractere 1 antes de uma vírgula (s/^\([^,]*\)1/\10/). Caso ocorra sucesso na substituição, pulamos para o label a (ta). Note que quando realizamos o desvio, o sed não irá ler a próxima linha, e sim executar o mesmo comando sobre a linha, mas a linha modificada pela substituição anterior. Assim, executamos a substituição enquanto encontramos algum caractere 1 antes de uma vírgula.

Passo-a-passo: Na primeira execução, ocorre sucesso na substituição, assim a linha 121,213 vira 120,213, como ocorreu sucesso na substituição pulamos com esta linha para o label 'a'. Então, sobre a linha 120,213 ocorre uma nova substituição, virando assim 020,213. Como ocorreu sucesso na substituição, pulamos novamente para o label 'a'. Deste modo, tentamos de novo realizar a substituição sobre a linha 020,213. Neste caso, ocorre falha, pois não existe mais o caractere 1 antes de uma vírgula e assim não pulamos para o label 'a' e o sed irá ler a próxima linha que é EOF, assim encerrando sua execução.

Só para testar, vamos experimentar trocar o desvio condicional (t) pelo desvio incondicional (b).

  prompt> echo 121,213 | sed ':a;s/^\([^,]*\)1/\10/;ba'

A execução entra em loop infinito. Nossa entrada após a primeira execução vira 120,213, após a segunda vira 020,213. Na próxima execução não ocorrerá mais nenhuma substituição, mas como nós não levamos em conta o sucesso ou não, nós simplesmente pulamos para o label 'a' e o sed não lê a próxima linha, ficaremos neste loop para sempre.

Para você não achar que o 'b' não é útil, vamos a um exemplo do seu uso. Imagine que queremos imprimir somente da linha que contém a palavra 'dois' até a que contém a palavra 'cinco'. Como entrada tempos o seguinte arquivo:

  prompt> cat arquivo.txt
  um
  dois
  três
  quatro
  cinco
  seis

Mas note que não sabemos quantas linhas existem antes da linha com 'dois', entre a linha 'dois' e 'cinco' e após a linha 'cinco'. Nossa primeira tentativa é:

  prompt> sed -n '/dois/,/cinco/p' arquivo.txt
  dois
  três
  quatro
  cinco

Funcionou perfeito, mas não sabemos quantas linhas existem antes de 'dois' e 'cinco', assim elas podem estar na mesma linha. Vamos testar esta solução.

  prompt> cat arquivo.txt
  um
  dois cinco
  três
  seis
  
  prompt> sed -n '/dois/,/cinco/p' arquivo.txt
  dois cinco
  três
  seis

Ou seja, esta solução não funciona se as palavras estiverem na mesma linha, visto que ele imprimiu da linha que contém 'dois' até o final do arquivo. Para solucionar vamos usar o desvio incondicional.

  prompt> sed -n '/dois/{:a;/cinco/!{N;ba;};p;}' arquivo.txt
  dois cinco

Quando chegamos na linha que contém 'dois' realizamos o que está entre chaves {:a;/cinco/!{N;ba;};p;}. Primeiro marcamos um label 'a', depois testamos se a linha contém a palavra 'cinco', caso contenha, não executamos o {N;ba;}, imprimimos a linha ';p;}' e encerramos. Isto é o que ocorre se 'dois' e 'cinco' estão na mesma linha. Caso elas não estejam ocorre o seguinte.

  prompt> cat arquivo.txt
  um
  dois
  três
  quatro
  cinco
  seis
  
  prompt> sed -n '/dois/{:a;/cinco/!{N;ba;};p;}' arquivo.txt
  dois
  três
  quatro
  cinco

Na linha que contém dois executamos o que está entre chaves '{:a;/cinco/!{N;ba;};p;}', primeiro marcamos um label 'a', então testamos se a linha contém a palavra 'cinco', como não contém executamos o que está entre chaves '{N;ba;}', ou seja, lemos a próxima linha e pulamos para para o label 'a'. Testamos novamente, caso não exista lemos a próxima linha e pulamos para o label 'a'. Entramos neste loop até encontrarmos uma linha que contém a palavra 'cinco'. Quando encontrarmos não entraremos nas chaves {N;ba;}, imprimimos o que está no espaço padrão ';p;}', ou seja, todas as linhas entre 'dois' e 'cinco' incluindo as mesmas, depois lemos a próxima linha (se existir) e voltamos a repetir todo o processo.

Simples não !? Agora é so usufruir destas características no seus SEDs. :)