Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] fails to read more than one line under certain conditions #36

Open
obfusk opened this issue Jan 22, 2020 · 1 comment
Open

[BUG] fails to read more than one line under certain conditions #36

obfusk opened this issue Jan 22, 2020 · 1 comment

Comments

@obfusk
Copy link

obfusk commented Jan 22, 2020

Using read to prompt multiple times fails after the first line when:

  • input is non-interactive (e.g. file/pipe); and
  • use is asynchronous wrt "nesting" (replacing setTimeout(f) with f() seems to "fix" the issue)

Test case x.js

const read = require("read")
const f = () => read({ prompt: ">" }, (e, line) => {
  if (e) { throw e }
  console.log("got:", line)
  setTimeout(f)
})
f()

Interactive user input in terminal: works

$ node x.js 
> foo
got: foo
> bar
got: bar
> baz
got: baz
> ^D

Non-interactive input from file/pipe: only reads first line

$ node x.js <<< $'foo\nbar\nbaz' 
> foo
got: foo
> $ 

Solution?

I've replaced my use of read with the following code, which has fewer features but works:

const _readline = require("readline")
const _rl_buf   = []

const read_line = (prompt = null) =>
  new Promise((resolve, reject) => {
    const p = prompt || "", i = process.stdin, o = process.stdout
    if (_rl_buf.length) {
      if (p) { o.write(p) }
      resolve(_rl_buf.shift())
      return
    }
    const rl = _readline.createInterface({
      input: i, output: o, prompt: p, terminal: false
    })
    let done = false
    const f = (g, h = null) => {
      if (!done) { done = true; rl.close(); g() }
      else if (h) { h() }
    }
    rl.on("line"  , line => f(() => resolve(line),
                              () => _rl_buf.push(line)))
    rl.on("close" , ()   => f(() => resolve(null)))
    rl.on("error" , e    => f(() => reject(e)))
    rl.prompt()
  })

const f = () => read_line("> ").then(line => {
  if (line == null) { return }
  console.log("got:", line)
  setTimeout(f)
})
f()
$ node y.js 
> foo
got: foo
> bar
got: bar
> baz
got: baz
> $ node y.js <<< $'foo\nbar\nbaz' 
> got: foo
> got: bar
> got: baz
> $
@cco3
Copy link

cco3 commented Jun 7, 2023

The first time the readline interface is created (in read), all your input is consumed. The second time, because the data is consumed, the event loop empties before the promise managing readline ever settles, and the program just exits.

Some options for changing read's behavior

  1. Check input.readableEnded and watching for the readline close event, but that will just give an opportunity for a more meaningful error message, not actual read the data the way you expect.
  2. Use fs.read to read the input stream one character at a time, provided it supplies a file descriptor. There are probably some edge cases where this won't work.
  3. Restructure the read module to provide an object that manages a readline interface so that calling code can hang on to it and not implicitly create several readline interfaces. The limitation here is that it still won't work for subsequent calls to read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants