Rのつく財団入り口

ITエンジニア関連の様々な話題を書いているはずのブログです。

【C#】PowerShellからC#アプリを実行して結果を得よう【Windows】

突然技術系ブログっぽい記事を書いてみるスタイル……

 昔書いたPHPC#Active Directoryを検索するサンプルの記事も定期的に検索流入があるんですよね~ということで、今回はC#製アプリをPowerShellから呼び出してみる記事です。

f:id:iwasiman:20210327170548p:plain
PowerShellスクリプトからC#アプリケーションを呼び出してみよう

PowerShellとは?

 Mjcrosoft製のコマンドラインインターフェースとスクリプト言語からなる仕組みです。UNIX/Linuxのシェルのような感覚でコマンドを打って使うことができ、コマンドプロンプトよりも高度な処理が行えます。
 *.ps1のテキストファイルのスクリプトにコマンド群を揃えて実行もでき、変数や条件分岐や繰り返しなど各種プログラム言語と同じようなこともでき、極めるとこれはこれでWindows上での作業を様々に自動化できます。
 コマンド(正式にはコマンドレットという)が大文字始まりだったりエラー表示がかなり分かりにくかったりWindows独特なところもあるのですが、Windows7から標準搭載、Windows10ではバージョン5まで進化しました。
 なぜか(というと失礼ですが/笑)、AWSLambda関数で使える7言語にもJava, Go, Node.js(JavaScript), C#, Python, Rubyと並んで入っています。

 このPowerShellWindowsのサービスを呼べたりC#のクラスが使えたり、なかなか奥が深いです。メモ帳を起動したりもできます。ちょいと機会があって調べたので、C#製の自作アプリケーションを起動して出力を得る方法をまとめようと思います。

docs.microsoft.com

C#側のコンソールアプリケーションのコード

 以下、C#のコンソールアプリケーションは対象のフレームワーク.NET Core 3.1 で確認していますが、その前の .NET Framework 4.7.* などでも同じです。
 OnitadaSample.exe を実行する同名のソリューション/プロジェクトを作り、最初に起動されるスタートアップオブジェクトはいつのものProgramクラスにします。

  • Mainメソッドの引数はstring配列なので引数が幾つあっても大丈夫ですが、PowerShellから呼ぶ場合は引数が多いとうまく渡らない場合があるとの情報あり。こうした異なる言語・技術間でやりとりする場合はなるべく安全な方に倒した方が良いという経験則より、念のためカンマ区切り文字列1つにしてから渡しています。
  • アプリの戻り値を呼び出し側で取得することができます。このサンプルでは Environment.Exit({数値}); にしていますがメソッドの戻り値でもOKです。
  • 昔から終了コードは数値で正常が0、異常が0以外にするのが伝統ですので、ここでは正常が0、異常が1にしています。
  • アプリ内の標準出力を呼び出し側で取得することができます。このサンプルでも普通にConsole.WriteLineして標準出力を渡しています。
  • Visual Studio 2019氏がナウいコードへのリファクタリングの提案をしてくれたので、switch構文は2019/12月頃に上がったC# 8.0で使えるモダンなswitch式に変えてみました。
    これを使うとcasebreak; で1行消費しなくてよくなるんですね。C#でもこれやラムダ式=> を書いたり、JavaScriptPHPみたいです。どんどん進化していますね……
using System;

namespace OnitadaSample
{
    /// <summary>
    /// PowerShellから起動されるサンプルアプリの処理スタートのクラス。
    /// </summary>
    public class Program
    {
        static void Main(string[] args)
        {
            // PowerShellから呼ぶと引数の配列が正常に渡せない場合があるので、
            // 念のためひとつの引数に結合して渡しています。
            string[] argsArray = args[0].Split(',');
            // 引数0番目:文字列
            string arg0 = argsArray[0];
            // 引数1番目:真偽値の文字列
            string arg1 = argsArray[1].ToLower();
            if (String.IsNullOrEmpty(arg0) || String.IsNullOrEmpty(arg1))
            {
                Console.WriteLine("引数不正!");
                Environment.Exit(1);
                // Mainメソッドの戻り値をvoidからintに変更、戻り値で示してもOK
                // return 1;
            }

            // リファクタリングすると C# 8.0 の switch 式に変換! (普通のswitch文でもok)
            string onitadaMsg = arg1 switch
            {
                "true" => "引数2は真で鬼のように正しい。つまり、おにただ!",
                "false" => "引数2は偽。",
                _ => "引数2は想定外。",
            };
            Console.WriteLine("引数1: " + arg0 + " " + onitadaMsg);
            Environment.Exit(0);
            // Mainメソッドの戻り値をvoidからintに変更、戻り値で示してもOK
            // return 0; 
        }
    }
}

PowerShell側の呼び出しスクリプト

Windows10 に搭載されているPowerShell バージョン 5.1で動作確認しています。

  • 上記のC#アプリでビルドされて生成されるおにただアプリ OnitataSample.exe の場所をフルパスで指定します。
  • Windows限定の話ですが、実行するexeファイルのパスに2バイト文字が含まれていると、PowerShellが認識できなくてエラーを吐きます。ファイル自体をUTF-8+BOM付 もしくは Shift-JIS で保存すると回避できます。今時UTF-8でないとは……このへんやっぱり独特です。
  • C#Processクラスが、以下のような単純なコードでも各種外部アプリケーションを起動できるクラス。

System.Diagnostics.Process p = System.Diagnostics.Process.Start("C:\\test.txt");

  • このProcess#Start()で起動時の様々なオプションを設定できるProcessStartInfoクラスも併用する方法をPowerShell上で書くことで、C#のアプリを実行しその終了を待つことができます。
  • 以下の onitada.ps1 ファイルの例にあるように、外部アプリケーションの標準出力ストリームと終了コードを得ることができます。
# -------------------------------------------------------------------------------------------------------
# おにただアプリの呼び出しスクリプトサンプル
# 実行方法 ./{このファイル} {第1引数:任意の文字列} {第2引数:"true"/"false"の文字列}
# sample > ./onitada.ps1 jaku-chara false
# sample > ./onitada.ps1 kyou-chara true
# -------------------------------------------------------------------------------------------------------

$type = $Args[0]
$onitada = $Args[1]
# PowerShellからだと正常に渡せないことがあるので、安全のためひとつの
# 文字列として渡しています。
$serivceArg = $type + "," + $onitada

# C#のSystem.Diagnostics.ProcessStartInfoが、
# プロセスを起動するときに使用する値のセットです。
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
# 実行するexeファイルはフルパスでないと動かないです。パスに日本語が
# 含まれている場合には、スクリプト自体を
# UTF-8+BOM付 もしくは Shift-JIS で保存する必要があります
$pinfo.FileName = "D:\OnitadaSample\bin\Debug\netcoreapp3.1\OnitadaSample.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = $serivceArg

# C#のSystem.Diagnostics.Processクラスが、
# 外部アプリケーションを実行できるクラス。
# 先ほどのProcessStartInfoを引数にして処理をスタートします。
# exeを直接実行するのでなくこの方式をとると、
# 実行時の標準出力やエラーコードを取得できます。
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()

# このクラスのStandardOutputプロパティが、外部アプリケーションの
# 標準出力ストリームを読みとってくれるので出力。
# 終了コードもExitCodeプロパティで取得できます。
$stReader = $p.StandardOutput
$strOutput = $stReader.ReadToEnd() + $p.ExitCode
Write-Host $strOutput

実行結果

 PowerShellウィンドウでコマンドからonitada.ps1を実行すると以下のようになります。実行のため、最初にポリシー変更が必要です。

S D:\OnitadaSample> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process

実行ポリシーの変更
実行ポリシーは、信頼されていないスクリプトからの保護に役立ちます。実行ポリシーを変更すると、about_Execution_Policies
のヘルプ トピック (https://go.microsoft.com/fwlink/?LinkID=135170)
で説明されているセキュリティ上の危険にさらされる可能性があります。実行ポリシーを変更しますか?
[Y] はい(Y)  [A] すべて続行(A)  [N] いいえ(N)  [L] すべて無視(L)  [S] 中断(S)  [?] ヘルプ (既定値は "N"): Y
PS D:\OnitadaSample> .\onitada.ps1
引数不正!
1
PS D:\OnitadaSample> .\onitada.ps1 jaku-chara false
引数1: jaku-chara 引数2は偽。
0
PS D:\OnitadaSample> .\onitada.ps1 kyou-chara true
引数1: kyou-chara 引数2は真で鬼のように正しい。つまり、おにただ!
0
PS D:\OnitadaSample> 

C#側の標準出力を標準出力ストリームとして受け取り、メソッドの戻り値/終了コードも受け取れることが確認できました。この方式でバッチ処理などなどを実行すると、別のプログラムや別の仕組みに結果を渡して制御することができます。

 これで毎週ノルマで定期的に、おにただ! (言ってみたかっただけ...)

github.com

f:id:iwasiman:20210327170548p:plain
PowerShellスクリプトからC#アプリケーションを呼び出してみよう

関連書籍

 Webアプリケーション関連だと言語の選択肢が多いためかいまいちユーザーサイド発の情報が少ない感じのあるC#ですが、ゲーム開発ではUnityに使われたり、Windows自体に高度なアクセスをしようとするとやはりC#の出番になったりします。最近の入門本では以下のあたりでしょうか。

こちらの記事も併せてどうぞ

iwasiman.hatenablog.com

iwasiman.hatenablog.com

iwasiman.hatenablog.com

iwasiman.hatenablog.com