1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-04

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';

const array: string[] = [];
const program = createCommand();

// コマンドの定義
program.command( 'push [value]' )
.action( ( value: string ) => {
  array.push( value );
  console.log( `  Pushed: ${value}` );
} );

program.command( 'pop' )
.action( () => {
  const popped = 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行ずつ読み込む
const rl = createInterface( {
  input: process.stdin,
  output: process.stdout
} );

function asyncQuestion( message: string ): Promise<string> {
  return new Promise( ( resolve ) => {
    rl.question( message, ( line: string ) => resolve( line ) );
  } );
}

// [3] 1行ずつ読み込んだコマンドを実行する
async function loop() {
  while(1) {
    const line = await asyncQuestion( 'sample> ' );

    // [3-1] 入力された文字列をコマンドライン引数風に分解する。
    const argv = parseArgsStringToArgv( line );

    // [3-2] コマンドを実行する
    try {
      await program.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 off
node dist\index.js %*
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?