tech-tips/Языки программирования/Shell/Цикл статей от RUVDS/Bash-скрипты, часть 11- expect и автоматизация интерактивных утилит - Хабр.md

369 lines
24 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[https://habr.com/ru/company/ruvds/blog/328436/](https://habr.com/ru/company/ruvds/blog/328436/)
---
В прошлый раз мы говорили о методике разработки bash-скриптов. Если же суммировать всё, что мы разобрали в предыдущих десяти материалах, то вы, если начинали читать их, ничего не зная о bash, теперь можете сделать уже довольно много всего полезного.
Сегодняшняя тема, заключительная в этой серии материалов, посвящена автоматизации работы с интерактивными утилитами, например, со скриптами, которые, в процессе выполнения, взаимодействуют с пользователем. В этом деле нам поможет expect — инструмент, основанный на языке Tcl.
Expect позволяет создавать программы, ожидающие вопросов от других программ и дающие им ответы. Expect можно сравнить с роботом, который способен заменить пользователя при взаимодействии со сценариями командной строки.
## Основы expect
Если expect в вашей системе не установлен, исправить это, например, в Ubuntu, можно так:
```
$ apt-get install expect
```
В чём-то вроде CentOs установка выполняется такой командой:
```
$ yum install expect
```
Expect предоставляет набор команд, позволяющих взаимодействовать с утилитами командной строки. Вот его основные команды:
- `spawn —` запуск процесса или программы. Например, это может быть командная оболочка, [FTP](https://likegeeks.com/ftp-server-linux/), Telnet, ssh, scp и так далее.
- `expect —` ожидание данных, выводимых программой. При написании скрипта можно указать, какого именно вывода он ждёт и как на него нужно реагировать.
- `send —` отправка ответа. Expect-скрипт с помощью этой команды может отправлять входные данные автоматизируемой программе. Она похожа на знакомую вам команду `echo` в обычных bash-скриптах.
- `interact —` позволяет переключиться на «ручной» режим управления программой.
## Автоматизация bash-скрипта
Напишем скрипт, который взаимодействует с пользователем и автоматизируем его с помощью expect. Вот код bash-скрипта `questions`:
```
#!/bin/bash
echo "Hello, who are you?"
read $REPLY
echo "Can I ask you some questions?"
read $REPLY
echo "What is your favorite topic?"
read $REPLY
```
Теперь напишем expect-скрипт, который запустит скрипт `questions` и будет отвечать на его вопросы:
```
#!/usr/bin/expect -f
set timeout -1
spawn ./questions
expect "Hello, who are you?\r"
send -- "Im Adam\r"
expect "Can I ask you some questions?\r"
send -- "Sure\r"
expect "What is your favorite topic?\r"
send -- "Technology\r"
expect eof
```
Сохраним скрипт, дав ему имя `answerbot`.
В начале скрипта находится строка идентификации, которая, в данном случае, содержит путь к expect, так как интерпретировать скрипт будет именно expect.
Во второй строке мы отключаем тайм-аут, устанавливая переменную expect
`timeout` в значение -1. Остальной код — это и есть автоматизация работы с bash-скриптом.
Сначала, с помощью команды `spawn`, мы запускаем bash-скрипт. Естественно, тут может быть вызвана любая другая утилита командной строки. Далее задана последовательность вопросов, поступающих от bash-скрипта, и ответов, которые даёт на них expect. Получив вопрос от подпроцесса, expect выдаёт ему заданный ответ и ожидает следующего вопроса.
В последней команде expect ожидает признака конца файла, скрипт, дойдя до этой команды, завершается.
Теперь пришло время всё это опробовать. Сделаем `answerbot` исполняемым файлом:
```
$ chmod +x ./answerbot
```
И вызовем его:
```
$./answerbot
```
![[45654e80149d0ffc4f4fa6616857ef44.png]]
_Expect-скрипт отвечает на вопросы bash-скрипта_
Как видно, expect-скрипт верно ответил на вопросы bash-скрипта. Если на данном этапе вы столкнулись с ошибкой, вызванной тем, что неправильно указано расположение expect, выяснить его адрес можно так:
```
$ which expect
```
Обратите внимание на то, что после запуска скрипта `answerbot` всё происходит в полностью автоматическом режиме. То же самое можно проделать для любой утилиты командной строки. Тут надо отметить, что наш bash-скрипт устроен очень просто, мы точно знаем, какие именно данные он выводит, поэтому написать expect-скрипт для взаимодействия с ним несложно. Задача усложняется при работе с программами, которые написаны другими разработчиками. Однако, здесь на помощь приходит средство для автоматизированного создания expect-скриптов.
## Autoexpect — автоматизированное создание expect-скриптов
Autoexpect позволяет запускать программы, которые надо автоматизировать, после чего записывает то, что они выводят, и то, что пользователь вводит, отвечая на их вопросы. Вызовем autoexpect, передав этой утилите имя нашего скрипта:
```
$ autoexpect ./questions
```
В этом режиме взаимодействие с bash-скриптом ничем не отличается от обычного: мы сами вводим ответы на его вопросы.
![[fb81310fe0054606d27778474d80079f.png]]
_Запуск bash-скрипта с помощью autoexpect_
После завершения работы с bash-скриптом, autoexpect сообщит о том, что собранные данные записаны в файл `script.exp`. Взглянем на этот файл.
![[10fe731bd35562c114a5158121450126.png]]
айл script.exp_
В целом, за исключением некоторых деталей, перед нами такой же скрипт, который мы писали самостоятельно. Если запустить этот скрипт, результат будет тем же.
![[0a20d545683e4b5b5469495a642671ab.png]]
_Запуск expect-скрипта, созданного автоматически_
При записи сеансов взаимодействия с некоторыми программами, вроде FTP-клиентов, вы можете столкнуться с тем, что они используют в выводимых данных сведения о времени проведения операции, или выводят данные, отражающие процесс выполнения неких продолжительных действий. В целом, речь идёт о том, что вывод программы при каждом её запуске, правильно воспринимаемый человеком и вызывающий ввод одних и тех же ответов, будет, в тех же условиях, выглядеть по-новому для expect.
Если в expect-скрипте строки, ожидаемые от такой программы, будут жёстко зафиксированы, такой скрипт не сможет нормально работать. Справиться с этим можно, либо удалив из expect-скрипта данные, которые выглядят по-новому при каждом запуске программы, либо использовав шаблоны, пользуясь которыми, expect сможет правильно понять то, что хочет от него программа.
Как видите, autoexpect — это весьма полезный инструмент, но и он не лишён недостатков, исправить которые можно только вручную. Поэтому продолжим осваивать язык expect-скриптов.
## Работа с переменными и параметрами командной строки
Для объявления переменных в expect-скриптах используется команда `set`. Например, для того, чтобы присвоить значение 5 переменной `VAR1`, используется следующая конструкция:
```
set VAR1 5
```
Для доступа к значению переменной перед её именем надо добавить знак доллара — `$`. В нашем случае это будет выглядеть как `$VAR1`.
Для того, чтобы получить доступ к аргументам командной строки, с которыми вызван expect-скрипт, можно поступить так:
```
set VAR [lindex $argv 0]
```
Тут мы объявляем переменную `VAR` и записываем в неё указатель на первый аргумент командной строки, `$argv 0`.
Для целей обновлённого expect-скрипта мы собираемся записать значение первого аргумента, представляющее собой имя пользователя, которое будет использовано в программе, в переменную `my_name`. Второй аргумент, символизирующий то, что пользователю нравится, попадёт в переменную `my_favorite`. В результате объявление переменных будет выглядеть так:
```
set my_name [lindex $argv 0]
set my_favorite [lindex $argv 1]
```
Отредактируем скрипт `answerbot`, приведя его к такому виду:
```
#!/usr/bin/expect -f
set my_name [lindex $argv 0]
set my_favorite [lindex $argv 1]
set timeout -1
spawn ./questions
expect "Hello, who are you?\r"
send -- "Im $my_name\r"
expect "Can I ask you some questions?\r"
send -- "Sure\r"
expect "What is your favorite topic?\r"
send -- "$my_favorite\r"
expect eof
```
Запустим его, передав в качестве первого параметра `SomeName`, в качестве второго — `Programming`:
```
$ ./answerbot SomeName Programming
```
![[850bb09e300047626407ed2b0c8ccee1.png]]
_Expect-скрипт, использующий переменные и параметры командной строки_
Как видите, всё работает так, как ожидалось. Теперь expect-скрипт отвечает на вопросы bash-скрипта, пользуясь переданными ему параметрами командной строки.
## Ответы на разные вопросы, которые могут появиться в одном и том же месте
Если автоматизируемая программа может, в одной ситуации, выдать одну строку, а в другой, в том же самом месте — другую, в expect можно использовать блоки, заключённые в фигурные скобки и содержащие варианты реакции скрипта на разные данные, полученные от программы. Выглядит это так:
```
expect {
   "something" { send -- "send this\r" }
   "*another" { send -- "send another\r" }
}
```
Здесь, если expect-скрипт увидит строку «something», он отправит ответ «send this». Если же это будет некая строка, оканчивающаяся на «another», он отправит ответ «send another».
Напишем новый скрипт, записав его в файл `questions`, случайным образом задающий в одном и том же месте разные вопросы:
```
#!/bin/bash
let number=$RANDOM
if [ $number -gt 25000 ]
then
echo "What is your favorite topic?"
else
echo "What is your favorite movie?"
fi
read $REPLY
```
Тут мы генерируем случайное число при каждом запуске скрипта, и, проанализировав его, выводим один из двух вопросов.
Для автоматизации такого скрипта нам и пригодится вышеописанная конструкция:
```
#!/usr/bin/expect -f
set timeout -1
spawn ./questions
expect {
   "*topic?" { send -- "Programming\r" }
   "*movie?" { send -- "Star wars\r" }
}
```
![[cceaeb72a9cfe0513d09353f1e01c485.png]]
_Ответы на разные вопросы, появляющиеся в одном и том же месте_
Как видно, когда автоматизированный скрипт выводит строку, оканчивающуюся на «topic?», expect-скрипт передаёт ему строку «Programming». Получив в том же месте, при другом запуске программы, вопрос, оканчивающийся на «movie?», expect-скрипт отвечает: «Star wars». Это очень полезная техника.
## Условный оператор
Expect поддерживает условный оператор `if-else` и другие управляющие конструкции. Вот пример использования условного оператора:
```
#!/usr/bin/expect -f
set TOTAL 1
if { $TOTAL < 5 } {
puts "\nTOTAL is less than 5\n"
} elseif { $TOTAL > 5 } {
puts "\nTOTAL greater than 5\n"
} else {
puts "\nTOTAL is equal to 5\n"
}
expect eof
```
![[8c69e055846d656af2cc6c11da767b73.png]]
_Условный оператор в expect_
Тут мы присваиваем переменной `TOTAL` некое число, после чего проверяем его и выводим текст, зависящий от результата проверки.
Обратите внимание на конфигурацию фигурных скобок. Очередная открывающая скобка должна быть расположена на той же строке, что и предыдущие конструкции.
## Цикл while
Циклы `while` в expect очень похожи на те, что используются в обычных bash-скриптах, но, опять же, тут применяются фигурные скобки:
```
#!/usr/bin/expect -f
set COUNT 0
while { $COUNT <= 5 } {
puts "\nCOUNT is currently at $COUNT"
set COUNT [ expr $COUNT + 1 ]
}
puts ""
```
![[19F2r17sDNowQV52cowtXGsMOl__tiRLfkzmj7MjygsMzuDL5RPeUu_Mhb2a2zYWrsfTT6ycbrAIKjtq0EPHhw58sTJqAo6HqZd5Vh8QCp15saZVlCHHZdPNoiiRxXU1w4bJXgAWpw_vIYb0BA]]
_Цикл while в expect_
## Цикл for
Цикл `for` в expect устроен по-особому. В начале цикла, в самостоятельных парах фигурных скобок, надо указать переменную-счётчик, условие прекращения цикла и правило модификации счётчика. Затем, опять же в фигурных скобках, идёт тело цикла:
```
#!/usr/bin/expect -f
for {set COUNT 0} {$COUNT <= 5} {incr COUNT} {
puts "\nCOUNT is at $COUNT"
}
puts ""
```
![[h3Hyo_lzUuZJnYZ9taZ1XvIY-IhWkG8eyyi8nEbELzQDP_M42RCOH2QPn1T-Qv41NMv9Zzk4O0vQUtW_SUy4Pset1Sl_wH14_YgJvYWl_ecMsl3wFzqkQIeHr_n3GU_aF6jTZqsgQhQ-wYyamw]]
_Цикл for в expect_
## Объявление и использование функций
Expect позволяет программисту объявлять функции, используя ключевое слово `proc`:
```
proc myfunc { MY_COUNT } {
set MY_COUNT [expr $MY_COUNT + 1]
return "$MY_COUNT"
}
```
Вот как выглядит expect-скрипт, в котором используется объявленная в нём же функция:
```
#!/usr/bin/expect -f
proc myfunc { MY_COUNT } {
set MY_COUNT [expr $MY_COUNT + 1]
return "$MY_COUNT"
}
set COUNT 0
while {$COUNT <= 5} {
puts "\nCOUNT is currently at $COUNT"
set COUNT [myfunc $COUNT]
}
puts ""
```
![[4rvWSMdIyQR0zXItxldVlUntwL36cCK0iQ4jOKbpE_YeD123l5-DhUCZbh10y4hELKAhAuc5mfz17ZxVBzGZz0Cvg3dQnpdn8NhaollSGPua6nBHFp_6LjEXSXYl6yBu521Eg-CuHoSPDcK_fw]]
ункции в expect_
## Команда interact
Случается так, что автоматизируемые с помощью expect программы требуют ввода конфиденциальных данных, вроде паролей, которые вам не хотелось бы хранить в виде обычного текста в коде скрипта. В подобной ситуации можно воспользоваться командой `interact`, которая позволит вам, автоматизировав некую часть взаимодействия с программой, самостоятельно ввести, скажем, пароль, а потом опять передать управление expect.
Когда выполняется эта команда, expect-скрипт переключается на чтение ответа на вопрос программы с клавиатуры, вместо того, чтобы передавать ей ранее записанные в нём данные.
Вот bash-скрипт, в общем-то, точно такой же, как мы рассматривали ранее, но теперь ожидающий ввод пароля в ответ на один из своих вопросов:
```
#!/bin/bash
echo "Hello, who are you?"
read $REPLY
echo "What is you password?"
read $REPLY
echo "What is your favorite topic?"
read $REPLY
```
Напишем expect-скрипт, который, когда ему предлагают предоставить пароль, передаёт управление нам:
```
#!/usr/bin/expect -f
set timeout -1
spawn ./questions
expect "Hello, who are you?\r"
send -- "Hi Im Adam\r"
expect "*password?\r"
interact ++ return
send "\r"
expect "*topic?\r"
send -- "Technology\r"
expect eof
```
![[QzISHnYVurZOqWM7NCNcVW8-248AGrBMf0qwzxf2WijbRWqI8eVOKcWECEod_AXg551sIKSnHJR-U5sz-qHHOk5evCg48H4kadYk0Kngk16BQSU0vYhML7xsb0TG-8R_9yABDjzws0YMbesWAQ]]
_Команда interact в expect-скрипте_
Встретив команду `interact`, expect-скрипт остановится, предоставив нам возможность ввести пароль. После ввода пароля надо ввести «++» и expect-скрипт продолжит работу, снова получив управление.
## Итоги
Возможностями expect можно пользоваться в программах, написанных на разных языках программирования благодаря соответствующим библиотекам. Среди этих языков — C#, Java, Perl, Python, Ruby, и другие. То, что expect доступен для разных сред разработки — далеко не случайность. Всё дело в том, что это действительно важный и полезный инструмент, который используют для решения множества задач. Здесь и проверка качества ПО, и выполнение различных работ по сетевому администрированию, автоматизация передачи файлов, автоматическая установка обновлений и многое другое.
Освоив этот материал, вы ознакомились с основными концепциями expect и научились пользоваться инструментом autoexpect для автоматического формирования скриптов. Теперь вы вполне можете продолжить изучение expect, воспользовавшись дополнительными источниками. Вот — [сборник](http://wiki.tcl.tk/11584) учебных и справочных материалов. Вот — достойная внимания серия из трёх статей ([1](https://www.ibm.com/developerworks/ru/library/l-expect_1/), [2](https://www.ibm.com/developerworks/ru/library/l-expect_2/), [3](https://www.ibm.com/developerworks/ru/library/l-expect_3/)). А вот — [официальная страница](https://www.nist.gov/services-resources/software/expect) expect, на которой можно найти ссылки на исходный код программы и список публикаций.
На этом мы завершаем серию материалов о bash-скриптах. Надеемся, её одиннадцать частей, а также бессчётное число комментариев к ним, помогли в достижении цели тем, кто хотел научиться писать сценарии командной строки.
Уважаемые читатели! Благодарим всех, кто был с нами. Желаем удачной автоматизации работы в Linux! И, если у вас есть опыт работы с expect — ждём ваших рассказов.