◆ ユーザ入力が一定時間ないと自動で実行する
◆ 実行確認したいけど入力が面倒なときや標準入力ができないときに使える

コマンドラインツールで標準入力からの入力を待機するときについての話です
yes/no などユーザに入力させるわけですが デフォルト値として進めるのにわざわざ入力しなくともいいようにしたいです
とは言え デフォルト以外の値を入力したいこともあります
ユーザの入力をずっと待つのではなく ある程度放置すれば勝手に進めてくれれば良さそうなのでそういうものを作ってみることにしました

データを上書きや削除する系のツールでは 間違って実行すると取り返しがつかないことになるので実行直後に「本当に実行していいですか?」の確認プロンプトを出すことがあります
実行直後に yes を毎回入力しないといけないのも手間ですし ここも時間経過で自動実行してくれたら助かります
以前はプロンプトじゃなく 10 秒程度のスリープを入れてから実行するようにして 間違って実行した場合は 10 秒以内に Ctrl-C で止めるという対策だったこともあります
これはこれで即実行を繰り返したいときに毎回 10 秒待つのも不便です
yes が入力されたら即実行してされないなら 10 秒で自動実行だと解決できる気がします

また プログラムから呼び出す場合など ユーザが標準入力へ入力できないようなケースはどれだけ待っても入力が来ないのでそこから先に進めません
これも時間経過で自動で実行するなら先に進めます

簡単な例

とりあえず簡単なものを作ってみました
10 秒経過で自動実行します
残り 3 秒になるとカウントダウンします
例なので実行内容は「開始しました」「終了しました」の出力だけです
y か n を入力すれば即実行・キャンセルできます

!async function() {
const wait = sec => new Promise((resolve) => setTimeout(resolve, sec * 1000))
const run = async () => {
console.log("開始しました")
await wait(1)
console.log("終了しました")
process.exit(0)
}
const cancel = () => {
console.log("キャンセルしました")
process.exit(1)
}

process.stdin.setRawMode(true)
process.stdin.on("data", (buf) => {
const char = buf.toString().toLowerCase()
if (char === "y") {
run()
} else if (char === "n") {
cancel()
}
})

console.log("実行しますか?[y]か[n]を押してください")
console.log("10秒後に自動で実行されます")

await wait(7)
console.log("3...")
await wait(1)
console.log("2..")
await wait(1)
console.log("1.")
await wait(1)
run()
}()

y か n の一文字だけならエンターを押す必要もないので キーを押したタイミングで入力を受け取るようにしています
このモードだと画面に入力した文字が出ず console.log の出力と混ざって変な表示にならないのでカウントダウンなどと相性も良いです

関数にまとめる

もう少し使いまわしやすいように関数化しました
y か n を押すという部分は固定です

function confirm(msg, timeout_sec) {
return new Promise((resolve, reject) => {
timeout_sec = ~~timeout_sec
if (timeout_sec < 5) timeout_sec = 5

let done = false
const org_mode = process.stdin.isRaw
process.stdin.setRawMode(true)
const listener = (buf) => {
const char = buf.toString().toLowerCase()
if (char === "y") {
decide(true)
} else if (char === "n") {
decide(false)
}
}
process.stdin.on("data", listener)
const decide = (yn) => {
console.log("")
done = true
process.stdin.off("data", listener)
if (!org_mode) {
process.stdin.setRawMode(false)
process.stdin.pause()
}
resolve(yn)
}
const logInline = (str) => {
process.stdout.write(String(str))
}

console.log(msg)
console.log("[y]か[n]を押してください")
logInline(`${timeout_sec}秒後に自動で実行します`)

const wait = (sec) => {
setTimeout(() => {
if (done) return
sec--
if (sec === 0) {
return decide(true)
}
logInline(sec <= 3 ? sec : ".")
wait(sec)
}, 1000)
}
wait(timeout_sec)
})
}

!async function() {
if (await confirm("実行しますか?", 5)) {
console.log("実行しました")
} else {
console.log("キャンセルしました")
}
}()