おしゃれCLIを作るためのnpmモジュール達

この記事は、 Node.js Advent Calendar 2013 の14日目です。


Yeoman とか tig とか触ってるとおしゃれなコマンドラインインターフェースだな〜、と思うことはありませんか。

ぼくもそんなおしゃれCLIを作ってモテたい!!

そんなおしゃれCLIを作るためのnpmモジュールについて調べました。

terminal-menu

substackさんが作った、stream-adventureの中で使われてるモジュール。

(趣旨は違うけど、stream-adventureはNode.jsのstreamの概念を学ぶのに非常に良い学習ツールです。ちなみに npm install -g stream-adventureでインストールできます。)

terminal-menuは超シンプルなモジュールで、基本的な機能としては、上下キー、vimバインドのjkキーでメニューを選択できる事、enterキーで項目を選択できて、選択した項目を取ることができます。

f:id:yosuke_furukawa:20131214211131p:plain

var menu = require('terminal-menu')({width : 29, x:4, y:2});
//resetでコンソールをクリア
menu.reset();

// メニューのタイトル等、writeでunselectableな項目を追加できる。
menu.write("test\n");
menu.write("--------\n");

// メニューの項目をaddで追加できる。
menu.add("abc");
menu.add("def");

//選択した項目をselectで受信する。
menu.on('select', function (label) {
    // closeでメニューを閉じる
    menu.close();
    console.log('SELECTED: ' + label);
});

//menuもstreamにしてprocess.stdoutに繋げられる所がsubstackらしい。
menu.createStream().pipe(process.stdout);

項目から選択するだけのCLIを作るならterminal-menuが一番簡単です。でも、これだけだとstdinから自由入力するようなyeomanのようなCLIを作るのは困難かと。

keypress

keypressはキーボードとマウスからの入力を扱いやすくしてくれるモジュールです。keypressを使うと、通常のprocess.stdinに対して新しく、'keypress'というイベントと'mousepress'というイベントが増えます。
これらのイベントを使ってキーイベントやマウスイベントを扱うことができます。

ちょっと前に流行ったtetrisアプリがこのモジュールを使ってますね。
f:id:yosuke_furukawa:20131214214243p:plain

(※ちなみに npm install tetris -g ってやるとインストールできます。)

var keypress = require('keypress')
keypress(process.stdin)

process.stdin.setRawMode(true)

// keyイベントを補足
process.stdin.on('keypress', function (c, key) {
  console.log(c, key)
  // ctrl-cでstdinをpauseさせてexitする
  if (key && key.ctrl && key.name == 'c') {
    process.stdin.pause()
  }
})

//mouseイベントを捕捉
process.stdin.on('mousepress', function (mouse) {
  console.log(mouse)
})

//mouseイベントを補足できるようにするためにenableにする。
keypress.enableMouse(process.stdout)
process.on('exit', function () {
  // mouseをenableにする場合、最後にキャンセルする必要が有る。
  keypress.disableMouse(process.stdout)
})

process.stdin.resume()

inquirer

Yeomanで使われてるCLIツールですね。inquirerはterminal-menuと異なりかなり豊富なインタラクションを作れます。Yeomanのgeneratorを作ったことがある方なら分かるかと思いますが、generatorを作る時にこんな感じで質問することができますよね。

f:id:yosuke_furukawa:20130714131100p:plain

この質問はinquirer moduleを使って作られてます。inquirerはその名前の通り、アンケートみたいな質問項目を生成する事に主眼が置かれています。

あと、generatorを作ったことがある方もyeoman generatorを作る時に通常の入力質問以外にもいくつかオプションが有ることを知らないんじゃないかと。yeoman tipsとしても抑えておいたほうがいいかもしれません。

いくつか質問タイプの作り方を説明します。

input type

通常の入力タイプの質問を生成する方法です。yeomanではこの質問が多いですよね。

"use strict";
var inquirer = require("inquirer");

var questions = [
  {
    type: "input",
    name: "first_name",
    message: "What's your first name"
  },
  {
    type: "input",
    name: "last_name",
    message: "What's your last name"
  }
];

inquirer.prompt( questions, function( answers ) {
  console.log( JSON.stringify(answers, null, "  ") );
});

validateとかで入力値の検証も可能です。

var inquirer = require("inquirer");

var questions = [
{
  type: "input",
  name: "zipcode",
  message: "What's your zipcode",
  validate: function( value ) {
    var pass = value.match(/^\d{3}-?\d{4}$/);
    if (pass) {
      return true;
    } else {
      return "Please enter a valid zipcode";
    }
  }
}
];

inquirer.prompt( questions, function( answers ) {
  console.log( JSON.stringify(answers, null, "  ") );
});

いい感じですね。
f:id:yosuke_furukawa:20131214223543p:plain

password type

パスワードみたいな外から見えなくしたい入力を "***" でマスキングする入力パターンですね。

inquirer.prompt([
  {
    type: "password",
    message: "Enter your password",
    name: "password"
  }
], function( answers ) {
  console.log( JSON.stringify(answers, null, "  ") );
});
confirm type

いわゆる yes or noの質問ですね。

var inquirer = require("inquirer");

inquirer.prompt([
{
    type: "confirm",
    name: "toBeDelivered",
    message: "Is it for a delivery",
    default: false
}
], function( answers ) {
  console.log( JSON.stringify(answers, null, "  ") );
});

f:id:yosuke_furukawa:20131214230452p:plain

list type

リストから選択するタイプですね。

var inquirer = require("inquirer");

inquirer.prompt([
{
  type: "list",
  name: "size",
  message: "What size do you need",
  choices: [ "Jumbo", "Large", "Standard", "Medium", "Small", "Micro" ],
  filter: function( val ) { return val.toLowerCase(); }
}
], function( answers ) {
  console.log( JSON.stringify(answers, null, "  ") );
});

f:id:yosuke_furukawa:20131214225834p:plain

ordered itemにしたい場合はrawlistにしてください。

var inquirer = require("inquirer");
inquirer.prompt([
{
  type: "rawlist",
  name: "size",
  message: "What size do you need",
  choices: [ "Jumbo", "Large", "Standard", "Medium", "Small", "Micro" ],
  filter: function( val ) { return val.toLowerCase(); }
}
], function( answers ) {
  console.log( JSON.stringify(answers, null, "  ") );
});

f:id:yosuke_furukawa:20131214225905p:plain

checkbox type

チェックボックスです。複数の選択肢から複数をチョイスできます。

var inquirer = require("inquirer");
inquirer.prompt([
  {
    type: "checkbox",
    message: "Select toppings",
    name: "toppings",
    choices: [
      new inquirer.Separator("The usual:"),
      {
        name: "Peperonni"
      },
      {
        name: "Cheese"
      },
      {
        name: "Mushroom"
      },
      new inquirer.Separator("The extras:"),
      {
        name: "Pineapple"
      },
      {
        name: "Bacon"
      },
      {
        name: "Extra cheese"
      }
    ],
    validate: function( answer ) {
      if ( answer.length < 1 ) {
        return "You must choose at least one topping.";
      }
      return true;
    }
  }
], function( answers ) {
  console.log( JSON.stringify(answers, null, "  ") );
});

f:id:yosuke_furukawa:20131214225435p:plain

選択するときはスペースキーを押下して下さい。

expand type

複数のリストから単数の項目を選択する選択形式なんですが、上下キーで選択するのではなく、任意のキーを選択に利用することができるものです。Vimでswpファイルが存在するファイルを開こうとすると開くモードのオプションが聞かれますが、それに似ています。

inquirer.prompt([
  {
    type: "expand",
    message: "Conflict on `file.js`: ",
    name: "overwrite",
    choices: [
      {
        key: "y",
        name: "Overwrite",
        value: "overwrite"
      },
      {
        key: "a",
        name: "Overwrite this one and all next",
        value: "overwrite_all"
      },
      {
        key: "d",
        name: "Show diff",
        value: "diff"
      },
      new inquirer.Separator(),
      {
        key: "x",
        name: "Abort",
        value: "abort"
      }
    ]
  }
], function( answers ) {
  console.log( JSON.stringify(answers, null, "  ") );
});

f:id:yosuke_furukawa:20131214225009p:plain

まとめ

  • 超シンプルなリストから選択するだけのものを作りたいならterminal-menu
  • keypressを使うとkeyとmouseの情報を簡単にコンソールから取れるようになる。
  • inquirerを使うとterminal-menuよりも複雑なインタラクションを実現できる。
  • ちなみにyeomanの質問形式はinquirerモジュールなので、ここを知っておくとgeneratorを作る時に捗る。

意外と多かった。みなさんもおしゃれCLI作ってみましょう。