[ネタ] C#コードをコンパイルせずにダブルクリックで実行できるようにする

# コード

使い道がないような気がものすごくしますが、やってみたらできたので晒してみる。

C#ファイルとしてそのままコンパイルできる状態を維持したまま(下のコードの文字色に注目)、
バッチファイル→Powershell経由でC#コードをコンパイルなしでダブルクリックで実行できます。

以下のコードをコピーしてbatファイルとして保存してダブルクリックしたら「はろ～わ～るど」と表示されます(文字コードに注意)。

これは説明用なので使いたい奇特な方がいるようでしたら下の改良版をどうぞ
```C#
/*---ここからBATコード---
@cls
@set args=%*
@powershell "iex((@('')*6+(cat '%~f0'|select -skip 6))-join[char]10)"
@pause
@exit /b %ERRORLEVEL%
<#---ここからPowerShellコード---#>


function Get-ScriptLineNumber { return $MyInvocation.ScriptLineNumber }
new-item alias:__LINE__ -value Get-ScriptLineNumber | Out-Null
$__ScriptLineNumber = __LINE__
$__ScriptBlank = (@('') * ($__ScriptLineNumber + 3)) -join[char]10
$__ScriptCode = @'
//*/
//---C#コードここから---

using System;

public class Test
{
    public void test()
    {
        Console.WriteLine("はろ～わ～るど");
    }
}


public class MainClass
{
    public static void Main()
    {
        var test = new Test();
        test.test();
    }
}

//---C#コードここまで---
/*
'@
$__ScriptCode += '*' + '/'

<#---ここから再びPowerShellコード---#>

Add-Type ($__ScriptBlank + $__ScriptCode)
[MainClass]::Main()

$__DummyString = '*///'
```

# よくわからない解説

## BAT部分(1～6行目)

1行目にC#のコメント開始文字(/*)を置くことでC#上ではコメントとして無効化します。
コードを見た人は1行目はバッチファイルとしてダメなんじゃないかと思われたかと思います。
その通りダメなんですが、結局は「コマンドがありません」と表示して次の行に行くだけなのでスルーすればOKということにしましたｗ
2行目で表示をクリアしてそのエラーをなかったことにしますｗｗｗ

3行目からは自身のファイルを上から6行削除した文字列をPowershellに渡しています。エラーのときに行番号がズレないようにあえて先頭に空行を6行分追加しています。
以下のサイトを参考にしました。

https://zenn.dev/zetamatta/books/c84cbe23093eee1b5830/viewer/adcc65

## PowerShell部分、前処理(7～14行目)

10～13行目はPowerShellの機能で行番号を取得して空白行を作ることで、エラー表示でC#コード部分の行番号がズレないようにしています。
以下の記事を参考にしました。

https://qiita.com/robozushi10/items/57cd1cf52b38b0173a19

14行目はヒアドキュメント機能を使ってC#コードを変数に代入します。

## C#部分 (15行目～39行目)

15行目の```//*/```はゴミではなくちゃんと意味のある文字列です。```/*```が前にある場合はブロックコメントの終了、ない場合は単独の行コメントと解釈されます。
これによってファイル全体としては1行目から15行目までC#上のコメントとなり、ヒアドキュメント内のC#コードとしては15行目は単独の行コメントとなるので、
ファイル全体もこのあとPowerShellの機能で実行されるC#コード部分もどちらも正しくC#コードとして処理されます。

39行目に単独で```/*```があるように見えますが、ここから下のPowerShellコードをC#上コメントアウトするためのものです、
このままではもちろんエラーになるので後で対処しています。

## PowerShell部分、後処理(40行目～)

40行目はヒアドキュメントの終了です。これやっててわかったんですが、PowerShellではヒアドキュメントの末端文字列は行頭にないとダメらしい。

41行目は変数代入したC#コードに```*/```を追加して39行目からのコメントを閉じます。そのまま```*/```を追加するとファイル全体としてもコメントが閉じられて都合が悪いので、```*```と```/```に分けて追加しています。

45～46行目はPowerShellからC#コードを実行するためのものです。

48行目はPowerShellにとっては文字列の代入にみせかけたファイル全体としての39行目からのコメント閉じです。```/```が2つ多いのはその後の```'```もコメントアウトするためです。

# 改良版

改良しました

- PowerShellとして実行する際にエンコードして文字コードを気にせずに使えるようにしました(22/7/8)
- スクリプト本体のパスを```$env:ScriptPath```で取得できるようにしました(22/7/8)
- コマンドライン引数がMainメソッドのargsに渡されるようにしました(22/7/8)
- Mainメソッドの戻り値が環境変数```ERRORLEVEL```に反映されるようにしました(22/7/8)
- PowerShellの変数展開機能をC#コード中でも使えるようにしました(22/7/8)
  - C#コード内でもエラーにならないようブロックコメントで囲んでも反映するようになっています```/*${test}*/```→```${test}```
  - 変数名の展開にも使えるようダミーの変数名を付けられます```int /*${test}*/dummy;```→```int ${test};```

```csharp
#if false
<# rem ----- ここからBATコード -----
cls
@set ScriptArgs=%*
@set ScriptPath=%~dp0
@powershell iex((cat '%~f0' -Encoding utf8) -join [char]10)
@pause
@exit /b %ERRORLEVEL%
@rem ----- ここまでBATコード -----
#endif
/*#>
<# ----- ここからPowerShellコード(前処理) ----- #>



<# ----- ここまでPowerShellコード ----- #>
$__ScriptExitCode = 0
$__ScriptAssemblies = @()
if (($env:ScriptArgs -eq '') -or ($null -eq $env:ScriptArgs)) {
    $__ScriptArgs = @()
} else {
    $__ScriptArgs = $env:ScriptArgs -split ' +'
}
function Get-ScriptLineNumber { return $MyInvocation.ScriptLineNumber }
new-item alias:__LINE__ -value Get-ScriptLineNumber | Out-Null
$__ScriptLineNumber = __LINE__
$__ScriptBlank = (@('') * ($__ScriptLineNumber + 3)) -join [char]10
$__ScriptBlank += '#define POWERSHELL' + [char]10
$__ScriptCode = @'
//*/
// ----- ここからC#コード -----
using System;

//エントリポイントクラス
public class MainClass
{
    public static int Main(string[] args)
    {
        //一番最初にこのメソッドが呼び出される



        return 0;
    }
}
// ----- ここまでC#コード -----
/*
'@
$__ScriptCode += ' *' + '/'
<# ----- ここから再びPowerShellコード(実行前処理) ----- #>



<# ----- ここまでPowerShellコード ----- #>
$__ScriptPattern = '/\*(\$.*?)\*' + '/[a-zA-Z0-9_@]*'
$__ScriptCode = $__ScriptCode -replace $__ScriptPattern, '$1'
$__ScriptCode = $ExecutionContext.InvokeCommand.ExpandString($__ScriptCode)
Add-Type ($__ScriptBlank + $__ScriptCode) -ReferencedAssemblies $__ScriptAssemblies
$__ScriptExitCode = [MainClass]::Main($__ScriptArgs)
<# ----- ここから再びPowerShellコード(実行後処理) ----- #>



<# ----- ここまでPowerShellコード ----- #>
exit $__ScriptExitCode
#*/
```




# 応用

変数展開や置換を使ってC#コードを吐き出すようにすればPowerShellをT4やプリプロセッサ代わりとして使えそうな気がしないでもない。

# 今後の課題

- コマンドライン引数を受け取ってMainメソッドに渡すようにする
- ~~C#9からの[トップレベルステートメント](https://ufcpp.net/study/csharp/misc/miscentrypoint/#top-level-statements)に対応できたらもっと使いやすくなりそう~~　→　実行可能形式でビルドする時にしか使えない仕様らしい(泣)