Os coringas do Shell que todo mundo usa e quase ninguém domina
O asterisco aceita zero ocorrências com a mesma boa vontade com que aceita mil.
$ whoami
Julio Cezar Neves
Quatro caracteres resolvem praticamente qualquer busca por nome de arquivo no terminal, e a maioria das pessoas conhece só metade do que eles fazem. O asterisco entra no repertório logo no primeiro dia, passa a ser usado sem pensar e o aprendizado para por ali. Parar por ali custa caro em produtividade, e a conta chega disfarçada de script que funciona quase sempre.
Esses caracteres formam o glob, o mecanismo de expansão que o Shell resolve antes de qualquer comando ser executado. Existe um conjunto mais avançado, o extglob, que fica guardado atrás de um shopt -s extglob e merece capítulo próprio, porque a densidade de casos de uso ali é grande demais para dividir espaço com o básico. Por hoje, o foco fica nos quatro coringas clássicos, os que aparecem em praticamente todo script decente.
O asterisco é o mais usado e o mais mal compreendido. Ele casa com qualquer sequência de caracteres, incluindo a sequência vazia, e por isso arq* casa tanto com arq quanto com arqzzz. Essa liberdade de zero ou mais confunde quem espera que o coringa sempre exija algo depois do prefixo. Ele tem um único pudor, que é deixar de fora os arquivos que começam com ponto, e é por isso que um rm * poupa o .bashrc, para alívio de muita gente.
A interrogação cobre exatamente esse ponto cego. Ela exige presença, e exige um caractere só. O padrão arq? recusa arq sozinho, porque falta o caractere obrigatório, e recusa também arq10, onde sobram dois caracteres no lugar de um. A interrogação é rígida na mesma medida em que o asterisco é frouxo, e entender essa diferença evita um bocado de script quebrado por hipótese errada.
Os colchetes trazem outra lógica, a da lista fechada. Eles casam com um único caractere entre os que estiverem listados, e arq[1459] casa com arq1 ou com arq9, sempre um dígito por vez, porque o colchete resolve uma posição só. Dentro deles cabe intervalo também, e arq[5-9] cobre o mesmo território de listar os cinco dígitos um por um, com sintaxe bem mais enxuta.
A negação completa o quarteto. Um ponto de exclamação logo depois do colchete de abertura inverte a lógica inteira, e arq[!5-9] casa com qualquer caractere fora daquela faixa. É o oposto exato do intervalo positivo, útil sempre que descrever a exclusão é mais fácil que descrever a inclusão. O Bash aceita também o circunflexo no lugar da exclamação, por cortesia a quem chega das expressões regulares.
Aqui mora um erro clássico, daqueles que rendem depuração frustrada numa sexta-feira à tarde. O padrão arq[1-14] parece casar com os arquivos de 1 até 14, e o colchete trabalha com caractere único, alheio a números de dois dígitos. O Shell lê ali um intervalo de 1 até 1 seguido do caractere 4, e o resultado real é arq1 e arq4, o mesmo que arq[14]. Quem precisa de uma faixa numérica de verdade usa chave, e arq{1..14} resolve sem ambiguidade.
Vale um teste simples para descobrir quem realmente expande esses coringas. A tentação natural é testar com ls, e o ls esconde uma informação importante. O echo ignora completamente o sistema de arquivos e, mesmo assim, devolve os nomes certos:
$ echo arq*
arq arq1 arq10 arq4 arq5 arq9 arqzzz
Se o echo devolve os nomes, a expansão aconteceu antes de qualquer coisa chegar ao comando, resolvida pelo Shell na hora de montar a linha. O ls recebe os nomes já prontos, como qualquer outro programa receberia, e trabalha apenas com o que o Shell entregou. A prova definitiva aparece quando nada casa. Num diretório sem arq7 e sem arq8, o padrão arq[7-8] volta inteiro, literal, e cada comando reage do seu jeito:
$ echo arq[7-8]
arq[7-8]
$ ls arq[7-8]
ls: arq[7-8]: No such file or directory
O echo imprime o padrão como recebeu, e o ls procura um arquivo chamado literalmente arq[7-8] e reclama da ausência. O glob só devolve nome que existe. Quem gera nome que ainda vai existir é a expansão de chaves, e confundir as duas rende outra fonte farta de surpresa. Para o padrão sumir quando nada casa, existe o shopt -s nullglob, muito útil dentro de loops que percorrem arquivos.
A parte que costuma surpreender quem cresceu ouvindo que Shell é lento vem agora. Rodei 2.000 vezes o ls arq* e 2.000 vezes o echo arq*, com a saída jogada no /dev/null. O ls levou 1,73 segundo, porque cada chamada dispara um processo externo completo. O echo, que é interno e recebe o glob já resolvido, terminou em 0,04 segundo, cerca de quarenta vezes menos.
A fama de lentidão do Shell nasce do costume de escrever mal, e a linguagem leva a culpa por tabela. Script cheio de chamada externa desnecessária paga o custo de cada processo criado, e isso vale para qualquer linguagem interpretada. Quem reconhece quando o próprio interpretador resolve a tarefa escreve código mais rápido sem trocar de linguagem. O Shell agradece, e o servidor agradece mais ainda.
Os quatro coringas também trabalham longe da listagem de arquivos. Dentro de um case, os mesmos metacaracteres testam o padrão de uma entrada digitada, sem expressão regular alguma. Testar se um caractere está entre A e Z vira [A-Z], e testar se está fora vira [!A-Z]. O resultado é um case mais expressivo que o equivalente de muita linguagem com fama de moderna. Um exemplo deixa isso concreto:
read -n1 letra
case $letra in
[AEIOU]) echo vogal ;;
[A-Z]) echo consoante ;;
esac
O primeiro padrão casa só com as cinco vogais maiúsculas. Se ele falhar, o segundo captura qualquer outra letra do alfabeto, e a consoante sai por eliminação. Vale um cuidado: em versões antigas do Bash, certos locales faziam [A-Z] casar também com minúsculas. Desde o Bash 5 a opção globasciiranges vem ligada, e o intervalo passa a seguir a tabela ASCII, como sempre se esperou dele.
O mesmo raciocínio resolve uma saudação por horário, dentro do próprio case:
case $hora in
0[0-9]|1[01]) echo "Bom dia" ;;
esac
O primeiro ramo casa com qualquer hora de 00 a 09, usando o zero fixo e o intervalo de dígitos, e o segundo casa com 10 e 11, unidos pela barra vertical, que no case significa ou. A mesma tarefa sai com aritmética, e aí mora uma armadilha. Dentro de [[ ]], a hora 08 vira número octal inválido e o Bash reclama, e a saída é escrever (( 10#$hora < 12 )). O case com glob atravessa esse campo minado sem perceber que ele existe.
O que separa o uso superficial do domínio está nessa reutilização. O mesmo glob que filtra nome de arquivo filtra a entrada do teclado dentro do case e filtra a faixa de horário com o mesmo raciocínio. São quatro caracteres e uma lógica só, aplicada em contextos diferentes sem sintaxe nova a cada situação. Aprender uma vez e usar em todo lugar é o tipo de investimento que o Shell recompensa com juros.
Essa economia mental aparece depois de muitas tentativas de decorar uma solução isolada para cada problema. Quem entende o mecanismo resolve qualquer variação da pergunta, e quem decora o exemplo pronto trava assim que o problema muda de roupa. O instinto de reconhecer o mesmo mecanismo em lugares diferentes economiza tempo de quem escreve e de quem vai ler o código depois, que costuma ser a mesma pessoa, seis meses mais velha e bem menos paciente.
PS: Se você ainda escreve arq[1-14] esperando os arquivos de 1 até 14, rode o comando agora e veja a surpresa com os próprios olhos. Depois conte nos comentários qual coringa já pregou uma peça em você, e compartilhe o texto com quem jura que o asterisco é o único que existe. O asterisco, por sinal, adora essa fama.
$ tee # compartilhe este artigo WhatsApp Telegram LinkedIn
$ cat comentarios.log
lendo o log…