De acordo com as Leis 12.965/2014 e 13.709/2018, que regulam o uso da Internet e o tratamento de dados pessoais no Brasil, ao me inscrever na newsletter do portal DICAS-L, autorizo o envio de notificações por e-mail ou outros meios e declaro estar ciente e concordar com seus Termos de Uso e Política de Privacidade.


Auxiliar do Wordle e uns truques do shell

Colaboração: Arnaldo Mandel

Data de Publicação: 7 de abril de 2022

Depois que comecei a jogar o wordle, senti necessidade ocasional de juntar minhas pistas e procurar palavras num dicionário. Dava para escrever tudo em uma linha, mas alguma hora resolvi automatizar. O resultado é um script útil, mas o mais interessante é que em poucas linhas ele junta montes de truques do shell, que podem ser úteis em outros contextos.

O wordle é um jogo bastante popular, em que se tenta adivinhar uma palavra alvo de 5 letras; é encontrado para várias línguas, como o original inglês e o português. Ao longo do jogo se recolhem três tipos de pistas, indicadas por cores:

  1. Letras corretamente posicionadas (verdes).
  2. Letras que ocorrem na palavra alvo, incorretamente posicionadas (amarelas).
  3. Letras que não ocorrem na palavra alvo.

Já existem vários sites que aceitam de alguma forma essas informações e soltam uma lista de palavras que satisfazem todas as restrições; às vezes, o número de possibilidades é surpreendente. Aqui veremos um utilitários desses para a linha de comando.

Ele será chamado com até três parâmetros:

$ wordle-helper info cinzas amarelas

onde cinzas e amarelas são as letras cinzas e as amarelas, justapostas. info é formada por 5 blocos, correspondentes às 5 letras, que são de três tipos:

  • . indicando que não há informação sobre a posição
  • letra, se a letra é verde nesta posição
  • a lista de letras amarelas nessa posição entre [].

Por exemplo, se a quarta letra é a (verde), na segunda posição aparecem amarelas e,n, e as letras b,d,u,o são cinzas, podemos chamar

$ wordle-helper .[en].a. bduo en

A ideia principal é submeter uma lista de palavras a filtros sucessivos e mostrar quem sobrevive. A lista que uso, peguei na rede e instalei como $HOME/tmp/enable2k o importante é ser um arquivo texto, com uma palavra por linha.

O primeiro filtro usa (quase) o primeiro argumento como expressão regular para busca no arquivo. Só que a expressão certa requer que o interior dos colchetes seja negado (é o que eu fazia na linha de comando) - mas é muito chato ficar digitando ^. Assim, em vez de $1, insiro o chapéu dentro de cada colchete, usando uma expansão de parâmetro, e o primeiro filtro fica

$ grep -w ${1//[[]/[^} $HOME/tmp/enable2k 

onde o parâmetro -w garante que só serão pegas palavras de 5 letras.

O segundo filtro é mais simples, rejeita as letras cinzas

$ grep -v [$2]

Ops, não! Os colchetes são especiais para o shell, e dentro deles não ocorre expansão de parâmetro; solução: colocar os colchetes entre aspas. De quebra, também, para o caso de não haver segundo parâmetro, vamos transformar o comando em algo inócuo, rejeitando linhas que contenham um asterisco.

$ grep -v "[${2-*}]"

O terceiro é o mais complicado. É possível escrever uma expressão regular que seleciona as palavras contendo todas as letras amarelas, mas são expressões muito complicadas. Por exemplo, se as amarelas são ao, uma expressão simples seria a.*o|o.*a; três letras pedem seis trechos e são 24 para 4 letras - ninguém merece. Melhor exigir uma letra por vez:

$ grep a | grep o

e isso se estende fácil para mais letras. Então é preciso produzir essa sequência de greps a partir das amarelas. Acontece que a substituição do shell é muito limitada para isso. Mas existe o sed e a transformação é bastante simples:

echo $3 | sed -e 's/./| grep &/g'

colocando | grep na frente de cada letra. Juntando tudo, temos a linha

grep -w ${1//[[]/[^} $HOME/tmp/enable2k | grep -v "[$2]" $(echo $3 | sed -e 's/./| grep &/g')

que colocamos num arquivo, com a linha inicial

#!/usr/bin/bash

tornamos executável, chamamos... e erro! Isso porque a saída do sed é interpretada como uma sequência de argumentos do segundo grep.

Solução: O shell tem o (perigoso) comando eval, que recebe um string e faz com que o shell o interprete como se estivesse no arquivo. Então vamos criar toda a linha de comando como um string, e entregar para o comando eval.

eval "grep -w ${1//[[]/[^} $HOME/tmp/enable2k | grep -v [${2:-*}] $(echo $3|sed -e 's/./| grep &/g')"

Pronto, com esta linha temos pronto o auxiliar para wordle em inglês. Para português é só fazer uma cópia do programa, procurar uma lista de palavras (uso br-sem-acentos.txt), e trocar o nome do arquivo na cópia. O mesmo valendo para outras línguas.

Mas isso é ruim porque se o programa mudar em algum detalhe, tem que editar todas as cópias. Bem melhor usar um programa só e escolher a língua através de um parâmetro. Mas é chato colocar mais um parâmetro além dos três, mais coisa para digitar. Existe, entretanto um truque bastante usado: um programa pode verificar o nome como foi chamado e usar esse nome para mudar seu comportamento. No shell, esse nome é o parâmetro 0. Ele contém o caminho completo, para ficar só o nome, usamos ${0##*/}.

Antes disso, vamos criar links simbólicos:

$ ln -s wordle-helper whi; ln -s wordle-helper ``wordle-helper` whp 

Vamos também criar uma tabela associativa, para não precisar fazer comparações diretas na hora de escolher dicionário:

$ declare -A dic=( [whi]=$HOME/tmp/enable2k [whp]=$HOME/tmp/br-sem-acentos.txt )

e agora o nome do arquivo pode ser selecionado por ${dic[${0##*/}]}. O programa fica assim:

#!/usr/bin/bash 
declare -A dic=( [whi]=$HOME/tmp/enable2k [whp]=$HOME/tmp/br-sem-acentos.txt )
eval "grep -w ${#/[123]/}${1//[[]/[^} ${dic[${0##*/}]} | grep -v [${2:-*}] $(echo $3|sed -e 's/./| grep &/g')"

Note que apareceu um ${#/[123]/} no primeiro grep. Isso expande para um string vazio se o programa for chamado com algum parâmetro, mas produz um 0 se o programa for chamado sem parâmetros; neste caso, como nenhuma palavra contem o caractere 0 [carece de fontes] o primeiro grep não produz nada, e o programa termina quieto, sem saída.

Agora, whi é o auxiliar para inglês, whp é o auxiliar para português. Quer francês ou espanhol? Simples: procure uma lista de palavras dessa língua, crie um link simbólico e acrescente ao dic uma nova entrada, seguindo o modelo das anteriores.

Terminou? Não! Uma sequência de greps pode ser substituída por uma única chamada do sed, e o wordle-helper fica assim:

#!/usr/bin/bash 
declare -A dic=( [whi]=$HOME/tmp/enable2k [whp]=$HOME/tmp/br-sem-acentos.txt )
sed -E -e "/\b${#/[123]/}${1//[[]/[^}\b/!d;/[${2:-*}]/d$(echo $3|sed -e 's/./;\/&\/!d/g')" ${dic[${0##*/}]}

O que aconteceu:

  1. Na falta da opção -w, a primeira expressão regular foi circundada por \b, para o mesmo efeito (valeria também para o grep).

  2. Se uma expressão seleciona uma linha desejada, ela é seguida por !d para descartar as linhas não selecionadas. Isso é usado no processamento do primeiro e terceiro parâmetros.

  3. No segundo parâmetro, o que se encontra é descartado.

  4. O terceiro parâmetro, como no primeiro script, é processado para produzir instruções; melhor ver um exemplo. Se ele for abc, o sed que o processa produz ;/a/!d;/b/!d;/c/!d. Os ; separam instruções sucessivas do sed e sucessivamente as palavras que não contém cada uma das três letras são descartadas.

Observações

  • Esses scripts foram feitos para uso pessoal, por isso não sanitizam seus parâmetros. Outros usos podem ter problemas de segurança.
  • O primeiro parâmetro descreve implicitamente o tamanho das palavras sendo buscadas. Se você encontrar uma variação do wordle que usa palavras de tamanho fixo diferente de 5, dá para usar os scripts sem mudança.
  • Ambos scripts são instantâneos, tanto faz do ponto de vista prático. O segundo é mais estético, chama um sed em vez de vários greps.



Veja a relação completa dos artigos de Arnaldo Mandel