Explodindo strings com ‘BASH_REMATCH’
O Bash oferece muitos recursos nativos para a manipulação de strings. Alguns desses recursos são bastante conhecidos e bem documentados, como as expansões de parâmetros, por exemplo, mas outros podem exigir alguma pesquisa a mais para entender como podemos utilizá-los nos nossos scripts.
Um desses recursos menos conhecidos é a variável especial BASH_REMATCH
. À primeira vista, pode não parecer que ela tenha alguma relação com manipulação de strings, mas vamos ver o que diz o manual do Bash:
BASH_REMATCH - Uma variável do tipo *array* cujos membros
são atribuídos pelo operador binário `=~`
ao comando condicional `[[`. O elemento de
índice `0` é a porção da string que casa
com toda a expressão regular. Os elementos
de índice `n` são as porções da string que
casam com os respectivos grupos entre
parêntesis na expressão regular. Esta
variável é apenas para leitura.
Se você não entendeu nada, não se preocupe, você não está sozinho. Às vezes eu acho que precisamos de um manual para entender o manual do Bash, mas nós vamos tentar entender juntos.
Resumindo a informação mais relevante que o manual fornece, BASH_REMATCH
é uma array apenas para leitura (read only) que tem seus valores atribuídos sempre que executamos a avaliação de uma expressão condicional entre colchetes duplos ([[ expressão ]]
) onde é utilizado o operador =~
.
Como sabemos, este tipo de comando condicional está ligado ao comando interno (builtin) test
, mas apresenta algumas características diferenciadas. Entre elas, a possibilidade de executar uma busca de padrões em uma string a partir de uma expressão regular. A sintaxe geral deste comando seria:
[[ STRING =~ REGEX ]]
Nota: Nós não entraremos em detalhes sobre as expressões regulares, que serão abordadas em outro tópico específico.
A avaliação dessa expressão, como qualquer outra avaliação condicional com outros operadores, retornará o status de saída 0
(sucesso) se houver correspondência da expressão regular com alguma parte da string, ou qualquer valor diferente de zero se não houver correspondência ou se houver algum erro na montagem dos padrões da expressão regular. Por exemplo:
:~$ str=foobarfozbaz
:~$ [[ $str =~ bar ]] && echo "achou" || echo "não achou"
Neste pequeno exemplo, nós estamos tentando saber se na string str
existe alguma ocorrência da sequência de caracteres bar
. Executando o exemplo, o resultado seria:
:~$ str=foobarfozbaz
:~$ [[ $str =~ bar ]] && echo "achou" || echo "não achou"
achou
Isso bastaria se a nossa necessidade fosse apenas detectar a ocorrência de um padrão em uma determinada string.
Vejamos outro exemplo:
:~$ str=foobarfozbaz
:~$ [[ $str =~ (foo).*(baz) ]]; echo $?
Aqui, a nossa regex buscará por padrões em dois grupos descritos entre parêntesis, (foo)
e (bar)
, com qualquer sequência de caracteres entre eles (.*
), inclusive nenhum caractere. Obviamente, a regex casa com a string str
, e isso faria o comando retornar o status de saída 0
:
:~$ str=foobarfozbaz
:~$ [[ $str =~ (foo).*(baz) ]]; echo $?
0
Mas, e se quiséssemos ver o que exatamente a nossa regex encontrou? É aí que entra a variável BASH_REMATCH
.
Como vimos na explicação do manual, sempre que utilizamos uma expressão condicional deste tipo, o Bash atribuí à array BASH_REMATCH
todas as correspondências encontradas. Automaticamente, pelo simples fato de executarmos o comando condicional com o operador =~
.
Para verificar isso, vamos repetir os comandos dos nossos exemplos, só que, desta vez, nós vamos realizar uma leitura do conteúdo em BASH_REMATCH
após cada um deles:
:~$ str=foobarfozbaz
:~$ [[ $str =~ bar ]] && echo "achou" || echo "não achou"
achou
:~$ echo ${BASH_REMATCH[@]}
bar
:~$ str=foobarfozbaz
:~$ [[ $str =~ (foo).*(baz) ]]; echo $?
0
:~$ echo ${BASH_REMATCH[@]}
foobarfozbaz foo baz
Novamente, segundo o manual, o primeiro elemento da array irá conter a porção da string que casa com toda a expressão regular e os demais elementos conterão as porções da string que corresponderem aos grupos entre parêntesis na regex.
Foi isso que vimos no primeiro exemplo, onde o conteúdo do primeiro elemento de BASH_REMATCH
era apenas bar
, e no segundo exemplo, onde o primeiro elemento continha foobarfozbaz
, enquanto os demais elementos contém as respectivas correspondências dos grupos entre parêntesis, foo
e baz
.
Para ter uma visão mais clara, vamos modificar um pouco a forma de ler o conteúdo em BASH_REMATCH
no segundo exemplo:
:~$ str=foobarfozbaz
:~$ [[ $str =~ (foo).*(baz) ]]
:~$ for n in ${!BASH_REMATCH[@]}; do echo "[$n] => ${BASH_REMATCH[$n]}"; done
[0] => foobarfozbaz
[1] => foo
[2] => baz
Neste ponto, já deve estar bastante claro como a variável BASH_REMATCH
pode ser um excelente recurso do Bash para manipulação de strings. Se você não concorda, veja o que podemos fazer com mais um exemplo.
:~$ str=banana
:~$ [[ $str =~ ${str//?/(.)} ]] && echo ${BASH_REMATCH[@]}
banana b a n a n a
Talvez, a novidade aqui seja o uso da expansão de parâmetro ${str//?/(.)}
, que nós utilizamos para substituir cada caractere em str
(utilizando o coringa ?
) pela string (.)
que, na expressão regular, corresponde a um grupo contendo apenas um caractere qualquer. Para ver como funciona essa expansão, execute:
:~$ str=banana
:~$ echo ${str//?/(.)}
(.)(.)(.)(.)(.)(.)
Ou seja, do ponto de vista do comando condicional, o que está escrito é:
[[ $str =~ (.)(.)(.)(.)(.)(.) ]]
Mas, como não podemos prever o comprimento da string que estará em str
, nos deixamos que a expansão de parâmetro cuide disso para nós. Por exemplo:
:~$ str=pera
:~$ echo ${str//?/(.)}
(.)(.)(.)(.)
O primeiro elemento de BASH_REMATCH
sempre irá conter o casamento completo da regex. Mas, se você não quiser isso na saída do seu comando ou da sua função, uma alternativa é fazer algo assim:
:~$ str=banana
:~$ [[ $str =~ ${str//?/(.)} ]] && \
> match=(${BASH_REMATCH[@]}) && \
> unset match[0] && \
> echo ${match[@]}
b a n a n a
Nota: a barra invertida (
\
) neste exemplo serve apenas para quebrar uma mesma linha de comando muito longa em várias linhas.
Nós precisamos criar outra variável (match
) porque BASH_REMATCH
não pode ser alterada e nós não poderíamos deletar o valor no elemento de índice 0
. Portanto, após o comando unset
, match
terá seus elementos numerados a partir do índice 1
, o que podemos comprovar percorrendo os elementos de match
:
:~$ for n in ${!match[@]}; do echo "[$n] => ${match[$n]}"; done
[1] => b
[2] => a
[3] => n
[4] => a
[5] => n
[6] => a
Este artigo foi apenas uma apresentação e uma demonstração prática de uso da variável BASH_REMATCH
. Além disso, os exemplos que apresentamos aqui servirão de base para futuros artigos dedicados exclusivamente às expressões regulares.
Mas, como tudo no Bash, existem várias formas de fazer a mesma coisa, e a nossa intenção é sempre ampliar o seu repertório de opções, especialmente se, por algum motivo, um dia você precisar converter strings numa array de caracteres.