Todos sabemos que o sed é um canivete suíço e que trabalha com caracteres, ou seja, ele não sabe a diferença entre um dígito e uma letra. Para ele, tudo são caracteres. Então como poderíamos utilizar o sed para contar ?
Neste texto tentarei explicar uma técnica proposta por Bruno Haible (incr_num.sed) para incrementar um número em SED e, no final, vamos extrapolá-la para letras. Existem outras técnicas, como a utilizada por Greg Ubben na lendária implementação da calculadora DC em sed. O que, você não conhece ?! Então acesse http://sed.sourceforge.net/local/scripts/dc.sed.html e tenha uma boa diversão =8)
Bom, tudo são caracteres, então temos que tratá-los como tal. Para incrementarmos um número de 0 a 9 é fácil. Trocamos 0 por 1, 1 por 2, 2 por 3.... até 9. Ficando assim:
prompt> cat incr_tentativa_1.sed #!/bin/sed -f s/0/1/ s/1/2/ s/2/3/ s/3/4/ s/4/5/ s/5/6/ s/6/7/ s/7/8/ s/8/9/ s/9/mais de 1 digito/
Pronto, vamos testar:
prompt> echo 1 | ./incr_tentativa_1.sed mais de 1 digito prompt> prompt> echo 5 | ./incr_tentativa_1.sed mais de 1 digito prompt>
ops! não funcionou =8(
Bom, para nos auxiliar no compreendimento da execução e entender o que o sed está fazendo, vamos utilizar a ferramenta sedsed (http://sedsed.sourceforge.net), escrita pelo nosso grande amigo Aurélio. sedsed é um programa escrito em python para sed :) Ele é um debug (mostrando passo a passo o hold space, o pattern space e a instrução executada), ele indenta script sed e gera um html colorido do script. Vale a pena conferir este programa.
Vamos utilizar o sedsed para vermos o que está acontecendo. Como não estamos
utilizando o hold space em nosso script, vou passar uma opção (--hide=HOLD
)
para omití-lo. Além disso, a saída do sedsed foi feita para console,
utilizando cores para facilitar. Como aqui é html, vou utilizar o script
abaixo para deixar sua saída mais visual para o nosso exemplo.
prompt> cat limpa.sed #!/bin/sed -f s/^[^:]*:\(.*\)\$$/\1/ s/^COMM:/ / prompt>
Vamos testar nossa primeira solução para contar:
prompt> echo 1 | sedsed -d --hide=HOLD -f incr_tentativa_1.sed | limpa.sed 1 s/0/1/ 1 s/1/2/ 2 s/2/3/ 3 s/3/4/ 4 s/4/5/ 5 s/5/6/ 6 s/6/7/ 7 s/7/8/ 8 s/8/9/ 9 s/9/mais de 1 digito/ mais de 1 digito mais de 1 digito prompt>
Destrinchando a saída.
s/2/3/
), assim
virando 3. Este processo ocorre até a última instrução e, assim, temos
a saída mais de 1 digito
.
Mais um exemplo:
prompt> echo 5 | sedsed -d --hide=HOLD -f incr_tentativa_1.sed | limpa.sed 5 s/0/1/ 5 s/1/2/ 5 s/2/3/ 5 s/3/4/ 5 s/4/5/ 5 s/5/6/ 6 s/6/7/ 7 s/7/8/ 8 s/8/9/ 9 s/9/mais de 1 digito/ mais de 1 digito mais de 1 digito prompt>
Note que usamos como entrada o número 5 e, assim, não ocorreu sucesso na substituição
até a instrução (s/5/6/
). Como esperado, após a primeira substituição com sucesso
as demais também tiveram sucesso.
Moral da história. Temos que cuidar a ordem das substituições, senão.... =8)
Vamos inverter a ordem para garantir que tenhamos somente 1 execução com sucesso.
prompt> cat ./incr_tentativa_2.sed #!/bin/sed -f s/9/mais de 1 digito/ s/8/9/ s/7/8/ s/6/7/ s/5/6/ s/4/5/ s/3/4/ s/2/3/ s/1/2/ s/0/1/ prompt>
Testando:
prompt> echo 0 | ./incr_tentativa_2.sed 1 prompt> prompt> echo 1 | ./incr_tentativa_2.sed 2 prompt> prompt> echo 4 | ./incr_tentativa_2.sed 5 prompt> echo 8 | ./incr_tentativa_2.sed 9 prompt> echo 9 | ./incr_tentativa_2.sed mais de 2 digito prompt>
Massa!! sabemos contar até 9. =8)
E agora, como ultrapassar a barreira de 1 dígito ?!
A primeira técnica que poderíamos tentar era fazer o 9 virar 10 :)
prompt> cat incr_tentativa_3.sed #!/bin/sed -f s/9/10/ s/8/9/ s/7/8/ s/6/7/ s/5/6/ s/4/5/ s/3/4/ s/2/3/ s/1/2/ s/0/1/ prompt>
prompt> echo 22 | ./incr_tentativa_3.sed 32 prompt> prompt> echo 9 | ./incr_tentativa_3.sed 21 prompt>
Que isso, esse negócio está louco ?! Vamos chamar o nosso amigo para nos dar uma ajudinha.
Eu usei esta solução para nós enxergarmos 2 problemas que teremos que resolver mais adiante. Primeiro: olhando para este sed, nós achamos que ele deveria funcionar para números como 22, 45, 78, etc, ou seja, números fáceis que basta trocar um caractere, mas não funciona:
prompt> echo 22 | sedsed -d --hide=HOLD -f incr_tentativa_3.sed | limpa.sed 22 s/9/10/ 22 s/8/9/ 22 s/7/8/ 22 s/6/7/ 22 s/5/6/ 22 s/4/5/ 22 s/3/4/ 22 s/2/3/ <<< --- trocou o 2 por 3 32 s/1/2/ 32 s/0/1/ 32 32 prompt>
Note que a nossa entrada só muda quando chegarmos à instrução (s/2/3
). Mas
o sed irá trocar somente a primeira ocorrência de 2 e não a última como
nós gostaríamos.
prompt> echo 22 | sed 's/2/3/' 32 prompt>
Para resolver o problema teremos que usar o cifrão, para dizer ao sed que, nesses casos simples, ele deve só "somar" 1 no último "dígito".
prompt> echo 22 | sed 's/2$/3/' 23 prompt>
Beleza, primeiro problema resolvido, vamos ao segundo: o que nós esperávamos que funcionasse também não funcionou, que era fazer 9 virar 10.
prompt> echo 9 | sedsed -d --hide=HOLD -f incr_tentativa_3.sed | limpa.sed 9 s/9/10/ <<< --- beleza, o 9 virou 10 10 s/8/9/ 10 s/7/8/ 10 s/6/7/ 10 s/5/6/ 10 s/4/5/ 10 s/3/4/ 10 s/2/3/ 10 s/1/2/ <<< --- entrada 10, casou o 1 virando 20 20 s/0/1/ <<< --- casou o 0, virou 21 21 21 prompt>
Note que o nosso 9, após a primeira instrução vira 10, mas vai casar também lá
em baixo, com s/1/2/
e s/0/1/
e assim virando o 21 :(
Para contornar, vamos adicionar um caractere diferente para não casar
nas demais instruções. Utilizaremos o caractere underscore _
. E somente no fim,
trocaremos o _ por 0.
prompt> cat ./teste_underscore.sed #!/bin/sed -f s/9/0_/ s/8/9/ s/7/8/ s/6/7/ s/5/6/ s/4/5/ s/3/4/ s/2/3/ s/1/2/ s/0/1/ s/_/0/ prompt>
passo a passo:
prompt> echo 9 | ./teste_underscore.sed 10 prompt> prompt> echo 9 | sedsed -d --hide=HOLD -f teste_underscore.sed | limpa.sed 9 s/9/0_/ <<< --- casou, 9 virou 0_ 0_ s/8/9/ 0_ s/7/8/ 0_ s/6/7/ 0_ s/5/6/ 0_ s/4/5/ 0_ s/2/3/ 0_ s/1/2/ 0_ s/0/1/ <<< --- trocou o 0 para 1 1_ s/_/0/ <<< --- trocou 0 _ para 0 10 10 prompt>
Barbada. Trocamos 9 por 0_, depois 0 -> 1
e _ -> 0
.
Mas isso só funciona para o 9, outros casos como 19, 29... não vai funcionar. Exemplo: 19, vira 10_ e no fim, trocando o 1 por 2 vira 20_, 0 por 1 vira 21_, que finalmente vira 210. Tudo bem, mas isso foi só para introduzir a tática que usaremos a seguir.
Chega de enrolar, hora de contar até 99 :D
prompt> cat incr_tentativa_4.sed #!/bin/sed -f s/9/_/ s/^_/0_/ s/8\(_\)\?$/9\1/ s/7\(_\)\?$/8\1/ s/6\(_\)\?$/7\1/ s/5\(_\)\?$/6\1/ s/4\(_\)\?$/5\1/ s/3\(_\)\?$/4\1/ s/2\(_\)\?$/3\1/ s/1\(_\)\?$/2\1/ s/0\(_\)\?$/1\1/ s/_/0/ prompt>
OPA! Agora confundiu. Mas tudo bem, vamos testar antes:
prompt> echo 0 | ./incr_tentativa_4.sed 1 prompt> echo 5 | ./incr_tentativa_4.sed 6 prompt> echo 9 | ./incr_tentativa_4.sed 10 prompt> echo 19 | ./incr_tentativa_4.sed 20 prompt> echo 45 | ./incr_tentativa_4.sed 46 prompt> echo 79 | ./incr_tentativa_4.sed 80 prompt>
Funciona! Por favor Mr. sedsed, nos ajude a entender que mágica é esta!! :)
Vamos analisar três casos especiais:
prompt> echo 9 | sedsed -d --hide=HOLD -f incr_tentativa_4.sed | limpa.sed 9 s/9/_/ <<< --- o 9 virou _ _ s/^_/0_/ <<< --- tática. se começa com _, então temos que aumentar o número de casas deciamis virando 0_ 0_ s/8\(_\)\?$/9\1/ 0_ s/7\(_\)\?$/8\1/ 0_ s/6\(_\)\?$/7\1/ 0_ s/5\(_\)\?$/6\1/ 0_ s/4\(_\)\?$/5\1/ 0_ s/3\(_\)\?$/4\1/ 0_ s/2\(_\)\?$/3\1/ 0_ s/1\(_\)\?$/2\1/ 0_ s/0\(_\)\?$/1\1/ <<< --- trocamos o 0 por 1, assim temos 1_ 1_ s/_/0/ <<< --- trocamos o _ por 0, e assim temos 10 10 10 prompt>
Primeira instrução, trocamos 9 por _. Na segunda instrução testamos
se _ é o primeiro caractere. Se for, então era o dígito 9 e
precisamos aumentar uma casa decimal. Assim ficando 0_.
Note que as próximas instruções não alteraram o pattern space (0_
).
Só casaremos lá em baixo com s/0\(_\)\?$/1\1/
, ou seja, é a mesma coisa que
s/0(_)?$/1_/
, assim 0_ vira 1_,. Na última instrução trocamos o _ por zero
e temos o número 10.
Mas por que o underscore '_' precisa ser opcional ? Próximo caso :)
prompt> echo 5 | sedsed -d --hide=HOLD -f incr_tentativa_4.sed | limpa.sed 5 s/9/_/ 5 s/^_/0_/ 5 s/8\(_\)\?$/9\1/ 5 s/7\(_\)\?$/8\1/ 5 s/6\(_\)\?$/7\1/ 5 s/5\(_\)\?$/6\1/ <<< --- casou, 5 vira 6. Mas note que não existe o underscore aqui 6 s/4\(_\)\?$/5\1/ 6 s/3\(_\)\?$/4\1/ 6 s/2\(_\)\?$/3\1/ 6 s/1\(_\)\?$/2\1/ 6 s/0\(_\)\?$/1\1/ 6 s/_/0/ 6 6 prompt>
Para ele virar 6, o underscore precisa ser opcional. No caso
vai ser a mesma coisa que antes: s/5$/6/
. Podemos ver que o valor só se altera
na instrução s/5\(_\)\?\$/6\1/
. Como não existe o underscore, o retrovisor
\1
, não vai ter nada, não alterando o comportamento da saída.
Neste caso o undercore também entra em ação.
prompt> echo 59 | sedsed -d --hide=HOLD -f incr_tentativa_4.sed | limpa.sed 59 s/9/_/ <<< --- trocamos o 9 por _, e assim temos 5_ 5_ s/^_/0_/ 5_ s/8\(_\)\?$/9\1/ 5_ s/7\(_\)\?$/8\1/ 5_ s/6\(_\)\?$/7\1/ 5_ s/5\(_\)\?$/6\1/ <<< --- casou, 5 seguido de 1 underscore e fim de linha, vira 6_ 6_ s/4\(_\)\?$/5\1/ 6_ s/3\(_\)\?$/4\1/ 6_ s/2\(_\)\?$/3\1/ 6_ s/1\(_\)\?$/2\1/ 6_ s/0\(_\)\?$/1\1/ 6_ s/_/0/ <<< --- trocamos o _ por 0 virando o 60 60 60 prompt>
Após a primeira instrução ele vira 5_ e ele não vai casar na segunda, pois
não começa com _. Ele só casará na s/5(_)?$/6\1/
, ficando 6_. Por fim, lá em
baixo vira o tão esperado 60.
Okay, seu filho cresceu e está sendo alfabetizado. Já aprendeu a contar até 99. Hora de ensiná-lo a quebrar barreiras e entender que, incrementar um número, é uma arte milenar.
Sr. Miyagi, cadê você ?
Calma Daniel-San. Concentre-se no seu objetivo.
prompt> cat incr_tentativa_5.sed #!/bin/sed -f :p s/9\(_*\)$/_\1/ tp s/^\(_*\)$/0\1/ s/8\(_*\)$/9\1/ s/7\(_*\)$/8\1/ s/6\(_*\)$/7\1/ s/5\(_*\)$/6\1/ s/4\(_*\)$/5\1/ s/3\(_*\)$/4\1/ s/2\(_*\)$/3\1/ s/1\(_*\)$/2\1/ s/0\(_*\)$/1\1/ s/_/0/g prompt>
A idéia é estender o uso do underscore.
repare que o underscore continua sendo opcional!! |
---|
Alguns exemplo de como as coisas funcionam. As três primeiras instruções
:p;s/9\(_*\)$/_\1/;tp
significam: enquando existir 9 seguido de zero ou mais
undercores e fim de linha, troque o 9 por undercore. Exemplo:
entrada | saída | comentário |
---|---|---|
9 | _ | um _ |
99 | __ | dois _ |
999 | ___ | três _ |
49 | 4_ | 4 mais um _ |
91 | 91 | não casou |
598 | 598 | não casou |
A quarta instrução (s/^\(_*\)$/0\1/
) pega casos como 9, 99, 999, etc. Casos onde temos que
aumentar o número de casas decimais. Note que, temos que aumentar o número de casas
decimais quando recebemos todos os caracteres sendo o dígito 9. Neste caso todos os 9s
virarão underscores e, assim, casando na quarta instrução, que adicionará um 0 no início. E,
como vimos, este 0 vai virar 1 e os underscores virarão 0s. Casos onde exista um número diferente
de 9 não casará nesta instrução, pois, o loop anterior só altera para undescore os 9 que
estiverem no final da linha. Exemplo: após as quatro primeiras instruções temos:
entrada | saída | comentário |
---|---|---|
9 | 0_ | 0 mais um _ |
99 | 0__ | 0 mais dois _ |
999 | 0___ | 0 mais três _ |
49 | 4_ | 4 mais um _ |
Note que, por exemplo em 49, a quarta instrução não altera o valor, visto que, o primeiro caractere não é underscore.
Primeiro vamos testar:
prompt> echo 0 | ./incr_tentativa_5.sed 1 prompt> echo 9 | ./incr_tentativa_5.sed 10 prompt> echo 45 | ./incr_tentativa_5.sed 46 prompt> echo 99 | ./incr_tentativa_5.sed 100 prompt> echo 999 | ./incr_tentativa_5.sed 1000 prompt> echo 1499 | ./incr_tentativa_5.sed 1500 prompt> echo 1449 | ./incr_tentativa_5.sed 1450 prompt>
Analise de novo as tabelas acima, pois o esquema está nas 4 primeiras instruções. Nas próximas instruções será como antes. Quer ver ?!
sedsed, me dê a visão além do alcance!!
prompt> echo 0 | sedsed -d --hide=HOLD -f incr_tentativa_5.sed | limpa.sed 0 : p s/9\(_*\)$/_\1/ 0 t p s/^\(_*\)$/0\1/ 0 s/8\(_*\)$/9\1/ 0 s/7\(_*\)$/8\1/ 0 s/6\(_*\)$/7\1/ 0 s/5\(_*\)$/6\1/ 0 s/4\(_*\)$/5\1/ 0 s/3\(_*\)$/4\1/ 0 s/2\(_*\)$/3\1/ 0 s/1\(_*\)$/2\1/ 0 s/0\(_*\)$/1\1/ <<< ----- alterou aqui. zero vira 1 1 s/_/0/g 1 1 prompt>
prompt> echo 9 | sedsed -d --hide=HOLD -f incr_tentativa_5.sed | limpa.sed 9 : p s/9\(_*\)$/_\1/ <<< ---- 9 virando _ _ t p s/9\(_*\)$/_\1/ _ t p s/^\(_*\)$/0\1/ <<< ---- aumentando uma casa decimal 0_ 0_ s/8\(_*\)$/9\1/ 0_ s/7\(_*\)$/8\1/ 0_ s/6\(_*\)$/7\1/ 0_ s/5\(_*\)$/6\1/ 0_ s/4\(_*\)$/5\1/ 0_ s/3\(_*\)$/4\1/ 0_ s/2\(_*\)$/3\1/ 0_ s/1\(_*\)$/2\1/ 0_ s/0\(_*\)$/1\1/ <<< --- casou, 0 vira 1 1_ s/_/0/g <<< --- troque todos os _ por 0 10 10 prompt>
prompt> echo 45 | sedsed -d --hide=HOLD -f incr_tentativa_5.sed | limpa.sed 45 : p s/9\(_*\)$/_\1/ 45 t p s/^\(_*\)$/0\1/ 45 s/8\(_*\)$/9\1/ 45 s/7\(_*\)$/8\1/ 45 s/6\(_*\)$/7\1/ 45 s/5\(_*\)$/6\1/ <<< ----- 45 virando 46, não esqueça do $ marcando fim de linha. trocamos somente o último caractere 46 s/4\(_*\)$/5\1/ 46 s/3\(_*\)$/4\1/ 46 s/2\(_*\)$/3\1/ 46 s/1\(_*\)$/2\1/ 46 s/0\(_*\)$/1\1/ 46 s/_/0/g 46 46 prompt>
prompt> echo 999 | sedsed -d --hide=HOLD -f incr_tentativa_5.sed | limpa.sed 999 : p s/9\(_*\)$/_\1/ <<< --- trocamos o último 9 por _ saída = 99_ 99_ t p s/9\(_*\)$/_\1/ <<< --- casa também 99_, vira 9__ 9__ t p s/9\(_*\)$/_\1/ <<< --- 9 seguido de 0 ou mais _ e fim de linha, troca o 9 por _ ___ t p s/9\(_*\)$/_\1/ ___ t p s/^\(_*\)$/0\1/ <<< --- primeiro caractere é _ então aumenta as casas decimais 0___ s/8\(_*\)$/9\1/ 0___ s/7\(_*\)$/8\1/ 0___ s/6\(_*\)$/7\1/ 0___ s/5\(_*\)$/6\1/ 0___ s/4\(_*\)$/5\1/ 0___ s/3\(_*\)$/4\1/ 0___ s/2\(_*\)$/3\1/ 0___ s/1\(_*\)$/2\1/ 0___ s/0\(_*\)$/1\1/ <<< --- troca o 0 por 1, ficando 1___ 1___ s/_/0/g <<< --- troca os _s por 0s, resultando em 1000 1000 1000 prompt>
prompt> echo 1499 | sedsed -d --hide=HOLD -f incr_tentativa_5.sed | limpa.sed 1499 : p s/9\(_*\)$/_\1/ <<< --- troca o 9 por _ 149_ t p s/9\(_*\)$/_\1/ <<< --- temos mais um 9 pra trocar 14__ t p s/9\(_*\)$/_\1/ 14__ t p s/^\(_*\)$/0\1/ 14__ s/8\(_*\)$/9\1/ 14__ s/7\(_*\)$/8\1/ 14__ s/6\(_*\)$/7\1/ 14__ s/5\(_*\)$/6\1/ 14__ s/4\(_*\)$/5\1/ <<< --- 4 seguido de 0 ou mais _s, "soma" 1 no último dígito = 15__ 15__ s/3\(_*\)$/4\1/ 15__ s/2\(_*\)$/3\1/ 15__ s/1\(_*\)$/2\1/ 15__ s/0\(_*\)$/1\1/ 15__ s/_/0/g <<< --- troca os _s por 0, virando o 1500 1500 1500 prompt>
E nosso último exemplo:
prompt> echo 1449 | sedsed -d --hide=HOLD -f incr_tentativa_5.sed | limpa.sed 1449 : p s/9\(_*\)$/_\1/ <<< --- 9 por _ = 144_ 144_ t p s/9\(_*\)$/_\1/ 144_ t p s/^\(_*\)$/0\1/ 144_ s/8\(_*\)$/9\1/ 144_ s/7\(_*\)$/8\1/ 144_ s/6\(_*\)$/7\1/ 144_ s/5\(_*\)$/6\1/ 144_ s/4\(_*\)$/5\1/ <<< --- 4 seguido de zero ou mais _s troca pra 5 = 145_ 145_ s/3\(_*\)$/4\1/ 145_ s/2\(_*\)$/3\1/ 145_ s/1\(_*\)$/2\1/ 145_ s/0\(_*\)$/1\1/ 145_ s/_/0/g <<< --- _ por 0 = 1450 1450 1450 prompt>
Resumão:
OBS: como primeira instrução eu coloquei o loop inteiro.
Com a tabela abaixo espero deixar mais visual o processo de mudança sobre os caracteres, em outras palavras, ver como ocorre a "soma".
Número | instrução | N. de entrada = 4 | N. de entrada = 99 | N. de entrada = 1499 | N. de entrada = 1449 |
---|---|---|---|---|---|
1 | :p |
||||
1 | s/9\(_*\)$/_\1/ |
__ | 14__ | 144_ | |
1 | tp |
||||
2 | s/^\(_*\)$/0\1/ |
0__ | |||
3 | s/8\(_*\)$/9\1/ |
||||
4 | s/7\(_*\)$/8\1/ |
||||
5 | s/6\(_*\)$/7\1/ |
||||
6 | s/5\(_*\)$/6\1/ |
||||
7 | s/4\(_*\)$/5\1/ |
5 | 15__ | 145_ | |
8 | s/3\(_*\)$/4\1/ |
||||
9 | s/2\(_*\)$/3\1/ |
||||
10 | s/1\(_*\)$/2\1/ |
||||
11 | s/0\(_*\)$/1\1/ |
1__ | |||
12 | s/_/0/g |
100 | 1500 | 1450 |
Como exemplo do uso "real" desta técnica, implementei um sed que conta quantas vezes uma determinada palavra aparece em um texto. Confira conta_palavra.sed.
Com esta técnica podemos utilizar outra ou fazer nossa própria linguagem.
Vamos supor que tenhamos que contar de 'a
' a 'f
'. Exemplo:
entrada | saída |
---|---|
a | b |
b | c |
aa | ab |
f | ba |
cd | ce |
ff | baa |
Como nós tratamos tudo como caractere, basta trocar 0-9
por a-f
.
prompt> cat inc_letras.sed #!/bin/sed -f :p s/f\(_*\)$/_\1/ tp s/^\(_*\)$/a\1/ s/e\(_*\)$/f\1/ s/d\(_*\)$/e\1/ s/c\(_*\)$/d\1/ s/b\(_*\)$/c\1/ s/a\(_*\)$/b\1/ s/_/a/g prompt>
Câmbio - testando.
prompt> echo a | ./inc_letras.sed b prompt> echo d | ./inc_letras.sed e prompt> echo f | ./inc_letras.sed ba prompt> echo cd | ./inc_letras.sed ce prompt> echo ff | ./inc_letras.sed baa prompt>
Pronto. Se quiser fazer de [a-z]
ou [a-zA-Z]
, basta colocar na ordem todos
os caracteres permitidos.
Neste outro texto, Lookup Tables & Incrementando em sed, utilizo a técnica de lookup tables para somar e no final tem um exemplo de como somar e diminuir levando em consideração se o valor é positivo ou negativo.
Fim! Hora de ZZZzzzzz
Agradecimentos: |
This HTML page is (see source)