Node.jsをシェバンする


はじめに

いちいちnodeにファイルの引数やnodeの引数を宛がうのが面倒になってくるときがあるので、シェバン化して直接Node.jsのプログラムを実行してやりたいと思ったときにやったこと。

そもそもシェバンとは

シェバン(shebang)というのは、実行権限を与えたスクリプトファイルをUNIXベースのOSで自己実行できるようにするための仕組み。
その昔はCGIなんかで

#!/usr/local/bin/perl

というのを最初に入れればいい魔法のワードみたいな話で教わった人は相当数いるはず。
今だとrubyやらpythonやらでこういうのを使うケースが多いのではないか。
だいたいローカル環境で実行するつもりのプログラムならば、上のように言語の実行先を指定してやるだけで、この場合だとこの実行ファイルをperlにかけて動かしてくれる。

だが、すべての環境でperlの置いてある場所が/usr/local/binとは限らず、もしかしたら/usr/binかもしれないときがある。そういう場合に役立つのがenvに探してもらう記述。

#!/usr/bin/env perl

これはより広範囲の環境に対して探してもらうことのできる記述であるが、envでプログラムを探すことができるのはPATHが通っていることが前提であり、自分でビルドしたverのnodeにPATHを通していなければそちらを参照してもらえない。また、UNIXはすべてLinuxとは限らず、例えばFreeBSDなんかでenvコマンドが有効でない場合、上の記述でperlを見つけられないことがある。
また、実行時に言語プログラムへ引数を指定したりしたい場合、この記述では正しく解釈されない可能性がある。
そのため、シェバンの冒頭部分に対象言語の解釈を邪魔しない形でシェルスクリプトを書いて、コードを実行してもらうようにすることで、ほぼすべての環境に対応したシェバンにすることができる。

rubyの場合はとても簡単にかける。

rubysample.rb
#! /bin/sh
exec ruby -S -x "$0" "$@"

もしPATHが通っていない場合であれば、rubyをフルパス記述(/home/myid/bin/rubyなど)すればよい。これは下記すべてのシェバン記述で共通。

perlだとこんな感じだそうだ。

perlsample.pl
#! /bin/sh
eval '(exit $?0)' && eval 'PERL_BADLANG=x;PATH="$PATH:.";export PERL_BADLANG\
;exec perl -x -S -- "$0" ${1+"$@"};#'if 0;eval 'setenv PERL_BADLANG x\
;setenv PATH "$PATH":.;exec perl -x -S -- "$0" $argv:q;#'.q
#!perl -w
+push@INC,'.';$0=~/(.*)/s;do(index($1,"/")<0?"./$1":$1);die$@if$@__END__+if 0
;#Don't touch/remove lines 1--7: http://www.inf.bme.hu/~pts/Magic.Perl.Header

Pythonでやる場合は以下の通り。

pythonsample.py
#!/bin/sh
""":" .

exec python "$0" "$@"
"""

__doc__ = """
The above defines the script's __doc__ string. You can fix it by like this."""

参考:
http://shyouhei.tumblr.com/post/587310084/usrbinenv
https://qiita.com/tueda/items/369215b99316b2e9f3d8

Nodeでシェバン

さて、上記の点を踏まえた、#!/usr/bin/env nodeではない汎用的なNodeのシェバンを考える。

実際のところ、Nodeは1行目のシェバンを読み飛ばして2行目以降から見てくれるが、例えばrubyのようなお手軽記述をすると2行目以降を解釈できなくてエラーする。

nodesample.js
#!/bin/sh
exec node "$0"
//これだと2行目のexecをnodeが解釈できない

この問題を解決するのが実行部分をjavascript風にコメントアウトすることだが、そうするとシェルの解釈が2行目はディレクトリです、と言って実行してくれない。

nodesample2.js
#!/bin/sh
/*
exec node "$0"
*/
//2行目/binはディレクトリです、といって実行できない。

つまりどうすればいいかというと、/*/shとしてシェルプログラムを動かす。/*/sh=shが存在する場所を無作為に指定するワイルドカードのため/bin/shと等価になる。
このシェバンは/bin/shを動かすことで始まる。つまり、2行目の/bin/shは絶対に存在し、必ず実行可能であると断定できる。
あとはこの/bin/shにエラー吐くか無難な引数を宛がって、その標準出力を捨ててしまえばいい。

nodesample3.js
#!/bin/sh
/*/sh --version > /dev/null 2>&1
exec node "$0" "$@"
*/
// これでnodeをシェバン稼働できる。

この記述の場合、node本体への引数(よく使う--inspactorや、--experimental-modulesといった試験的な機能などの引数)はあらかじめコード内に記述し、nodeコマンドの引数として有効な形になるようにする。
これは、シェバン対象のスクリプトに対しては、実行コードの後ろにしか引数を入れられないため。
簡易ではあるが、これでnodeを自己実行できるシェバンを組むことができた。

npm script

もっとも、Nodeには標準でnpmが搭載されており、nodeの動く環境ならnpm remove -g npmなどのあほなことをしていないかぎりnpmが存在するので、このnpm scriptでこのシェバン相当の記述をしてしまえばもっと拡張的な実行を行うことができる。そのため、果たしてシェバン対応の必要性があるのかと言われると微妙なところではあるのだが……
実はそこに大きな落とし穴があった。

npmはシェバンで動いている

npmはnodeをかまさなくても動作するNode様式のJavascriptプログラムであるが、そのコマンド実行を行うために#/usr/bin/env nodeのシェバンを使っている。
例えばディストリビューションのパッケージからであったり、nvmやnodebrewなどのバージョン管理ツールでNodeをインストールしたのであれば、きちんとバージョン管理をしてenvがnodeを探り当ててくれるのだが、Prebinaryをtar解凍して使用していたり、ソースコードビルドなどでローカルインストールしていると、たとえpathを通していたとしても、他のnodeがある場合には、pathの優先順位が正しくないと、目的のnodeでnpmを動かすことができない。

ソースビルドしたnodeが/home/myid/nodeにインストールされているが、そのパスは通っていない。かつnvm使用でv14.7.0がonになっている出力。

$ /home/myid/node/bin/node -v
15.0.0-pre
$ /home/myid/node/bin/npm version
{
  npm: '6.14.7',
  ares: '1.16.0',
  brotli: '1.0.7',
  cldr: '37.0',
  icu: '67.1',
  llhttp: '2.0.4',
  modules: '83',
  napi: '6',
  nghttp2: '1.41.0',
  node: '14.7.0',
  openssl: '1.1.1g',
  tz: '2020a',
  unicode: '13.0',
  uv: '1.38.1',
  v8: '8.4.371.19-node.12',
  zlib: '1.2.11'
}

npmについての応急的な対応方法

envに指定される予定のPATHの値を実行毎に塗り替えてnpmを実行することで、いったん解決することはできる。これなら既存の環境変数を破壊せずにビルドしたnpdeを試験利用できる。
もちろん本運用するならPATH値自体をexportで書き換えてPATHを通す必要がある。

$ PATH=/home/myid/node/bin npm version
(node:15983) [DEP0139] DeprecationWarning: Calling process.umask() with no arguments is prone to race conditions and is a potential security vulnerability.
(Use `node --trace-deprecation ...` to show where the warning was created)
{
  npm: '6.14.7',
  ares: '1.16.0',
  brotli: '1.0.7',
  cldr: '37.0',
  icu: '67.1',
  llhttp: '2.0.4',
  modules: '86',
  napi: '6',
  nghttp2: '1.41.0',
  node: '15.0.0-pre',
  openssl: '1.1.1g',
  tz: '2020a',
  unicode: '13.0',
  uv: '1.38.1',
  v8: '8.4.371.19-node.13',
  zlib: '1.2.11'
}

これはnpmによってglobal領域にインストールされたnodeプログラムも同様。実行結果で、nodeの値が変更されていることが伺える。
新バージョンでDeprecateされている関数呼び出しをnpmでしているので警告が出ているがご愛嬌。