Branches no sed

Thobias Salazar Trevisan
07/07/2003

Tipos de desvios

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 para clariar as idéias =8)

t desvio condicional

Às 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,123, gostariamos de transformá-lo em 020,123. Nossas primeiras tentativas seriam:

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

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, 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,123 | sed ':a;s/^\([^,]*\)1/\10/;ta'
  020,123

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,123 vira 120,123, como ocorreu sucesso na substituição pulamos com esta linha para o label 'a'. Então, sobre a linha 120,123 ocorre uma nova substituição, virando assim 020,123. 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,123. 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,123 | sed ':a;s/^\([^,]*\)1/\10/;ba'

A execução entra em loop infinito. Nossa entrada após a primeira execução vira 120,123, após a segunda vira 020,123. 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".

b desvio incondicional

Para ilustrar o seu funcionamento utilizaremos um exemplo de como usar funções em sed, em outras palavras, o uso tradicional do GOTO.

Vamos ver um "diagrama" do fluxo de execução. Eu sei que ficou tosco, mas meu lado artista ainda nao desabrochou =8)

  prompt> cat func.sed
  #!/bin/sed -f
  #
  # exemplo: emulando o uso de funções com o sed
  #
  
  # início da execução, pulamos para o label 'main'
  b main
  
  # código da função 1. Ela troca vogais de minúsculas 
  # para maiúsculas
  :func1
  	s/f1/f1\
  /
  	y/aeiou/AEIOU/
  	# lemos a próxima linha ('instrução')
  	n
  	# pulamos para o 'main'
  	b main
  
  # código da função 2. Ela troca espaço por \n (newline)
  # e coloca três espaços no início da linha
  :func2
  	#troca espaço por \n
  	s/ /\
     /g
      # lê a próxima linha
  	n
  	# pula pro label 'main'
  	b main
  
  # função principal, que recebe a função desejada com seus 
  # parâmetros e chama a função.
  :main
  	# se recebemos f1, pulamos para o label 'func1'
  	/f1/ b func1
  	# se recebemos f2, pulamos para o label 'func2'
  	/f2/ b func2
  	# se recebemos sair, finalizamos a execução (q)
  	/sair/ q
  	# se não entrou em nenhuma destas opções, é opção desconhecida
  	s/.*/opcao invalida/

A idéia é usar o 'f[12]' para especificar a função desejada e o 'b label' para pular de um ponto para outro do programa, alterando o fluxo 'normal' de execução. Então, para testarmos executamos este script sed:

  prompt> ./func.sed 
  texto digitado via teclado
  opcao invalida
  f1 chamando a funcao 1
  f1
   chAmAndO A fUncAO 1
  f2 a funcao 2 troca espaco por newline
  f2
     a
     funcao
     2
     troca
     espaco
     por
     newline
  nenhuma opcao
  opcao invalida
  sair
  sair
  prompt> 

Podemos chamar este script passando as instruções via "|" (pipe).

  prompt> echo -e 'f1 bagunce estas vogais \nf2 bagunce estas vogais' | ./func.sed
  f1
   bAgUncE EstAs vOgAIs 
  f2
     bagunce
     estas
     vogais
  prompt> 
  prompt> 
  prompt> echo -e 'f1 bagunce estas vogais \n sair \n f2 bagunce estas vogais' | ./func.sed
  f1
   bAgUncE EstAs vOgAIs 
   sair 
  prompt>

Note que na segunda execução, colocamos a palavra 'sair', então o script ao encontrá-la, finaliza sua execução sem executar a próxima linha, no caso o 'f2'.

Vamos à um outro exemplo do uso do desvio incondicional.

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 a palavra 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. :)


This HTML page is (see source)