Help us understand the problem. What is going on with this article?

Windowsでshebangもどき、またはバッチにスクリプトを埋め込む方法

一覧

以下の例はすべて、各言語のインタプリタにパスが通っていることを前提としています。

Ruby

Rubyの例.rb.bat
@ruby -x "%~f0" %*
@exit /b %errorlevel%
#!ruby
## 以下スクリプトの内容 ##
puts "foo"

Perl

Perlの例.pl.bat
@perl -x "%~f0" %*
@exit /b %errorlevel%
#!perl
## 以下スクリプトの内容 ##
print "foo\n";

PHP

PHPの例.php.bat
<?php /*^
:
@php "%~f0" %*
@exit /b %errorlevel%
*/
// 以下スクリプトの内容
declare(strict_types=1);

echo "foo";

Python

Pythonの例.py.bat
@python -x "%~f0" %* & exit /b %errorlevel%
## 以下スクリプトの内容 ##
print("foo")

JavaScript (Node.js)

Nodeの例.js.bat
<!-- : ^
/*
@node "%~f0" %*
@exit /b %errorlevel%
*/
// 以下スクリプトの内容 //
"use strict";
console.log("foo");

JavaScript (Chakra)

Chakraの例.js.bat
<!-- : ^
/*
@cscript /nologo /e:{1b7cd997-e5ff-4932-a7a6-2a9e636da385} "%~f0" %*
@exit /b %errorlevel%
*/
// 以下スクリプトの内容 //
"use strict"
WScript.stdout.writeline("foo");

JScript

JScriptの例.js.bat
@if(0)==(0) echo off
cscript /nologo /E:JScript "%~f0" %*
exit /b %errorlevel%
@end
// 以下スクリプトの内容 //
WScript.stdout.writeline("foo");

VBScript

VBScriptの例.vbs.bat
<!-- :
@cscript /nologo "%~f0?.wsf"
@exit /b %errorlevel%
-->
<job><script language="VBScript">
' 以下スクリプトの内容
Option Explicit
WScript.Echo "VBScript output called by batch"
' 以上スクリプトの内容
</script></job>

Lua

Luaの例.lua.bat
@lua -e "_,_,b=io.input([[%~f0]]):read('*l','*l','*a');assert(loadstring('\n'..b,[[%~f0]]))()"
@exit /b %errorlevel%
-- 以下スクリプトの内容 --
print("foo")

Haskell (GHC)

GHCの例.lhs.bat
@runghc --ghc-arg=-x --ghc-arg=lhs "%~f0" %*
@exit /b %errorlevel%
\begin{code}
-- 以下スクリプトの内容 --
main = print "foo"
\end{code}

Scheme (Gauche)

Gaucheの例.scm.bat
;@gosh -x "%~f0" %*
;@exit /b %errorlevel%
;;; 以下スクリプトの内容 ;;;
(display 'foo)

PowerShell

PowerShellの例.ps1.bat
<# :
@setlocal
@set _args_=%*
@powershell -noprofile "[ScriptBlock]::Create((${%~f0} | out-string)).Invoke(@(iex('&{$args} ' + $env:_args_)))"
@exit /b %errorlevel%
#>
## 以下スクリプトの内容 ##
echo foo

VB.NET

VBScriptの例.vb.bat
@setlocal
@set _args_=%*
@powershell -NoProfile -ExecutionPolicy RemoteSigned -command "Add-Type -Language VisualBasic -TypeDefinition ((cat -encoding utf8 \"%~f0\"|?{$_.ReadCount -gt 4}) -join\"`n\"); iex('&{ [Program]::Main($args) }' + $env:_args_)"
@exit /b %errorlevel%
' 以下スクリプトの内容

Option Explicit
Option Strict
Option Infer
Imports System

Public Module Program
    Sub Main(args As String())
        Console.WriteLine("foo")
    End Sub
End Module

C#

C#の例.cs.bat
@setlocal
@set _args_=%*
@powershell -NoProfile -ExecutionPolicy RemoteSigned -command "Add-Type -TypeDefinition ((cat -encoding utf8 \"%~f0\"|?{$_.ReadCount -gt 4})-join\"`n\"); iex('&{ [Program]::Main($args) }' + $env:_args_)"
@exit /b %errorlevel%
// 以下スクリプトの内容
using System;
public static class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine("foo");
    }
}

解説

rubyやperl製のスクリプトは、パスを通せばコマンドプロンプトで通常のexeツールのように使うことができます。ただし、 hoge.rb のように拡張子を含めて打ちこむ必要があります。これを省略するにはPATHEXTをいじるのが簡単です。

しかし、スクリプトファイルをすでにインタプリタでなくエディタなどに関連付けしている場合はこの方法は使えません。

そこで、スクリプトを標準でPATHEXTに含まれているバッチファイルにラッピングします。つまり、.rb.pl.pyなどの拡張子を.batに変えて無理やり使うということです。

当然、スクリプトの拡張子を.batに変えただけでは動きません。コマンドプロンプトはスクリプトのことは知らず、普通にバッチファイルとして実行するためです。処理系を起動し、埋め込んだスクリプトを実行させる内容のバッチが必要になります。

コマンドプロンプトから見ると、このファイルはバッチファイルの末尾にゴミがあるように見えます。

スクリプト処理系から見ると、このファイルはスクリプトの先頭にゴミがあるように見えます。

それぞれに対して対策して、両方でエラーが起きないようにしたものが冒頭のサンプルたちです。

共通のテクニック

バッチファイルでは、以下のものを利用します。(コマンドプロンプトのhelp call参照)。

  • "%~f0" はシェルでいう$0に相当し、バッチファイル自身のフルパスに展開されます。
  • @ はバッチファイル内で実行したコマンドのエコーバックを抑制します。
  • exit /b %errorlevel% はバッチファイルの実行を終了します。そのまま実行が続くと埋め込んだスクリプト本体までバッチファイルとして実行されるので、それを阻止します。

スクリプト側では、以下のどれかを利用します(楽な順)。

  1. 処理系にある、スクリプト以外の部分を無視するオプションを使う
  2. バッチの冒頭部分をコメントや文字列に包み、エラーにならないようにする
  3. 標準入力からスクリプトを読んで実行する
  4. 普通にファイルを読んで実行する

ruby (タイプ1)

rubyコマンドにバッチファイル自身を渡して呼び出させています。その際、-xオプションによって#!までの記述が読み飛ばされます。

rubyの-xオプション#!の行に当たるまでを無視するので、一行目をバッチとして書くことができます。

perl (タイプ1)

perlコマンドにバッチファイル自身を渡して呼び出させています。その際、-xオプションによって#!までの記述が読み飛ばされます。

rubyと同様です。

また、PerlとRubyは頭だけでなく__END__を使ってスクリプトの終わりを明示できるので、exitの代わりにgoto を使うことで複数のRubyスクリプトを内蔵することができます(使い道はともかく)。

python (タイプ1)

pythonコマンドにバッチファイル自身を渡して呼び出させています。その際、-xオプションによって先頭の一行が読み飛ばされます。

Pythonの-x#!を探さず一行目を飛ばすだけなので、#!を書く必要はありません(書いてもかまいません)。

PHP (タイプ2)

phpコマンドにバッチファイル自身を渡して呼び出させています。

自力で見つけましたが、VBScriptと原理は同様です。<?php /*^\n: いう記述はバッチとして見ると"?phpというファイルをラベル/*の標準入力にリダイレクトする"という意味になります。通常のコマンドならファイルが存在しないというエラーが出るところですが、ラベル定義はバッチで特別扱いされてエラーにならずちょうど無視してくれるようです。

GHC (タイプ2)

ghcコマンドにバッチファイル自身を渡して呼び出させています。

runghcはPOSIXの場合同様 Literate Haskell を使うことでバッチ部分を迂回できます。

GHCiで読む際はghci -x lhs hoge.batのようにすると読み込めます。

Gauche (タイプ2)

goshコマンドにバッチファイル自身を渡して呼び出させています。

Gaucheは#!のみを見るので、バッチファイルの @ を無視してくれません。

追記: しかし、; と組み合わせることで迂回することが出来ます。 これはGaucheの文法では行コメントとして解釈される一方、コマンドプロンプトの文法では(おそらく)空白として解釈されて無害になります。anohanaさんよりコメントで教えていただきました。ありがとうございました。

Lua (タイプ4)

一旦Luaを-eで呼び出し、その中でバッチファイル自身を先頭を覗いて読み込んでから評価しています。

hymkorさんの記事で、Lua5.2のラベル構文とコメント文を組み合わせて埋め込みに成功されました。その後、zetamattaさんがLua5.1に対応し、かつ短い方法を発見されたので、冒頭にはこちらを載せています。

JScript(WSHのJavaScript) (タイプ2)

cscriptコマンドにバッチファイル自身を渡して呼び出させています。

JScript でハマる日々 - m2 を参照。@if文なるものがあり、それを使っています。このテクニックを発見されたのはおそらく ウィンドウズスクリプトプログラマ さんです。こちらもzetamatta さんに教えていただきました。ありがとうございました。

Node.js/ts-node/Chakra (タイプ2)

各エンジン(node/ts-node/cscript)にバッチファイル自身を渡して呼び出させています。

自力で見つけましたが、VBScriptと原理は同様です。<!-- がNode.jsやChakraではコメント扱いになることを利用してどうにかならないか試していたらうまくいきました。<!-- :という記述はバッチとして見ると"!--というファイルを:コマンドの標準入力にリダイレクトする"という意味になります。通常のコマンドならファイルが存在しないというエラーが出るところですが、ラベル定義はバッチで特別扱いされてエラーにならずちょうど無視してくれるようです。

VBScript (タイプ1)

cscriptコマンドにバッチファイル自身を渡して呼び出させています。

Is it possible to embed and execute VBScript within a batch file without using a temporary file? - Stack Overflow でdbenham氏が紹介している、Liviu氏が発見した方法です。

WSFは寛容なXML形式なのでコメントが使えます。Node.js/Chakraと同様に<!--:の組み合わせでなぜかうまくいくようです。

PowerShell/C#/VB.NET (タイプ4)

これらは、バッチファイル自身を読み込んで文字列にしたあと先頭を取り除いて実行する、という方法です。

バッチの引数(%*)を渡すのに環境変数を使っています。これは、Powershellの引数に直接埋め込むとクォートが消えてしまう対策です。

汎用的な方法

shebangに対応する機能のない言語は、バッチファイル内に埋め込めないことが多いです。

その場合は、以下のような方法があります。

ワンライナー

@インタプリタ -e "スクリプトの内容" %*

短いスクリプトなら-eオプションを使う方がコンパクトにすみます。
Ruby, Perl, Python, Gauche, GHCいずれでも使えます。

Goの場合は gore というツールが使えます。

標準入力から読んで実行 (タイプ3)

Perlなど上で挙がった多くのスクリプト処理系は、ファイル名を - とすることで標準入力からスクリプトを読んで実行することができます。-x に比べると $0 が設定されないのが玉に瑕ですが、コメント文とトリッキーに闘わずともすみます。mattnさんに教えていただきました。ありがとうございました。

一時ファイルを使う (タイプ4)

バッチ内に埋め込んだスクリプトを一旦一時ファイルに書き出し、それを実行します。

一時ファイルへの出力が必要ですが、理論上どんな言語にも適用できます。

JScript.NET

(長くなるので別記事に分けます)

Rhino

一行コメントを使った例です。jrunscript -l js -fの部分をcscriptv8nodeに置き換えれば他のJavaScriptエンジンでも動かせます。また、一行コメントのある他の言語(coffeescriptやbashの#、 VBScriptの'など)にも応用できそうです。

コメントでtatesukeさんに教えていただきました。ありがとうございました。元の例

rhinoの例.js.bat
@SET /P X=/* < NUL > "%~dp0_TEMP_%~n0" & TYPE "%~f0" >> "%~dp0_TEMP_%~n0" & jrunscript -l js -f "%~dp0_TEMP_%~n0" %* & DEL "%~dp0_TEMP_%~n0"  & EXIT /B & REM */
// 以下スクリプトの内容

おわりに

そもそもコマンド名補完がないから拡張子を邪魔に感じるわけで、PowerShellなりcygwinなりnyaosなりを使えばすむ話ですが他にshebangできそうな言語があれば教えてください。

snipsnipsnip
宇宙飛行士タイプのケントベック信者 特記のない記事は CC BY 3.0 で扱ってください
http://snipsnipsnip.github.io
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした