Connor
Connor

Reputation: 4844

How to use `input()` after reading a file from `stdin`?

Context

I want a simple script that selects 1 of multiple piped inputs without EOF when reading a line error on Unix/Linux.

It tries to:

  1. accept multiple lines of piped text
  2. wait for user to select one option
  3. print that option to stdout

Desired usage:

$ printf "A\nB" | ./select.py | awk '{print "OUTPUT WAS: " $0}'
Select 0-1:
  0) A
  1) B
> 1
OUTPUT WAS: B

The awk '{print "[OUTPUT WAS] " $0}' at the end is just to show that the only stdout output should be the selection.

Current approach:

#!/bin/python3
import sys
from collections import OrderedDict

def print_options(options):
    """print the user's options"""
    print(f"Select 0-{len(options)-1}:", file=sys.stderr)
    for n, option in options.items():
        print(f"  {n}) {option}", file=sys.stderr)

def main():
    # options are stored in an ordered dictionary to make order consistent
    options = OrderedDict()
    # read in the possible options one line at a time
    for n, line in enumerate(sys.stdin):
        options[n] = line.rstrip('\n')
        
    valid_selection = False
    # loop until we get a valid selection
    while not valid_selection:
        print_options(options)
        try:
            print('> ', end='', file=sys.stderr)
            selection = int(input()) # <- doesn't block like it should
            # use the selection to extract the output that will be printed
            output = options[selection]
            valid_selection = True
        except Exception as e:
            print(f"Invalid selection. {e}", file=sys.stderr)
                
    print(output)

if __name__ == '__main__':
    main()

Error:

The script gets trapped in an infinite loop printing:

...
> Invalid selection. EOF when reading a line
Select 0-1:
  0) A
  1) B
> Invalid selection. EOF when reading a line
Select 0-1:
  0) A
  1) B
> Invalid selection. EOF when reading a line
...

Minimum script to reproduce error:

#!/bin/python3
import sys

options = []
# read in the possible options one line at a time
for line in sys.stdin:
    options.append(line.rstrip('\n'))
    
user_input = input('> ')
            
print(user_input)

This throws:

EOFError: EOF when reading a line

When I want to see and input:

$ printf "text" | ./testscript.py
> sometext
sometext

Desired solution:

I gather this is due to the fact that stdin has reached an EOF. But my question is how to reset/remove the impact of the EOF so that input() once again blocks and waits for the user like it normally would.

In short: how to use input() after reading a file from stdin?

If that's impossible, as this answer implies, what elegant solutions are there to get similar behavior to what I describe at the beginning of this question? I'm open to non-python solutions (e.g. bash|zsh, rust, awk, perl).

Upvotes: 2

Views: 440

Answers (2)

Connor
Connor

Reputation: 4844

VPfB's answer is what I needed, here's the finalized script in case anyone would like to use it.

Usage

$ printf "A\nB\nC" | ./select.py | awk '{print "Selected: " $0}'
Select 0-1:
  0) A
  1) B
  2) C
> 2 <- your input
Selected: C

Full Solution

#!/bin/python3
"""
A simple script to allow selecting 1 of multiple piped inputs. 

Usage: 
printf "A\nB" | ./choose.py

If your input is space separated, make sure to convert with: 
printf "A B" | sed 's+ +\n+g' | ./choose.py

Source: https://stackoverflow.com/a/66143667/7872793
"""
import sys
from collections import OrderedDict

def print_options(options):
    """print the user's options"""
    print(f"Select 0-{len(options)-1}:", file=sys.stderr)
    for n, option in options.items():
        print(f"  {n}) {option}", file=sys.stderr)

def select_loop(options):
    valid_selection = False
    # loop until we get a valid selection
    while not valid_selection:
        print_options(options)
        try:
            print('> ', end='', file=sys.stderr)
            selection = int(input())
            # use the selection to extract the output that will be printed
            output = options[selection]
            valid_selection = True
        except Exception as e:
            print(f"Invalid selection. {e}", file=sys.stderr)
            
    return output

def main():
    # options are stored in an ordered dictionary to fix iteration output
    options = OrderedDict()
    # read in the possible options one line at a time
    for n, line in enumerate(sys.stdin):
        options[n] = line.rstrip('\n')
        
    # restore input from the terminal
    sys.stdin.close()
    sys.stdin=open('/dev/tty')
        
    # if only one option is given, use it immediately
    output = options[0] if len(options) == 1 else select_loop(options)
    print(output)

if __name__ == '__main__':
    main()

Upvotes: 0

VPfB
VPfB

Reputation: 17247

I can answer your question for Linux/Unix OS. I'm sorry, I do not work with Windows.

I added two lines to your example code, see below.

The special device /dev/tty is connected to your terminal. It is your standard input/output unless redirected. Basically you want to restore the state that your stdin is connected to your terminal. On the low level it works with file descriptor 0. The close closes it and the open takes the first free one which is in that case the 0.

import sys 

options = []
# read in the possible options one line at a time
for line in sys.stdin:
    options.append(line.rstrip('\n'))

# restore input from the terminal   
sys.stdin.close()
sys.stdin=open('/dev/tty')

user_input = input('> ')
                 
print(user_input)

Upvotes: 3

Related Questions