nodejs / node

Node.js JavaScript runtime βœ¨πŸ’πŸš€βœ¨
https://nodejs.org
Other
107.82k stars 29.71k forks source link

Functions to read from stdin #37925

Closed anirudhgiri closed 2 years ago

anirudhgiri commented 3 years ago

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 commented 3 years ago

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.

aduh95 commented 3 years ago

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

If https://github.com/nodejs/node/issues/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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

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 .

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 commented 3 years ago

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 .

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.

targos commented 3 years ago

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

Ayase-252 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

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

anirudhgiri commented 3 years ago

@artembykov Sounds good to me!

github-actions[bot] commented 2 years ago

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[bot] commented 2 years ago

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[bot] commented 2 years ago

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 commented 1 year ago

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 commented 12 months ago

~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 https://github.com/whatwg/streams/issues/1019#issuecomment-902309992 ❗

GCSBOSS commented 12 months ago

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:

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

sparecycles commented 12 months ago

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 commented 12 months ago
  • 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.