【Node.js Tips】定番のCLI用パッケージyargsの使い方

JavaScript

Node.jsでMicrosoft Azure認証のサンプルプログラムを見ていたところ、yargsというコマンドラインオプションを処理してくれる便利なパッケージを使っていたので、私も使えるようになりたくて調査してみた。
ちょっとしたCLIツールも作ってみたいと思っていたので、現時点で最低限これだけ知っていれば問題なさそうというところまでのサンプルを作ってみたのでシェアする。
どっかのサンプルで閏年の判定スクリプトがあったので、そのアイデアを拝借し、コードはゼロから作ってみた。

コマンドライン引数の受け渡しについて確認

コマンドライン引数の処理は、どのようなプログラミング言語でもコマンドラインインタフェースのための基本的な処理の一つ。

Node.jsではshellによって受け取ったコマンドライン引数は”process.argv”という変数に格納される。
試しに、これをオプションなしで実行した時の”process.argv”をコンソールに出力してみると、process.argvの先頭には、常にnodeのパスと実行スクリプトファイルパスの2つの文字列が含まれることがわかる。

console.log(process.argv);
$> node ./index.js
[
  '/Users/masa/.nvm/versions/node/v17.2.0/bin/node',
  '/Users/masa/work/misc/index.js'
]

この2つの引数のあとに、実際の引数が渡される。
再度、今度はいくつかオプションを指定してもう一度実行してみる。

$> node ./index.js --a=test1 --b test2 -c test3 test4
[
  '/Users/masa/.nvm/versions/node/v17.2.0/bin/node',
  '/Users/masa/work/misc/index.js',
  '--a=test1',
  '--b',
  'test2',
  '-c',
  'test3',
  'test4'
]

コマンドライン引数は、スペースで区切られた配列としてprocess.argvに格納されていることがわかる。

ここまでは、Java、C#、C、C++等、この辺りの言語どれもコマンドライン引数の扱いについては似ておりデファクトっぽくてわかりやすい。
ただ、これらオプションをゼロから処理するプログラムを書こうとすると、それだけで結構なボリュームのコードを書くことになる。

npmには、これらのオプションを期待値の通りに解析し、プログラム側で使いやすいオブジェクトに変換してくれるyargsというパッケージがある。小さな1本のサンプルプログラムでyargsの最低限知っておくべき使い方をまとめてみた。

実用性があり、かつ最低限知っているべきyargsの使い方

yargsについては以下を参照あれ。

yargs
the yargs.js.org website.
yargs
yargs the modern, pirate-themed, successor to optimist.. Latest version: 17.7.2, last published: 10 months ago. Start us...

yargsは少しトリッキーなところがあり、いくつかのAPIを組み合わせてコマンドライン解析を実行するとエラーになることがあったし、APIがかなり充実しており、何をどう使えば良いのか迷子になる。
「まずはこれだけ使えるようになればOK」と思えるところまで理解したので、それらをピックアップして1つのサンプルスクリプトで確認する。

サンプルスクリプト

const yargs = require('yargs');

/**
 * Format a year to short (which means 2 digits number) or long (which means 4 digits number) and return it as a string.
 * If the year less than 2 or 4 digits, it is padded zero to head of string.
 * 
 * @param {string | number} year - a year that is formatted.
 * @param {string} format - format name that is string either "short" or "long".
 * @returns {string} formatted year as string like a "0999" or "09"
 * @throws {Error} If these arguments are invalid values, throw Error object within some detail messages.
 * 
 * @example
 * const shortYear = getFormattedYear(0001, short);
 * // shortYear = "01"
 * const longYear = getFormattedYear(0001, long);
 * // longYear = "0001"
 */
const getFormattedYear = (year, format) => {

    if (typeof aYear === undefined) {
        throw new Error('aYear is required.');
    }
    if (!Number.isInteger(aYear)) {
        throw new Error(`aYear [${aYear}] is not number.`);
    }
    if (typeof format === undefined) {
        throw new Error('format is required');
    }
    const validForms = ['short', 'long'];
    if (!validForms.includes(format)) {
        throw new Error(`format [${format}] is invalid.`);
    }

    const paddedYear = aYear.toString().padStart(4, '0');
    return (format === 'short') ? paddedYear.substr(2) : paddedYear;
}

const argv = yargs
    .locale('en')
    .usage('$0 checks whether the year is a leap year or not.')
    .option('year', {
        description: 'a year that check for.',
        alias: 'y', type: 'number', demandOption: true
    })
    .option('format', {
        description: 'format of year.',
        alias: 'f', choices: ['short','long'], default: 'long'
    })
    .alias('h', 'help')
    .alias('v', 'version')
    .example('$0 -y 2022 -f short', 'Show message such as "22 is NOT a Leap Year". "22" is formatted year as you specified by -f parameter, and tell whether the year is leap year or not.')
    .epilog('Copyright 2022 Masa all Rights Reserved.')
    .argv;

const formattedYear = getFormattedYear(argv.year, argv.format);
const year = argv.year;
if ((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)) {
    console.log(`${formattedYear} is a Leap Year`);
}
else {
    console.log(`${formattedYear} is NOT a Leap Year`);
}

console.log(argv);

※JSDocsも使いたかったので少し書いてみたのだが、JSDocsは別の記事で少し触れたいと思うので今回は割愛。

Use JSDoc: Index
Official documentation for JSDoc.

ヘルプドキュメントを表示させてコードと対比させていく。

$> node ./index.js --help
index.js checks whether the year is a leap year or not.
Options:
  -y, --year     a year check for.                           [number] [required]
  -f, --format   format of year.    [choices: "short", "long"] [default: "long"]
  -h, --help     Show usage instructions.                              [boolean]
  -v, --version  Show version number                                   [boolean]
Examples:
  index.js -y 2022 -f short  Show message such as "22 is NOT a Leap Year". "22"
                             is formatted year as you specified by -f parameter,
                             and tell whether the year is leap year or not.
Copyright 2022 Masa all Rights Reserved.

最低限必要なコマンドライン解析処理の書き方

yargsによるコマンドライン引数の解析処理はサンプルスクリプトの以下の部分にまとめられている。

const argv = yargs
    .locale('en')
    .usage('$0 checks whether the year is a leap year or not.')
    .option('year', {
        description: 'a year that check for.',
        alias: 'y', type: 'number', demandOption: true
    })
    .option('format', {
        description: 'format of year.',
        alias: 'f', choices: ['short','long'], default: 'long'
    })
    .alias('h', 'help')
    .alias('v', 'version')
    .example('$0 -y 2022 -f short', 'Show message such as "22 is NOT a Leap Year". "22" is formatted year as you specified by -f parameter, and tell whether the year is leap year or not.')
    .epilog('Copyright 2022 Masa all Rights Reserved.')
    .argv;

locale関数

コマンドラインのヘルプメッセージの表示言語を指定する。
私は英語で表示したかったので”en”を指定した。
指定しない場合はデフォルトのロケールが使用されるようで、おそらく環境変数のLangを参照しているのだと思う、私が指定した英文以外のヘルプ文字の一部が日本語で表示された。
日本語でも良いのだが、個人的にはコンソールやログなどの出力系処理には日本語は使用しない。理由は文字化けが発生すると出力した情報が全く役に立たないから。
コンピューターというのはいつ何時にどのような理由であっても文字化けが発生する可能性があり、その時に出力した情報は死んだ情報として復活しない。
システム間のインタフェース仕様として文字コードが確定しているのであればそれに従うが、プログラムが環境に依存する場合、文字化け発生の可能性が高い言語は使用したくないというのが個人的な理由である。
説明が長くなったが、私は上記の理由により”en”を指定。

usage関数

コマンドの使い方、引数の指定方法について概説を設定する。
以上。

option関数

このスクリプトは2つのオプションを取るので、option関数も2回定義している。

1つめが”year”というオプションで処理対象の年を指定する数値型”type: number”のオプション。
“alias: ‘y'”を定義することで”-y”という省略形でも指定できるようにしている。
このオプションを指定しないとそもそもスクリプトの意味がないので”demandOption: true”を指定して必須としている。

2つめが”format”というオプションで処理対象年の出力フォーマットを選択するオプション。
選択肢は”choices: [‘short’,’long’]”という文字列配列で指定している。
“alias: ‘f””を定義することで”-f”という省略形でも指定できるようにしている。
このオプションは任意(”demandOption”指定なし)にしている。
その代わりこのオプションが指定されなかった場合はデフォルトで”long”が設定されるように”default: ‘long'”を指定している。

alias関数

2つのオプションに対してalias関数を指定している。
1つめは”–help”オプションに対する省略形”-h”を定義。
2つめは”–version”オプションに対する省略形”-v”を定義。
どちらもoption関数で定義していないyargsのデフォルトオプションだが、デフォルトでは省略形が指定されていないのでここで指定している。
option関数で省略形を省いて、省略形の定義にはalias関数を使用するというポリシーでも良いのだが、個人的には、1つの関心事(今回の関心事は「オプション定義」)は一箇所(局所化)にした方が後々プログラムの見通しが良いという信念があり、それに準じて今回はそのように定義した。

example関数

このスクリプトを実行する時のオプション指定サンプルを定義し、その内容について説明を加えている。
以上。

epilog関数

ヘルプメッセージの最後を締めくくるメッセージを定義する。
yargsのサンプルでCopyrightを定義していたので、私もCopyrightを定義してみた。

まとめ

yargsのAPI Docにはもっと多くのAPIが存在しているが、今回のサンプルスクリプトではかなり限られたAPIしか使っていない。だが現時点ではこれだけ使えるようになれば、一応本格的な単一コマンドのオプション処理とヘルプメッセージの表示ができるプログラムが作れると思う。

Azure CLIの”az”コマンドのように、複数のコマンドが指定できる巨大なプログラム群が必要となる場合にはyargsの”Command”のようなAPIも使えるようにならなければいけない。
だがその頃には、yargsの知見も増えて、APIドキュメントが今よりもすんなり理解できるようになっていることだろう。

今回初めてyargsというパッケージを使ってコマンドラインオプションの処理やヘルプメッセージを作成した。
最初はかなり取っ付きにくいように感じたが、コマンドライン系の処理は他の言語でもほとんど同じような処理であり、いづれかの言語でプログラムした経験がある人であればすんなり理解できると思う。
ここで定義したyargsの利用方法は1例であり、yargsのAPI Docをみると、他にも簡単な使い方から、より複雑なコマンドラインオプションの処理まで対応できる使い方まで掲載しているので、自分がやりたい方法が決まったらそこからどのような使い方ができるのかを適宜調べつつ使い方の幅を広げていくと良いと思う。

参考URL:Node.jsでのコマンドライン引数処理パッケージ

yargs
the yargs.js.org website.
yargs
yargs the modern, pirate-themed, successor to optimist.. Latest version: 17.7.2, last published: 10 months ago. Start us...
https://nodejs.org/en/knowledge/command-line/how-to-parse-command-line-arguments/

コメント

タイトルとURLをコピーしました