PHP CLI: How to read a single character of input from the TTY (without waiting for the enter key)?
Asked Answered
A

6

30

I want to read a single character at-a-time from the command line in PHP, however it seems as though there is some kind of input buffering from somewhere preventing this.

Consider this code:

#!/usr/bin/php
<?php
echo "input# ";
while ($c = fread(STDIN, 1)) {
    echo "Read from STDIN: " . $c . "\ninput# ";
}
?>

Typing in "foo" as the input (and pressing enter), the output I am getting is:

input# foo
Read from STDIN: f
input# Read from STDIN: o
input# Read from STDIN: o
input# Read from STDIN: 

input# 

The output I am expecting is:

input# f
input# Read from STDIN: f

input# o
input# Read from STDIN: o

input# o
input# Read from STDIN: o

input# 
input# Read from STDIN: 

input# 

(That is, with characters being read and processed as they are typed).

However, currently, each character is being read only after enter is pressed. I have a suspicion the TTY is buffering the input.

Ultimately I want to be able to read keypresses such as UP arrow, DOWN arrow, etc.

Advertising answered 10/9, 2010 at 11:53 Comment(0)
A
37

The solution for me was to set -icanon mode on the TTY (using stty). Eg.:

stty -icanon

So, the the code that now works is:

#!/usr/bin/php
<?php
system("stty -icanon");
echo "input# ";
while ($c = fread(STDIN, 1)) {
    echo "Read from STDIN: " . $c . "\ninput# ";
}
?>

Output:

input# fRead from STDIN: f
input# oRead from STDIN: o
input# oRead from STDIN: o
input# 
Read from STDIN: 

input# 

Props to the answer given here:
Is there a way to wait for and get a key press from a (remote) terminal session?

For more information, see:
http://www.faqs.org/docs/Linux-HOWTO/Serial-Programming-HOWTO.html#AEN92

Don't forget to restore the TTY when you're done with it...

Restoring the tty configuration

Resetting the terminal back to the way it was can be done by saving the tty state before you make changes to it. You can then restore to that state when you're done.

For example:

<?php

// Save existing tty configuration
$term = `stty -g`;

// Make lots of drastic changes to the tty
system("stty raw opost -ocrnl onlcr -onocr -onlret icrnl -inlcr -echo isig intr undef");

// Reset the tty back to the original configuration
system("stty '" . $term . "'");

?>

This is the only way to preserve the tty and put it back how the user had it before you began.

Note that if you're not worried about preserving the original state, you can reset it back to a default "sane" configuration simply by doing:

<?php

// Make lots of drastic changes to the tty
system("stty raw opost -ocrnl onlcr -onocr -onlret icrnl -inlcr -echo isig intr undef");

// Reset the tty back to sane defaults
system("stty sane");

?>
Advertising answered 10/9, 2010 at 12:24 Comment(2)
"Don't forget to reset the TTY when you're done with it!" -- how do we reset it?Putative
Mark: At it's most basic you can do stty sane. However, to guarantee you're resetting the tty to the exact state it was previously you should save it's state first. I've expanded the answer to include examples on how to do that.Advertising
S
23

Here is a way that works for me with readline and stream functions, without needing to mess with tty stuff.

readline_callback_handler_install('', function() { });
while (true) {
  $r = array(STDIN);
  $w = NULL;
  $e = NULL;
  $n = stream_select($r, $w, $e, null);
  if ($n && in_array(STDIN, $r)) {
    $c = stream_get_contents(STDIN, 1);
    echo "Char read: $c\n";
    break;
  }
}

Tested with PHP 5.5.8 on OSX.

Sibilla answered 7/2, 2014 at 13:24 Comment(5)
readline needs to compiled into PHP. Check your configure for the --with-readline=/opt/local using this command: php -i | grep readlineCarbonate
Using (very recently updated) Arch Linux, and PHP 7.0.5. The break caused this to read one char then stall; removing it made everything work great. I also changed the 0 at the end of the select to a NULL, and now PHP does not use 100% CPU (!). This function appears to work perfectly on my machine (catching \033s and everything) based on initial testing.Mullinax
@Mullinax Thanks for pointing this out, I edited the answer and set tv_sec of stream_select to null to fix the CPU usage issue. However, I couldn't reproduce any stalling.Sibilla
Ah, great! About the stalling, I discovered that the issue is reproducible on ideone! ideone.com/EkJwIC (un/comment the break to test). I suspect it's because OS X's default tty settings are different to Linux's.Mullinax
Note that it's impossible to differentiate between meta keys and such using this method. Virtually any key that does not correspond to an output character (arrow keys, F1, F2, etc) will return x1B or ESC in $c however at least with my terminal emulator, ex CTRL+W did return "^W" or 0x13 for meAndria
L
8

The function below is a simplified version of @seb's answer that can be used to capture a single character. It does not require stream_select, and uses readline_callback_handler_install's inherent blocking rather than creating a while loop. It also removes the handler to allow further input as normal (such as readline).

function readchar($prompt)
{
    readline_callback_handler_install($prompt, function() {});
    $char = stream_get_contents(STDIN, 1);
    readline_callback_handler_remove();
    return $char;
}

// example:
if (!in_array(
    readchar('Continue? [Y/n] '), ["\n", 'y', 'Y']
    // enter/return key ("\n") for default 'Y'
)) die("Good Bye\n");
$name = readline("Name: ");
echo "Hello {$name}.\n";
Liana answered 5/7, 2016 at 17:10 Comment(0)
B
0
<?php
`stty -icanon`;
// this will do it
stream_set_blocking(STDIN, 0);
echo "Press 'Q' to quit\n";
while(1){
   if (ord(fgetc(STDIN)) == 113) {
       echo "QUIT detected...";
       break;
   }
   echo "we are waiting for something...";
}
Brewton answered 1/6, 2017 at 20:38 Comment(2)
It doesn't seem to work. As per stream_set_blocking manual says This function works for any stream that supports non-blocking mode (currently, regular files and socket streams). I'm afraid that STDIN doesn't belong there.Deuterium
Thank you for this code snippet, which might provide some limited, immediate help. A proper explanation would greatly improve its long-term value by showing why this is a good solution to the problem and would make it more useful to future readers with other, similar questions. Please edit your answer to add some explanation, including the assumptions you’ve made. refStreet
S
0

The following function will wait until the user enters a character and then returns it immediately. This approach supports multibyte characters so will also work for detecting arrow key presses.

function waitForInput(){

    $input = '';

    $read = [STDIN];
    $write = null;
    $except = null;

    readline_callback_handler_install('', function() {});

    // Read characters from the command line one at a time until there aren't any more to read
    do{
        $input .= fgetc(STDIN);
    } while(stream_select($read, $write, $except, 0, 1));

    readline_callback_handler_remove();

    return $input;

}

Here is an example of using the above function to identify an arrow key press:

$input = waitForInput();

switch($input){
    case chr(27).chr(91).chr(65):
        print 'Up Arrow';
        break;
    case chr(27).chr(91).chr(66):
        print 'Down Arrow';
        break;
    case chr(27).chr(91).chr(68):
        print 'Left Arrow';
        break;
    case chr(27).chr(91).chr(67):
        print 'Right Arrow';
        break;
    default:
        print 'Char: '.$input;
        break;
}
Samaria answered 4/6, 2023 at 5:13 Comment(0)
C
0

If you're not in Windows powershell/cmd, use stream_set_blocking(), as joeldg already said. I'm use WSL (Ubuntu) and it's worked:

stream_set_blocking(STDIN, false); 
echo "write 'q' to quit\n"; 
while (1) {
    $in = fgets(STDIN);
    if (!empty($in)) {
        if ($in === 'q') {
            echo "exit\n";
            break;
        } else
            echo "write 'q' to quit\n";
    }
    echo "do my job 1 sec\n"; sleep(1);
}

In my code I use fgets() to read all line in every loop rather than characters one by one. So if user type abracadabra between second, then loop wouldn't iterate every character.

Churchwarden answered 11/12, 2023 at 17:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.