Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8934

commander.js でシェル風のCLIツールを作る

$
0
0

Node.jsではCLIツールを作る手段として、commander.jsinquirer.jsが使える。ここでは、commander.jsを使ってシェル風のCLIツール(mkdir, rmのようなコマンドを打って操作する)を実現してみる。なお、定型的な作業を、ユーザに質問しながら実行するCLIツールを作る場合はinquirer.jsを使った方が良さそうだが、ここでは扱わない。

コマンド本体

事例として、文字列を追加・削除・表示するコマンドを作成した。pushは文字列を配列に文字通りpushする。popは最後にpushした文字列から取り除く。showは配列内の文字列を表示する。exitはプログラムを終了して、コマンドプロンプトに戻す。

これらのコマンドは、そのままprogram.parse()を実行しただけでは、1回コマンドを実行してプログラム自体が終了してしまいあまり意味がないので、シェル風にする工夫を付け加える。

index.ts
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行ずつコマンドを実行するシェル風にすることができる。

index.ts
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標準のreadlinequestion関数を使って、ユーザの入力を受け付ける。readlinequestion関数はユーザの入力が終わるのを待たずに再度呼び出すことができてしまうので無限ループに適さない。そこで、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に必要な引数を与えて実行することができるようになる。

cli.bat
@echo offnodedist\index.js %*

Viewing all articles
Browse latest Browse all 8934

Trending Articles