CLI - действительно богатый возможностями автоматизации интерфейс. Однако некоторые утилиты (как например ftp) к сожалению имеют интерактивный интерфейс. К счастью, человечество придумало библиотеку expect и в частности, pexpect (для Python), которую можно использовать например так:
import pexpect
child = pexpect.spawn ('ftp ftp.openbsd.org')
child.expect ('Name .*: ')
child.sendline ('anonymous')
child.expect ('Password:')
child.sendline ('noah@example.com')
child.expect ('ftp> ')
...
Она использует эмулятор терминала pty (тот же самый механизм используется например xterm или другим графическим терминалом)
Напишем простейший пример использования pexpect, вызывающий сначала bash, а уже в нем команду ls -1 (да, я знаю про subprocess, это всего лишь пример)
import pexpect
import sys
import re
from StringIO import StringIO
bash = pexpect.spawn('bash --norc') # --norc нужна, чтобы не читался bashrc
# Читаем приглашение
try:
prompt = bash.read_nonblocking(size=128, timeout=10)
except pexpect.TIMEOUT:
pass
prompt = prompt.strip()
print 'Prompt:', prompt
bash.sendline('ls -1')
# re.escape чтобы символы $ и т.п. в приглашении
# не воспринимались как спец-символы регулярного выражения
bash.expect(re.escape(prompt))
# Читаем вывод ls
ls_out = bash.before
for line in StringIO(ls_out).readlines():
print 'Read:', line.strip()
Вывод нашей программы несколько неожиданный:
Solaris |
Linux |
Prompt: bash-3.00#
Read: ls -1
Read: pexpect_patched.py
Read: pexpect.py
Read: pexpect.pyc
Read: pexptest1.py
|
Prompt: bash-4.2$
Read: ls -1
Read: pexpect_patched.py
Read: pexpect.py
Read: pexpect.pyc
Read: pexptest1.py
|
Как видим, что мы вводили в bash - то и прочитали. Чтобы избежать этого, можно использовать метод setecho, сбрасывающий флаг терминала ECHO (он заставляет терминал выводить символы, которые вводятся пользователем) - того же самого эффекта можно добиться, используя команду stty -echo. Добавим в наш скрипт строчку bash.setecho(False).
Результаты становятся интереснее:
Solaris |
Linux |
Prompt: bash-3.00#
Traceback (most recent call last):
File "pexptest2.py", line 16, in ?
bash.setecho(False)
File "/root/pexpect/pexpect.py", line 760, in setecho
attr = termios.tcgetattr(self.child_fd)
termios.error: (22, 'Invalid argument')
|
Linux:
Prompt: bash-4.2$
Read: ls -1
Read: pexpect_patched.py
Read: pexpect.py
Read: pexpect.pyc
Read: pexptest1.py
Read: pexptest2.py |
Проблема в Solaris 10 связана особенностью реализации pexpect - при открытии пары терминалов она сохраняет fd мастер-терминала (того, который /dev/ptmx) а не дочернего - /dev/pts/X, а в Solaris опции терминала поддерживаются только дочерним (на самом деле, для этого нужно подгрузить STREAMS-модули ptem и ldterm, но это уже делает CPython).
Проблема же в Linux - с тем, что bash восстанавливает настройки терминала после в момент ввода команды функцией rl_deprep_terminal - save_tty_settings. Поэтому имеет смысл ввести параметр echoing в конструкторе pexpect.spawn, устанавливающий свойства терминала перед выполнением execve. Я написал измененную версию pexpect, исправляющие эти проблемы:
import pexpect_patched as pexpect
...
bash = pexpect.spawn('bash --norc', echoing=False)
Solaris |
Linux |
Prompt: bash-3.00#
Read: ls -1
Read: pexpect_patched.py
Read: pexpect_patched.pyc
Read: pexpect.py
Read: pexpect.pyc
Read: pexptest1.py
Read: pexptest2.py
|
Prompt: bash-4.2$
Read: pexpect_patched.py
Read: pexpect_patched.pyc
Read: pexpect.py
Read: pexpect.pyc
Read: pexptest1.py
Read: pexptest2.py
|
Как видим в Solaris 10 проблема не решилась, что связано с тем, что в bash 3.0 termios-терминалы не поддерживаются (только BSD). В Solaris 11 используется bash-4.2, который поддерживает оба типа терминалов.
Ложка дегтя
К сожалению изначальная задача так решена и не была, так как MDB в Solaris вообще не учитывает атрибуты терминалов, что хорошо видно в коде: http://src.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/cmd/mdb/common/mdb/mdb_termio.c Поэтому как в той сказке про золотую рыбку, я остался у разбитого корыта, то есть со стандартным модулем subprocess
pexpect_patched.py