おしゃれ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キーで項目を選択できて、選択した項目を取ることができます。
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アプリがこのモジュールを使ってますね。
(※ちなみに 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を作る時にこんな感じで質問することができますよね。
この質問は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, " ") ); });
いい感じですね。
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, " ") ); });
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, " ") ); });
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, " ") ); });
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, " ") ); });
選択するときはスペースキーを押下して下さい。
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, " ") ); });
まとめ
- 超シンプルなリストから選択するだけのものを作りたいならterminal-menu
- keypressを使うとkeyとmouseの情報を簡単にコンソールから取れるようになる。
- inquirerを使うとterminal-menuよりも複雑なインタラクションを実現できる。
- ちなみにyeomanの質問形式はinquirerモジュールなので、ここを知っておくとgeneratorを作る時に捗る。
意外と多かった。みなさんもおしゃれCLI作ってみましょう。