本記事は、Dan Princeが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
Node.jsは「伝統的な」Webアプリケーションでものすごい威力を発揮しますが、その潜在的利用価値はずっと広範におよびます。マイクロサービス(Microservices)、REST API、ツール、IoT、さらにはデスクトップアプリケーションでの作業においてさえ、強い味方になってくれます。
Node.jsが大いに活躍する別の分野は、コマンドラインアプリケーションの構築です。記事ではこの点を紹介します。はじめにコマンドラインでの作業に役立つように設計されたサードパーティパッケージをいくつか見たあと、スクラッチで実例を構築します。
これから構築するのは、Gitリポジトリの初期化ツールです。もちろんgit initでも実行できますが、Gitリポジトリの初期化ツールを使えばもっと多くのことができます。コマンドラインからGitHub上にリモートリポジトリもしっかり作成でき、ユーザーは.gitignoreファイルをインタラクティブに作成して、最初のコミットとプッシュを実行できます。
いつものように、記事で出てくるコードはGitHub repoにあります。
Node.jsでコマンドラインツールを構築する理由
本題に入って構築を始める前に、コマンドラインアプリケーションの構築にNode.jsを選ぶ理由についてぜひ考えておきましょう。
もっとも明らかなメリットは、これを読んでいるみなさんならば、おそらくNode.jsに、また実際JavaScriptにすでに精通しているという点です。
別の主要なメリットは、使っていくうちに分かることですが、Node.jsはエコシステムが強力で、あらゆる目的に利用できる膨大な数のパッケージの中に、パワフルなコマンドラインツールの構築に役立つように設計されたものが数多くあることです。
最後に、Aptitude、Yum、HomebrewなどOSに固有のパッケージマネジャーを使わなくても、npmを使用して依存オブジェクトを管理できる点があります。
とはいえ、使っているコマンドラインツールにほかの外部依存オブジェクトが存在する場合、このことが当てはまるとは限りません。
これから構築する「ginit」の紹介
この記事では、「ginit」というコマンドラインユーティリティを作成します。これはgit initを強化したものです。
いったいどういう意味だろう、と思いますよね。
すでに知られているように、git initはカレントフォルダでGitリポジトリを初期化します。でもこれは普通、新規または既存のプロジェクトをGitにフックするプロセスに関係した多くの繰り返しステップのうちの1つに過ぎません。たとえば、通常のワークフローの一部として次のようにすることがあります。
- git initを実行してローカルリポジトリを初期化する
- 通常コマンドラインを離れ、Webブラウザーを立ち上げて、GitHubやBitbucket上にリモートリポジトリを作成する
- リモートを追加する
- .gitignoreファイルを作成する
- プロジェクトファイルを追加する
- ファイルの初期設定をコミットする
- リモートリポジトリにプッシュする
もっと多くのステップが関係することがしばしばですが、このアプリでは上に挙げたステップだけを使用します。とはいえ、これらのステップは何度も繰り返されます。GitのURLのコピー&ペーストなどをしなくても、コマンドラインからすべて実行できるとしたら、そのほうが良いと思いませんか?
ginitでできることは、カレントフォルダーでのGitリポジトリの作成、これまでGitHubを使っていたリモートリポジトリの作成、そしてリモートとしての追加です。このように、ginitは.gitignoreファイル作成用のシンプルでインタラクティブな「ウィザード」を供給し、フォルダーの内容を追加してリモートリポジトリにプッシュします。時間の節約にはなりませんが、新規プロジェクトスタート時の最初のもめ事をいくらか解消できます。
こうした点を踏まえて、始めていきます。
アプリケーション依存オブジェクト
1つ確かなこととして、外見に関しては、コンソールは決してグラフィカルユーザーインターフェイスのように洗練されてはいません。とはいえ、これはコンソールが飾り気が無く見苦しい、白黒のテキストでなければならないという意味ではありません。コンソールは、機能性を維持したまま、驚くほど視覚的に拡張できます。コンソールの表示方法を拡張する、いくつかのライブラリーを見ていきます。chalkは出力に色付けをし、cluiは付加的なビジュアル要素を加えます。楽しくするために、figletを使ってASCIIベースのおしゃれなバナーを作成し、clearを使ってコンソールをクリアもします。
入出力の点では、Node.jsのローレベルモジュールReadlineを使ってユーザーにプロンプトを表示したり、入力をリクエストできたりと、簡単なケースはこれで十二分です。しかし、ここでは高度に洗練されたサードパーティーパッケージ、Inquirerを使います。Inquirerには質問機能があり、同時にシンプルなインプットコントロールを実装します。コンソールなのに、ラジオボタンとチェックボックスがあるのを考えてみてください。
コマンドライン引数のパースに、minimistも使います。
コマンドラインでの開発専用パッケージの一覧です。
- chalk:出力の色付け
- clear:ターミナル画面の消去
- clui:コマンドラインで表、ゲージ、スピナーを表示
- figlet:テキストからアスキーアートを作成
- inquirer:インタラクティブなコマンドラインユーザーインターフェイスを作成
- minimist:引数オプションのパース
- preferences:CLIアプリケーションの暗号化プリファレンスの管理
以下も使用します。
- github:GitHub API用のノードラッパー
- lodash:JavaScriptユーティリティーライブラリー
- simple-git:Node.jsのアプリケーションでGitコマンドの実行
- touch:*Nix touchコマンドの実装
はじめましょう
スクラッチでアプリケーションを作成するといっても、この記事に付属するリポジトリからコードのコピーも取得できます。
プロジェクト用に新規ディレクトリを作成します。名前はginitとしなくても、もちろん大丈夫です。
mkdir ginit
cd ginit
新規のpackage.jsonファイルを作成します。
npm init
たとえば、簡単なウィザードを次のように作ります。
name: (ginit)
version: (1.0.0)
description: "git init" on steroids
entry point: (index.js)
test command:
git repository:
keywords: Git CLI
author: [YOUR NAME]
license: (ISC)
次に依存オブジェクトをインストールします。
npm install chalk clear clui figlet inquirer minimist preferences github lodash simple-git touch --save
または次のpackage.jsonファイルをコピー&ペーストし、authorの部分を適切なものに変えるだけでも大丈夫です。この記事に付属するリポジトリからファイルの取得もできます。
{
"name": "ginit",
"version": "1.0.0",
"description": "\"git init\" on steroids",
"main": "index.js",
"keywords": [
"Git",
"CLI"
],
"author": "Lukas White <hello@lukaswhite.com>",
"license": "ISC",
"dependencies": {
"chalk": "^1.1.3",
"clear": "0.0.1",
"clui": "^0.3.1",
"figlet": "^1.1.2",
"github": "^2.1.0",
"inquirer": "^1.1.0",
"lodash": "^4.13.1",
"minimist": "^1.2.0",
"preferences": "^0.2.1",
"simple-git": "^1.40.0",
"touch": "^1.0.0"
}
}
次に、同じフォルダー内にindex.jsファイルを作成し、すべての依存オブジェクトをrequireします。
var chalk = require('chalk');
var clear = require('clear');
var CLI = require('clui');
var figlet = require('figlet');
var inquirer = require('inquirer');
var Preferences = require('preferences');
var Spinner = CLI.Spinner;
var GitHubApi = require('github');
var _ = require('lodash');
var git = require('simple-git')();
var touch = require('touch');
var fs = require('fs');
ちなみに、simple-gitパッケージは、呼び出しが必要な関数をエクスポートします。
ヘルパーメソッドの追加
「ginit」アプリケーションでは、以下が必要になります。
- カレントディレクトリを取得する(デフォルトのリポジトリ名取得のため)
- ディレクトリが存在するかどうか確認する(.gitフォルダーを探索して、カレントフォルダーがすでにGitリポジトリになっているかどうか判定するため)
とても簡単に聞こえますが、頭に入れておきたいコツがいくつかあります。
まず、カレントディレクトリの取得にfsモジュールのrealpathSyncメソッドを使いたくなることがありますよね。
path.basename(path.dirname(fs.realpathSync(__filename)));
このコードは、同じディレクトリからアプリケーションを呼び出す(たとえばnode index.jsを使う)場合は動作します。でも、ここではコンソールアプリケーションをグローバルに使えるように構築していることを思い出してください。つまり、アプリケーションが存在するディレクトリの名前ではなく、作業中のディレクトリ名を取得することが必要なのです。作業中のディレクトリ名を取得するためには、process.cwdを使うのがベターです。
path.basename(process.cwd());
また、ファイルやディレクトリの存在を確認する推奨されるメソッドは変化し続けています。今、使用すべきメソッドはfs.stat / fs.statSyncです。これらのメソッドはファイルが存在しない場合にエラーを表示するので、try..catchブロックを使う必要があります。
最後に、コマンドラインアプリケーションを書く場合、同期したこれらのメソッドの使用を特に勧めます。
まとめとして、lib/files.jsにユーティリティパッケージを作成します。
var fs = require('fs');
var path = require('path');
module.exports = {
getCurrentDirectoryBase : function() {
return path.basename(process.cwd());
},
directoryExists : function(filePath) {
try {
return fs.statSync(filePath).isDirectory();
} catch (err) {
return false;
}
}
};
index.jsに戻って、この新規ファイルを必ずrequireしてください。
var files = require('./lib/files');
これで、アプリケーションの開発に取りかかれます。
Node CLIの初期化
次に、コンソールアプリケーションのスタートアップ段階を実装していきます。
コンソール出力の拡張用にインストールしておいたいくつかのパッケージを使ってみるために、画面を消去してバナーを表示します。
clear();
console.log(
chalk.yellow(
figlet.textSync('Ginit', { horizontalLayout: 'full' })
)
);
出力は次のようになります。
次に、カレントフォルダーがまだGitリポジトリになっていないことを確かめるため、簡単なチェックをします。先ほど作成したユーティリティメソッドを使って.gitフォルダーを確認するだけの簡単なものです。
if (files.directoryExists('.git')) {
console.log(chalk.red('Already a git repository!'));
process.exit();
}
ちなみに、chalkモジュールを使ってメッセージを赤で表示しています。
入力待ちのプロンプトを表示する
次に必要なのは、ユーザーがGithub認証情報を入力するためのプロンプトを表示する関数の作成です。
関数の作成にはInquirerを使います。Inquirerは、プロンプト用のいろいろなタイプのメソッドを多く含んでおり、おおかたHTMLフォームコントロールによく似ています。ユーザーのGithubユーザー名とパスワードの採取にtypeでinputとpasswordをそれぞれ使います。コードは次のとおりです。
function getGithubCredentials(callback) {
var questions = [
{
name: 'username',
type: 'input',
message: 'Enter your Github username or e-mail address:',
validate: function( value ) {
if (value.length) {
return true;
} else {
return 'Please enter your username or e-mail address';
}
}
},
{
name: 'password',
type: 'password',
message: 'Enter your password:',
validate: function(value) {
if (value.length) {
return true;
} else {
return 'Please enter your password';
}
}
}
];
inquirer.prompt(questions).then(callback);
}
見てのとおり、inquirer.prompt()はユーザーに一連の質問をしますが、これは第1引数として配列の形で与えられます。それぞれの質問は、フィールドのname、type(ここではinputとpasswordをそれぞれ使っていますが、あとでもう少し進んだ例を紹介します)、表示のためのプロンプト(message)、バリデーションコールバック(validate)を定義するオブジェクトで構成されています。
ユーザーからの入力はコールバックに渡されます。このように、usernameとpasswordの2つのプロパティで完結する簡単なオブジェクトです。
以下のコードをindex.jsに追加して、すべてのテストをします。
getGithubCredentials(function(){
console.log(arguments);
});
node index.jsを使ってスクリプトを実行した結果です。
GitHub認証の処理
次のステップは、GitHub APIのOAuthトークンを取得する関数の作成です。実質上、ユーザー名とパスワードをトークンに「引き換え」ます。
もちろん、ツールを使うたびにユーザーに認証情報を入力させるのは避けたいところです。代わりに、以降のリクエスト用にOAuthトークンを保存しておきます。ここでpreferencesパッケージの出番です。
Preferencesでの保存
preferencesでの保存は、外見上はとても簡単です。JSONファイルとの間で読み書きさえすればよく、サードパーティパッケージも必要ありません。にもかかわらず、preferencesパッケージには主なメリットがいくつかあります。
- オペレーティングシステムと現在のユーザーを考慮して、ファイルの最適な位置を判断してくれる
- 明示的にファイルの読み書きをする必要がなく、preferencesオブジェクトの修正だけでよく、しかもバックグラウンドで実行してくれる
- preferencesデータは暗号化されており、ユーザーのデリケートなデータを保存するため、作成中のアプリケーションでは重要である
次のようにインスタンスを作成し、アプリケーションの識別子を渡すだけでpreferencesを使えます。
var prefs = new Preferences('ginit');
preferencesファイルが存在しない場合、空のオブジェクトを返してバックグラウンドでファイルを作成します。すでにpreferencesファイルが存在する場合、内容はJSONにデコードされ、アプリケーションで使えるようになります。次にprefsを単一のオブジェクトとして使い、必要に応じてプロパティを取得または設定できます。すでに述べたとおり、あとで保存しなければと心配しなくても大丈夫です。もう保存してくれてますから。
OS X、Linuxの場合、ファイルは次の場所にあります。
/Users/[YOUR-USERNME]/.config/preferences/ginit.pref
GitHub APIとの通信
Github APIのインスタンスを作成します。このインスタンスはいろいろなところで使うため、スクリプトでグローバルに使えるように作成します。
var github = new GitHubApi({
version: '3.0.0'
});
アクセストークンが取得済みかどうかをチェックする関数です。
function getGithubToken(callback) {
var prefs = new Preferences('ginit');
if (prefs.github && prefs.github.token) {
return callback(null, prefs.github.token);
}
// Fetch token
getGithubCredentials(function(credentials) {
...
});
}
prefsオブジェクトが存在し、そこにgithub、github.tokenがある場合、ストレージ内にすでにトークンが存在していることになります。この場合、コールバック関数(引数として渡される)が実行され、呼び出す関数の戻り値を制御します。これについてはあとで触れます。
トークンが検出されない場合、フェッチが必要です。もちろんOAuthトークンの取得にはネットワークリクエストが関係しているので、ユーザーは少し待たなければなりません。ここで、スピナーアニメーションなど、コンソールベースアプリケーションの拡張を搭載したcluiパッケージの出番です。
スピナーの作成は簡単です。
var status = new Spinner('Authenticating you, please wait...');
status.start();
トークンが取得されたら、あとはスピナーを止めて画面から消すだけです。
status.stop();
updateメソッドを使って、キャプションを動的にも設定できます。これは、完了のパーセンテージを表示するなど、進捗状況を表示したいときに便利です。
GitHubの認証のためのコードです。
getGithubCredentials(function(credentials) {
var status = new Spinner('Authenticating you, please wait...');
status.start();
github.authenticate(
_.extend(
{
type: 'basic',
},
credentials
)
);
github.authorization.create({
scopes: ['user', 'public_repo', 'repo', 'repo:status'],
note: 'ginit, the command-line tool for initalizing Git repos'
}, function(err, res) {
status.stop();
if ( err ) {
return callback( err );
}
if (res.token) {
prefs.github = {
token : res.token
};
return callback(null, res.token);
}
return callback();
});
});
次のようになります。
- 先に定義したgetGithubCredentialsメソッドを使ってユーザーに認証情報用のプロンプトを表示する
- OAuthトークンの取得に入る前に、basic authenticationを使用する
- アプリケーション用のアクセストークンを作成する
- アクセストークンの取得を管理する場合、次回に備えてpreferencesにアクセストークンを設定する
- トークンを返す
作成したアクセストークンは、手動でもAPI経由でもここで見られます。上のコードにあるnoteパラメータから分かることですが、開発中、アクセストークンを再生成できるようにginitのアクセストークンを削除する必要があることが理解できます。
Githubアカウントで2要素認証を有効にしている場合、プロセスはいくらか複雑になります。確認コード(たとえばSMS経由で送信される)をリクエストし、次いでX-Github-OTPヘッダを使って送信する必要があります。詳しくは、このドキュメントを参照してください。
ここまででテストをする場合、これまでのコードを必ずindex.jsの末尾に置いて実行してください。
リポジトリの作成
OAuthトークンの取得ができたたら、OAuthトークンを使ってGithubでリモートリポジトリを作成できます。
また、Inquirerを使って一連の質問ができます。必須として「リポジトリ名」(name)を、任意項目として「リポジトリに関する説明」(description)を要求し、さらにpublicにするかprivateにするかを教えてもらうことが必要です。
minimistを使って、オプションのコマンドライン引数からデフォルトのリポジトリ名とリポジトリに関する説明を取得します。たとえば次のようになります。
ginit my-repo "just a test repository"
ここでは、デフォルトのリポジトリ名をmy-repo、リポジトリに関する説明をjust a test repositoryと設定しています。
次の行では、アンダースコアによってインデックスされた配列に引数を置いています。
var argv = require('minimist')(process.argv.slice(2));
// { _: [ 'my-repo', 'just a test repository' ] }
minimistパッケージについては、少しだけかいつまんで説明しています。フラグ、スイッチ、name、valueのペアの解釈にもこれを使えます。詳しくはドキュメントを確認してください。
コマンドライン引数をパースして一連の質問をするコードです。
function createRepo(callback) {
var argv = require('minimist')(process.argv.slice(2));
var questions = [
{
type: 'input',
name: 'name',
message: 'Enter a name for the repository:',
default: argv._[0] || files.getCurrentDirectoryBase(),
validate: function( value ) {
if (value.length) {
return true;
} else {
return 'Please enter a name for the repository';
}
}
},
{
type: 'input',
name: 'description',
default: argv._[1] || null,
message: 'Optionally enter a description of the repository:'
},
{
type: 'list',
name: 'visibility',
message: 'Public or private:',
choices: [ 'public', 'private' ],
default: 'public'
}
];
inquirer.prompt(questions).then(function(answers) {
var status = new Spinner('Creating repository...');
status.start();
var data = {
name : answers.name,
description : answers.description,
private : (answers.visibility === 'private')
};
github.repos.create(
data,
function(err, res) {
status.stop();
if (err) {
return callback(err);
}
return callback(null, res.ssh_url);
}
);
});
}
情報を取得したら、あとはgithubパッケージを使ってリポジトリを作成し、新規に作成したリポジトリのURLを取得するだけです。その後、Gitのローカルリポジトリにリモートとしてセットアップします。しかしまず.gitignoreファイルをインタラクティブに作成します。
.gitignoreファイルの作成
次のステップとして、.gitignoreファイルを生成する簡単なコマンドライン「ウィザード」を作成します。ユーザーが既存のプロジェクトディレクトリでアプリケーションを実行する場合、すでにカレント作業ディレクトリに存在しているファイルとディレクトリのリストをユーザーに示し、どれを無視するかユーザーが選択できるようにします。
Inquirerパッケージで、ちょうど次のようなcheckboxタイプの入力画面を実現できます。
まず必要なのは、カレントディレクトリをスキャンして.gitフォルダーと、存在する場合は既存の.gitignoreファイルを無視することです(lodashのwithoutメソッドを使って実行します)。
function createGitignore(callback) {
var filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore');
...
}
追加するファイルが存在しない場合、続行する部分がないので、カレントの.gitignoreファイルをtouchするのみで関数を除けます。
function createGitignore(callback) {
var filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore');
if (filelist.length) {
...
} else {
touch( '.gitignore' );
return callback();
}
}
最後に、Inquirerのチェックボックス「ウィジェット」を利用してファイルをリストします。「提出」されるとすぐに、ファイルの選択されたリストを結合し、改行を挟んで.gitignoreを生成します。
function createGitignore(callback) {
var filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore');
if (filelist.length) {
inquirer.prompt(
[
{
type: 'checkbox',
name: 'ignore',
message: 'Select the files and/or folders you wish to ignore:',
choices: filelist,
default: ['node_modules', 'bower_components']
}
]
).then(function( answers ) {
if (answers.ignore.length) {
fs.writeFileSync( '.gitignore', answers.ignore.join( '\n' ) );
} else {
touch( '.gitignore' );
}
return callback();
}
);
} else {
touch('.gitignore');
return callback();
}
}
ちなみに、デフォルトのリストも作成できます。この場合、あらかじめ選択したnode_modulesとbower_componentsが存在する必要があります。
作成した関数で.gitignoreファイルをバッチリ取得できました。これでGitリポジトリの初期化に進めます。
アプリからGitを使ってみる
Gitの使用方法はたくさんありますが、一番簡単なのはおそらくsimple-gitパッケージを使うことです。simple-gitパッケージは、バックグラウンドでGitを実行できる、連結可能なメソッドのセットを供給します。
simple-gitパッケージを使って自動化できる繰り返しタスクは次のとおりです。
- git initを実行する
- .gitignoreファイルを追加する
- 作業ディレクトリの残りの内容を追加する
- 最初のコミットを実行する
- 新規に作成されたリモートリポジトリを追加する
- 作業ディレクトリをリモートにプッシュする
コードは次のとおりです。
function setupRepo( url, callback ) {
var status = new Spinner('Setting up the repository...');
status.start();
git
.init()
.add('.gitignore')
.add('./*')
.commit('Initial commit')
.addRemote('origin', url)
.push('origin', 'master')
.then(function(){
status.stop();
return callback();
});
}
ここまでのまとめ
最後に、トークンを取得してユーザーを認証する関数を作成します。
function githubAuth(callback) {
getGithubToken(function(err, token) {
if (err) {
return callback(err);
}
github.authenticate({
type : 'oauth',
token : token
});
return callback(null, token);
});
}
話を進める前に、アプリの主要なロジックをハンドルするコードを見てください。
githubAuth(function(err, authed) {
if (err) {
switch (err.code) {
case 401:
console.log(chalk.red('Couldn\'t log you in. Please try again.'));
break;
case 422:
console.log(chalk.red('You already have an access token.'));
break;
}
}
if (authed) {
console.log(chalk.green('Sucessfully authenticated!'));
createRepo(function(err, url){
if (err) {
console.log('An error has occured');
}
if (url) {
createGitignore(function() {
setupRepo(url, function(err) {
if (!err) {
console.log(chalk.green('All done!'));
}
});
});
}
});
}
});
見てのとおり、ほかのすべての関数(createRepo、createGitignore、setupRepo)を順に呼び出す前に、ユーザーが認証されたことを確認しています。また、エラーをハンドルし、ユーザに適切なフィードバックを供給しています。
GitHub repoで、完全版のindex.jsを確認できます。
ginitコマンドをグローバルに使えるようにする
あと1つすべきことは、コマンドをグローバルに使えるようにすることです。そのためには、index.jsの先頭に(shebang)行の追加が必要です。
#!/usr/bin/env node
次に、package.jsonファイルにbinプロパティを追加する必要があります。追加することにより、package.jsonに関連して実行されるファイル名にコマンド名(ginit)をマッピングします。
"bin": {
"ginit": "./index.js"
}
このようにモジュールをグローバルにインストールすると、シェルコマンドが動くようになります。
npm install -g
幸い、npmがスクリプトと一緒にコマンドラッパーをインストールしてくれるので、このコマンドはWindowsでも動きます。
さらなるステップアップ
シンプルとはいえとても気の利いたGitリポジトリを初期化するコマンドラインアプリができました。でもこれは、もっともっと拡張できます。
Bitbucketを使っているなら、このプログラムでBitbucket APIを使ってリポジトリを作成できます。実行するにあたって役立つNode.js APIラッパーがあります。GitHubとBitbucketのどちらを使いたいかをユーザーに質問するコマンドラインオプションやプロンプトを追加したり(これにはInquirerがまさにぴったりです)、ただGitHub専用のコードをBitbucketに置き換えてみたりするのもいいですね。
ハードコードされたリストを使わずに、デフォルトのセットを.gitgnoreファイルで指定する機能も実現できます。これにはpreferencesパッケージが向いていて、あるいはユーザーにプロジェクトのタイプを入力させるための「テンプレート」のセットも作れます。まとめたものを、.gitignore.ioのcommand-line tool / APIで見てみてください。
(原文:Build a JavaScript Command Line Interface (CLI) with Node.js)
[翻訳:新岡祐佳子]
[編集:Livit]