Edited at

Node.jsでの標準入力(コンソール)について

More than 1 year has passed since last update.


getch(ReadKey),readlineサンプル(C#比較付き)


Node.js

const readline=require("readline");

//keypressイベントを使用可能にする
readline.emitKeypressEvents(process.stdin);

//readlineするためのオブジェクト
const rl=readline.createInterface({
input:process.stdin, //標準入力を指定
output:process.stdout, //標準出力を指定
prompt:"", //デフォルトのプロンプトを指定(指定なしで"> ")
terminal:false //process.stdin.setRawModeでエコーバックの切り替えを可能にする
});

//keypress使わなければこれだけでも良い。(promptオプションはお好みで)
//const rl=readline.createInterface(process.stdin,process.stdout);

//非同期関数を使用するためasyncを定義。
//(await、Promiseを多用する)
(async function(){
for(;;){
//エコーバックを無効にする
process.stdin.setRawMode(true);

console.log("Enterを押すまで文字を読み取り続ける(getch, ReadKey)");
await new Promise(resolve=>{
//onはイベントが削除されるまで動作し続ける(無限ループ)
process.stdin.on("keypress",function self(key,ch){
if(ch.name=="return") {
console.log();
//自分のイベントを削除
process.stdin.removeListener("keypress",self);
return resolve();
}
//文字として取得
console.log(key);
//キーボードステータスの取得
console.log(ch);
});
});

console.log("一回分だけキーボード入力を取得");
//onceは一回だけでイベントが破棄される。
console.log(await new Promise(res=>process.stdin.once("keypress",res))+"\n");

//エコーバックを有効にする
process.stdin.setRawMode(false);

console.log("4回分ReadLine");
console.log(await new Promise(res=>rl.once("line",res)));
console.log(await new Promise(res=>rl.once("line",res)));

//questionでは任意の文字列をpromptに付けられる。
console.log(await new Promise(res=>rl.question("Question?: ",res)));

//rl.setPromptとrl.promptで一時的にプロンプトを変更できる。
rl.setPrompt("Prompt!!: ");
rl.prompt();
console.log(await new Promise(res=>rl.once("line",res)));

console.log();
}
})();



C#

using System;

class Program{
static void Main(){
for(;;){
Console.WriteLine("Enterを押すまで文字を読み取り続ける(getch, ReadKey)");
for(;;){
var ch=Console.ReadKey(true);
if(ch.Key==ConsoleKey.Enter) break;
//文字として取得
Console.WriteLine("char:"+ch.KeyChar);
//キーボードとして取得
Console.WriteLine("keyboard:"+ch.Key);
//キーボードステータスの取得
Console.WriteLine("status:"+ch.Modifiers);
}

Console.WriteLine("一回分だけキーボード入力を取得");
Console.WriteLine(Console.ReadKey(true).KeyChar+"\n");

Console.WriteLine("4回分ReadLine");
Console.WriteLine(Console.ReadLine());
Console.WriteLine(Console.ReadLine());

Console.Write("Question?: ");
Console.WriteLine(Console.ReadLine());

Console.Write("Prompt!!: ");
Console.WriteLine(Console.ReadLine());

Console.WriteLine();
}
}
}


続編…?(パイプについての検証)


内容

僕はNode.jsで同期的な標準入力関数が用意されていないことについて長い間苦しまされてきた。

そこで長い間調べてきて「これだ」と言える答えにたどり着いたのでここに備忘録とまとめておく。


その名はreadline

他の言語と勝手が違ったので面を食らったがそれは紛うことなきreadlineであった。

最小の構成では次から使用できる。

const rl=require("readline").createInterface(process.stdin,process.stdout);

rl.on("line",function(str){
console.log("get:"+str);
});

ただし、この時点で普通のreadlineと使用しようとすると様々な点で苦難する点多くある。

第一として非同期関数であるため、同期的に書こうとすると実行順序が保証されないことだ。

const rl=require("readline").createInterface(process.stdin,process.stdout);

rl.on("line",function(str){
console.log("get:"+str);
});
console.log("end");

この場合では"end"が最初に表示される。

正しくはこうする必要がある。

const rl=require("readline").createInterface(process.stdin,process.stdout);

rl.on("line",function(str){
console.log("get:"+str);
console.log("end");
});

さらにこれでもさらに問題がある。

この状態であると標準入力のみが無限ループしているように見える。

これを解消するにはonceを使用する。

process.stdin並びにreadlineオブジェクトはこれを継承しているようなので同じように適用できるようだ。

const rl=require("readline").createInterface(process.stdin,process.stdout);

rl.once("line",function(str){
console.log("get:"+str);
console.log("end");
});

ただし、このままではプログラムの終端に達してもいつまでも終了しないため、process.exitによって明示的に終了させる。

const rl=require("readline").createInterface(process.stdin,process.stdout);

rl.once("line",function(str){
console.log("get:"+str);
console.log("end");

process.exit();
});

ここで複数の標準入力を行わせたいとき新たな問題が生じる。

const rl=require("readline").createInterface(process.stdin,process.stdout);

rl.once("line",function(str){
console.log("getA:"+str);
rl.once("line",function(str){
console.log("getB:"+str);
rl.once("line",function(str){
console.log("getC:"+str);
console.log("end");

procesrs.exit();
});
});
});

それは代々JavaScriptに伝えられてきたコールバック地獄であった。

この状態ではループするにしても再帰するしかなかった。

そこでEcmaScript6(2015)の出現で解消されることとなった。


Generator&Promise

Generatorを使用すると次のように書き換えることができる。

const rl=require("readline").createInterface(process.stdin,process.stdout);

const g=(function*(){
var str=yield rl.once("line",s=>g.next(s))
console.log("getA:"+str);
str=yield rl.once("line",s=>g.next(s))
console.log("getB:"+str);
str=yield rl.once("line",s=>g.next(s))
console.log("getC:"+str);
console.log("end");

//関数化も可能。
const gets=()=>rl.once("line",s=>g.next(s));

str=yield gets();
console.log("getA:"+str);
str=yield gets();
console.log("getB:"+str);
str=yield gets();
console.log("getC:"+str);
console.log("end");

process.exit();
})();
g.next();

また、Promissでは、

const rl=require("readline").createInterface(process.stdin,process.stdout);

new Promise(res=>rl.once("line",res))
.then(str=>{
console.log("getA:"+str);
return new Promise(res=>rl.once("line",res));
}).then(str=>{
console.log("getB:"+str);
return new Promise(res=>rl.once("line",res));
}).then(str=>{
console.log("getC:"+str);
console.log("end");

process.exit();
});

const rl=require("readline").createInterface(process.stdin,process.stdout);

//Promiseも関数化可能。
const gets=()=>new Promise(res=>rl.once("line",res));

gets().then(str=>{
console.log("getA:"+str);
return gets();
}).then(str=>{
console.log("getB:"+str);
return gets();
}).then(str=>{
console.log("getC:"+str);
console.log("end");

process.exit();
});


async&await

ES2016(ES7)によってPromiseをさらに同期的に書けるようになった。

const rl=require("readline").createInterface(process.stdin,process.stdout);

(async function(){
var str=await new Promise(res=>rl.once("line",res));
console.log("getA:"+str);
str=await new Promise(res=>rl.once("line",res));
console.log("getB:"+str);
str=await new Promise(res=>rl.once("line",res));
console.log("getC:"+str);
console.log("end");

//関数化
const gets=()=>new Promise(res=>rl.once("line",res))
str=await gets();
console.log("getA:"+str);
str=await gets();
console.log("getB:"+str);
str=await gets();
console.log("getC:"+str);
console.log("end");

process.exit();
})();

ここで冒頭のサンプルの内容となる。


さいごに

getch相当の機能を使うことにもだいぶ苦労したのだけどここので割愛します。

readlineオブジェクトを定義しちゃうとエコーバックが常にされてしまったので苦労したとだけ。