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)