ちょっとしたスクリプトをWindows/Linux/macOS兼用で動作させる

ちょっとそういうのが必要になったので

確か Embulk でこんな感じのことをやっていたはず…という記憶を頼りに作ってみた。 とりあえず動くけど、後述のとおり行儀のいい方法ではない&制約事項もあるので、使う場合は要注意。

環境

条件

  • スクリプト内ではNode.jsで複数行のソースコードを実行したい。Node.jsはインストール済みでパスが通っている前提。
  • 実行は1ファイルで完結させたい。実行時引数とかも無し。
  • Windows(バッチファイル)でもLinux/macOSbash/zsh)でも全く同じファイルを使う。

できあがったもの

こんな感じ。

: <<BAT
@echo off

echo ^
console.log('Node.js on Windows'); ^
let a = 1; ^
let b = 2; ^
console.log("a + b = " + (a + b)); | node.exe

exit /b
BAT

node <<-EOS
console.log('Node.js on Linux/macOS')
let a = 1
let b = 2
console.log("a + b = " + (a + b))
EOS

runanywhere.bat 的な名前で保存し、各環境で実行する。 改行コードは LF にする。

実行例

Windowsコマンドプロンプト

C:\work> runanywhere.bat
Node.js on Windows
a + b = 3

Linuxbash), macOSzsh

$ ./runanywhere.bat
Node.js on Linux/macOS
a + b = 3

仕組み

スクリプトの大枠は下記のとおり。

: <<BAT
バッチファイル(Windows)として動作するコード
exit /b
BAT

シェルスクリプト(Linux/macOS)として動作するコード

<<BATBAT にバッチファイルのコードを書いてあり、その後ろにシェルスクリプトのコードが続くという構成になっている。

シェルスクリプトLinux/macOS)から見ると <<BATBAT の部分はコードはヒアドキュメントとして扱われ、: という何もしないコマンドに渡される。つまり、ただの文字列を何もしないコマンドに渡しているだけなので何も起こらず、その次に続くシェルスクリプトのコードが普通に実行される。

バッチファイル(Windows)から見ると :GOTO コマンドのラベルであり、それだけでは特に何も処理は行われない。また、 : <<BAT のような表記も構文上は問題が無い。 このため、その次に続くバッチファイルのコードが普通に実行されて、最後に exit /b で終了する。それ以降のコードは実行されないので、後ろにシェルスクリプトのコードがあっても問題は無い。

あとは各環境でNode.jsにソースコードを渡して実行する処理を書けばOK。

シェルスクリプトの場合は普通にヒアドキュメントが書けるので、それをそのままNode.jsに流し込む。

バッチファイルの場合はヒアドキュメントが無いので、1行にまとめて標準入力経由でNode.jsにコードを渡している。 今回は読みやすさを考慮して ^ で改行しているが、実態は下記のようなコードと同じ。

echo console.log('Node.js on Windows'); let a = 1; let b = 2; console.log("a + b = " + (a + b)); | node.exe

制約事項

改行コードは LF にする必要がある。でないとLinux/macOSでエラーになる。バッチファイル的には良くないが、だいたい動くので許容する。

# CR/LFにすると…
$ ./runanywhere.bat
: not foundre.bat: 12:
Node.js on Linux/macOS
a + b = 3

ただし、日本語(マルチバイト文字)は使えない。バッチファイルで改行コードが LF の場合、日本語が含まれるとコマンドの一部が削られてしまい、エラーになる。 ちなみにこの挙動は マルチバイト文字を含まない場合にも発生することがあるらしい ので注意。

# こんな感じでコメントに日本語を入れてみると…

: <<BAT
@echo off

rem 日本語

echo ^
...
# 実行時にコマンドが削られてエラーになる
C:\work>runanywhere.bat

C:\work>ho off
'ho' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

C:\work>m 日本語
'm' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
...

バッチファイルの部分を CR/LFシェルスクリプトの部分を LF にすれば解決するはずだけど、手作業で作るスクリプトでそれを維持し続けるのはさすがに厳しいので、LF に統一というのが落としどころだと思う。

まとめ

とりあえず動くので使えるんだけど、改行コードが LF のバッチファイルってことであまり良くないので、多用は禁物。