node.js 用「command line argument parser」

本来なら2秒で答えが出るような内容なので、書くまでもないかとも思ったのだけれども。

ちょっと思うこともあったので一応。

「node.js 用「command line argument parser」」という限定はこれは、「node.js のためのものなのだから無論 javascript であり、だけれどもブラウザ組み込み言語としての本来の javascript とは無縁」ということ。ゆえ、調べる前に思うことは「標準化されていないおそれが高い」ということだけれど、そもそも「ブラウザから開放されてやるぜ」というモチベーションから始まっている node.js にとって、これはきっと「一番最初に組み込み機能候補になったであろうもの」なのじゃないかと思うわけね、だから「ひょっとして node.js をインストールしただけで使える標準添付ライブラリだったりするや否や」という期待もよぎるわけだ。けどやっぱりそんなことはなく、node.js が組み込みで提供するのは process.argv という「いつものやつ」のみ。

ゆえ、まぁこういうのって「保守しづれーよな」となるわけな:

 1 /*
 2  * Usage: node thisscript.js url out.png viewportwidth viewportheight scalefactor
 3  * ex)
 4  *    node thisscript.js http://example.com example.png 1280 720 1.2
 5  */
 6 'use strict';
 7 
 8 /*
 9  * puppeteer-core doesn't automatically download Chromium when installed.
10  * see:
11  *   https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteer-vs-puppeteer-core
12  */
13 const puppeteer = require('puppeteer-core');
14 
15 /*
16  * in the case of "puppeteer-core",
17  * we need to call puppeteer.connect([options]) or puppeteer.launch([options])
18  * with an explicit executablePath option.
19  */
20 const executablePath = "c:/Program Files (x86)/Google/Chrome/Application/chrome.exe";
21 
22 (async() => {
23     const browser = await puppeteer.launch(
24         {
25             headless: true,  /* Defaults to true unless the devtools option is true. */
26             executablePath: executablePath,
27 
28             /* Slows down Puppeteer operations by the specified amount of milliseconds. */
29             /*slowMo: 1000,*/
30 
31             defaultViewport: {
32                 width: parseInt(process.argv[4]),
33                 height: parseInt(process.argv[5]),
34                 deviceScaleFactor: parseFloat(process.argv[6]), /* --force-device-scale-factor */
35             },
36         });
37     const page = await browser.newPage();
38     await page.goto(
39         process.argv[2]  /* [node, myscript.js, ...] */
40     );
41     /*
42      * If you want control equivalent to the zoom level control that you can do with
43      * Ctrl-Plus/Ctrl-Minus on a non-headless Chrome instance, unfortunately you can't
44      * do that with the launch options in Chrome or Puppeteer launch. Instead, we need
45      * an idea to achieve this by directly giving "style", the DevTools API can eventually
46      * be directly involved in browser rendering, so it is relatively easy to achieve with
47      * Puppeteer like this:
48      *
49      *     await page.addScriptTag({url: 'https://code.jquery.com/jquery-3.2.1.min.js'});
50      *     await page.evaluate(({}) => {jQuery('body').css('zoom', '1.7');},{});
51      *
52      * or:
53      *     // you_style.css: external stylesheet containing "zoom" style
54      *     await page.addStyleTag({path: 'you_style.css'});
55      *     //await page.addStyleTag({url: 'http://yourdomain/css/your_style.css'});
56      *
57      */
58     await page.screenshot({
59         path: process.argv[3],
60         fullPage: true
61     });
62     await browser.close();
63 })();

そもそも数値でのインデキシングにてスクリプトを記述するのは「読みづらいし書きづらい」のみならず、ユーザインターフェイスの変更を簡単に出来なくなってしまう。特に既存のコマンドライン引数の途中に新たな引数を追加するのは、これは地獄だ。ハイライトした「process.argv」を使っている部分全部を書き換える必要も生じうる。(process.argv を直接使うのではなく、いったん一時変数に移送するようにすれば少しは保守しやすくはなるけれど、「途中に新たな引数を追加」時に必要な改造の本質はいっしょ。)

そう、今まさにワタシが直面していたのは、コメントアウトされてるズームレベル(css)の操作部分も、コマンドラインから制御出来るようにしたい、ということなわけ。process.argv がさらに数個増えるだろう、てことだし、ここまで数が増えてくると、いい加減スクリプトを使う側からも使いにくいと感じ始める頃で、つまりは省略を許したいとか、ある程度引数の順番を柔軟にしたいなぁと思うわけ。ゆえ、「すわ、getopt とか argparse ねば」というタイミングてことよ。

ところが。「2秒」で duckduckgo がトップで引っ掛けてきた How to parse command line arguments はこれは、node.js 公式による説明なので、まぁ確かに検索最上位に来るのは理解は出来る。けれども、「そのせいで」悶々とするハメになった。このサイトが「Luckily, there are many third party modules that makes all of this trivial – one of which is …」として唯一紹介してくれたのが「yargs」…。

getopt だぜ、やったね」のつもりならばこれでも十分に助けにはなるとは思うのだが、けれども「所詮は getopt」なのだ、こやつ。伝統的な getopt を知っている人にとっては、yargs はとても「中途半端」なものに映ると思う。なにせこやつ、「getopt にはない command の概念がある」から。そしてこの command が曲者。

node.js 公式による誘導さえなければ、ほんとに2秒で「argparse」に辿り着けるんだわ。duckduckgo 検索でも1ページ目には出てる。この argparse と「伝統的な getopt と yargs 流儀」の決定的な違いは「オプションでないポジショナル引数のほうも面倒をみてくれる」こと、である。getopt は結局は「オプション(「ハイフンで始まる引数」)引数とそうでない引数とに二分し、オプションの方だけ面倒みる」ためのもの。yargs はそれに加えて「command」を扱えるようにしているが、この「拡張」がどれだけ中途半端(言い換えるなら「アホ」)なものなのかは、実際に使おうとしてみれば、絶対に理解できるはずである。(日本語で説明しようかと思ったがなかなか難儀なので、嘘だと思うなら example を眺めて「これは違う」と理解してみてくれ。)

argparse は名前から想像出来るかもしれないが python の argparse モジュールを移植したものなので、それとほぼ同じノリで使える:

 1 /*
 2  * Usage: node thisscript.js url out.png -w=viewportwidth -h=viewportheight -f=scalefactor
 3  * ex)
 4  *    node thisscript.js http://example.com example.png -W=1280 -H=720 -f=1.2
 5  *
 6  * Require: argparse, util, puppeteer-core
 7  */
 8 'use strict';
 9 
10 const { ArgumentParser } = require('argparse');
11 const ap = new ArgumentParser();
12 ap.add_argument("url")
13 ap.add_argument("out")
14 ap.add_argument("--viewportwidth", "-W", {type: 'int', default: 1280})
15 ap.add_argument("--viewportheight", "-H", {type: 'int', default: 720})
16 ap.add_argument("--scalefactor", "-f", {type: 'float', default: 1.2})
17 const args = ap.parse_args();
18 
19 /*
20  * puppeteer-core doesn't automatically download Chromium when installed.
21  * see:
22  *   https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteer-vs-puppeteer-core
23  */
24 const puppeteer = require('puppeteer-core');
25 
26 /*
27  * in the case of "puppeteer-core",
28  * we need to call puppeteer.connect([options]) or puppeteer.launch([options])
29  * with an explicit executablePath option.
30  */
31 const executablePath = "c:/Program Files (x86)/Google/Chrome/Application/chrome.exe";
32 
33 (async() => {
34     const browser = await puppeteer.launch(
35         {
36             headless: true,  /* Defaults to true unless the devtools option is true. */
37             executablePath: executablePath,
38 
39             /* Slows down Puppeteer operations by the specified amount of milliseconds. */
40             /*slowMo: 1000,*/
41 
42             defaultViewport: {
43                 width: args["viewportwidth"],
44                 height: args["viewportheight"],
45                 deviceScaleFactor: args["scalefactor"], /* --force-device-scale-factor */
46             },
47         });
48     const page = await browser.newPage();
49     await page.goto(
50         args["url"]
51     );
52     /*
53      * If you want control equivalent to the zoom level control that you can do with
54      * Ctrl-Plus/Ctrl-Minus on a non-headless Chrome instance, unfortunately you can't
55      * do that with the launch options in Chrome or Puppeteer launch. Instead, we need
56      * an idea to achieve this by directly giving "style", the DevTools API can eventually
57      * be directly involved in browser rendering, so it is relatively easy to achieve with
58      * Puppeteer like this:
59      *
60      *     await page.addScriptTag({url: 'https://code.jquery.com/jquery-3.2.1.min.js'});
61      *     await page.evaluate(({}) => {jQuery('body').css('zoom', '1.7');},{});
62      *
63      * or:
64      *     // you_style.css: external stylesheet containing "zoom" style
65      *     await page.addStyleTag({path: 'you_style.css'});
66      *     //await page.addStyleTag({url: 'http://yourdomain/css/your_style.css'});
67      *
68      */
69 
70     await page.screenshot({
71         path: args["out"],
72         fullPage: true
73     });
74     await browser.close();
75 })();

yargs に対して言った文句に相当する部分は、このケースの場合は「url」と「out」で、もしも yargs を使っていたならばこれは「_[0]と_[1]」となっていたところ。まぁそういうわけで、公式が勧めていたからと yargs を使うことに拘らずに、argparse (などほかのもの)を使うがよかろう、てハナシ。(繰り返すけれど「getopt 相当」という意味でなら yargs でも十分、「command」を無視しさえすれば。)