SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor

Bash globbing: curing wildcards, nullglob e extglob

Entenda como o Bash expande curingas antes do comando e evite resultados inesperados com nullglob, dotglob, globstar e negacao no extglob.

O que o globbing do bash faz antes de executar o seu comando

O globbing do Bash consiste em o shell transformar um padrão como *.log numa lista ordenada de nomes de ficheiros reais, antes de o comando arrancar. rm *.log nunca fornece um padrão a rm. O Bash lê o diretório, mantém os nomes que correspondem ao padrão, ordena-os e executa rm com cada nome como um argumento separado. O programa rm não consegue saber que existiu um padrão.

Quase tudo o que é surpreendente no globbing resulta deste facto. A correspondência é feita pelo shell, com base nos ficheiros existentes naquele momento, e o comando recebe apenas a lista final. O nome formal deste processo é expansão de nomes de caminhos. Globbing é o termo usado no dia a dia, e ambos significam a mesma coisa.

Crie uma pequena árvore para praticar

Todos os exemplos abaixo usam os mesmos ficheiros. Crie-os num diretório temporário, para manter afastado tudo o que for importante e fazer a limpeza com um único comando.

lab=$(mktemp -d)
cd "$lab"
mkdir -p logs archive/2025 archive/2026
touch app.log app.log.1 app.log.2 app.log.10 error.log debug.LOG
touch notes.txt notes.md README 'weekly report.log'
touch .env .apprc
touch logs/nginx.log logs/nginx.log.1
touch archive/2025/dec.log archive/2026/jan.log

mktemp -d cria um diretório novo e devolve o respetivo caminho, e $(...) guarda esse caminho na variável lab. Se esta sintaxe for nova para si, como a substituição de comandos captura a saída de um comando explica o processo em detalhe.

Veja o que criou e confirme a sua shell, porque uma nota no final depende da versão:

ls -a
bash --version

Tudo aqui funciona no bash 4.0 e posteriores. A nota globskipdots perto do final aplica-se ao bash 5.2 e posteriores, que é a versão fornecida nas releases atuais do Ubuntu e Debian em agosto de 2026.

O comando de pré-visualização usado ao longo do texto é printf '%s\n' PATTERN. Imprime uma correspondência por linha e não altera nada no disco, por isso é seguro executá-lo com qualquer padrão sobre o qual tenha dúvidas.

Os três curingas: asterisco, ponto de interrogação e colchetes

  • * corresponde a qualquer sequência de caracteres, incluindo uma sequência vazia.
  • ? corresponde exatamente a um caractere.
  • [...] corresponde exatamente a um caractere escolhido do conjunto dentro dos colchetes.
printf '%s\n' *.log
printf '%s\n' app.log.?
printf '%s\n' app.log.[0-9]
printf '%s\n' notes.*
printf '%s\n' *

Compare a segunda linha com a lista de ficheiros que criou. ? representa um caractere, e nada mais, portanto um sufixo com dois algarismos não corresponde a esse padrão. Alargue-o para app.log.[0-9]* e execute-o novamente para ver a diferença.

Aplicam-se duas regras aos três curingas. Nenhum curinga corresponde a um /, portanto um padrão permanece dentro de um único nível de diretório. Além disso, um . inicial tem de ser introduzido literalmente, portanto um padrão que começa por * ignora todos os nomes ocultos. É por isso que rm * não inclui os seus ficheiros ocultos.

Os resultados do glob são ordenados segundo a ordem de agrupamento da localidade atual, que é uma ordenação textual caractere a caractere. Por isso, um sufixo 10 é ordenado antes de um sufixo 2, porque a comparação nunca interpreta nenhum dos dois como um número. Execute printf '%s\n' app.log.* e depois ls -v app.log.* e compare os dois: ls -v ordena os números incorporados pelo seu valor, e sort -V faz o mesmo numa pipeline.

Classes e intervalos de caracteres

Dentro dos colchetes, você pode escrever uma lista, um intervalo ou uma negação.

  • [ch] corresponde a um caractere, c ou h.
  • [a-f] corresponde a um caractere no intervalo de a a f.
  • [!0-9] corresponde a um caractere que não é um dígito. [^0-9] significa a mesma coisa.
printf '%s\n' [dn]*
printf '%s\n' *.[A-Z]*
printf '%s\n' [A-Z]*

Os intervalos são a parte que varia entre máquinas. Um intervalo é resolvido usando a ordem de agrupamento da localidade atual, e não o ASCII puro. Por isso, em alguns sistemas, [a-z] também corresponde a letras maiúsculas. O Bash tem uma opção globasciiranges que força a ordem ASCII pura. Compilações recentes a ativam por padrão, portanto verifique a sua com shopt globasciiranges em vez de presumir. Um script que precisa corresponder da mesma forma em qualquer ambiente pode fixar a ordem definindo LC_ALL=C em uma linha própria, perto do início. Isso faz todos os intervalos voltarem à ordem de bytes simples.

Essa atribuição precisa ser um comando separado. Escrita como prefixo, LC_ALL=C ls [a-z]* define a localidade para ls e nada mais. O shell já expandiu o padrão usando a própria localidade antes de ls ser iniciado. Essa é a regra da primeira seção aparecendo novamente.

A expansão de chaves não é globbing

As chaves parecem um wildcard, mas comportam-se de forma completamente diferente. A expansão de chaves ocorre muito antes do globbing e nunca consulta o disco.

echo file{1,2,3}.txt
echo {01..10}
echo {a..e}
echo {0..20..5}

Nenhum desses ficheiros existe, e a expansão ocorre na mesma. Essa é a diferença fundamental: um glob faz uma pergunta ao sistema de ficheiros, enquanto uma chave apenas gera texto. É também por isso que as chaves são a ferramenta certa para criar itens, e não para os localizar.

mkdir -p site/{css,js,img}
cp app.log{,.bak}
touch report-{2025,2026}-{01,02}.csv

cp app.log{,.bak} expande-se para duas palavras, app.log e app.log.bak, porque o item vazio antes da vírgula reproduz o texto original. {01..10} mantém os zeros à esquerda, porque um zero à esquerda em qualquer um dos limites faz com que o bash preencha todos os números gerados até à mesma largura.

A ordem da expansão também explica uma falha comum. O Bash expande primeiro as chaves, depois as variáveis e, por último, faz a expansão de nomes de caminho.

n=5
echo {1..$n}

A expansão de chaves já terminou quando $n se torna num número, por isso nunca é criado um intervalo. Use seq 1 "$n" ou um loop no estilo C escrito como for ((i=1; i<=n; i++)).

O que acontece quando um padrão não encontra correspondências

Este é o primeiro modo de falha, e acontece pelo menos uma vez com toda a gente. Por predefinição, o bash deixa um padrão sem correspondências completamente inalterado. A palavra chega ao comando exatamente como foi escrita, por isso o comando recebe os caracteres *, ., t, m e p e tenta tratá-los como um nome de ficheiro.

printf '%s\n' *.tmp

Isto é inofensivo com printf. Não é inofensivo com um comando que cria ficheiros. touch *.tmp num diretório sem ficheiros .tmp cria um ficheiro cujo nome literal é *.tmp. Para o apagar depois, é necessário usar aspas para impedir que a shell o expanda novamente: rm -- '*.tmp'.

Duas opções da shell alteram esta regra. Teste cada uma com um padrão que não encontre correspondências e, depois, desative-a.

shopt -s nullglob
printf '%s\n' *.tmp
shopt -u nullglob

shopt -s failglob
printf '%s\n' *.tmp
shopt -u failglob

nullglob faz com que um padrão sem correspondências seja expandido para nada, pelo que o comando é executado com menos argumentos do que os escritos. É isso que pretende num ciclo. Com nullglob desativado, for f in *.tmp executa o corpo uma vez com f a conter o padrão literal. Isto é um erro em quase todos os scripts que fazem isso. Com nullglob ativado, o corpo é executado zero vezes.

nullglob tem o seu próprio risco, porque um comando ao qual foram removidos todos os argumentos continua a ser executado. ls *.nope transforma-se num ls isolado, que lista o diretório inteiro. grep -l needle *.nope transforma-se em grep -l needle, que não tem nenhum ficheiro para ler, por isso fica à espera da entrada padrão e parece ter bloqueado. Ative nullglob em torno do ciclo que precisa dele e desative-o novamente depois.

failglob segue o caminho oposto: um padrão sem correspondências é um erro, o bash comunica-o e o comando não é executado. Esta é uma boa configuração para uma shell interativa, porque impede que um padrão escrito incorretamente seja passado a rm como um nome literal.

Ficheiros ocultos: dotglob

shopt -s dotglob
printf '%s\n' *
shopt -u dotglob

Execute esse bloco e compare a listagem com o printf '%s\n' * simples apresentado anteriormente. dotglob remove a regra do ponto inicial, por isso * também corresponde aos nomes ocultos. As entradas . e .. continuam sempre excluídas quando dotglob está ativo.

Sem dotglob, .* é a forma habitual de aceder a ficheiros ocultos, mas essa abordagem já foi perigosa. No bash anterior à versão 5.2, .* também correspondia a . e .., por isso um comando recursivo como chmod -R 755 .* entrava diretamente no diretório pai. O Bash 5.2 adicionou a opção globskipdots, ativada por predefinição, que mantém . e .. fora de todas as expansões. Execute shopt globskipdots antes de depender desse comportamento, porque um sistema mais antigo não terá essa opção.

globstar: correspondência em toda a árvore

shopt -s globstar
printf '%s\n' **/*.log
printf '%s\n' **/
shopt -u globstar

Com globstar definido, um ** que forme um componente de caminho completo corresponde a ficheiros e diretórios em qualquer profundidade, incluindo zero níveis abaixo. Assim, **/*.log abrange o diretório atual, bem como logs/ e tudo o que estiver sob archive/. Um **/ com a barra final corresponde apenas a diretórios, o que permite ver rapidamente a estrutura de uma árvore.

Sem globstar definido, ** comporta-se como um * normal e permanece num único diretório. Execute o mesmo padrão com a opção desativada e compare os resultados: quando um padrão recursivo copiado de outro local devolve muito menos resultados do que o esperado, esta é normalmente a causa. globstar está desativado por predefinição em todas as versões do bash.

Há um limite importante: ** não segue ligações simbólicas para diretórios. Para uma árvore interligada por symlinks, é necessário usar find -L.

nocaseglob, para nomes que diferem apenas entre maiúsculas e minúsculas

shopt -s nocaseglob
printf '%s\n' *.log
shopt -u nocaseglob

nocaseglob faz a correspondência de nomes de ficheiros ignorar maiúsculas e minúsculas. Assim, um sufixo .LOG em maiúsculas corresponde a um padrão .log em minúsculas. Compare esta listagem com a da primeira secção. Desative a opção imediatamente a seguir. Se ficar ativa, altera a correspondência de todos os comandos seguintes nessa shell, o que dificulta a análise de um problema uma hora mais tarde.

nocaseglob aplica-se apenas à expansão de nomes de ficheiros. A opção relacionada com a correspondência de padrões dentro de uma instrução case ou de um teste com colchetes duplos é nocasematch. As duas opções são configuradas separadamente.

extglob: padrões que significam "tudo menos isto"

Os padrões estendidos estão desativados por predefinição. Ative-os numa linha própria:

shopt -s extglob

Existem cinco formas. Cada uma recebe uma lista de padrões separados por |:

  • ?(list) corresponde a zero ou uma ocorrência.
  • *(list) corresponde a zero ou mais ocorrências.
  • +(list) corresponde a uma ou mais ocorrências.
  • @(list) corresponde exatamente a uma das alternativas.
  • !(list) corresponde a qualquer coisa que não corresponda a nenhuma alternativa.

Um padrão estendido tem de ser interpretado por um shell que já tenha a opção ativada. Por isso, o bloco abaixo é colocado num pequeno script iniciado com bash -O extglob. O sinalizador -O define uma opção de shopt antes de o novo shell ler uma única linha do ficheiro. Assim, todos os padrões são analisados corretamente. O último parágrafo desta secção explica por que motivo a ordem é importante.

cat > patterns.sh <<'EOF'
printf '%s\n' @(app|error).log
printf '%s\n' app.log.+([0-9])
printf '%s\n' *.@(md|txt)
printf '%s\n' !(*.log)
printf '%s\n' !(*.log|*.md|README)
EOF
bash -O extglob patterns.sh

+([0-9]) significa um ou mais dígitos. Por isso, corresponde a um sufixo numerado de qualquer comprimento. Esta é a correção para o limite ? apresentado anteriormente. Vale a pena executar os dois padrões em sequência para observar a diferença.

A forma de negação é a resposta a "tudo exceto". É também o motivo para ativar extglob. Há dois aspetos que surpreendem alguns utilizadores.

Ela corresponde tanto a diretórios como a ficheiros. Por isso, ls !(*.log) lista o conteúdo de qualquer diretório incluído no resultado, em vez do nome do diretório. Use ls -d ou a pré-visualização printf para ver os próprios nomes.

Ela também segue a regra dos pontos, como qualquer outro glob. Assim, !(*.log) corresponde a todos os nomes visíveis que não terminem em .log. Para incluir nomes ocultos, dotglob também tem de estar definido.

O ! em !(...) é a negação de glob. Não tem relação com expansão do histórico e o caractere de exclamação, que é uma funcionalidade separada e é executada noutra fase.

Existe ainda outra armadilha, que ocorre no momento da análise. O Bash analisa um comando completo antes de executar qualquer parte dele. Por isso, ativar extglob e usar um padrão estendido na mesma linha falha: o padrão é analisado enquanto a opção ainda está desativada. Mantenha shopt -s extglob numa linha própria, antes dos padrões e perto do início do script. O corpo de uma função é analisado quando a função é definida. Por isso, a opção tem de estar ativada antes da definição, não antes da chamada. Iniciar o shell com bash -O extglob, como faz o bloco acima, resolve a ordem antes de qualquer análise começar.

O shell expande o padrão, não o comando

Este é o segundo modo de falha. Ele explica toda uma família de erros.

grep -l needle *.log
find . -name '*.log'
find . -name *.log

Na primeira linha, o bash faz a correspondência e grep recebe uma lista de nomes de ficheiros. Na segunda, as aspas protegem o padrão. Assim, find recebe os cinco caracteres *.log e faz a sua própria correspondência em todos os níveis abaixo do ponto inicial. A terceira linha contém o erro: o bash expande primeiro o padrão, mas apenas no diretório atual. Por isso, find recebe instruções para procurar um nome específico. Num diretório com uma única correspondência, procura silenciosamente a coisa errada. Com duas ou mais correspondências, find apresenta um erro de utilização, porque os nomes adicionais ocupam o lugar onde espera uma expressão.

A regra é simples. Se o padrão se destina ao comando, coloque-o entre aspas. Se se destina ao shell, deixe-o sem aspas. A mesma distinção aplica-se aos padrões --exclude em tar e rsync, e aos filtros --include em grep.

Os padrões glob também não são expressões regulares, embora partilhem alguns caracteres. Num glob, * significa qualquer sequência de caracteres. Numa expressão regular, * significa zero ou mais ocorrências do elemento anterior. Por isso, grep '*.log' procura algo bastante diferente do que parece.

As variáveis seguem a mesma ordem de operações. A expansão de glob ocorre depois da expansão de variáveis. Portanto, uma variável sem aspas que contenha um padrão é expandida em relação ao diretório.

pat='*.log'
printf '%s\n' $pat
printf '%s\n' "$pat"

Sem aspas, o bash expande o glob resultante da variável. Com aspas, o padrão permanece literal. Há uma consequência relacionada que deve considerar: as palavras produzidas pela expansão do glob nunca são novamente divididas nos espaços. Assim, for f in *.log trata um nome de ficheiro que contenha um espaço como um único nome, enquanto for f in $(ls *.log) divide esse nome em duas palavras. Faça o loop diretamente sobre o glob.

Antecipe um padrão destrutivo antes de executá-lo

Nunca deixe rm ser o primeiro comando usado para testar um padrão novo. Execute o padrão com algo inofensivo, leia a lista e só depois altere o comando no início da linha.

target='!(*.log)'
printf '%s\n' $target
ls -ld -- $target
rm -- $target

Escrever o padrão em target uma única vez evita que as três linhas fiquem diferentes, e $target permanece sem aspas de propósito, porque é isso que permite que o bash o expanda. printf '%s\n' imprime um nome por linha e não altera nada. ls -ld mostra cada entrada em si, em vez de listar o conteúdo dos diretórios, e a coluna de modo indica quais entradas são diretórios, que é o que deve ser verificado antes de qualquer exclusão recursiva. Leitura dos bits de permissão drwxr-xr-x explica essa coluna.

Recupere a linha anterior com a seta para cima e edite apenas o comando, para que o padrão permaneça idêntico. É ao redigitar um padrão de memória que o erro aparece.

Coloque -- antes do padrão. Isso marca o fim das opções, de modo que um nome de ficheiro iniciado por - seja tratado como um nome, e não como uma opção.

Se quiser desativar a expansão de padrões durante parte de um script, set -f desativa-a e set +f volta a ativá-la.

Este hábito é importante porque rm não tem desfazer. Recuperar ficheiros eliminados com rm -rf é demorado e normalmente incompleto, por isso uma pré-visualização custa muito menos do que a alternativa.

Limpe o diretório de prática quando terminar. Imprima o caminho e leia-o primeiro, porque apontar rm -rf para a variável errada é exatamente o acidente abordado nesta seção.

cd ~
echo "$lab"
rm -rf -- "$lab"

FAQ

Por que find . -name *.log encontra os ficheiros errados?

Porque o bash expande *.log antes de find sequer iniciar. A shell compara o padrão com o diretório atual e passa o nome do ficheiro resultante a find, que depois procura esse único nome em todos os níveis. Se dois ou mais ficheiros corresponderem, o find recebe argumentos adicionais onde espera uma expressão e apresenta um erro de utilização. Coloque o padrão entre aspas, como find . -name '*.log', para que find faça a correspondência diretamente, com as suas próprias regras e a sua própria recursão.

O que significa quando o meu padrão aparece como texto literal?

Significa que nada correspondeu. Por predefinição, o bash deixa inalterado um padrão sem correspondências e passa os caracteres originais ao comando, que os trata como um nome de ficheiro. Defina shopt -s failglob para transformar um padrão sem correspondências num erro que interrompe o comando, ou shopt -s nullglob para o expandir para nada. Com nullglob, confirme que o comando continua a fazer sentido sem argumentos, porque ls *.nope passa a ser um ls isolado e lista todo o diretório.

Como faço a correspondência de todos os ficheiros, exceto um padrão, no bash?

Ative os padrões estendidos com shopt -s extglob numa linha própria e, depois, use a forma de negação. !(*.log) corresponde a nomes que não terminam em .log, e !(*.log|*.md) exclui ambos. Os ficheiros ocultos continuam fora do resultado, a menos que dotglob também esteja definido, e os diretórios são incluídos. Por isso, pré-visualize o padrão com printf '%s\n' !(*.log) antes de o passar a rm.

Por que * ignora os ficheiros ocultos?

Um ponto inicial tem de ser correspondido literalmente, por isso * nunca corresponde a um nome que comece por um ponto. Use .* para alcançar nomes ocultos ou defina shopt -s dotglob para os incluir em todos os padrões normais. As entradas . e .. são sempre excluídas com dotglob, e o bash 5.2 e posteriores também as mantêm fora de .* através da opção globskipdots, que está ativa por predefinição.

Preciso de globstar para ** funcionar?

Sim. Sem shopt -s globstar, o bash trata ** como um * normal, que corresponde dentro de um diretório e para nesse nível. Com globstar ativo, um ** que forme um componente de caminho completo corresponde em qualquer nível. Assim, **/*.log alcança o diretório atual e todos os subdiretórios abaixo dele, e **/ isolado corresponde apenas a diretórios. Tenha em atenção que ** não segue ligações simbólicas para diretórios. Para uma árvore criada com ligações simbólicas, use find -L.

#bash#globbing#shell#linux#scripting