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

Casamento de padrões com globs estendidos

Colaboração: Blau Araújo

Data de Publicação: 12 de março de 2020

No Bash, quando usamos os caracteres coringa (wildcards) *, ? e [ ] para casar padrões de nomes de arquivos, o que acontece é uma das várias expansões do shell, a expansão de nomes de arquivos.

Isso quer dizer que o Bash irá expandir esses símbolos para nomes de arquivos que correspondam ao padrão definido e, apenas depois, o comando que utilizará esses nomes de arquivos como argumentos será executado. Por exemplo...

:~$ ls
arquivo1.doc arquivo2.doc teste1.txt teste2.txt
:~$ ls *.txt
teste1.txt teste2.txt

Aqui, o Bash expandiu *.txt para teste1.txt e teste2.txt, e passou para o comando ls esses dois nomes como argumentos. Do "ponto de vista" do ls, ele nunca chegou a ver *.txt, mas apenas o resultado da expansão, como na linha abaixo:

:~$ ls teste1.txt teste2.txt

E a prova disso é a saída do comando:

:~$ echo *.txt
teste1.txt teste2.txt

Como echo não tem nada a ver com listagem de arquivos, podemos entender facilmente que ele só está exibindo uma expansão do shell, a expansão de nomes de arquivos.

A rigor, no caso específico do uso dos coringas para casar com nomes de arquivos, nós podemos dizer que estamos fazendo um globbing, que é como chamamos qualquer casamento de padrões com nomes de arquivos, mas este não é o único uso dos coringas no shell.

Os mesmos símbolos e sintaxes também podem ser usados em outros comandos e expansões do shell, como nas expansões de parâmetros e nas opções do comando composto case, por exemplo. Em todos esses casos, inclusive quando trabalhamos com nomes de arquivos, eles serão utilizados para realizar casamentos de padrões de texto (caracteres, strings, etc).

As opções de 'globbing' no Bash

Por padrão, o Bash vem configurado para permitir o uso dos coringas de que falamos acima (*, ? e [ ]), mas estes não são os únicos recursos que ele oferece para o casamento de padrões. Através do comando bultin shopt, nós podemos listar e alterar o estado das diversas outras opções relativas ao globbing. Esta é a sintaxe do comando shopt:

shopt [-pqsu] [-o] [NOME-OPÇÃO ...]

Opções:
  -o	restringe NOME-OPÇÃO àqueles definidos para usar com `set -o'
  -p	imprime cada opção shell com uma indicação de seu status
  -q	suprime a saída
  -s	habilita (set) com NOME-OPÇÃO
  -u	desabilita (unset) com NOME-OPÇÃO

Vamos entender melhor essas opções do comando shopt:

Opção Descrição
-o Algumas opções do shell precisam ser gerenciadas com o comando set -o, mas nós podemos listá-las com o shopt utilizando a opção -o.
-p As opções do shell serão exibidas no formato shopt -s\|-u NOME-OPÇÃO, onde -s indica que ela está habilitada e -u indica que está desabilitada.
-q Não exibe uma saída, mas retorna status 0 (sucesso) quando a opção está habilitada ou 1 (erro), quando está desabilitada.
-s Habilita (set) a opção do shell.
-u Desabilita (unset) a opção do shell.

Além dessas opções, executando o comando shopt (ou shopt -o) sem argumentos, nós temos a lista de todas as opções do shell seguidas de on ou off, indicando se estão ligadas ou desligadas.

Opções do shell para casamento de padrões

Por padrão, como já vimos, nós podemos contar com os seguintes símbolos para casar padrões no Bash:

Símbolo Padrão
* Casa com zero ou mais caracteres quaisquer
? Casa com apenas um caractere existente
[ ] Lista de caracteres permitidos na posição

NOTA: Repare que eles não possuem o mesmo significado de seus semelhantes nas expressões regulares, onde * e ? seriam quantificadores e a lista [ ] seria um representante.

Além desses, o Bash também oferece algumas sintaxes opcionais muito interessantes e úteis que podem ampliar em muito a nossa capacidade de casar padrões. São os chamados globs estendidos ou extglobs.

Então, vamos listar as opções do shell relacionadas ao globbing. Esta seria uma das formas:

:~$ shopt | grep glob
dotglob        	off
extglob        	on
failglob       	off
globasciiranges	on
globstar       	off
nocaseglob     	off
nullglob       	off

Para ver as opções de globbing que devem ser gerenciadas com o comando set -o...

:~$ shopt -o | grep glob
noglob         	off

Vejamos o que cada uma delas faz e qual a configuração geralmente encontrada em sistemas GNU/Linux.

Opção Descrição Shell interativo Scripts
dotglob Permite o casamento de padrões com nomes de arquivos iniciados com ponto (arquivos ocultos) sejam expandidos. Desligada Desligada
extglob Permite o uso de sintaxes adicionais para casamento de padrões. Ligada Desligada
failglob Faz com que padrões sem correspondência gerem uma mensagem de erro de expansão. Desligada Desligada
globasciiranges Define que o conjunto de caracteres que irão corresponder às faixas definidas dentro dos colchetes ([ ]) não seguirão as definições de sequências caracteres do idioma (locale), e sim as sequências de caracteres ASCII. Ligada Ligada
globstar No contexto do casamento de nomes de arquivos, permite o uso de dois asteriscos (**) para incluir casamentos com nomes de diretórios e subdiretórios, ou apenas de diretórios e subdiretórios se vier seguido da barra (/). Desligada Desligada
nocaseglob Permite casar padrões independente dos caracteres estarem em caixa alta ou baixa. Desligada Desligada
nullglob Quando não é encontrado um nome de arquivo que corresponda ao padrão definido, o shell retornará o próprio padrão na forma de uma string. Com esta opção habilitada, ele retornará uma string nula. Desligada Desligada
noglob (opção gerenciada com set -o) Se habilitada, não permite expansões de nomes de arquivos. Desligado Desligado

A opção 'nocasematch'

No caso específico do casamento de padrões de texto, como nas opções de uma declaração case, por exemplo, ainda é possível ignorar diferenças de caixa alta ou baixa atrvés da opção do shell nocasematch, que vem desabilitada tanto para o shell interativo (terminal) quanto para o não-interativo (scripts).

Exemplo:

#!/usr/bin/env bash

# Habilita a opção 'nocasematch'...
shopt -s nocasematch

case $1 in
    banana) echo "Você digitou $1";;
    laranja) echo "Você digitou $1";;
    *) echo "Digite 'laranja' ou 'banana'...";;
esac

# Desabilita a opção 'nocasematch'...
shopt -u nocasematch

exit

Sem habilitar a opção nocasematch, "BANANA" seria diferente de "banana", e o case não reconheceria essa alternativa e cairia na opção *. Mas, com ela habilitada, nós podemos digitar, por exemplo...

:~$ ./testeglob.sh laranJA
Você digitou laranJA
:~$ ./testeglob.sh BANANA
Você digitou BANANA

IMPORTANTE! Existem formas mais interessantes e eficazes de tratar caixas altas e baixas do que alterando a configuração do Bash, mas é válido você estar ciente da existência deste recurso.

A variável 'GLOBIGNORE'

Além das opções do shell, nós ainda podemos trabalhar com a variável GLOBIGNORE para alterar a expansão de nomes de arquivos. Com ela, você pode listar os padrões, separados por dois pontos (:), que representarão o conjunto de nomes de arquivos que devem ser removidos do resultado da expansão.

Por exemplo, supondo um diretório com os arquivos exemplo1.txt, exemplo2.txt, exemplo3.png...

:~$ ls e*
exemplo1.txt  exemplo2.txt  exemplo3.png

Definindo GLOBIGNORE para ignorar *.png...

:~$ GLOBIGNORE=*.png
:~$ ls e*
exemplo1.txt  exemplo2.txt

Ou ainda...

:~$ GLOBIGNORE=*.png:*2*
:~$ ls e*
exemplo1.txt

NOTA: Apesar de bastante útil, também é possível ignorar padrões de nomes de arquivos com a opção do shell extglob, como veremos a seguir, mas a variável especial GLOBIGNORE é interessante pela possibilidade que oferece de listarmos vários padrões que queremos ignorar sem alterarmos as configurações do shell.

Globs estendidos

Como vimos, geralmente os sistemas vêm com o extglob habilitado para o shell interativo mas desabilitado para o shell não-interativo. Por isso, caso você queira utilizar as sintaxes estendidas para casamento de padrões, é muito provável que seja necessário habilitar a opção extglob no seu script, o que pode ser feito com o comando:

shopt -s extglob

Normalmente, não seria necessário restaurar o estado original do extglob, já que o fim do script encerrará a sessão do shell em que estiver rodando e, com isso, o estado original seria automaticamente restaurado. Porém, é considerada uma boa prática garantir que o estado original de todas as configurações do shell seja restaurado assim que elas não forem mais necessárias, especialmente no caso do seu script ser chamado pelo comando source. Então, para desabilitar o extglob, nós podemos usar o comando:

shopt -u extglob

O código abaixo mostra uma forma bastante interessante de alternar o estado da opção extglob:

shopt -q extglob; extglob_status=$?
(($extglob_status)) && shopt -s extglob

# ... seu código ...

(($extglob_status)) && shopt -u extglob

Na primeira linha, o comando shopt -q extglob retornará status 0 se extglob estiver habilitado ou 1, caso esteja desabilitado. Através da variável especial $?, nós armazenamos o status retornado na variável extglob_status.

Em seguida, nós utilizamos o comando composto (( ... )) para avaliar o valor em extglob_status. Se ele for 1 (extglob desabilitado), a expressão retornará 0 e fará o comando shopt -s extglob ser executado. Caso seja 0 (extglob habilitado), a expressão retornará 1 e nada será feito.

IMPORTANTE! O comando composto (( ... )) é utilizado para avaliar expressões aritméticas. Quando o valor da expressão avaliada for diferente de zero, ela retornará um status de saída 0 (sucesso). Mas, se o valor da expressão for igual a zero, ela retornará um status de saída 1 (erro). O nosso código está se aproveitando justamente dessa característica para simular um tipo de flag, indicando se extglob está ligado (saída 1) ou desligado (saída 0).

Minha gratidão ao professor Julio Neves (o Papai do Shell) por me alertar da necessidade de explicar melhor este ponto.

Por último, após a execução do seu código, nós avaliamos novamente o valor em extglob_status que, obviamente, armazena o estado original da opção extglob. Se esse valor for 0, significa que nada foi alterado na avaliação anterior. Mas, se for 1 significa que o estado de extglob foi alterado, como vimos, e precisa ser restaurado, o que é feito com o comando shopt -u extglob.

O que é possível fazer com os globs estendidos?

Os globs estendidos ampliam muito a nossa capacidade de representar padrões mais complexos, especialmente em situações onde não podemos utilizar expressões regulares. Abaixo, podemos ver as novas sintaxes que são adicionadas ao nosso repertório quando a opção extglob está habilitada:

Sintaxe Descrição
?(lista-de-padrões) Casa com zero ou uma ocorrência dos padrões na lista.
*(lista-de-padrões) Casa com zero ou mais ocorrências dos padrões na lista.
+(lista-de-padrões) Casa com uma ou mais ocorrências dos padrões na lista.
@(lista-de-padrões) Casa com um dos padrões na lista.
!(lista-de-padrões) Casa com tudo menos os padrões na lista.

IMPORTANTE: Nas descrições acima, lista-de-padrões é uma lista de representações de padrões separados por uma barra vertical (|), o que pode ser lido como a palavra "ou": este padrão, ou este padrão, etc...

Casando nomes de arquivos com globs estendidos

Vamos considerar uma pasta contendo os seguintes arquivos...

aa.txt  a.txt  .txt

Utilizando o padrão *.txt, nós teríamos...

ls *.txt
aa.txt  a.txt

Repare que aqui, como dissemos no começo, o comando ls verá a expansão de *.txt, que será aa.txt e a.txt, já que o shell, por padrão, não irá expandir nomes de arquivos iniciados com ponto (.), a menos que ele venha explicitado antes dos caracteres coringa. Isso, por exemplo, expandiria o arquivo .txt:

:~$ ls .*txt
.txt

Este comportamento é definido pela opção do shell dotglob que, como vimos, vem desabilitada na maioria dos sistemas, mas pode ser habilitada através do comando:

shopt -s dotglob

Portanto, se habilitarmos o dotglob...

:~$ shopt -s dotglob
:~$ ls *.txt
aa.txt  a.txt  .txt
:~$ shopt -u dotglob

Neste tópico, porém, nós trabalharemos com o comportamento padrão do Bash, já levando em conta que, ao menos para uso no terminal, é bem provável que a opção extglob já esteja habilitada.

Voltando ao nosso exemplo, o padrão *.txt seria expandido da seguinte forma:

ls *.txt
aa.txt  a.txt

Utilizando os globs estendidos, nós poderíamos fazer coisas desse tipo:

# Casa apenas com zero ou um caractere 'a'...
:~$ ls ?(a).txt
a.txt  .txt

# Casa com zero ou mais caracteres 'a'...
:~$ ls *(a).txt
aa.txt  a.txt .txt

# Casa com um ou mais caracteres 'a'...
:~$ ls +(a).txt
aa.txt  a.txt

# Casa apenas com um único caractere 'a'...
:~$ ls @(a).txt
a.txt

# Casa com tudo que não for apenas um 'a'...
:~$ ls !(a).txt
aa.txt

Repare que nos dois primeiros exemplos, ?(a).txt e *(a).txt, mesmo com dotglob desabilitado, o Bash também expandiu o arquivo .txt. Porém, no último exemplo, onde pedimos tudo que não casasse com o nome de arquivo a.txt, o resultado foi apenas aa.txt.

Esse tipo de inconsistência de comportamento é algo a que sempre devemos estar atentos. Quer dizer, nós temos duas regras para expansões que fazem basicamente a mesma coisa:

  • Com * e !(lista-de-padrões), o Bash não expande nomes de arquivos iniciados com ponto, a menos que a opção dotglob esteja habilitada.

  • Com ?(lista-de-padrões) e *(lista-de-padrões), ele já expande nomes de arquivos iniciados com ponto (.), mesmo com dotglob desabilitado.

NOTA: Uma possível explicação é que, no caso do casamento dos padrões negados, é possível que o Bash primeiro expanda os nomes de arquivos como faria com o asterisco, caso em que arquivos com nomes iniciados com ponto não são expandidos, para só depois excluir os nomes que casem com o padrão e, a essa altura os arquivos iniciados com ponto já não estariam mais lá.

Apesar disso, aqui está um exemplo de globbing negado que, além de muito útil, é uma dúvida bastante comum: como listar todos os arquivos ocultos menos . e ..? Experimente isso:

ls -a .!(|.|..)

Sem o globbing, o comando ls -a mostraria os arquivos ocultos (iniciados com ponto) nos resultados, inclusive . e ... Para resolver isso, nós os incluímos na lista de padrões a serem ignorados. Contudo, sem o padrão vazio que pusemos no início da lista, o arquivo . continuaria sendo exibido.

Utilizando globs estendidos para casar padrões gerais

Assim como os globs normais (*, ? e [ ]), os globs estendidos também podem ser utilizados em outras situações além do casamento de nomes de arquivos. Vejamos um exemplo:

:~$ fruta=abacate
:~$ [[ $fruta == abaca+(te|xi) ]] && echo "casou" || echo "não casou"
casou
:~$ fruta=abacaxi
:~$ [[ $fruta == abaca+(te|xi) ]] && echo "casou" || echo "não casou"
casou

:~$ fruta=abacaba
:~$ [[ $fruta == abaca+(te|xi) ]] && echo "casou" || echo "não casou"
não casou

Aqui, nós utilizamos o padrão abaca+(te|xi) para testar (comando [[ ... ]]) se a variável fruta continha as palavras abacate ou abacaxi pela definição do quais seriam os dois caracteres válidos após abaca: te ou xi.

Sem extglob habilitado, a única forma de fazer esse mesmo tipo de teste seria com o operador =~, que avalia uma expressão regular, por exemplo:

:~$ fruta=abacate
:~$ [[ $fruta =~ abaca(te|xi) ]] && echo "casou" || echo "não casou"
casou
:~$ fruta=abacaba
:~$ [[ $fruta =~ abaca(te|xi) ]] && echo "casou" || echo "não casou"
não casou

Também podemos utilizar globs estendidos nas opções de uma declaração case, como no script exemplo.sh abaixo:

#!/usr/bin/env bash

# Habilitando o 'extglob' se for preciso...
shopt -q extglob; extglob_status=$?
(($extglob_status)) && shopt -s extglob

case $1 in
  !(*.gif|*.jpg|*.png)) echo "$1 não é imagem!";;
  *) echo "$1 é uma imagem";;
esac

# Restaurando o estado original de 'extglob'...
(($extglob_status)) && shopt -s extglob

exit

Com esse script, nós estamos verificando se o usuário digitou ou não alguma das três extensões válidas como imagens: .gif, .jpg e .png. Caso o parâmetro passado para o script não corresponda a um nome de arquivo com uma dessas três extensões, a saída será a mensagem: ...não é uma imagem.

Isso é feito com o padrão estendido !(*.gif|*.jpg|*.png), onde estão listados os três padrões que serão negados (lembre-se do tópico anterior sobre a variável GLOBIGNORE). Qualquer coisa que não corresponda a um dos três padrões fará com que o case execute os comandos desta opção. Mas, se o usuário digitar qualquer coisa que termine com um desses padrões, a opção executada será a do *, que aceita nada ou qualquer coisa.

Executando...

:~$ ./exemplo.sh teste.txt
teste.txt não é imagem!

:~$ ./exemplo.sh teste.png
teste.png é uma imagem

:~$ ./exemplo.sh teste.jpg
 teste.jpgé uma imagem

:~$ ./exemplo.sh teste.gif
teste.gif é uma imagem

Conclusão

Como vimos, as sintaxes estendidas para casamento de textos e nomes de arquivos podem ser muito úteis em algumas situações. Obviamente, se compatibilidade e portabilidade estiverem entre os critérios do seu projeto, pode ser mais conveniente pensar em outras alternativas, como o uso de ferramentas como grep, sed e awk, por exemplo. Porém, tendo a certeza da compatibilidade, não há dúvida de que esse recurso pode nos ajudar a construir scripts mais simples e com alto desempenho.

Este artigo foi publicado originalmente no portal Gitlab.com


Veja a relação completa dos artigos de Blau Araújo