Search code examples
bashbindstdinttystty

Why will a script not accept input on stdin when invoked from bash's bind?


I have a number of bash/bind tools that I've written to simplify my command-line existence, and have recently wanted to make one of these tools interactive. If I try to read from stdin in one of these scripts, execution locks up at the point of reading. My example here is in python, but I have seen the exact same behavior when the invoked script is written in ruby:

~> cat tmp.py
import sys
sys.stdout.write(">>>")
sys.stdout.flush()
foo = sys.stdin.readline()
print "foo: %s" % foo,


~> python tmp.py
>>>yodeling yoda
foo: yodeling yoda

So the script works. When I invoke it, I can give it input and it prints what I fed it.

~> bind -x '"\eh":"echo yodeling yoda"'
[output deleted]

~> [Alt-H]
yodeling yoda

bind works as expected. The bound keystroke invokes the command. I use this stuff all the time, but until now, I've only invoked scripts that required no stdin reads.

Let's bind [Alt-H] to the script:

~> bind -x '"\eh":"python tmp.py"'
[output deleted]

Now we're configured to have the script read from stdin while invoked by the bound keystroke. Hitting [Alt-H] starts the script but nothing typed is echoed back. Even hitting [Crl-D] doesn't end it. The only way to get out is to hit [Crl-C], killing the process in the readline. (sys.stdin.read() suffers the same fate.)

~> [Alt-H]
>>>Traceback (most recent call last):
  File "tmp.py", line 7, in <module>
    foo = sys.stdin.readline()
KeyboardInterrupt

As I mentioned at the top, I see the same issue with ruby, so I know it's nothing to do with the language I'm using. (I've omitted the script.)

~> bind -x '"\eh":"ruby tmp.rb"'
[Output deleted]

~> [Alt-H]
>>>tmp.rb:3:in `gets': Interrupt
    from tmp.rb:3

I've looked through the Bash Reference Manual entry on bind, and it says nothing about a restriction on input. Any thoughts?

EDIT:

If I cat /proc/[PID]/fd/0 while the process is stuck, I see the script's input being displayed. (Oddly enough, a fair number of characters - seemingly at random - fail to appear here. This symptom only appears after I've given a few hundred bytes of input.)

Found this, a description of how and when a terminal switches between cooked and raw modes. Calling stty cooked and stty echo at the beginning of prompting, then stty sane or stty raw afterward triggers a new cascade of problems; mostly relating to how bound characters are handled, but suffice it to say that it destroys most alt bindings (and more) until return has been hit a couple times.


Solution

  • In the end, the best answer proved to be cooking the tty and manually turning on echo, then reverting the tty settings back to where they were when I started:

    def _get_terminal_settings():                                                      
      proc = subprocess.Popen(['/bin/stty', '-g'], stdout=subprocess.PIPE)             
      settings = proc.communicate()[0]                                                 
      os.system('stty cooked echo')                                                    
      return settings                                                                  
    
    def _set_terminal_settings(settings):                                              
      os.system('stty %s' % settings)    
    
    ...
    ...
    settings = _get_terminal_settings()
    user_input = sys.stdin.readline()
    _set_terminal_settings(settings)
    ...
    ...
    

    You should be able to do this in any language you choose.

    If you're curious about why this insanity is required, I would encourage you to read the link I added (under EDIT), above. The article doesn't cover anywhere enough detail, but you'll at least understand more than I did when I started.