如何从另一个应用程序获取管道输入,然后再获取用户输入


import sys
stdin_input = sys.stdin.read()
print(f"Info loaded from stdin: {stdin_input}")
user_input = input("User input goes here: ")

收到错误:

C:>echo "hello" | python winput.py
Info loaded from stdin: "hello"
User input goes here: Traceback (most recent call last):
File "C:winput.py", line 6, in <module>
user_input = input("User input goes here: ")
EOFError: EOF when reading a line

我最近了解到这是因为sys.stdin用于FIFO,读取后会关闭它。

基于这个问题,我可以在stdin_input = sys.stdin.read()之后添加sys.stdin = open("/dev/tty"),使其在CentOS上运行,但这不适用于Windows。

与其识别操作系统并相应地为sys.stdin分配一个新值,我更愿意动态地处理它。有没有一种方法可以确定在任何情况下/dev/tty的等效物是什么,而不必知道/dev/tty或等效物是用于特定操作系统的?

编辑:

sys.stdin.read()的原因是接收来自另一个应用程序的JSON输入。我还可以选择从文件中读取JSON数据,但能够使用管道数据非常方便。一旦收到数据,我想单独获得用户输入。

我目前正在解决以下问题:

if os.name == "posix":
sys.stdin = open("/dev/tty")
elif os.name == "nt":
sys.stdin = open("con")
else:
raise RunTimeError(
f"Error trying to assign to sys.stdin due to unknown os {os.name}"
)

这在所有情况下都可以很好地工作,但最好知道什么是/dev/ttycon,或者OS的等效物是动态的。如果这不可能,而我的变通方法是最好的解决方案,我可以接受。

由于您使用的是Bash,因此可以通过使用进程替换来避免此问题,进程替换类似于管道,但通过临时文件名参数而不是通过stdin传递。

这看起来像:

winput.py <(another-application)

然后在您的Python脚本中,接收参数并相应地进行处理:

import json
import sys
with open(sys.argv[1]) as f:
d = json.load(f)
print(d)
user_input = input("User input goes here: ")
print('User input:', user_input)

(sys.argv只是用于演示。在真实的脚本中,我会使用argparse。)

示例运行:

$ tmp.py <(echo '{"someKey": "someValue"}')
{'someKey': 'someValue'}
User input goes here: 6
User input: 6

这样做的另一个巨大优势是,它可以与实际文件名无缝配合,例如:

$ cat test.json
{"foo": "bar"}
$ tmp.py test.json
{'foo': 'bar'}
User input goes here: x
User input: x

所以真正的问题是sys.stdin只能是两件事中的一件:

  1. 连接到来自终端的键入输入
  2. 连接到某个类似文件的对象,该对象是而不是终端(实际文件、管道等)

通过执行sys.stdin.read()消耗了所有sys.stdin并不重要,当sys.stdin重定向到某个文件系统对象时,您失去了通过sys.stdin从终端读取的能力。

在实践中,我强烈建议不要尝试这样做。使用argparse,从命令行通过input接受您考虑接受的任何内容,避免整个问题(在实践中,我基本上从来没有看到过真正的生产代码不是某种REPL,而是通过stdin/stdout交互与用户动态交互;对于非REPL的情况,sys.stdin基本上总是未使用或从文件/程序中通过管道传输,因为编写这样干净的用户交互代码是一种痛苦,而用户必须键入他们的响应而不犯错误也是一种痛苦takes)。文件或stdin的输入可以通过将type=argparse.FileType()传递给有问题的add_argument调用来处理,然后用户可以选择传递文件名或-(其中-的意思是"从stdin读取"),使代码看起来像:

parser = argparse.ArgumentParser('Program description here')
parser.add_argument('inputfile', type=argparse.FileType(), help='Description here; pass "-" to read from stdin')
parser.add_argument('-c', '--cmd', action='append', help='User commands to execute after processing input file')
args = parser.parse_args()
with args.inputfile as f:
data = f.read()
for cmd in args.cmd:
# Do stuff based on cmd

然后用户可以执行:

otherprogram_that_generates_data | myprogram.py - -c 'command 1' -c 'command 2'

或:

myprogram.py file_containing_data -c 'command 1' -c 'command 2'

或者(在具有过程替换的shell上,如bash,作为第一个用例的替代方案):

myprogram.py <(otherprogram_that_generates_data) -c 'command 1' -c 'command 2'

无论哪种方式都有效。

如果必须这样做,那么您现有的解决方案确实是唯一合理的解决方案,但您可以将其分解,使其更干净,只使路径动态,而不是整个代码路径:

import contextlib
import os
import sys
TTYNAMES = {"posix": "/dev/tty", "nt": "con"}
@contextlib.contextmanager
def stdin_from_terminal():
try:
ttyname = TTYNAMES[os.name]
except KeyError:
raise OSError(f"{os.name} does not support manually reading from the terminal")
with open(ttyname) as tty:
sys.stdin, oldstdin = tty, sys.stdin
try:
yield
finally:
sys.stdin = oldstdin

如果在没有连接终端的情况下运行open调用上的OSError子类,例如在Windows上使用pythonw启动时(不使用此设计的另一个原因),或者在类似UNIX的平台上以非终端方式启动时,这种情况可能会消失,但这总比默默地行为不端要好。

你只需要使用它:

with stdin_from_terminal():
user_input = input("User input goes here: ")

并且当CCD_ 32块退出时它将自动恢复原始的CCD_。

最新更新