Linux: Pipe into Python (ncurses) script, stdin and termios

时间:2021-02-27 07:29:22

Apparently this is almost a duplicate of "Bad pipe filedescriptor when reading from stdin in python - Stack Overflow"; however, I believe this case is slightly more complicated (and it is not Windows specific, as the conclusion of that thread was).

显然,这几乎是“从python中的stdin读取时出现错误的管道文件描述符 - Stack Overflow”;但是,我认为这种情况稍微复杂一些(并且它不是Windows特定的,因为该线程的结论是)。

I'm currently trying to experiment with a simple script in Python: I'd like to supply input to the script - either through command line arguments; or by 'pipe'-ing a string to this script - and have the script show this input string using a curses terminal interface.

我目前正在尝试用Python编写一个简单的脚本:我想为脚本提供输入 - 通过命令行参数;或者通过“管道”字符串到此脚本 - 并让脚本使用curses终端接口显示此输入字符串。

The full script, here called testcurses.py, is given below. The problem is that whenever I try the actual piping, that seems to mess up stdin, and the curses window never shows. Here is a terminal output:

完整的脚本,这里称为testcurses.py,如下所示。问题是每当我尝试实际的管道时,这似乎搞乱了stdin,而curses窗口从未显示过。这是一个终端输出:

## CASE 1: THROUGH COMMAND LINE ARGUMENT (arg being stdin):
##
$ ./testcurses.py -
['-'] 1
stdout/stdin (obj): <open file '<stdout>', mode 'w' at 0xb77dc078> <open file '<stdin>', mode 'r' at 0xb77dc020>
stdout/stdin (fn): 1 0
env(TERM): xterm xterm
stdin_termios_attr [27906, 5, 1215, 35387, 15, 15, ['\x03', ... '\x00']]
stdout_termios_attr [27906, 5, 1215, 35387, 15, 15, ['\x03', ... '\x00']]
opening -
obj <open file '<stdin>', mode 'r' at 0xb77dc020>
TYPING blabla HERE
wr TYPING blabla HERE

at end
before curses TYPING blabla HERE
#
# AT THIS POINT:
# in this case, curses window is shown, with the text 'TYPING blabla HERE'
# ################


## CASE 2: THROUGH PIPE
##
## NOTE I get the same output, even if I try syntax as in SO1057638, like:
## python -c "print 'TYPING blabla HERE'" | python testcurses.py -
##
$ echo "TYPING blabla HERE" | ./testcurses.py -
['-'] 1
stdout/stdin (obj): <open file '<stdout>', mode 'w' at 0xb774a078> <open file '<stdin>', mode 'r' at 0xb774a020>
stdout/stdin (fn): 1 0
env(TERM): xterm xterm
stdin_termios_attr <class 'termios.error'>::(22, 'Invalid argument')
stdout_termios_attr [27906, 5, 1215, 35387, 15, 15, ['\x03', '\x1c', '\x7f', '\x15', '\x04', '\x00', '\x01', '\xff', '\x11', '\x13', '\x1a', '\xff', '\x12', '\x0f', '\x17', '\x16', '\xff', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00']]
opening -
obj <open file '<stdin>', mode 'r' at 0xb774a020>
wr TYPING blabla HERE

at end
before curses TYPING blabla HERE
#
# AT THIS POINT:
# script simply exits, nothing is shown 
# ################

As far as I can see, the issue is: - whenever we pipe strings into the Python script, the Python script loses the reference to the terminal as stdin, and notices that the replaced stdin is not a termios structure anymore - and since stdin is no longer a terminal, curses.initscr() exits immediately without rendering anything.

据我所知,问题是: - 每当我们将字符串传递给Python脚本时,Python脚本就会失去对终端的引用作为stdin,并注意到被替换的stdin不再是termios结构 - 并且因为stdin是不再是终端,curses.initscr()立即退出而不呈现任何内容。

So, my question is - in brief: can I somehow achieve, that the syntax echo "blabla" | ./testcurses.py - ends up showing the piped string in curses? More specifically: is it possible to retrieve a reference to the calling terminal's stdin from a Python script, even if this script is being "piped" to?

所以,我的问题是 - 简而言之:我可以以某种方式实现,语法echo“blabla”| ./testcurses.py - 最终在curses中显示管道字符串?更具体地说:是否可以从Python脚本中检索对调用终端的stdin的引用,即使此脚本被“管道”到?

Thanks in advance for any pointers,

提前感谢任何指针,

Cheers!

 

 

PS: the testcurses.py script:

PS:testcurses.py脚本:

#!/usr/bin/env python 
# http://www.tuxradar.com/content/code-project-build-ncurses-ui-python
# http://diveintopython.net/scripts_and_streams/stdin_stdout_stderr.html
# http://bytes.com/topic/python/answers/42283-curses-disable-readline-replace-stdin
#
# NOTE: press 'q' to exit curses - Ctrl-C will screw up yer terminal

# ./testcurses.py "blabla"                  # works fine (curseswin shows)
# ./testcurses.py -                     # works fine, (type, enter, curseswins shows):
# echo "blabla" | ./testcurses.py "sdsd"        # fails to raise curses window 
# 
# NOTE: when without pipe: termios.tcgetattr(sys.__stdin__.fileno()): [27906, 5, 1215, 35387, 15, 15, ['\x03', 
# NOTE: when with pipe |   : termios.tcgetattr(sys.__stdin__.fileno()): termios.error: (22, 'Invalid argument') 

import curses
import sys
import os
import atexit
import termios

def openAnything(source):            
    """URI, filename, or string --> stream

    http://diveintopython.net/xml_processing/index.html#kgp.divein

    This function lets you define parsers that take any input source
    (URL, pathname to local or network file, or actual data as a string)
    and deal with it in a uniform manner.  Returned object is guaranteed
    to have all the basic stdio read methods (read, readline, readlines).
    Just .close() the object when you're done with it.
    """
    if hasattr(source, "read"):
        return source

    if source == '-':
        import sys
        return sys.stdin

    # try to open with urllib (if source is http, ftp, or file URL)
    import urllib                         
    try:                                  
        return urllib.urlopen(source)     
    except (IOError, OSError):            
        pass                              

    # try to open with native open function (if source is pathname)
    try:                                  
        return open(source)               
    except (IOError, OSError):            
        pass                              

    # treat source as string
    import StringIO                       
    return StringIO.StringIO(str(source)) 



def main(argv):

    print argv, len(argv)
    print "stdout/stdin (obj):", sys.__stdout__, sys.__stdin__ 
    print "stdout/stdin (fn):", sys.__stdout__.fileno(), sys.__stdin__.fileno()
    print "env(TERM):", os.environ.get('TERM'), os.environ.get("TERM", "unknown")

    stdin_term_attr = 0
    stdout_term_attr = 0
    try:
        stdin_term_attr = termios.tcgetattr(sys.__stdin__.fileno())
    except:
        stdin_term_attr = "%s::%s" % (sys.exc_info()[0], sys.exc_info()[1]) 
    try:
        stdout_term_attr = termios.tcgetattr(sys.__stdout__.fileno())
    except:
        stdout_term_attr = `sys.exc_info()[0]` + "::" + `sys.exc_info()[1]` 
    print "stdin_termios_attr", stdin_term_attr
    print "stdout_termios_attr", stdout_term_attr


    fname = ""
    if len(argv):
        fname = argv[0]

    writetxt = "Python curses in action!"
    if fname != "":
        print "opening", fname
        fobj = openAnything(fname)
        print "obj", fobj
        writetxt = fobj.readline(100) # max 100 chars read
        print "wr", writetxt
        fobj.close()
        print "at end"

    sys.stderr.write("before ")
    print "curses", writetxt
    try:
        myscreen = curses.initscr()
        #~ atexit.register(curses.endwin)
    except:
        print "Unexpected error:", sys.exc_info()[0]

    sys.stderr.write("after initscr") # this won't show, even if curseswin runs fine

    myscreen.border(0)
    myscreen.addstr(12, 25, writetxt)
    myscreen.refresh()
    myscreen.getch()

    #~ curses.endwin()
    atexit.register(curses.endwin)

    sys.stderr.write("after end") # this won't show, even if curseswin runs fine


# run the main function - with arguments passed to script:
if __name__ == "__main__":
    main(sys.argv[1:])
    sys.stderr.write("after main1") # these won't show either, 
sys.stderr.write("after main2")     #  (.. even if curseswin runs fine ..)

2 个解决方案

#1


1  

This can't be done without getting the parent process involved. Fortunately, there's a way to get bash involved using I/O redirection :

如果不涉及父进程,则无法完成此操作。幸运的是,有一种方法可以使用I / O重定向来实现bash:

$ (echo "foo" | ./pipe.py) 3<&0

That will pipe foo to pipe.py in a subshell with stdin duplicated into file descriptor 3. Now all we need to do is use that extra help from our parent process in the python script (since we'll inherit fd 3):

这将把foo管道传递给子shell中的pipe.py,并将stdin复制到文件描述符3.现在我们需要做的就是在python脚本中使用父进程的额外帮助(因为我们将继承fd 3):

#!/usr/bin/env python

import sys, os
import curses

output = sys.stdin.readline(100)

# We're finished with stdin. Duplicate inherited fd 3,
# which contains a duplicate of the parent process' stdin,
# into our stdin, at the OS level (assigning os.fdopen(3)
# to sys.stdin or sys.__stdin__ does not work).
os.dup2(3, 0)

# Now curses can initialize.
screen = curses.initscr()
screen.border(0)
screen.addstr(12, 25, output)
screen.refresh()
screen.getch()
curses.endwin()

Finally, you can work around the ugly syntax on the command line by running the subshell first:

最后,您可以通过首先运行子shell来解决命令行上的丑陋语法:

$ exec 3<&0  # spawn subshell
$ echo "foo" | ./pipe.py  # works
$ echo "bar" | ./pipe.py  # still works

That solves your problem, if you have bash.

如果你有bash,那就解决了你的问题。

#2


8  

The problem is that whenever I try the actual piping, that seems to mess up stdin, and the curses window never shows. [...snip...] As far as I can see, the issue is: - whenever we pipe strings into the Python script, the Python script loses the reference to the terminal as stdin, and notices that the replaced stdin is not a termios structure anymore - and since stdin is no longer a terminal, curses.initscr() exits immediately without rendering anything.

Actually, the curses window does show, but since there is no more input on your brave new stdin, myscreen.getch() returns immediately. So it has nothing to do with curses testing whether stdin is a terminal.

实际上,curses窗口确实显示,但由于你的braten new stdin上没有更多输入,myscreen.getch()会立即返回。所以它与测试stdin是否是终端的curses无关。

So if you want to use myscreen.getch() and other curses input functions, you'll have to reopen your terminal. On Linux and *nix systems there is usually a device called /dev/tty that refers to the current terminal. So you can do something like:

因此,如果您想使用myscreen.getch()和其他curses输入函数,则必须重新打开终端。在Linux和* nix系统上,通常有一个名为/ dev / tty的设备引用当前终端。所以你可以这样做:

f=open("/dev/tty")
os.dup2(f.fileno(), 0)

before your call to myscreen.getch().

在调用myscreen.getch()之前。

#1


1  

This can't be done without getting the parent process involved. Fortunately, there's a way to get bash involved using I/O redirection :

如果不涉及父进程,则无法完成此操作。幸运的是,有一种方法可以使用I / O重定向来实现bash:

$ (echo "foo" | ./pipe.py) 3<&0

That will pipe foo to pipe.py in a subshell with stdin duplicated into file descriptor 3. Now all we need to do is use that extra help from our parent process in the python script (since we'll inherit fd 3):

这将把foo管道传递给子shell中的pipe.py,并将stdin复制到文件描述符3.现在我们需要做的就是在python脚本中使用父进程的额外帮助(因为我们将继承fd 3):

#!/usr/bin/env python

import sys, os
import curses

output = sys.stdin.readline(100)

# We're finished with stdin. Duplicate inherited fd 3,
# which contains a duplicate of the parent process' stdin,
# into our stdin, at the OS level (assigning os.fdopen(3)
# to sys.stdin or sys.__stdin__ does not work).
os.dup2(3, 0)

# Now curses can initialize.
screen = curses.initscr()
screen.border(0)
screen.addstr(12, 25, output)
screen.refresh()
screen.getch()
curses.endwin()

Finally, you can work around the ugly syntax on the command line by running the subshell first:

最后,您可以通过首先运行子shell来解决命令行上的丑陋语法:

$ exec 3<&0  # spawn subshell
$ echo "foo" | ./pipe.py  # works
$ echo "bar" | ./pipe.py  # still works

That solves your problem, if you have bash.

如果你有bash,那就解决了你的问题。

#2


8  

The problem is that whenever I try the actual piping, that seems to mess up stdin, and the curses window never shows. [...snip...] As far as I can see, the issue is: - whenever we pipe strings into the Python script, the Python script loses the reference to the terminal as stdin, and notices that the replaced stdin is not a termios structure anymore - and since stdin is no longer a terminal, curses.initscr() exits immediately without rendering anything.

Actually, the curses window does show, but since there is no more input on your brave new stdin, myscreen.getch() returns immediately. So it has nothing to do with curses testing whether stdin is a terminal.

实际上,curses窗口确实显示,但由于你的braten new stdin上没有更多输入,myscreen.getch()会立即返回。所以它与测试stdin是否是终端的curses无关。

So if you want to use myscreen.getch() and other curses input functions, you'll have to reopen your terminal. On Linux and *nix systems there is usually a device called /dev/tty that refers to the current terminal. So you can do something like:

因此,如果您想使用myscreen.getch()和其他curses输入函数,则必须重新打开终端。在Linux和* nix系统上,通常有一个名为/ dev / tty的设备引用当前终端。所以你可以这样做:

f=open("/dev/tty")
os.dup2(f.fileno(), 0)

before your call to myscreen.getch().

在调用myscreen.getch()之前。