Python中使用标准输入流的程序中带有行编辑的调试器

4
为了在Python脚本中添加一个即席调试器断点,我可以插入以下代码行:
import pdb; pdb.set_trace()

Pdb 从标准输入读取,所以如果脚本本身也从标准输入读取,它就不能正常工作。为了解决这个问题,在类 Unix 系统上,我可以让 pdb 从终端读取:参考链接

import pdb; pdb.Pdb(stdin=open('/dev/tty', 'r'), stdout=open('/dev/tty', 'w')).set_trace()

这样做是有效的,但与普通的pdb.set_trace不同,我无法获得readline库提供的命令行编辑功能(箭头键等)。如何在不干扰脚本的标准输入和输出的情况下进入pdb,并仍然获得命令行编辑?理想情况下,相同的代码应适用于Python 2和Python 3。与非Unix系统的兼容性将是一个奖励。作为测试用例的玩具程序:
#!/usr/bin/env python
import sys
for line in sys.stdin:
    #import pdb; pdb.set_trace()
    import pdb; pdb.Pdb(stdin=open('/dev/tty', 'r'), stdout=open('/dev/tty', 'w')).set_trace()
    sys.stdout.write(line)

使用方法:{ echo one; echo two; } | python cat.py
1个回答

2
我希望我没有遗漏任何重要的内容,但似乎你不能以完全平凡的方式做到这一点,因为只有当(或其子类)具有非零的时,才会被使用,然而这将导致忽略你的并混合调试器和脚本本身的输入。话虽如此,到目前为止,我想到的最好的方法是:
#!/usr/bin/env python3
import os
import sys
import pdb

pdb_inst = pdb.Pdb()

stdin_called = os.fdopen(os.dup(0))
console_new = open('/dev/tty')
os.dup2(console_new.fileno(), 0)
console_new.close()
sys.stdin = os.fdopen(0)

for line in stdin_called:
    pdb_inst.set_trace()
    sys.stdout.write(line)

虽然它可以至少被放置在脚本之外并且作为包装器导入和调用,但是它对您的原始脚本相对侵入性较大。

我将传入的STDIN重定向(复制)到文件描述符,并将其打开为stdin_called。 然后(基于您的示例),我打开了/dev/tty以进行读取,并替换进程的文件描述符0(对于STDIN;它应该使用sys.stdin.fileno()返回的值),将刚打开的文件描述符重新分配给sys.stdin的相应类似文件的对象。 这样程序循环和pdb都使用自己的输入流,而pdb可以与看起来只是一个“普通”控制台STDIN交互,它很高兴启用readline

这不太美观,但应该能够做到您想要的,并且希望提供有用的提示。如果可用,它在pdb中使用readline(行编辑,历史记录,完成):

$ { echo one; echo two; } | python3 cat.py
> /tmp/so/cat.py(16)<module>()
-> sys.stdout.write(line)
(Pdb) c
one
> /tmp/so/cat.py(15)<module>()
-> pdb_inst.set_trace()
(Pdb) con[TAB][TAB]
condition  cont       continue   
(Pdb) cont
two

注意,从3.7版本开始,您可以使用breakpoint()代替import pdb; pdb.Pdb().set_trace()以方便调试,并且您还可以检查dup2调用的结果,以确保文件描述符按预期创建/替换。
编辑:如前所述并由OP在评论中指出,这种方法既难看又具有侵入性,不会让代码更加美观,但我们可以采用一些技巧来减少其周围环境的影响。我已经破解了其中一种选项:
import sys

# Add this: BEGIN
import os
import pdb
import inspect

pdb_inst = pdb.Pdb()

class WrapSys:
    def __init__(self):
        self.__stdin = os.fdopen(os.dup(0))
        self.__console = open('/dev/tty')
        os.dup2(self.__console.fileno(), 0)
        self.__console.close()
        self.__console = os.fdopen(0)
        self.__sys = sys

    def __getattr__(self, name):
        if name == 'stdin':
            if any((f.filename.endswith("pdb.py") for f in inspect.stack())):
                return self.__console
            else:
                return self.__stdin
        else:
            return getattr(self.__sys, name)

sys = WrapSys()
# Add this: END

for line in sys.stdin:
    pdb_inst.set_trace()  # Inject breakpoint
    sys.stdout.write(line)

我并没有完全深入研究,但是目前来看,pdb/cmd 似乎不仅需要 sys.stdin ,而且还需要它使用 fd 0 才能启用 readline。上面的示例在我们的脚本中升级了内容,并在 pdb.py 的堆栈上预设了不同含义的 sys.stdin。一个明显的注意事项是,如果除了 pdb 之外的任何其他内容也希望和依赖于 sys.stdin fd 为 0,那么它仍将毫无办法(或者如果只是进行了尝试,那么就从另一个流读取其输入)。


很抱歉,遍历脚本并将所有stdin的使用替换为其他内容是不可行的。就像cmdraw_input似乎硬编码了stdin的使用一样,脚本可能正在使用其他硬编码stdin的库。 - Gilles 'SO- stop being evil'
@Gilles:是的,我同意这很丑陋。我猜可能最好的方法是通过pdb/cmd/readline来深入挖掘并可能进行子类化。我也考虑了一下最少的代码来获得所需的结果。总之,我扩展了这个例子,也许把它放在模块内(除了sys重新分配之外),可能对您的情况更有帮助? - Ondrej K.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接