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