将命令传入Python REPL
我有一个文件,里面写着一些Python代码,我想运行Python,让它把在REPL(交互式命令行)中执行这些代码时会显示的内容打印出来。
比如,如果这个文件的内容是
1 + 4
'a' + 'b'
那么输出应该是
>>> 1 + 4
5
>>> 'a' + 'b'
'ab'
有没有办法做到这一点呢?
5 个回答
我们可以通过让Python命令启动一个交互式会话来实现这个目的。这可以通过使用 unbuffer
来完成,通常它和 expect
工具一起在Linux系统中提供。这种方法比较通用,适用于各种在交互模式下表现不同的程序。
下面这个命令会启动一个REPL(交互式解释器),即使输入(目前是空的)是通过管道传来的:
$ printf '' | unbuffer -p python3
Python 3.8.8 (default, Feb 19 2021, 11:04:50)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
有几个烦人的行为让这个过程和真正的交互会话有点不同。首先,unbuffer
一旦遇到EOF(文件结束符)就会退出,所以我们需要稍微延迟一下,以确保Python有足够的时间启动:
$ (sleep 1; printf '') | unbuffer -p python3
Python 3.8.8 (default, Feb 19 2021, 11:04:50)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
注意这次我们成功得到了一个 >>>
提示符。
其次,我们输入的命令不会在输出中回显。例如:
$ (sleep 1; printf 'print("a" * 10)\n'; sleep 1) | unbuffer -p python3
Python 3.8.8 (default, Feb 19 2021, 11:04:50)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> aaaaaaaaaa
>>>
注意我们的输入 print("a" * 10)\n
并没有出现在 >>>
提示符后面,尽管结果确实被打印出来了。
我们可以使用 tee
来解决这个问题,让我们的命令同时输出到标准输出和REPL(我们通过进程替换来执行):
$ (sleep 1; printf 'print("a" * 10)\n'; sleep 1) | tee >(unbuffer -p python3)
Python 3.8.8 (default, Feb 19 2021, 11:04:50)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("a" * 10)
aaaaaaaaaa
>>>
这样似乎就正常工作了,但我们必须在每一行之间加上延迟。这里有一个脚本可以自动做到这一点,它会从标准输入读取行:
#!/usr/bin/env bash
set -e
cat | (sleep 1; while IFS='' read -r LINE
do
sleep 0.2
echo "$LINE"
done; sleep 1) | tee >(unbuffer -p python3)
这似乎可以完成任务(我使用的是 printf
,但用一个单独的文件也可以;注意REPL需要两个换行符才能执行一个缩进块,就像在交互会话中一样):
$ printf 'if True:\n print("hello")\nelse:\n print("world")\n\n12345\n' | ./repl.sh
Python 3.8.8 (default, Feb 19 2021, 11:04:50)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> if True:
... print("hello")
... else:
... print("world")
...
hello
>>> 12345
12345
>>>
如果你想去掉最后的 >>>
和启动时的杂音,我们可以通过标准文本处理工具,比如 head
和 tail
来处理这个:
$ printf 'if True:\n print("hello")\nelse:\n print("world")\n\n12345\n' | ./repl.sh | tail -n+4 | head -n-1
>>> if True:
... print("hello")
... else:
... print("world")
...
hello
>>> 12345
12345
你可以把你的输入传给Python的“code”模块。它会显示输出结果,但不会显示你输入的内容。
$ echo '1 + 1' | python -m code
Python 2.7.10 (v2.7.10:15c95b7d81dc, May 23 2015, 09:33:12)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 2
这里有一些神奇的技巧可以帮助你:
import ast
import itertools
def main():
with open('test.txt', 'r') as sr:
parsed = ast.parse(sr.read())
sr.seek(0)
globals_ = {}
locals_ = {}
prev_lineno = 0
for node in ast.iter_child_nodes(parsed):
source = '\n'.join(itertools.islice(sr, 0, node.lineno - prev_lineno))[:-1]
print('>>> {}'.format(source))
if isinstance(node, ast.Expr):
print(eval(source, globals_, locals_))
else:
exec(source, globals_, locals_)
prev_lineno = node.lineno
if __name__ == '__main__':
main()
输入:
1 + 4
'a' + 'b'
a = 1
a
输出:
>>> 1 + 4
5
>>> 'a' + 'b'
ab
>>> a = 1
>>> a
1
这个方法的作用是通过使用 ast
模块来解析源代码,找到每个独立语句的开始和结束行号,然后根据是语句还是表达式来调用 eval
或 exec
。
上下文信息保存在 globals_
和 locals_
中。
你可以通过使用一些 Python 沙箱来执行 eval
和 exec
,这样可能会更安全。
这段内容是说,使用 code 模块来实现一个(不太)快速且(大部分)简单的方法:
import sys
import code
infile = open('cmd.py')
def readcmd(prompt):
line = infile.readline()
if not line:
sys.exit(0)
print prompt,line.rstrip()
return line.rstrip()
code.interact(readfunc=readcmd)
还有很多可以改进的地方,不过现在已经很晚了。总之,下面是一个例子:
sh$ cat cmd.py
1 + 4
'a' + 'b'
1/0
def f(x):
return x*2
f(3)
sh$ python console.py
Python 2.7.3 (default, Mar 13 2014, 11:03:55)
[GCC 4.7.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 1 + 4
5
>>> 'a' + 'b'
'ab'
>>>
>>> 1/0
Traceback (most recent call last):
File "<console>", line 1, in <module>
ZeroDivisionError: integer division or modulo by zero
>>>
>>> def f(x):
... return x*2
...
>>> f(3)
6
你可以使用replwrap
这个工具,它是pexpect
库的一部分,来实现你的目标。这个工具甚至有一个python
的方法:
from pexpect import replwrap
with open("commands.txt", "r") as f:
commands = [command.strip() for command in f.readlines()]
repl = replwrap.python()
for command in commands:
print ">>>", command
print repl.run_command(command),
这个方法会返回:
python replgo.py
>>> 1 + 4
5
>>> 'a' + 'b'
'ab'
你需要获取最新版本的pexpect。