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.logmktemp -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 --versionTudo 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,couh.[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}.csvcp 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' *.tmpIsto é 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 failglobnullglob 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 dotglobExecute 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 globstarCom 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 nocaseglobnocaseglob 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 extglobExistem 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 *.logNa 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 -- $targetEscrever 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.