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:
- Letras corretamente posicionadas (verdes).
- Letras que ocorrem na palavra alvo, incorretamente posicionadas (amarelas).
- 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 grep
s 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 grep
s 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:
- 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
).
- 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.
- No segundo parâmetro, o que se encontra é descartado.
- 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
grep
s.