diff --git a/README.md b/README.md index dab77c4..186b40b 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ $stdio = new Stdio($loop); See below for waiting for user input and writing output. The `Stdio` class is a well-behaving duplex stream (implementing ReactPHP's `DuplexStreamInterface`) that emits each complete -line as a `data` event (including the trailing newline). +line as a `data` event, including the trailing newline. #### Output @@ -79,8 +79,12 @@ $stdio->write('hello'); $stdio->write(" world\n"); ``` -Alternatively, you can also use the `Stdio` as a writable stream. -You can `pipe()` any readable stream into this stream. +Because the `Stdio` is a well-behaving writable stream, +you can also `pipe()` any readable stream into this stream. + +```php +$logger->pipe($stdio); +``` #### Input @@ -99,10 +103,34 @@ $stdio->on('data', function ($line) { }); ``` -Because the `Stdio` is a well-behaving redable stream that will emit incoming +Note that this class takes care of buffering incomplete lines and will only emit +complete lines. +This means that the line will usually end with the trailing newline character. +If the stream ends without a trailing newline character, it will not be present +in the `data` event. +As such, it's usually recommended to remove the trailing newline character +before processing command line input like this: + +```php +$stdio->on('data', function ($line) { + $line = rtrim($line, "\r\n"); + if ($line === "start") { + doSomething(); + } +}); +``` + +Similarly, if you copy and paste a larger chunk of text, it will properly emit +multiple complete lines with a separate `data` event for each line. + +Because the `Stdio` is a well-behaving readable stream that will emit incoming data as-is, you can also use this to `pipe()` this stream into other writable streams. +``` +$stdio->pipe($logger); +``` + You can control various aspects of the console input through the [`Readline`](#readline), so read on.. @@ -124,7 +152,7 @@ See above for waiting for user input. Alternatively, the `Readline` is also a well-behaving readable stream (implementing ReactPHP's `ReadableStreamInterface`) that emits each complete -line as a `data` event (without the trailing newline). +line as a `data` event, including the trailing newline. This is considered advanced usage. #### Prompt diff --git a/src/Readline.php b/src/Readline.php index c92142c..d095187 100644 --- a/src/Readline.php +++ b/src/Readline.php @@ -628,7 +628,7 @@ public function onKeyEnter() if ($this->echo !== false) { $this->output->write("\n"); } - $this->processLine(); + $this->processLine("\n"); } /** @internal */ @@ -741,7 +741,7 @@ public function deleteChar($n) * * @uses self::setInput() */ - protected function processLine() + protected function processLine($eol) { // reset history cycle position $this->historyPosition = null; @@ -758,7 +758,7 @@ protected function processLine() } // process stored input buffer - $this->emit('data', array($line)); + $this->emit('data', array($line . $eol)); } private function strlen($str) @@ -818,7 +818,7 @@ public function strwidth($str) public function handleEnd() { if ($this->linebuffer !== '') { - $this->processLine(); + $this->processLine(''); } if (!$this->closed) { diff --git a/src/Stdio.php b/src/Stdio.php index 3bd2831..82e6f69 100644 --- a/src/Stdio.php +++ b/src/Stdio.php @@ -46,9 +46,7 @@ public function __construct(LoopInterface $loop, ReadableStreamInterface $input $this->readline->on('data', function($line) use ($that, &$incomplete) { // readline emits a new line on enter, so start with a blank line $incomplete = ''; - - // emit data with trailing newline in order to preserve readable API - $that->emit('data', array($line . PHP_EOL)); + $that->emit('data', array($line)); }); // handle all input events (readline forwards all input events) diff --git a/tests/ReadlineTest.php b/tests/ReadlineTest.php index d675414..9e7853d 100644 --- a/tests/ReadlineTest.php +++ b/tests/ReadlineTest.php @@ -193,7 +193,7 @@ public function testMovingCursorWithoutEchoDoesNotNeedToRedraw() public function testDataEventWillBeEmittedForCompleteLine() { - $this->readline->on('data', $this->expectCallableOnceWith('hello')); + $this->readline->on('data', $this->expectCallableOnceWith("hello\n")); $this->input->emit('data', array("hello\n")); } @@ -209,7 +209,7 @@ public function testDataEventWillNotBeEmittedForIncompleteLineButWillStayInInput public function testDataEventWillBeEmittedForCompleteLineAndRemainingWillStayInInputBuffer() { - $this->readline->on('data', $this->expectCallableOnceWith('hello')); + $this->readline->on('data', $this->expectCallableOnceWith("hello\n")); $this->input->emit('data', array("hello\nworld")); @@ -218,7 +218,7 @@ public function testDataEventWillBeEmittedForCompleteLineAndRemainingWillStayInI public function testDataEventWillBeEmittedForEmptyLine() { - $this->readline->on('data', $this->expectCallableOnceWith('')); + $this->readline->on('data', $this->expectCallableOnceWith("\n")); $this->input->emit('data', array("\n")); } @@ -905,14 +905,14 @@ public function testAutocompleteShowsLimitedNumberOfAvailableOptionsWhenMultiple public function testEmitEmptyInputOnEnter() { - $this->readline->on('data', $this->expectCallableOnceWith('')); + $this->readline->on('data', $this->expectCallableOnceWith("\n")); $this->readline->onKeyEnter(); } public function testEmitInputOnEnterAndClearsInput() { $this->readline->setInput('test'); - $this->readline->on('data', $this->expectCallableOnceWith('test')); + $this->readline->on('data', $this->expectCallableOnceWith("test\n")); $this->readline->onKeyEnter(); $this->assertEquals('', $this->readline->getInput()); diff --git a/tests/StdioTest.php b/tests/StdioTest.php index 8d9facb..924690c 100644 --- a/tests/StdioTest.php +++ b/tests/StdioTest.php @@ -383,7 +383,22 @@ public function testDataEventWillBeForwarded() $stdio->on('data', $this->expectCallableOnceWith("hello\n")); - $readline->emit('data', array('hello')); + $readline->emit('data', array("hello\n")); + } + + public function testDataEventWithoutNewlineWillBeForwardedAsIs() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + //$readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock(); + $readline = new Readline($input, $output); + + $stdio = new Stdio($this->loop, $input, $output, $readline); + + $stdio->on('data', $this->expectCallableOnceWith("hello")); + + $readline->emit('data', array("hello")); } public function testEndEventWillBeForwarded()