Skip to content

Support all CLI inputs in PHP interactive shell #118

Open
@adamziel

Description

@adamziel

What is the problem?

Running PHP CLI in an interactive mode (-a) does not support certain keys:

  • Alphanumeric symbols all work
  • TTY sequences like backspace, ctrl+c, ctrl+z, ctrl+w all work
  • XTERM control sequences like ctrl+a, ctrl+e are printed as ^A or ^E
  • ANSI Escape Codes like the arrow keys are printed as, e.g., ^[[D

Presumably, it's because PHP.wasm does not have the same kind of access to the input and output streams as a native build does. Notably, PHP.wasm is compiled with libedit and ncurses and uses the xterm TERMINFO database.

Investigation

The PHP runtime is connected to host's stdin through Emscripten's NODERAWFS.

At the same time, the wrapping node.js process seems to buffer the stdin input and handle the key inputs on its own. Nothing gets sent to the PHP process until the enter key is pressed – I confirmed this by adding console.log() calls to the NODERAWFS.read function.

Once the relevant key codes reach PHP, they are somewhat processed. Here I pressed 123456789←←←←ab<ctrl+a>1_ and 12345ab1_ was printed:

php > echo <<<TTY
<<< > 123456789^[[D^[[D^[[D^[[Dab^A1_
<<< > TTY;
12345ab1_

The arrow keys clearly worked, but the ab overwritten 67 instead of being inserted before. Also, the ctrl+a key combination did not return to the beginning of the line.

Manually sending raw bytes

I thought maybe something's wrong with reading stdin and provided manually sent raw bytes throught stdin:

const stdIn = new Uint8Array([
	// send echo "a":
	...new TextEncoder().encode('echo "a";'),
	// Press left arrow
	27, 91, 68,
	// Press "d", "e", "f"
	100, 101, 102
]);
let stdInIterator = 0;

// Provide PHP with the raw bytes through the `stdin` callback:
async function main() {
	const php = await startPHP(phpLoaderModule, 'NODE', {
		stdin: () => {
			const ord = stdIn[stdInIterator++];
			return ord === undefined ? null : ord;
		},
	});

CleanShot 2023-01-17 at 12 59 57@2x

It did correctly move the caret to the left and the input buffer seemed to contain echo "a"def;. Unfortunately, that's not what got rendered in the terminal.

stdin.setRawMode(true)

Node.js can be told to stop buffering the input lines and just pass through any bytes it receives as follows:

process.stdin.setRawMode(true);
process.stdin.resume();

Weirdly, that stops printing the line buffer to stdout:

$fp = fopen('php://stderr', 'w'); fwrite($fp, "STDERR!\n"); fclose($fp);
$fp = fopen('php://stdout', 'w'); fwrite($fp, "STDOUT!\n"); fclose($fp);

CleanShot 2023-01-17 at 13 30 48@2x

I tried manually printing characters to process.stdout but didn't get anywhere – PHP CLI does a lot of work there to, e.g., replace the current line buffer with the last history entry when the up arrow is pressed.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions