você está aqui: Home  → Arquivo de Mensagens Programação Shell Linux: Inscrições Abertas

Expressões Regulares - Retrovisores

Colaboração: Julio Cezar Neves

Data de Publicação: 08 de abril de 2020

Esta dica é um pouco longa, mas vale cada minuto que você gastar lendo-a. Como diz o Julio ao final deste texto, programador que não conhece expressões regulares é um programador meia-boca. Conhecer bem expressões regulares é um pré-requisito para contratação em muitos lugares.

Só mais um recado rápido, as vagas com desconto para o curso Programação Shell Linux, com o Prof. Julio Neves estão se esgotando. Você vai mesmo perder esta chance?

»»» Saiba mais


Expressões Regulares - Retrovisores

Em uma apresentação, sempre que falo em Expressões Regulares, fico atônito com a indiferença da plateia ao assunto. Só posso atribuir essa passividade ao desconhecimento, pois elas são extremamente úteis e as maiores aliadas de um programador, reduzindo drasticamente o tempo de desenvolvimento do código e de sua execução. Mas não para por aí, elas são sensacionais para definir regras de proxy e de firewall tornando as consultas bem mais rápidas., além de serem usadas por todas as linguagens de programação, também são usadas em todos os editores de texto.

Esse artigo é para incentivar o mergulho mais profundo daqueles que estão ali na beirinha d'água molhando o pé, isto é, daqueles que já deram uma pesquisada sobre o tema, mas que ainda não o estudaram a fundo para conhecerem a totalidade do seu potencial.

O básico do básico

1. As Expressões Regulares sempre podem ser usadas quando não conhecemos um valor mas conhecemos as suas possíveis formações.

Exemplo


Descobrir todos os CEPs de um arquivo, usando ou não o hífen (-) e o ponto separador de milhares. Pense: como você programaria isso na sua linguagem predileta? Para quem conhece Expressões Regulares, basta usá-las num simples grep.

 $ grep -E '[0-9]{2}\.?[0-9]{3}-?[0-9]{3}' ARQUIVO

Pronto! Tá resolvido!

Vamos desmembrá-la:

[0-9]{2} Números ([0-9]) que ocorrem duas vezes ({2});
\.? Um ponto (.) opcional (?) a contrabarra (\) foi usado porque o ponto também é um metacaractere de Expressões Regulares, mas nesse caso, estava sendo usado como literal;
[0-9]{3} Números ([0-9]) que ocorrem três vezes ({3});
-? Um traço (-) opcional (?);
[0-9]{3} Números ([0-9]) que ocorrem três vezes ({3}).

2. Os parênteses formam grupos que permitem aplicar outros metacaracteres sobre o seu todo.

Exemplo



pegadas? Casa com pegada e com pegadas;
pega(das)? Casa com pega e pegadas porque o opcional (?) foi aplicado a todo o grupo.

E o que são os retrovisores?

Além do que acabamos de ver os parênteses (grupos) também retêm o texto que casou com a Expressão Regular do seu interior e portanto podemos usá-lo em outra parte da mesma Expressão Regular e é a isso que chamamos de retrovisores (tradução livre de back reference ou referência anterior).

Exemplo


(a)br\1c\1d\1br\1 casa com abracadabra, mas:

(a)(br)\1c\1d\1\2\1 também casa.

Explicação



(a) Grupo que casou com o texto a;
br\1 Literal br seguido do 1º retrovisor (\1);
c\1 Literal c seguido do 1º retrovisor (\1);
d\1 Literal d seguido do 1º retrovisor (\1);
br\1 Literal br seguido do 1º retrovisor (\1);

Como o literal br se repetia duas vezes, no 2º exemplo também criamos um grupo com ele e por ser o 2º grupo criado, foi batizado como \2.

Exemplo


$ sed -r 's/(.)/\1 /g' <<< abcdefgjij
a b c d e f g j i j

Observação: o comando acima é a forma mais rápida e mais limpa que:

$ echo abcdefgjij | sed -r 's/(.)/\1 /g'

O ponto é um coringa que casa com qualquer caractere e a flag g no final diz que a substituição é geral, assim sendo o ponto (.) casou com cada uma das letras que foi substituída pelo texto (a letra) casado (\1) seguida de um espaço em branco.

Olha esse mesmo exemplo (mal) escrito de outra forma:

$ sed -r 's/(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)/\1 \2 ... \10/' <<< abcdefghij
a b ... a0

Nesse caso salvamos cada uma das 10 letras em um retrovisor e vimos que ele entendeu o \10 como um \1 seguido de um zero, isso porque só podemos usar até 9 retrovisores, mas lhes garanto que essa quantidade é mais que suficiente para todas as tarefas.

Para transformar datas no formato DD/MM/AAAA em AAAAMMDD:

$ sed -r 's|([0-9]{2})/([0-9]{2})/([0-9]{4})|\3\2\1|' <<< 05/04/1947
19470405

Onde usei as barras verticais (|) como os separadores do sed para não confundir com as barras (/) da data. Desmembrando a Expressão Regular, vem:

([0-9]{2}) Número [0-9] de 2 algarismos {2} - casa com o dia
/ O literal / entre o dia e o mês
([0-9]{2}) Número [0-9] de 2 algarismos {2} - casa com o mês
/ O literal / entre o mês e o ano
([0-9]{4}) Número [0-9] de 4 algarismos {4} - casa com o ano

Só mais um exemplo para podermos usar o grep com Expressões Regulares.

A disposição do arquivo /etc/passwd é a seguinte:

UserName:x:Uid:Gid:...

Pense na dificuldade que você teria para listar todos os usuário que tivessem o Uid idêntico ao Gid. Com Expressões Regulares, fazemos isso em uma única linha:

$ grep -Eo '[[:alnum:]]+:x:([0-9]+):\1:' /etc/passwd
root:x:0:0:
daemon:x:1:1:
bin:x:2:2:
...

Desmembrando a Expressão Regular:

[[:alnum:]]+: Todas as alfanuméricas até encontrar o dois pontos (:)
x: O literal x:
([0-9]+): Montando um grupo com algarismos (para casar com o Uid) e terminando com dois pontos (:)
\1: Casará com um texto igual ao que foi salvo no Uid

É interessante explicar que sempre usei o dois pontos (:) como um delimitador. Se não o fizesse, 123 casaria com 1234. Veja:

$ grep -oE '(123):\1:' <<< 123:1234: &&
    echo Casou ||
    echo Não casou
Não casou

Tirando o dois pontos após o retrovisor:

$ grep -oE '(123):\1' <<< 123:1234: &&
    echo Casou ||
    echo Não casou
123:123
Casou

Usei a opção -o (Only match) que mostra somente o texto casado, para que você possa ver na segunda forma o casamento capenga que foi realizado.

A sintaxe que usei no início deste exemplo foi a mais simples de explicar, mas também poderia (e deveria) ter feito da seguinte forma:

$ grep -Eo '[[:alnum:]]+:x:([0-9]+:)\1' /etc/passwd

Embutindo o delimitador dois pontos (:) no grupo, já que ele terá de aparecer após os dois números.

Um vetor interessante

A partir do Bash 4.0 o novo comando test ([[ ... ]]) passou a aceitar Expressões Regulares, mas ele tem uma diferença interessante na forma como salva os retrovisores. Ele usa o vetor (array) BASH_REMATCH, que no seu índice zero tem o casamento total feito pela Expressão Regular, no índice um, o que seria o retrovisor \1, e assim sucessivamente.

Exemplo


$ cat explica_test
#!/bin/bash
clear
read -p "Infome um endereço IP: " IP
[[ $IP =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]] ||
{
    echo Isso não tem formato de endereço IP
    exit 1
}
echo -e "
Casamento total:\t${BASH_REMATCH[0]}
Primeiro octeto:\t${BASH_REMATCH[1]}
Segundo octeto: \t${BASH_REMATCH[2]}
Terceiro octeto:\t${BASH_REMATCH[3]}
Quarto octeto:  \t${BASH_REMATCH[4]}"

Algumas observações sobre esse cara:

  1. Para usarmos Expressões Regulares, temos de testar usando o operador =~
  2. Os grupos montados eram desnecessários, foram criados só para poder demonstrar o uso do vetor
  3. Essa Expressão Regular, nem de perto está otimizada, mas serve para fins didáticos
  4. Ela não pretende ser uma crítica de um endereço IP. Serve unicamente para verificar se um dado lido tem formato de endereço IP. Ela dentro de um grep poderia listar todos os endereços IP de um arquivo (experimente usar esse grep na saída do ifconfig)
  5. Se não usássemos os metacaracteres ^ e $, o 1? e o 4? octetos poderiam ter qualquer tamanho, casando 3 algarísmos e dando o endereço como correto. Experimente...

    Dois testes:

    $ explica_test
    Informe um endereço IP: 12.34.43.21
    Casamento total:	12.34.43.21
    Primeiro octeto:	12
    Segundo octeto: 	34
    Terceiro octeto:	43
    Quarto octeto:  	21
    $ explica_test
    
    Informe um endereço IP: 12.34.43.2123
    Isso não tem formato de endereço IP
    
    

A forma otimizada de fazer esta Expressão Regular seria:

[[ $IP =~ ^([0-9]{1,3}\.){3}([0-9]{1,3})$ ]]

Onde:

([0-9]{1,3}\.){3} diz que o grupo formado por de 1 a 3 ({1,3}) algarismos ([0-9]) mais o ponto (\,) ocorre 3 vezes ({3})

Mas nesse caso a demonstração estaria prejudicada, já que no primeiro retrovisor ficaria o último octeto casado por ele, ou seja o terceiro.

Apesar de ter abordado superficialmente as Expressões Regulares, já deu para dar uma boa introdução ao assunto.

Estou escrevendo esse artigo no LibreOffice e quando faço um <CTRL>+H nele e clico em Outras opções, veja o que aparece:

Se você é um cara que gosta de escrever código ou se vive às voltas com regras de firewall, aproveite o tempo que você tem disponível para estudar e, o mais importante praticar Expressões Regulares, pois aprender é muito fácil, mas para usá-las é necessário muita prática.

Existem na internet alguns emuladores de Expressões Regulares, neles a medida que você vai montando a expressão, ele vai mostrando paulatinamente o casamento. Não acho esse um bom meio de aprender, porque o software pensa por você e por isso não te motiva a aprender. Aconselho praticar Expressões Regulares usando sites (existem diversos) que você treina fazendo palavras cruzadas (crossword) cujas dicas são dadas por Expressões Regulares. Você aprende e se diverte.

Para finalizar vou te dar um choque de realidade: eu não contrato programadores que não conhecem Expressões Regulares, porque considero-os profissionais pouco produtivos, ou seja, quem não conhece Expressões Regulares é um programador meia boca ;)



Veja a relação completa dos artigos de Julio Cezar Neves