Node.jsではCLIツールを作る手段として、commander.js
やinquirer.js
が使える。ここでは、commander.js
を使ってシェル風のCLIツール(mkdir, rmのようなコマンドを打って操作する)を実現してみる。なお、定型的な作業を、ユーザに質問しながら実行するCLIツールを作る場合はinquirer.js
を使った方が良さそうだが、ここでは扱わない。
コマンド本体
事例として、文字列を追加・削除・表示するコマンドを作成した。push
は文字列を配列に文字通りpushする。pop
は最後にpushした文字列から取り除く。show
は配列内の文字列を表示する。exit
はプログラムを終了して、コマンドプロンプトに戻す。
これらのコマンドは、そのままprogram.parse()
を実行しただけでは、1回コマンドを実行してプログラム自体が終了してしまいあまり意味がないので、シェル風にする工夫を付け加える。
import{createCommand}from'commander';constarray:string[]=[];constprogram=createCommand();// コマンドの定義program.command('push [value]').action((value:string)=>{array.push(value);console.log(` Pushed: ${value}`);});program.command('pop').action(()=>{constpopped=array.pop();console.log(` Popped: ${popped}`);});program.command('show').action(()=>{console.log(' Array:');console.log(array);});program.command('exit').action(()=>{console.log(' Bye!');process.exit(0);});
コマンドをシェル風にする
readline
を使うことで、1行ずつコマンドを実行するシェル風にすることができる。
import{createCommand}from'commander';import{createInterface}from'readline';import{parseArgsStringToArgv}from'string-argv';// コマンドの定義は省略// [1] エラーが出ても中断しないようにするprogram.exitOverride();// [2] readlineで1行ずつ読み込むconstrl=createInterface({input:process.stdin,output:process.stdout});functionasyncQuestion(message:string):Promise<string>{returnnewPromise((resolve)=>{rl.question(message,(line:string)=>resolve(line));});}// [3] 1行ずつ読み込んだコマンドを実行するasyncfunctionloop(){while(1){constline=awaitasyncQuestion('sample> ');// [3-1] 入力された文字列をコマンドライン引数風に分解する。constargv=parseArgsStringToArgv(line);// [3-2] コマンドを実行するtry{awaitprogram.parseAsync(argv,{from:'user'});}catch(err){// 続行}}}// [4] コマンドを実行するif(process.argv.length>2){// 引数あり: 1回だけコマンドを実行program.parse();process.exit(0);// 明示的に終了}else{// 引数なし: シェル風の操作を提供loop();}
[1] エラー時にexitさせない
commander.js
はデフォルトではコマンドが存在しないなどのエラー時に、process.exit
でプログラムを終了してしまう。
コマンドが存在していなくても動作は続けたいので、プログラムの終了を抑制するためexitOverride()
をコールしている。(引数にコールバック関数を入れることもできるが、ここではexitさえしなければ良いので何も入れていない)
[2] readlineで1行ずつユーザの入力を受け付ける
Node.js標準のreadline
のquestion
関数を使って、ユーザの入力を受け付ける。readline
のquestion
関数はユーザの入力が終わるのを待たずに再度呼び出すことができてしまうので無限ループに適さない。そこで、asyncQuestion
で入力を待つようPromise
化している。
[3] 1行ずつ読み込んだコマンドを実行する
[2]で作成した関数を用いて、ユーザが入力した文字列1行を読み込んだら、[3-1]でコマンドライン引数風に分解している。
(こうしないと、""や空白を含むパスをうまく扱えないという不便なことになる)。
そして[3-2]で、コマンドライン引数風の配列をコマンドに与え、該当するaction
を実行させる。ここで、{ from: 'user' }
が無いと正しく動作しないことに注意する。commander.js
は、デフォルトではprocess.argv
をコマンドライン引数と扱うため、argv[0]
=Node.jsのパスとargv[1]
=スクリプトのパスの後に引数が与えられることをと想定している。{ from: 'user' }
を指定することで、コマンドライン引数はユーザ指定であり、argv[0]
から引数が入っていると扱ってもらえるようになる。
[4] コマンドを実行する
シェル風のコマンドを提供したいが、引数を与えたらそのコマンドだけ実行したい場合もあると思う。コマンドライン引数の数が2(Node.jsのパスとスクリプトのパスのみ)より大きければ1回だけコマンドを実行(program.parse()
)し、それ以外の場合はシェル風にするloop()
を呼び出すことで実現できる。なお、exitOverride
しているので、明示的に終了する必要があることに注意する。
実行結果
上記プログラムがdist/index.js
に格納されている場合、node dist/index.js
を実行すると以下のようになる。なんとなくシェルっぽい動作をしていることが分かる。
> node dist/index.js
sample> push 01234
Pushed: 01234
sample> push 56789
Pushed: 56789
sample> show
Array:
[ '01234', '56789' ]
sample> pop
Popped: 56789
sample> show
Array:
[ '01234' ]
sample> exit
Bye!
> (元のコマンドプロンプトに戻る)
1回だけ実行する場合は、node dist index.js show
のように引数を与えて実行することで、1回だけコマンドを実行して終了する。
> cli show
Array:
[]
> (元のコマンドプロンプトに戻る)
nodeを付けずに呼び出す
起動コマンドにいちいちnode dist/index.js
を付けるのは格好悪いので、以下のようにバッチファイルから呼び出せるようにする。
例えば以下のバッチファイルはコマンドラインからcli
と呼び出せば、dist/index.js
に必要な引数を与えて実行することができるようになる。
@echo offnodedist\index.js %*