PowerShell で C# コードを実行

# 前提

[**こちら**](https://qiita.com/Kosen-amai/items/d68b9590c9e8e8b06377 "PowerShellメモ　C#コードを実行 - Qiita") で紹介されているように PowerShell で `Add-Type` コマンドレットを介して C# のコードが実行できます。

# 問題点

上記の記事でもふれられているように C# コードを修正して `Add-Type` を再実行すると

```console:Console
Add-Type : 型を追加できません。型名 'Example' は既に存在しています。
```

なるエラーがでます。

# 対策

ワーク アラウンドとしては
* 別プロセスの `powershell.exe` を起動して `Add-Type` しなおす。
* `Start-Job` をつかう。ただし、`Console.Write()` などのコンソールへの直接的出力はすてられるという制限あり。

## 別プロセスの `powershell.exe` を起動して `Add-Type` しなおす

別プロセスの `powershell.exe` を起動して `Add-Type` しなおすのはめんどうなので、多少なりともの自動化として、ラッパー関数をかいてみました。[**ここ**](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expressions#example-3-dynamically-build-a-culture-sensitive-regular-expression ".NET Regular Expressions | Microsoft Docs") のサンプル コードもエラーなしで実行できるように culture にもきをくばってみました。

```powershell:PowerShell
function Invoke-CSharp {
    [CmdletBinding(DefaultParameterSetName = 'Code')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Code', Position = 0)]
        [String] $Code,

        [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
        [ValidateScript({ Test-Path -Path $_ })]
        [String] $FilePath,

        [String] $ClassName,

        [String] $MethodName = 'Main',

        [String[]] $ArgumentList,

        [String] $CultureName = 'en-US'
    )

    if ($FilePath) {
        $Code = Get-Content -Path $FilePath -Raw
    }

    if (! $ClassName) {
        $ClassName = $Code -split '\r?\n' |
            Select-String -Pattern '^\s*public(?:\s\w+)*?\s+class\s+(\w+)' |
            Select-Object -First 1 |
            ForEach-Object -Process { $_.Matches.Groups[1].Value }
    }

    $ArgumentList = $ArgumentList |
        ForEach-Object -Process {
            if ($_ -is [String]) {
                '"' + $_.Replace('"', '`"') + '"'
            } else {
                [String] $_
            }
        }
    [String] $ArgumentList = $ArgumentList -join ', '

    $command = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes(@"
[System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::GetCultureInfo('$CultureName')
Add-Type -TypeDefinition @'
$Code
'@ -Language CSharp
[$ClassName]::$MethodName($ArgumentList)  # Qiita の記事の ``` のなかでも \[.*\]:.*\( をふつうにかくと非表示になり、 # をうしろにおくと復活します。なにかバギーです。
"@))

    Start-Process -FilePath powershell.exe -ArgumentList '-NoLogo', '-NoExit', "-EncodedCommand $command"
}
```

### 使用例

上記の記事よりおかりしまして、

```powershell:PowerShell
Invoke-CSharp -Code @'
using System;
public static class TestClass1
{
    public static string TestMethod(string name, string greet)
    {
        string msg = name + "さん、" + greet;

        Console.WriteLine(msg);
        return msg;
    }
}
'@ -MethodName 'TestMethod' -ArgumentList '太郎', 'おはよう' -CultureName 'ja-JP'
```

自分の用途としては Visual Studio などいれられない、いれたくない環境でもサンプル コードをきがるに「写経」できることです。よって、 `-FilePath` パラメーターを多用します。

## `Start-Job` をつかう

`Console.Write()` などのコンソールへの直接的出力はすてられるという制限があります。

```powershell:PowerShell
$j = Start-Job -ScriptBlock {
    Add-Type -TypeDefinition @'
using System;
public static class TestClass1
{
    public static string TestMethod(string name, string greet)
    {
        string msg = name + "さん、" + greet;

        Console.WriteLine(msg);
        return msg;
    }
}
'@ -Language CSharp

    [TestClass1]::TestMethod('太郎', 'おはよう')
}

Wait-Job -Job $j | Out-Null
Receive-Job -Job $j
```

この C# コードを「ふつうに」実行すると「太郎さん、おはよう」が二行でます。一行目は `Console.WriteLine(msg);` から、二行目は `return msg;` からのです。しかし、この `Start-Job` をかませた実行では  `return msg;` からのぶんしかでてきません。

# 補足

コードを実行させたいだけなら

```console:Console
C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe .\example.cs
```

とすれば `.\example.exe` が生成されるのでした (2020 年 7 月現在) 。

ただ、このばあい [**ここ**](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expressions#example-3-dynamically-build-a-culture-sensitive-regular-expression ".NET Regular Expressions | Microsoft Docs") のサンプル コードは `Main()` の最初に

```csharp:C#
System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-US");
```

を追加することでしか正常に実行させられませんでした。 PS のコマンドラインで

```powershell:PowerShell
[System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::GetCultureInfo('en-US')
.\example.exe
```

とするのは効果がありませんでした。

# 参考文献

* [How to remove a type added by Add-Type](https://social.technet.microsoft.com/Forums/ja-JP/f0b6327d-23c2-492b-bbba-ba4f026722bb/how-to-remove-a-type-added-by-addtype?forum=winserverpowershell "How to remove a type added by Add-Type")
* [windows - powershell : changing the culture of current session - Stack Overflow](https://stackoverflow.com/questions/7052765/powershell-changing-the-culture-of-current-session "windows - powershell : changing the culture of current session - Stack Overflow")
* [Visual Studioではなく、Windows付属のcsc.exe だけでC#実行ファイルを作る - Qiita](https://qiita.com/toshirot/items/dcf7809007730d835cfc "Visual Studioではなく、Windows付属のcsc.exe だけでC#実行ファイルを作る - Qiita")