Reputation: 4844
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:
$ 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.
#!/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()
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
...
#!/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
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
Reputation: 4844
VPfB's answer is what I needed, here's the finalized script in case anyone would like to use it.
$ printf "A\nB\nC" | ./select.py | awk '{print "Selected: " $0}'
Select 0-1:
0) A
1) B
2) C
> 2 <- your input
Selected: C
#!/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
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