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

Functions to read from stdin #37925

Closed
anirudhgiri opened this issue Mar 26, 2021 · 21 comments
Closed

Functions to read from stdin #37925

anirudhgiri opened this issue Mar 26, 2021 · 21 comments
Labels
discuss Issues opened for discussions and feedbacks. feature request Issues that request new features to be added to Node.js. readline Issues and PRs related to the built-in readline module. stale

Comments

@anirudhgiri
Copy link

The Problem

Javascript, used in conjunction with NodeJS, is increasingly being used as a general purpose programming language. While I acknowledge the fact that it was originally intended to be used as a language for server-side development, but the language has grown into something much more general from being used to create executables, GUIs, video games, mobile applications and even operating systems. The world of educational computer science is moving away from teaching beginners languages like C and Java as their first language to teaching them Python and NodeJS instead.

There are some features who's existence in a modern general purpose programming language is imperative like printing to the console, doing basic array and string operations, and getting user input. Unfortunately, getting user input from
stdin through nodejs is an absolute pain. You should either declare a buffer, connect it to the stdin stream and use the buffer to read input line-by-line or use third party libraries.

Having your users of your language, especially beginners, go through such a process just for the luxury of getting input from the user through the terminal for such a popular and widely used language should simply be unacceptable. Yet, it is the norm.

NodeJS is an available option in online learning platforms like HackerRank and Leetcode, and in hiring software used for coding interviews. Since most input is received through stdin, programmers either have to rely on the platform taking care of getting the input and just give the user a function with the inputs given as parameters to work with (like Leetcode does) or the programmers have to write all the boilerplate code to receive user input themselves (and loosing time and competitive edge in the process) like in Google Kickstart. This only drives people away from using Node in such competitive environments to other to use languages like Python (where they can just use input()) or C++ (where they can just do cin >>).

The solution

Much like C's scanf() or Python's input(), Node should have a simple and beginner friendly way to recieve user input from the terminal. My suggestion is two functions - input() and inputSync().

input() and inputSync() both return a line from stdin as a String but just like readFile() and readFileSync(), input() does it asynchronusly while inputSync() does it synchronusly.

Example Usage

//Getting input synchronusly
let name = inputSync("Enter your name: ");
console.log("Hello, "+ name + "!");
//Getting input asynchronusly
input("Enter your name: ", (name) => { 
    console.log("Hello, "+ name + "!");
    }
@Ayase-252
Copy link
Member

Ayase-252 commented Mar 26, 2021

Hi, thank you for your feature request.

There is a Readline module in Node.js. You could try rl.question to see whether it meets your need.

If it does not fit your need, feel free to share your view here please.

@Ayase-252 Ayase-252 added the feature request Issues that request new features to be added to Node.js. label Mar 26, 2021
@aduh95
Copy link
Contributor

aduh95 commented Mar 26, 2021

Regarding having a synchronous API, I don't think it would very useful now that we have support for top-level await.

If #37287 was implemented, you could do:

import { createInterface } from 'node:readline/promises';

const rl = createInterface({
  input: process.stdin,
  output: process.stdout,
  prompt: 'OHAI> '
});

const name = await rl.question("What's your name?");
console.log(`Hello ${name}!`);

If you wanted to implement it today, you can use util.promisify:

import { createInterface } from 'node:readline';
import util from 'node:util':

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  prompt: 'OHAI> '
});

export const input = util.promisify(rl.question).bind(rl);

@anirudhgiri
Copy link
Author

Hello @Ayase-252,

Thank you for pointing me to readline. I would argue that importing a module, creating an Interface object and linking it to process.stdin and then using it's question command, all for getting an input from stdin seems is very beginner unfriendly.

I'm not saying there is no way to get user input from node, I'm saying that the way to do it is unnessecarily long and complicated for beginner programmers.

Getting user input in Python:

name = input("Enter your name")

Getting user input in C++:

cout << "Enter your name"
cin >> name

Meanwhile, getting user input in JS:

const readline = require('readline');

const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

rl.question("What is your name?", name =>{
    console.log(`Hello, ${name}!`);
    rl.close();
})

The fact that you need all this boilerplate code for something as basic and imperative to a general purpose programming language as getting user input instead of just using one simple function like you do in Python is what I'm arguing against.

The solution @aduh95 provided wouldn't work either because you can only use promisify on functions that are (error,callback) which rl.question is not. You would need to use promisify.custom, further complicating something that should be simple in the first place 😕.

@Linkgoron
Copy link
Member

Linkgoron commented Mar 26, 2021

The solution @aduh95 provided wouldn't work either because you can only use promisify on functions that are (error,callback) which rl.question is not. You would need to use promisify.custom, further complicating something that should be simple in the first place 😕.

rl.question already has a promisify.custom implementation, so util.promisify works on it correctly.

https://nodejs.org/api/readline.html#readline_rl_question_query_options_callback

@anirudhgiri
Copy link
Author

rl.question already has a promisify.custom implementation

Ah, apologies. I didn't know that, my bad.
Everything else I raised still stands. It's too much boilerplate for a basic and frequently used functionality.

@Ayase-252
Copy link
Member

Ayase-252 commented Mar 26, 2021

@anirudhgiri
Hello

I'm neutral to implement the feature, but I would argue that complexity in some degree is unavoidable for new player in a new technology.

It reflects me as a completely naive learner without any prior knowledge about programming as a freshman in university. I was tought C as my first programming language. I have absolutely no idea about what #include <stdio.h> is and does. The only thing I knew was it would not work without the magical #include <stdio.h> if I want to print or input something to/from the gaint dark screen.

With C, the minimal CLI program will be something like

#include <stdio.h>

int main() {
  char fav_food[100];
  printf("What's your favorite food?");
  scanf("%s", fav_food);
  printf("That is your food" + fav_food);
  return 0;
}

it would confuse a lot people who are new to programming from my experience. the #include directive, library, memory allocate, pointer, format symbol etc. and Why we need return 0; here.

As @aduh95 and @Linkgoron mentioned, thanks to awesome top-level await, the eqivalent CLI program in Node.js can be

import utils from 'node:util';
import { createInterface } from 'node:readline';

const rl = createInterface({
  input: process.stdin,
  output: process.stdout,
});

const input = utils.promisify(rl.question).bind(rl);
const favFood = await input('What is your favorite food?\n');
console.log(`That is your food: ${favFood}`);

Still too complex? We can hide the readline module behind module in userland say input.

// input.mjs
import utils from 'node:util';
import { createInterface } from 'node:readline';

const rl = createInterface({
  input: process.stdin,
  output: process.stdout,
});

export const input = utils.promisify(rl.question).bind(rl);

In the learner side, the only thing she/he needs to do is import { input } from './input.mjs' just like the magical #include <studio.h>.

// main.mjs
import { input } from './input.mjs'

const favFood = await input('What is your favorite food?\n')
console.log(`That is your food: ${favFood}`)

Then run node main.mjs

➜  node git:(triaging/main) ✗ node main.mjs
What is your favorite food?
Sushi!
That is your food: Sushi!

I think it is much simpler and explainable than a C example for new learners.

Edit: multiple grammar problems

@anirudhgiri
Copy link
Author

Still too complex? We can hide the readline module behind module in userland say input.

// input.mjs
import utils from 'node:util';
import { createInterface } from 'node:readline';

const rl = createInterface({
input: process.stdin,
output: process.stdout,
});

export const input = utils.promisify(rl.question).bind(rl);
In the learner side, the only thing she/he needs to do is import { input } from './input.mjs' just like the magical #include <studio.h>.

Excellent! Don't you think the short module you just wrote, input.mjs, should already be available as a standard NodeJS function instead of relegating it to the user? Don't leave it up to the programmers to implement their own input.mjs and instead have input() be a part of Node's standard library because it is needed so frequently. Also the unnecessary complexity added from having to use the readline module and createInterface can be hidden away to make it easier for beginners to learn.

@Ayase-252
Copy link
Member

Still too complex? We can hide the readline module behind module in userland say input.
// input.mjs
import utils from 'node:util';
import { createInterface } from 'node:readline';
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
export const input = utils.promisify(rl.question).bind(rl);
In the learner side, the only thing she/he needs to do is import { input } from './input.mjs' just like the magical #include <studio.h>.

Excellent! Don't you think the short module you just wrote, input.mjs, should already be available as a standard NodeJS function instead of relegating it to the user? Don't leave it up to the programmers to implement their own input.mjs and instead have input() be a part of Node's standard library because it is needed so frequently. Also the unnecessary complexity added from having to use the readline module and createInterface can be hidden away to make it easier for beginners to learn.

It's alright. Personally, I would perfer to incoperate input.mjs into readline module as static methods
to support usecase like

import { input } from 'node:readline'

const favFood = await input('What is your favorite food?\n')
console.log(`That is your food: ${favFood}`)

I think the implementation is simple. But I'm not sure about whether it is right to add one method input on module readline. I'd label with discuss and readline to see whether it is doable and there is other concern.

@Ayase-252 Ayase-252 added discuss Issues opened for discussions and feedbacks. readline Issues and PRs related to the built-in readline module. labels Mar 27, 2021
@targos
Copy link
Member

targos commented Mar 27, 2021

Another possibility could be to add a method to process.stdin.

Ayase-252 added a commit to Ayase-252/node that referenced this issue Mar 27, 2021
@Ayase-252
Copy link
Member

@targos

Thanks, I did some experiments around process, but input(message) may write to process.stdout in order to display message. It may introduce some coupling btw process.stdout and process.stdin.

In this case, could be process.input(message) an option?

@artembykov
Copy link

Hi @anirudhgiri and @Ayase-252,

What are your opinions about having that function named prompt instead of input?

window.prompt does exist in the browsers and I thought it would be neat to have the same name, especially since more web (browser) APIs are coming to Node.js.

A quick check shows that Deno uses prompt too.

Although I'm not sure whether it may be confused with .prompt() method of readline instances.

@Ayase-252
Copy link
Member

@artembykov I like the idea, it would be ideal.

@anirudhgiri
Copy link
Author

@artembykov Sounds good to me!

Ayase-252 added a commit to Ayase-252/node that referenced this issue May 5, 2021
Ayase-252 added a commit to Ayase-252/node that referenced this issue May 5, 2021
@github-actions
Copy link
Contributor

There has been no activity on this feature request for 5 months and it is unlikely to be implemented. It will be closed 6 months after the last non-automated comment.

For more information on how the project manages feature requests, please consult the feature request management document.

@github-actions github-actions bot added the stale label Mar 24, 2022
@github-actions
Copy link
Contributor

There has been no activity on this feature request for 5 months and it is unlikely to be implemented. It will be closed 6 months after the last non-automated comment.

For more information on how the project manages feature requests, please consult the feature request management document.

@github-actions
Copy link
Contributor

There has been no activity on this feature request and it is being closed. If you feel closing this issue is not the right thing to do, please leave a comment.

For more information on how the project manages feature requests, please consult the feature request management document.

@GCSBOSS
Copy link

GCSBOSS commented Jul 27, 2023

I have just bumped into this while composing a beginner's programming course trying to use node/js as the first platform/language of a rookie. Will probably have to use process.argv to get input in the initial lessons.

@sparecycles
Copy link

sparecycles commented Nov 21, 2023

Any counterarguments to using this?

Buffer.concat(await process.stdin.toArray()).toString("utf-8")

EDIT: the counter-argument would be to use text from the builtin stream/consumers module.

import { text } from "node:stream/consumers"

await text(process.stdin);

As I just discovered in whatwg/streams#1019 (comment)

@GCSBOSS
Copy link

GCSBOSS commented Nov 21, 2023

Any counterarguments to using this?

Buffer.concat(await process.stdin.toArray()).toString("utf-8")

Assuming it works (I haven't tried), all the arguments in the discussion above:

  • Too long to memorize.
  • Too 'composite' to explain each part to a complete beginner.
  • In older (LTS?) node versions it doesn't work without an async function wrapper. Of course, anything that we could add will not work in older node versions.

The hopes were that at least this feature would be available as a straightforward function from the get go.

@sparecycles
Copy link

Yeah, this is purely a "read all stdin", not the readline solution.

It's just the shortest/clearest incantation I've found (much better than using .on('data' | 'end') handlers for this usecase).

@aduh95
Copy link
Contributor

aduh95 commented Nov 22, 2023

  • In older (LTS?) node versions it doesn't work without an async function wrapper. Of course, anything that we could add will not work in older node versions.

You have to go back to Node.js 12.x to find a version where top-level await is not supported. The oldest non-EOL release line is 18.x at the time of writing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discuss Issues opened for discussions and feedbacks. feature request Issues that request new features to be added to Node.js. readline Issues and PRs related to the built-in readline module. stale
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants