Aprenda Expect escrevendo e automatizando um jogo simples
Codifique um jogo de "adivinhe o número" no Expect. Em seguida, aprenda o verdadeiro poder do Expect com um script separado para automatizar o jogo.
Ao tentar automatizar meu fluxo de trabalho, encontrei um utilitário de configuração que desafiava uma automação significativa. Era um processo Java que não suportava um instalador silencioso, nem suportava stdin
, e tinha um conjunto inconsistente de prompts. O módulo expect
do Ansible era inadequado para esta tarefa. Mas descobri que o comando expect
era a ferramenta certa para o trabalho.
Minha jornada para aprender o Expect significou aprender um pouco de Tcl. Agora que tenho experiência para criar programas simples, posso aprender melhor a programar no Expect. Achei que seria divertido escrever um artigo que demonstrasse a funcionalidade interessante deste venerável utilitário.
Este artigo vai além do típico formato de jogo simples. Pretendo usar partes do Expect para criar o jogo em si. Em seguida, demonstro o verdadeiro poder do Expect com um script separado para automatizar o jogo.
Este exercício de programação mostra vários exemplos clássicos de programação de variáveis, entrada, saída, avaliação condicional e loops.
Instalar Esperar
Para sistemas baseados em Linux, use:
$ sudo dnf install expect
$ which expect
/bin/expect
Descobri que minha versão do Expect estava incluída no sistema operacional básico do macOS:
$ which expect
/usr/bin/expect
No macOS, você também pode carregar uma versão um pouco mais recente usando brew:
$ brew install expect
$ which expect
/usr/local/bin/expect
Adivinhe o número em Expect
O jogo de adivinhação de números usando Expect não é muito diferente do Tcl base que usei em meu artigo anterior.
Todas as coisas em Tcl são strings, incluindo valores de variáveis. As linhas de código são melhor contidas entre chaves (em vez de tentar usar a continuação de linha). Colchetes são usados para substituição de comandos. A substituição de comandos é útil para derivar valores de outras funções. Ele pode ser usado diretamente como entrada quando necessário. Você pode ver tudo isso no script subsequente.
Crie um novo arquivo de jogo numgame.exp
, configure-o como executável e insira o script abaixo:
#!/usr/bin/expect
proc used_time {start} {
return [expr [clock seconds] - $start]
}
set num [expr round(rand()*100)]
set starttime [clock seconds]
set guess -1
set count 0
send "Guess a number between 1 and 100\n"
while { $guess != $num } {
incr count
send "==> "
expect {
-re "^(\[0-9]+)\n" {
send "Read in: $expect_out(1,string)\n"
set guess $expect_out(1,string)
}
-re "^(.*)\n" {
send "Invalid entry: $expect_out(1,string) "
}
}
if { $guess < $num } {
send "Too small, try again\n"
} elseif { $guess > $num } {
send "Too large, try again\n"
} else {
send "That's right!\n"
}
}
set used [used_time $starttime]
send "You guessed value $num after $count tries and $used elapsed seconds\n"
Usar proc
configura uma definição de função (ou procedimento). Consiste no nome da função, seguido por uma lista contendo os parâmetros (1 parâmetro {start}
) e depois seguido pelo corpo da função. A instrução return mostra um bom exemplo de substituição de comando Tcl aninhado. As instruções set
definem variáveis. Os dois primeiros usam substituição de comando para armazenar um número aleatório e a hora atual do sistema em segundos.
O loop while
e a lógica if-elseif-else devem ser familiares. Observe novamente o posicionamento específico das chaves para ajudar a agrupar várias sequências de comandos sem a necessidade de continuação de linha.
A grande diferença que você vê aqui (do programa Tcl anterior) é o uso das funções expect
e send
em vez de usar puts
e obtém
. Usar expect
e send
formam o núcleo da automação do programa Expect. Nesse caso, você usa essas funções para automatizar uma pessoa em um terminal. Mais tarde você poderá automatizar um programa real. Usar o comando send
neste contexto não é muito mais do que imprimir informações na tela. O comando expect
é um pouco mais complexo.
O comando expect
pode assumir algumas formas diferentes, dependendo da complexidade das suas necessidades de processamento. O uso típico consiste em um ou mais pares padrão-ação, como:
expect "pattern1" {action1} "pattern2" {action2}
Necessidades mais complexas podem colocar vários pares de ações de padrão entre chaves opcionalmente prefixadas com opções que alteram a lógica de processamento. O formulário que usei acima encapsula vários pares padrão-ação. Ele usa a opção -re
para aplicar o processamento regex (em vez do processamento glob) ao padrão. Segue-se isso com chaves encapsulando uma ou mais instruções a serem executadas. Eu defini dois padrões acima. O primeiro é destinado a corresponder a uma sequência de 1 ou mais números:
"^(\[0-9]+)\n"
O segundo padrão foi projetado para corresponder a qualquer outra coisa que não seja uma sequência de números:
"^(.*)\n"
Observe que esse uso de expect
é executado repetidamente a partir de uma instrução while
. Esta é uma abordagem perfeitamente válida para a leitura de múltiplas entradas. Na automação, mostro uma pequena variação do Expect que faz a iteração para você.
Finalmente, a variável $expect_out
é um array usado por expect
para armazenar os resultados de seu processamento. Neste caso, a variável $expect_out(1,string)
contém o primeiro padrão capturado da regex.
Execute o jogo
Não deve haver surpresas aqui:
$ ./numgame.exp
Guess a number between 1 and 100
==> Too small, try again
==> 100
Read in: 100
Too large, try again
==> 50
Read in: 50
Too small, try again
==> 75
Read in: 75
Too small, try again
==> 85
Read in: 85
Too large, try again
==> 80
Read in: 80
Too small, try again
==> 82
Read in: 82
That's right!
You guessed value 82 after 8 tries and 43 elapsed seconds
Uma diferença que você pode notar é a impaciência que esta versão exibe. Se você hesitar por tempo suficiente, espere o tempo limite com uma entrada inválida. Em seguida, ele solicitará novamente. Isso é diferente de gets
que espera indefinidamente. O tempo limite de expect
é um recurso configurável. Ajuda a lidar com programas travados ou durante uma saída inesperada.
Automatize o jogo no Expect
Neste exemplo, o script de automação Expect precisa estar na mesma pasta que o script numgame.exp
. Crie o arquivo automate.exp
, torne-o executável, abra seu editor e digite o seguinte:
#!/usr/bin/expect
spawn ./numgame.exp
set guess [expr round(rand()*100)]
set min 0
set max 100
puts "I'm starting to guess using the number $guess"
expect {
-re "==> " {
send "$guess\n"
expect {
"Too small" {
set min $guess
set guess [expr ($max+$min)/2]
}
"Too large" {
set max $guess
set guess [expr ($max+$min)/2]
}
-re "value (\[0-9]+) after (\[0-9]+) tries and (\[0-9]+)" {
set tries $expect_out(2,string)
set secs $expect_out(3,string)
}
}
exp_continue
}
"elapsed seconds"
}
puts "I finished your game in about $secs seconds using $tries tries"
A função spawn
executa o programa que você deseja automatizar. Leva o comando como strings separadas seguidas pelos argumentos a serem passados para ele. Defino o número inicial para adivinhar e a verdadeira diversão começa. A instrução expect
é consideravelmente mais complicada e ilustra o poder deste utilitário. Observe que não há nenhuma instrução de loop aqui para iterar nos prompts. Como meu jogo tem prompts previsíveis, posso pedir que expect
faça um pouco mais de processamento para mim. O expect
externo tenta corresponder ao prompt de entrada do jogo `==>` . Vendo isso, ele usa send
para adivinhar e depois usa um expect
adicional para descobrir os resultados da estimativa. Dependendo da saída, as variáveis são ajustadas e calculadas para definir a próxima estimativa. Quando o prompt `==>` é correspondido, a instrução exp_continue
é invocada. Isso faz com que o expect
externo seja reavaliado. Portanto, um loop aqui não é mais necessário.
Este processamento de entrada depende de outro comportamento do processamento do Expect. Expect armazena em buffer a saída do terminal até que ela corresponda a um padrão. Esse buffer inclui qualquer fim de linha incorporado e outros caracteres não imprimíveis. Isso é diferente da típica correspondência de linha regex com a qual você está acostumado com Awk e Perl. Quando um padrão é correspondido, tudo o que vem depois da correspondência permanece no buffer. Fica disponível para a próxima tentativa de partida. Eu explorei isso para encerrar de forma limpa a instrução expect
externa:
-re "value (\[0-9]+) after (\[0-9]+) tries and (\[0-9]+)"
Você pode ver que o padrão interno corresponde ao palpite correto e não consome todos os caracteres impressos pelo jogo. A última parte da string (segundos decorridos) ainda é armazenada em buffer após a estimativa bem-sucedida. Na próxima avaliação do expect
externo, essa string é correspondida do buffer para terminar de forma limpa (nenhuma ação é fornecida). Agora, a parte divertida, vamos executar a automação completa:
$ ./automate.exp
spawn ./numgame.exp
I'm starting to guess with the number 99
Guess a number between 1 and 100
==> 99
Read in: 99
Too large, try again
==> 49
Read in: 49
Too small, try again
==> 74
Read in: 74
Too large, try again
==> 61
Read in: 61
Too small, try again
==> 67
Read in: 67
That's right!
You guessed value 67 after 5 tries and 0 elapsed seconds
I finished your game in about 0 seconds using 5 tries
Uau! Minha eficiência em adivinhar números aumentou dramaticamente graças à automação! Alguns testes resultaram em média de 5 a 8 suposições. Também sempre foi concluído em menos de 1 segundo. Agora que esta diversão incómoda e demorada pode ser despachada tão rapidamente, não tenho desculpa para atrasar outras tarefas mais importantes, como trabalhar nos meus projectos de melhoria da casa :P
Nunca pare de aprender
Este artigo foi um pouco extenso, mas valeu a pena o esforço. O jogo de adivinhação de números ofereceu uma boa base para demonstrar um exemplo mais interessante de processamento do Expect. Aprendi bastante com o exercício e consegui concluir minha automação de trabalho com sucesso. Espero que você tenha achado este exemplo de programação interessante e que o ajude a atingir seus objetivos de automação.