tags:

views:

186

answers:

2

Having looked at this question, I have the following code:

$/ = "\0"
answer = STDIN.gets

Now, I was hoping that this would allow the user to:

  • enter a multi-line input, terminating by pressing Ctrl-D.
  • enter a single line input, terminating by pressing Ctrl-D.
  • enter a "nothing" input, terminating by pressing Ctrl-D.

However, the behaviour I actually see is that:

  • The user can enter a multi-line input fine.
  • The user can not enter a single line input, unless they hit Ctrl-D twice.
  • The user can enter a "nothing" input if they hit Ctrl-D straight away.

So, why does the single line situation (i.e. if the user has entered some text but no newline and then hit Ctrl-D) require two presses of Ctrl-D? And why does it work then if the user enters nothing? (I have noted that if they enter nothing and hit Ctrl-D, I don't get an empty string but the nil class - I discovered this when trying to call .empty? on the result, since it suddenly failed horribly. If there is a way to get it to return an empty string as well, that would be nice. I prefer checking .empty? to ==, and don't particularly want to define .empty? for the nil class.)

EDIT: Since I really would like to know the "correct way" to do this in Ruby, I am offering a bounty of 200 rep. I will also accept answers that give another way of entering terminal multi-line input with a sensible "submit" procedure - I will be the judge of 'suitable'. For example, we're currently using two "\n"s, but that's not suitable, as it blocks paragraphs and is unintuitive.

+1  A: 

When reading STDIN from a terminal device you are working in a slightly different mode to reading STDIN from a file or a pipe.

When reading from a tty Control-D (EOF) only really sends EOF if the input buffer is empty. If it is not empty it returns data to the read system call but does not send EOF.

The solution is to use some lower level IO and read a character at a time. The following code (or somethings similar) will do what you want

#!/usr/bin/env ruby

answer = ""
while true
  begin
    input = STDIN.sysread(1)
    answer += input
  rescue EOFError
    break
  end
end

puts "|#{answer.class}|#{answer}|"

The results of running this code with various inputs are as follows :-

INPUT This is a line<CR><Ctrl-D>

|String|This is a line
|

INPUT This is a line<Ctrl-D>

|String|This is a line|

INPUT<Ctrl-D>

|String||
Steve Weet
Thank you for the answer, but I still run into the double-Ctrl-D problem on the single line input. This could be specific to my environment (running Fedora 13, Gnome Terminal 2.30.1), but this environment is the expected usage one, and so it is a problem. Are you sure that you can just type "This is a line<Ctrl-D>" and have it submit?
Stephen
Just confirmed that its not just Gnome Terminal - Konsole 2.4.5 and xTerm (version... something) also exhibit this behaviour. So, either it's a Fedora problem, or this does not work I'm afraid.
Stephen
Sorry for the delay. That was the exact input on BSD (OS/X 10). When I get back in later I'll try it on Linux
Steve Weet
No problem. I have a friend who runs OS/X 10, I'll ask him to confirm your experience. Thanks for your help with this problem.
Stephen
@Steve I just confirmed that it works on OS/X 10, and does not work on Ubuntu 10.04.
Stephen
+1  A: 

The basic problem is the terminal itself. See many of the related links to the right of your post. To get around this you need to put the terminal in a raw state. The following worked for me on a Solaris machine:

#!/usr/bin/env ruby
# store the old stty settings
old_stty = `stty -g`
# Set up the terminal in non-canonical mode input processing
# This causes the terminal to process one character at a time
system "stty -icanon min 1 time 0 -isig"
answer = ""
while true
  char = STDIN.getc
  break if char == ?\C-d # break on Ctrl-d
  answer += char.chr
end
system "stty #{old_stty}" # restore stty settings
answer

I'm not sure if the storing and restoring of the stty settings is necessary but I've seen other people do it.

Jason
@Jason - Thank you, this is working quite well so far. One problem is that backspaces don't work - it prints out a `^?` character instead. Is there any way around this issue?
Stephen
More accurately, I'd like to only enable the ^D as capturable, and not also enable things like ^C and backspace.
Stephen
I managed to get ^C back to normal by removing the isig flag which overrides it. However, as far as I can see the icanon flag overrides both KILL and ERASE processing. I think I only want to override KILL...
Stephen
You can check for the DELETE key with this: char.ord == 127. However, I'm not sure how to echo that back to the terminal to actually cause the DELETE to happen. I tried STDOUT.putc with various things but couldn't get it to work. I read that if you escape the DELETE key it should still work in non-canonical mode so potentially it should work.
Jason
Sorry, do you mean escape it in the `stty` command? I shall try that. I currently have a horrible half-working delete that does remove characters but replaces them with "^" which are then over-written when the user continues to type. It's very odd.
Stephen
I think you're supposed to escape the DELETE key somehow when you send it to the terminal. Something like STDOUT.putc( <escaped delete key> )
Jason
@Jason Unfortunately that's no good, it throws garbage to the screen when you backspace - if the user types `Hello<backspace><backspace>` they get `Hello^^?`... I've tried playing with adding multiple backspaces and it just doesn't work. I cannot believe there is no easy way to simply take the input keypress by keypress and check if it is Ctrl+D or not...
Stephen
I think adding -echoctl will prevent the ^? but that still doesn't cause the character to be deleted when the user presses backspace.
Jason
@Jason - Thanks for the idea, but `-echoctl` merely makes it print out unicode characters in my terminal - or at least unrecognisable characters. I believe it merely suppresses the hat notation for command characters. You are also right that it does not delete the character. I love how all the literature I can find online about how to do multiline terminal input merely results in a multiline input that needs to have either a Ctrl-D or two Ctrl-Ds depending on the circumstance... has no-one ever considered how crap that is for UI? >_<
Stephen
I'm all out of ideas but I'd appreciate any reputation points since I'm kind of new at this. :-)
Jason
@Jason You are welcome to it. Seems there is just no decent way to do multiline input on a Linux terminal. Disappointing.
Stephen