89
78

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2013-07-30

一覧

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

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");

J

J言語の例.ijs.bat
: (0 : 0) 
@jconsole "%~f0" %*
@exit /b
)
NB. 以下スクリプトの内容
echo 'foo' ; ARGV

NB. J言語はスクリプト実行後対話モードに入ってしまう。バッチとして使うには明示的に終了させる必要がある
NB. (エラーが起きても対話モードに入るのでその対策も本来必要。FIXME)
exit ''

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>

F# Script

F#の例.fsx.bat
@more +2 "%~f0" | dotnet fsi --quiet --nologo --readline- -- %*
@exit /b %errorlevel%
(* 以下スクリプトの内容 *)
printfn "foo";;

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}

Shell (bash)

Bashの例.sh.bat
: <<'EOS'
@bash -- "%~f0" %*
@exit /b
EOS
# 以下スクリプトの内容
echo foo

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に対応し、かつ短い方法を発見されたので、冒頭にはこちらを載せています。

J (タイプ2)

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

J言語では、0 : 0はヒアドキュメント的なもので、複数行の文字列を作るものです。0 : 0から)までは文字列扱いされて読み飛ばされるので、これを利用してバッチコマンドを埋め込むことができます。:はJ言語からすると副詞の一種であり、文字列リテラルを受け取っても動詞を計算結果として返すだけで無害です(エラーにならず、何も出力しない)。バッチ側としてもラベルの宣言として読み取られ、無害です。

試行錯誤して自力で見つけました。動いているようですがこれは実用したことがないので、なにか罠を見つけたりした場合は教えて下さい。(投稿時点では、最後に必ずexit ''しないと対話モードが始まってしまうことがわかっています)

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と同様に<!--:の組み合わせでなぜかうまくいくようです。

F# Script (タイプ3)

自力で発見しました。標準入力に打ち込んでいるのと同じ実行方式になります。

#loadでできればコンパイルされて動作が早いと思うのですが、まだ見つけられていません。

Shell (Bash) (タイプ2)

自力で発見しました。シェルは普通改行コードをCRLFでなくLFにしなければならない問題がありますが、自分の使っているGit付属のBashではうまく読み込んでくれるようです。

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

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

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

汎用的な方法

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

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

ワンライナー

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

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

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

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

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

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

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

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

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できそうな言語があれば教えてください。

89
78
10

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
89
78

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?