Node.jsアプリケーションでCLIを実装する

以下のようなやりとりができる、簡単なCLIを実装します。

```console
Plase enter some text: abc
Your text: "abc"
Plase enter some text: quit
Really? n
Plase enter some text: def
Your text: "def"
Plase enter some text: quit
Really? y
Bye.
以下、続きの処理
```

Node.js以外なら楽勝です。
例えばPython。

```cli.py
while (True):
    text = input('Plase enter some text: ')
    if text == 'quit':
        if input('Really? ') == 'y':
            print('Bye.')
            break
        else:
            continue
    print('Your text: "%s"' % text)

print('以下、続きの処理')
```

しかしノンブロッキングを謳うNode.jsでは、標準入力を受け付けようとした瞬間に問答無用で非同期送りなので、割と難易度が上がります。
同期処理で標準入力を扱うライブラリもあるっぽいですが、とりあえずネイティブの`Readline`モジュールで実装してみましょう。

なお、使用した Node.js は v10.11.0 です。

## `Readline`の`line`イベントを利用する
標準入力に1行入力がある度に発火するイベントです。
ここにコールバック関数を登録していきます。

```event-cli.js
const readline = require('readline')

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

// 今のフェーズを管理する
const PHASE = {
  NORMAL: 0,
  WILL_QUIT: 1  // quitするか聞いてる時
}
let phase = PHASE.NORMAL

console.log('Please enter some text:')
rl.prompt()
rl.on('line', input => {
  switch (phase) {
    case PHASE.NORMAL:
      if (input === 'quit') {
        console.log('Really?')
        rl.prompt()
        phase = PHASE.WILL_QUIT
        return
      }
      console.log(`Your text: "${input}"`)
      console.log('Please enter some text:')
      rl.prompt()
      break

    case PHASE.WILL_QUIT:
      if (input === 'y') {
        console.log('Bye.')
        rl.close()
        return
      }
      phase = PHASE.NORMAL
      console.log('Please enter some text:')
      rl.prompt()
      break
  }
})
rl.on('close', () => {
  console.log('以下、続きの処理')
})
```
```console
Please enter some text:
> abc
Your text: "abc"
Please enter some text:
> quit
Really?
> n
Please enter some text:
> def
Your text: "def"
Please enter some text:
> quit
Really?
> y
Bye.
以下、続きの処理
```

DRYな部分が気になりますか？私も気になります。
各自で良い感じにリファクタしておいてください。

`rl.prompt()`のおかげで、コンソールの様子が上記のPythonのやつより良い感じですね。

それにしても厄介なのは、`line`イベント発火時の処理として全フェーズでの応答パターンを1つに固めないといけないところです。
quit入力時とそれ以外の時で別々のカスタムイベントを発火し、振り分けた方が綺麗になるかもしれません。

## `Readline`の`question`メソッドを利用する
ユーザーの入力1回に対し、応答処理をコールバック関数として登録するメソッドです。
柔軟に使えますが、今回のようなパターンでは上手にループさせる必要があります。

以下の記事が参考になります。
[Node.jsの非同期処理を複数回繰り返す](https://qiita.com/ikngtty/items/7f5260aeac062072a7ab)
誰が書いたんでしょうね。非常に分かりやすいです。

さて、上の記事を書いてみて分かったことは以下。

* 不特定回数ループする時は再帰しか無いっぽい。あとこの場合にPromiseを使う意味は特に無い。
* 分かんなくなったらとりあえずベタ書きしてみるとイメージしやすい。

今回も案の定混乱したのでベタ書きから始めたのですが、まあ今回はそれを晒すのは省略します。

```question-cli.js
const readline = require('readline')

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

const ask = () => {
  rl.question('Please enter some text: ', answer => {
    if (answer === 'quit') {
      rl.question('Really? ', answer => {
        if (answer === 'y') {
          console.log('Bye.')
          rl.close()
          endQuestion()
        } else {
          ask()
        }
      })
    } else {
      console.log(`Your text: "${answer}"`)
      ask()
    }
  })
}
ask()

const endQuestion = () => {
  console.log('以下、続きの処理')
}
```
```console
Please enter some text: abc
Your text: "abc"
Please enter some text: quit
Really? n
Please enter some text: def
Your text: "def"
Please enter some text: quit
Really? y
Bye.
以下、続きの処理
```

多少ごちゃついてますが、こっちの方が分かりやすそうですね。

## 結論
Readline系のライブラリはたくさんあるようなので、良いのを探しましょう。