package.json を避けて Node.js スクリプトを起動する Bash スクリプト
ちょっと何言ってるのか分からないタイトルだけど、特殊な状況で変なことをやろうとしている記事。
以下のように、構文エラーとなる package.json
が存在するディレクトリ配下で、その package.json
に依存しない Node.js スクリプトを実行したい、というのが今回の状況。
# 構文エラーとなる `package.json` が存在する
$ cat ./package.json
{
"name": example,
// この package.json には構文エラーになる記述が含まれています
}
# Node.js スクリプトは何も `require()` していない、`package.json` を必要としない内容
$ cat ./test.js
#!/usr/bin/env node
console.log('Hello World');
# 実行しようとするとエラーになる
$ node ./test.js
internal/modules/cjs/loader.js:285
throw e;
^
SyntaxError: Error parsing /home/neo/test-directory/package.json: Unexpected token e in JSON at position 12
at parse (<anonymous>)
at readPackage (internal/modules/cjs/loader.js:272:20)
at readPackageScope (internal/modules/cjs/loader.js:297:19)
at shouldUseESMLoader (internal/modules/run_main.js:42:15)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:70:24)
at internal/main/run_main_module.js:17:47 {
path: '/home/neo/test-directory/package.json'
}
このように、参照しなくて良い package.json
なのだが、どうしてもコレを先にロードしようとして JSON パースできずに異常終了してしまう。
package.json
をリネームしたり、test.js
から見て子ディレクトリにでも移しておけば問題は起こらなくなるのだが、今回はこのような package.json
と test.js
が同ディレクトリにある状態で、何とか package.json
の存在を無視して test.js
を動かす方法はないかと考えてみた。
こういう時は、以前 Windows バッチでやったような、「あるシェルスクリプトの中に別言語のコードを埋め込んで実行する」方法を応用して考えてみる。
- 過去記事 : Windows バッチに JScript・VBScript・Oracle SQL スクリプトを混在させてバッチ処理の中で実行する
- 過去記事 : Bash 上で直接実行できる Windows バッチファイルを作る
- 過去記事 : Windows バッチファイルに JScript を混ぜ込む他のやり方
今回は MacOS や WSL (Linux) 上で動けば良かったので、自身を Node.js スクリプトとして動かす Bash スクリプトを書いてやることにした。コード全量は以下のとおり。
bash-to-node.js
- shell-scripts/bash-to-node.js at master · Neos21/shell-scripts … GitHub リポジトリに同様のコードを置いている
#!/usr/bin/env bash
# Bash ココカラ
temp_file_path="$(mktemp)---$(basename $0)"
start_line_number="$(awk '/^#!\/usr\/bin\/env node$/ { print FNR }' "$0")"
tail --lines "+${start_line_number}" "$0" > "${temp_file_path}"
node "${temp_file_path}"
rm -f "${temp_file_path}"
exit 0
# Bash ココマデ
#!/usr/bin/env node
// ココカラ Node.js スクリプトを自由に書いて OK
console.log(['Hello World', process.cwd(), __dirname, __filename, 'End'].join('\n'));
拡張子は .js
としているが、1行目から9行目までは Bash スクリプトであり、1行目の Shebang は Bash なので、$ ./bash-to-node.js
と実行するとまずは Bash スクリプトとして実行される。
Node.js スクリプトは11行目の #!/usr/bin/env node
という Shebang 以降であり、この下には任意の Node.js スクリプトを書けば良い。require()
できるのは Node.js の組み込みモジュールのみである。
node
コマンドで JS ファイルを実行すると、そのファイルがあるディレクトリの package.json
が先に読み込まれてしまうので、1~9行目の Bash スクリプト自身で、自身の Node.js コード部分のみを Temp ディレクトリ配下にコピーしてから実行している。
もう少し Bash 部分のコードを細かく見てみよう。
#!/usr/bin/env bash
# Bash ココカラ
# `mktemp` コマンドを使い、一時ディレクトリ配下にファイルを安全に作成する
temp_file_path="$(mktemp)---$(basename $0)"
# `awk` を駆使して自ファイル内から `#!/usr/bin/env node` という Shebang の行を探し出す
start_line_number="$(awk '/^#!\/usr\/bin\/env node$/ { print FNR }' "$0")"
# `tail` で Shebang の行以降を出力し、一時ファイルに書き込む
tail --lines "+${start_line_number}" "$0" > "${temp_file_path}"
# `node` コマンドで一時ファイルを実行する・一時ディレクトリの方には `package.json` が存在しないので上手く動くというワケ
node "${temp_file_path}"
# `node` コマンドの実行後、一時ファイルを削除する
rm -f "${temp_file_path}"
# Bash としての処理を終了し、以降の Node.js スクリプト部分が実行されないようにする
exit 0
# Bash ココマデ
こういう仕組み。mktemp
コマンドと、tail
の --lines +11
という +
を使った指定方法がミソである。
mktemp
コマンドで一時ディレクトリ配下にコピーを作ってから実行しているので、__dirname
と __filename
のパスが一時ディレクトリになることに留意。process.cwd()
ならカレントディレクトリがズレずに取得できるので、カレントディレクトリを参照する処理があるならコチラを使うと良いかと。
$ pwd
/home/neo/test-directory
$ ls
bash-to-node.js* package.json
# 実行時の一例
$ ./bash-to-node.js
process.cwd() : /home/neo/test-directory
__dirname : /tmp
__filename : /tmp/tmp.pDIlNEmpgl---bash-to-node.js
結局のところ、このファイルは最初に Bash として実行され、11行目以降の処理はどんなプログラミング言語であっても良いので、他の言語にも転用できる。…まぁ、こんなことやる必要がないだろうが。w