3

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.

2 Answers 2

2
+200

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.

Sign up to request clarification or add additional context in comments.

11 Comments

@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?
More accurately, I'd like to only enable the ^D as capturable, and not also enable things like ^C and backspace.
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...
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.
I'm all out of ideas but I'd appreciate any reputation points since I'm kind of new at this. :-)
|
2

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||

5 Comments

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?
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.
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
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.
I just confirmed that it works on OS/X 10, and does not work on Ubuntu 10.04.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.