将命令传入Python REPL

4 投票
5 回答
1148 浏览
提问于 2025-04-18 18:53

我有一个文件,里面写着一些Python代码,我想运行Python,让它把在REPL(交互式命令行)中执行这些代码时会显示的内容打印出来。

比如,如果这个文件的内容是

1 + 4
'a' + 'b'

那么输出应该是

>>> 1 + 4
5
>>> 'a' + 'b'
'ab'

有没有办法做到这一点呢?

5 个回答

1

我们可以通过让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
>>>

如果你想去掉最后的 >>> 和启动时的杂音,我们可以通过标准文本处理工具,比如 headtail 来处理这个:

$ 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
1

你可以把你的输入传给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
3

这里有一些神奇的技巧可以帮助你:

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 模块来解析源代码,找到每个独立语句的开始和结束行号,然后根据是语句还是表达式来调用 evalexec

上下文信息保存在 globals_locals_ 中。

你可以通过使用一些 Python 沙箱来执行 evalexec,这样可能会更安全。

4

这段内容是说,使用 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
4

你可以使用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。

撰写回答