From 8122d243ae010f3a5c1d50e4d0ef6374d4e407b4 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 27 Apr 2021 16:14:39 +0200 Subject: readline: introduce promise-based API PR-URL: https://github.com/nodejs/node/pull/37947 Fixes: https://github.com/nodejs/node/issues/37287 Reviewed-By: Matteo Collina Reviewed-By: Benjamin Gruenbaum Reviewed-By: Robert Nagy --- doc/api/readline.md | 467 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 417 insertions(+), 50 deletions(-) (limited to 'doc/api') diff --git a/doc/api/readline.md b/doc/api/readline.md index 8f056c942e3..b886c5b2bb6 100644 --- a/doc/api/readline.md +++ b/doc/api/readline.md @@ -7,22 +7,48 @@ The `readline` module provides an interface for reading data from a [Readable][] -stream (such as [`process.stdin`][]) one line at a time. It can be accessed -using: +stream (such as [`process.stdin`][]) one line at a time. -```js +To use the promise-based APIs: + +```mjs +import * as readline from 'node:readline/promises'; +``` + +```cjs +const readline = require('readline/promises'); +``` + +To use the callback and sync APIs: + +```mjs +import * as readline from 'node:readline'; +``` + +```cjs const readline = require('readline'); ``` The following simple example illustrates the basic use of the `readline` module. -```js +```mjs +import * as readline from 'node:readline/promises'; +import { stdin as input, stdout as output } from 'process'; + +const rl = readline.createInterface({ input, output }); + +const answer = await rl.question('What do you think of Node.js? '); + +console.log(`Thank you for your valuable feedback: ${answer}`); + +rl.close(); +``` + +```cjs const readline = require('readline'); +const { stdin: input, stdout: output } = require('process'); -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout -}); +const rl = readline.createInterface({ input, output }); rl.question('What do you think of Node.js? ', (answer) => { // TODO: Log the answer in a database @@ -36,16 +62,18 @@ Once this code is invoked, the Node.js application will not terminate until the `readline.Interface` is closed because the interface waits for data to be received on the `input` stream. -## Class: `Interface` + +## Class: `InterfaceConstructor` * Extends: {EventEmitter} -Instances of the `readline.Interface` class are constructed using the -`readline.createInterface()` method. Every instance is associated with a -single `input` [Readable][] stream and a single `output` [Writable][] stream. +Instances of the `InterfaceConstructor` class are constructed using the +`readlinePromises.createInterface()` or `readline.createInterface()` method. +Every instance is associated with a single `input` [Readable][] stream and a +single `output` [Writable][] stream. The `output` stream is used to print prompts for user input that arrives on, and is read from, the `input` stream. @@ -56,18 +84,18 @@ added: v0.1.98 The `'close'` event is emitted when one of the following occur: -* The `rl.close()` method is called and the `readline.Interface` instance has +* The `rl.close()` method is called and the `InterfaceConstructor` instance has relinquished control over the `input` and `output` streams; * The `input` stream receives its `'end'` event; * The `input` stream receives Ctrl+D to signal end-of-transmission (EOT); * The `input` stream receives Ctrl+C to signal `SIGINT` and there is no `'SIGINT'` event listener registered on the - `readline.Interface` instance. + `InterfaceConstructor` instance. The listener function is called without passing any arguments. -The `readline.Interface` instance is finished once the `'close'` event is +The `InterfaceConstructor` instance is finished once the `'close'` event is emitted. ### Event: `'line'` @@ -220,12 +248,12 @@ The `'SIGTSTP'` event is _not_ supported on Windows. added: v0.1.98 --> -The `rl.close()` method closes the `readline.Interface` instance and +The `rl.close()` method closes the `InterfaceConstructor` instance and relinquishes control over the `input` and `output` streams. When called, the `'close'` event will be emitted. Calling `rl.close()` does not immediately stop other events (including `'line'`) -from being emitted by the `readline.Interface` instance. +from being emitted by the `InterfaceConstructor` instance. ### `rl.pause()` + +### Class: `readlinePromises.Interface` + + +* Extends: {readline.InterfaceConstructor} + +Instances of the `readlinePromises.Interface` class are constructed using the +`readlinePromises.createInterface()` method. Every instance is associated with a +single `input` [Readable][] stream and a single `output` [Writable][] stream. +The `output` stream is used to print prompts for user input that arrives on, +and is read from, the `input` stream. + +#### `rl.question(query[, options])` + + +* `query` {string} A statement or query to write to `output`, prepended to the + prompt. +* `options` {Object} + * `signal` {AbortSignal} Optionally allows the `question()` to be canceled + using an `AbortController`. +* Returns: {Promise} A promise that is fulfilled with the user's + input in response to the `query`. + +The `rl.question()` method displays the `query` by writing it to the `output`, +waits for user input to be provided on `input`, then invokes the `callback` +function passing the provided input as the first argument. + +When called, `rl.question()` will resume the `input` stream if it has been +paused. + +If the `readlinePromises.Interface` was created with `output` set to `null` or +`undefined` the `query` is not written. + +Example usage: + +```mjs +const answer = await rl.question('What is your favorite food? '); +console.log(`Oh, so your favorite food is ${answer}`); +``` + +Using an `AbortController` to cancel a question. + +```mjs +const ac = new AbortController(); +const signal = ac.signal; + +const answer = await rl.question('What is your favorite food? ', { signal }); +console.log(`Oh, so your favorite food is ${answer}`); + +signal.addEventListener('abort', () => { + console.log('The food question timed out'); +}, { once: true }); + +setTimeout(() => ac.abort(), 10000); +``` + +### Class: `readlinePromises.Readline` + + +#### `new readlinePromises.Readline(stream)` + + +* `stream` {stream.Writable} A [TTY][] stream. + +#### `rl.clearLine(dir)` + + +* `dir` {integer} + * `-1`: to the left from cursor + * `1`: to the right from cursor + * `0`: the entire line +* Returns: this + +The `rl.clearLine()` method adds to the internal list of pending action an +action that clears current line of the associated `stream` in a specified +direction identified by `dir`. +You need to call `rl.commit()` to see the effect of this method. + +#### `rl.clearScreenDown()` + + +* Returns: this + +The `rl.clearScreenDown()` method adds to the internal list of pending action an +action that clears the associated stream from the current position of the +cursor down. +You need to call `rl.commit()` to see the effect of this method. + +#### `rl.commit()` + + +* Returns: {Promise} + +The `rl.commit()` method sends all the pending actions to the associated +`stream` and clears the internal list of pending actions. + +#### `rl.cursorTo(x[, y])` + + +* `x` {integer} +* `y` {integer} +* Returns: this + +The `rl.cursorTo()` method adds to the internal list of pending action an action +that moves cursor to the specified position in the associated `stream`. +You need to call `rl.commit()` to see the effect of this method. + +#### `rl.moveCursor(dx, dy)` + + +* `dx` {integer} +* `dy` {integer} +* Returns: this + +The `rl.moveCursor()` method adds to the internal list of pending action an +action that moves the cursor *relative* to its current position in the +associated `stream`. +You need to call `rl.commit()` to see the effect of this method. + +#### `rl.rollback()` + + +* Returns: this + +The `rl.rollback` methods clears the internal list of pending actions without +sending it to the associated `stream`. + +### `readlinePromises.createInterface(options)` + + +* `options` {Object} + * `input` {stream.Readable} The [Readable][] stream to listen to. This option + is *required*. + * `output` {stream.Writable} The [Writable][] stream to write readline data + to. + * `completer` {Function} An optional function used for Tab autocompletion. + * `terminal` {boolean} `true` if the `input` and `output` streams should be + treated like a TTY, and have ANSI/VT100 escape codes written to it. + **Default:** checking `isTTY` on the `output` stream upon instantiation. + * `history` {string[]} Initial list of history lines. This option makes sense + only if `terminal` is set to `true` by the user or by an internal `output` + check, otherwise the history caching mechanism is not initialized at all. + **Default:** `[]`. + * `historySize` {number} Maximum number of history lines retained. To disable + the history set this value to `0`. This option makes sense only if + `terminal` is set to `true` by the user or by an internal `output` check, + otherwise the history caching mechanism is not initialized at all. + **Default:** `30`. + * `removeHistoryDuplicates` {boolean} If `true`, when a new input line added + to the history list duplicates an older one, this removes the older line + from the list. **Default:** `false`. + * `prompt` {string} The prompt string to use. **Default:** `'> '`. + * `crlfDelay` {number} If the delay between `\r` and `\n` exceeds + `crlfDelay` milliseconds, both `\r` and `\n` will be treated as separate + end-of-line input. `crlfDelay` will be coerced to a number no less than + `100`. It can be set to `Infinity`, in which case `\r` followed by `\n` + will always be considered a single newline (which may be reasonable for + [reading files][] with `\r\n` line delimiter). **Default:** `100`. + * `escapeCodeTimeout` {number} The duration `readlinePromises` will wait for a + character (when reading an ambiguous key sequence in milliseconds one that + can both form a complete key sequence using the input read so far and can + take additional input to complete a longer key sequence). + **Default:** `500`. + * `tabSize` {integer} The number of spaces a tab is equal to (minimum 1). + **Default:** `8`. +* Returns: {readlinePromises.Interface} + +The `readlinePromises.createInterface()` method creates a new `readlinePromises.Interface` +instance. + +```js +const readlinePromises = require('readline/promises'); +const rl = readlinePromises.createInterface({ + input: process.stdin, + output: process.stdout +}); +``` + +Once the `readlinePromises.Interface` instance is created, the most common case +is to listen for the `'line'` event: + +```js +rl.on('line', (line) => { + console.log(`Received: ${line}`); +}); +``` + +If `terminal` is `true` for this instance then the `output` stream will get +the best compatibility if it defines an `output.columns` property and emits +a `'resize'` event on the `output` if or when the columns ever change +([`process.stdout`][] does this automatically when it is a TTY). + +#### Use of the `completer` function + +The `completer` function takes the current line entered by the user +as an argument, and returns an `Array` with 2 entries: + +* An `Array` with matching entries for the completion. +* The substring that was used for the matching. + +For instance: `[[substr1, substr2, ...], originalsubstring]`. + +```js +function completer(line) { + const completions = '.help .error .exit .quit .q'.split(' '); + const hits = completions.filter((c) => c.startsWith(line)); + // Show all completions if none found + return [hits.length ? hits : completions, line]; +} +``` + +The `completer` function can also returns a {Promise}, or be asynchronous: + +```js +async function completer(linePartial) { + await someAsyncWork(); + return [['123'], linePartial]; +} +``` + +## Callback API + + +### Class: `readline.Interface` + + +* Extends: {readline.InterfaceConstructor} + +Instances of the `readline.Interface` class are constructed using the +`readline.createInterface()` method. Every instance is associated with a +single `input` [Readable][] stream and a single `output` [Writable][] stream. +The `output` stream is used to print prompts for user input that arrives on, +and is read from, the `input` stream. + +#### `rl.question(query[, options], callback)` + + +* `query` {string} A statement or query to write to `output`, prepended to the + prompt. +* `options` {Object} + * `signal` {AbortSignal} Optionally allows the `question()` to be canceled + using an `AbortController`. +* `callback` {Function} A callback function that is invoked with the user's + input in response to the `query`. + +The `rl.question()` method displays the `query` by writing it to the `output`, +waits for user input to be provided on `input`, then invokes the `callback` +function passing the provided input as the first argument. + +When called, `rl.question()` will resume the `input` stream if it has been +paused. + +If the `readline.Interface` was created with `output` set to `null` or +`undefined` the `query` is not written. + +The `callback` function passed to `rl.question()` does not follow the typical +pattern of accepting an `Error` object or `null` as the first argument. +The `callback` is called with the provided answer as the only argument. + +Example usage: + +```js +rl.question('What is your favorite food? ', (answer) => { + console.log(`Oh, so your favorite food is ${answer}`); +}); +``` + +Using an `AbortController` to cancel a question. + +```js +const ac = new AbortController(); +const signal = ac.signal; + +rl.question('What is your favorite food? ', { signal }, (answer) => { + console.log(`Oh, so your favorite food is ${answer}`); +}); + +signal.addEventListener('abort', () => { + console.log('The food question timed out'); +}, { once: true }); + +setTimeout(() => ac.abort(), 10000); +``` + +If this method is invoked as it's util.promisify()ed version, it returns a +Promise that fulfills with the answer. If the question is canceled using +an `AbortController` it will reject with an `AbortError`. + +```js +const util = require('util'); +const question = util.promisify(rl.question).bind(rl); + +async function questionExample() { + try { + const answer = await question('What is you favorite food? '); + console.log(`Oh, so your favorite food is ${answer}`); + } catch (err) { + console.error('Question rejected', err); + } +} +questionExample(); +``` + +### `readline.clearLine(stream, dir[, callback])` + +* `stream` {stream.Writable} +* `dx` {number} +* `dy` {number} +* `callback` {Function} Invoked once the operation completes. +* Returns: {boolean} `false` if `stream` wishes for the calling code to wait for + the `'drain'` event to be emitted before continuing to write additional data; + otherwise `true`. + +The `readline.moveCursor()` method moves the cursor *relative* to its current +position in a given [TTY][] `stream`. + ## `readline.emitKeypressEvents(stream[, interface])` * `stream` {stream.Readable} -* `interface` {readline.Interface} +* `interface` {readline.InterfaceConstructor} The `readline.emitKeypressEvents()` method causes the given [Readable][] stream to begin emitting `'keypress'` events corresponding to received input. @@ -720,26 +1107,6 @@ if (process.stdin.isTTY) process.stdin.setRawMode(true); ``` -## `readline.moveCursor(stream, dx, dy[, callback])` - - -* `stream` {stream.Writable} -* `dx` {number} -* `dy` {number} -* `callback` {Function} Invoked once the operation completes. -* Returns: {boolean} `false` if `stream` wishes for the calling code to wait for - the `'drain'` event to be emitted before continuing to write additional data; - otherwise `true`. - -The `readline.moveCursor()` method moves the cursor *relative* to its current -position in a given [TTY][] `stream`. - ## Example: Tiny CLI The following example illustrates the use of `readline.Interface` class to -- cgit v1.2.3