Skip to content

Commit

Permalink
feat: use async/await
Browse files Browse the repository at this point in the history
BREAKING CHANGE: this module has been refactored to use promises
- the API is now promise only and no longer accepts a callback
- the Promise is resolved to a string and no longer returns `isDefault`
  • Loading branch information
lukekarrys committed Dec 13, 2022
1 parent 5a7563b commit c5b56f6
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 383 deletions.
31 changes: 8 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ For reading user input from stdin.
Similar to the `readline` builtin's `question()` method, but with a
few more features.

## USAGE
## Usage

```javascript
var read = require("read")
read(options, callback)
try {
const result = await read(options, callback)
} catch (er) {
console.error(er)
}
```

The callback gets called with either the user input, or the default
specified, or an error, as `callback(error, result, isDefault)`
node style.

## OPTIONS
## Options

Every option is optional.

Expand All @@ -33,21 +33,6 @@ Every option is optional.
If silent is true, and the input is a TTY, then read will set raw
mode, and read character by character.

## COMPATIBILITY

This module works sort of with node 0.6. It does not work with node
versions less than 0.6. It is best on node 0.8.

On node version 0.6, it will remove all listeners on the input
stream's `data` and `keypress` events, because the readline module did
not fully clean up after itself in that version of node, and did not
make it possible to clean up after it in a way that has no potential
for side effects.

Additionally, some of the readline options (like `terminal`) will not
function in versions of node before 0.8, because they were not
implemented in the builtin readline module.

## CONTRIBUTING
## Contributing

Patches welcome.
13 changes: 0 additions & 13 deletions example/example.js

This file was deleted.

150 changes: 58 additions & 92 deletions lib/read.js
Original file line number Diff line number Diff line change
@@ -1,116 +1,82 @@

module.exports = read

var readline = require('readline')
var Mute = require('mute-stream')

function read (opts, cb) {
if (opts.num) {
throw new Error('read() no longer accepts a char number limit')
}

if (typeof opts.default !== 'undefined' &&
typeof opts.default !== 'string' &&
typeof opts.default !== 'number') {
const readline = require('readline')
const Mute = require('mute-stream')

module.exports = async function read ({
default: def = '',
input = process.stdin,
output = process.stdout,
prompt = '',
silent,
timeout,
edit,
terminal,
replace,
}) {
if (typeof def !== 'undefined' && typeof def !== 'string' && typeof def !== 'number') {
throw new Error('default value must be string or number')
}

var input = opts.input || process.stdin
var output = opts.output || process.stdout
var prompt = (opts.prompt || '').trim() + ' '
var silent = opts.silent
var editDef = false
var timeout = opts.timeout
let editDef = false
prompt = prompt.trim() + ' '
terminal = !!(terminal || output.isTTY)

var def = opts.default || ''
if (def) {
if (silent) {
prompt += '(<default hidden>) '
} else if (opts.edit) {
} else if (edit) {
editDef = true
} else {
prompt += '(' + def + ') '
}
}
var terminal = !!(opts.terminal || output.isTTY)

var m = new Mute({ replace: opts.replace, prompt: prompt })
const m = new Mute({ replace, prompt })
m.pipe(output, { end: false })
output = m
var rlOpts = { input: input, output: output, terminal: terminal }

if (process.version.match(/^v0\.6/)) {
var rl = readline.createInterface(rlOpts.input, rlOpts.output)
} else {
var rl = readline.createInterface(rlOpts)
}

output.unmute()
rl.setPrompt(prompt)
rl.prompt()
if (silent) {
output.mute()
} else if (editDef) {
rl.line = def
rl.cursor = def.length
rl._refreshLine()
}

var called = false
rl.on('line', onLine)
rl.on('error', onError)

rl.on('SIGINT', function () {
rl.close()
onError(new Error('canceled'))
})

var timer
if (timeout) {
timer = setTimeout(function () {
onError(new Error('timed out'))
}, timeout)
}
return new Promise((resolve, reject) => {
const rl = readline.createInterface({ input, output, terminal })
const timer = timeout && setTimeout(() => onError(new Error('timed out')), timeout)

function done () {
called = true
rl.close()
output.unmute()
rl.setPrompt(prompt)
rl.prompt()

if (process.version.match(/^v0\.6/)) {
rl.input.removeAllListeners('data')
rl.input.removeAllListeners('keypress')
rl.input.pause()
if (silent) {
output.mute()
} else if (editDef) {
rl.line = def
rl.cursor = def.length
rl._refreshLine()
}

clearTimeout(timer)
output.mute()
output.end()
}

function onError (er) {
if (called) {
return
const done = () => {
rl.close()
clearTimeout(timer)
output.mute()
output.end()
}
done()
return cb(er)
}

function onLine (line) {
if (called) {
return
const onError = (er) => {
done()
reject(er)
}
if (silent && terminal) {
output.unmute()
output.write('\r\n')
}
done()
// truncate the \n at the end.
line = line.replace(/\r?\n$/, '')
var isDefault = !!(editDef && line === def)
if (def && !line) {
isDefault = true
line = def
}
cb(null, line, isDefault)
}

rl.on('error', onError)
rl.on('line', (line) => {
if (silent && terminal) {
output.unmute()
output.write('\r\n')
}
done()
// truncate the \n at the end.
const res = line.replace(/\r?\n$/, '') || def || ''
return resolve(res)
})

rl.on('SIGINT', () => {
rl.close()
onError(new Error('canceled'))
})
})
}
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"version": "4.6.0"
},
"tap": {
"statements": 77,
"branches": 75,
"functions": 57,
"lines": 78,
"test-ignore": "fixtures/",
"nyc-arg": [
"--exclude",
"tap-snapshots/**"
Expand Down
89 changes: 41 additions & 48 deletions test/basic.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,54 @@
var read = require('../lib/read.js')
const t = require('tap')
const read = require('../')
const spawnRead = require('./fixtures/setup')

if (process.argv[2] === 'child') {
return child()
}

var tap = require('tap')
var CLOSE = 'close'
if (process.version.match(/^v0\.6/)) {
CLOSE = 'exit'
async function child () {
const user = await read({ prompt: 'Username: ', default: 'test-user' })
const pass = await read({ prompt: 'Password: ', default: 'test-pass', silent: true })
const verify = await read({ prompt: 'Password again: ', default: 'test-pass', silent: true })

console.error(JSON.stringify({
user,
pass,
verify,
passMatch: pass === verify,
}))

if (process.stdin.unref) {
process.stdin.unref()
}
}

var spawn = require('child_process').spawn

tap.test('basic', function (t) {
var child = spawn(process.execPath, [__filename, 'child'])
var output = ''
var write = child.stdin.write.bind(child.stdin)
child.stdout.on('data', function (c) {
console.error('data %s', c)
output += c
if (output.match(/Username: \(test-user\) $/)) {
process.nextTick(write.bind(null, 'a user\n'))
} else if (output.match(/Password: \(<default hidden>\) $/)) {
process.nextTick(write.bind(null, 'a password\n'))
} else if (output.match(/Password again: \(<default hidden>\) $/)) {
process.nextTick(write.bind(null, 'a password\n'))
} else {
console.error('prompts done, output=%j', output)
}
t.test('basic', async (t) => {
const { stdout, stderr } = await spawnRead(__filename, {
'Username: (test-user)': 'a user',
'Password: (<default hidden>)': 'a password',
'Password again: (<default hidden>)': 'a password',
})

var result = ''
child.stderr.on('data', function (c) {
result += c
console.error('result %j', c.toString())
})
t.same(JSON.parse(stderr),
{ user: 'a user', pass: 'a password', verify: 'a password', passMatch: true })
t.equal(stdout,
'Username: (test-user) Password: (<default hidden>) Password again: (<default hidden>) ')
})

child.on(CLOSE, function () {
result = JSON.parse(result)
t.same(result, { user: 'a user', pass: 'a password', verify: 'a password', passMatch: true })
t.equal(output, 'Username: (test-user) Password: (<default hidden>) Password again: (<default hidden>) ')
t.end()
t.test('defaults', async (t) => {
const { stdout, stderr } = await spawnRead(__filename, {
'Username: (test-user)': '',
'Password: (<default hidden>)': '',
'Password again: (<default hidden>)': '',
})

t.same(JSON.parse(stderr),
{ user: 'test-user', pass: 'test-pass', verify: 'test-pass', passMatch: true })
t.equal(stdout,
'Username: (test-user) Password: (<default hidden>) Password again: (<default hidden>) ')
})

function child () {
read({ prompt: 'Username: ', default: 'test-user' }, function (er, user) {
read({ prompt: 'Password: ', default: 'test-pass', silent: true }, function (er, pass) {
read({ prompt: 'Password again: ', default: 'test-pass', silent: true }, function (er, pass2) {
console.error(JSON.stringify({ user: user,
pass: pass,
verify: pass2,
passMatch: (pass === pass2) }))
if (process.stdin.unref) {
process.stdin.unref()
}
})
})
})
}
t.test('errors', async (t) => {
t.rejects(() => read({ default: {} }))
})

0 comments on commit c5b56f6

Please sign in to comment.