C#からPowerShellを実行する方法|コマンド実行・結果取得・エラー対処まで解説

はじめに

C#アプリケーションからPowerShellを実行できるようになると、Windowsの管理操作、ファイル操作、サービス制御、環境情報の取得、既存のPowerShellスクリプトの再利用などを柔軟に実装できます。

たとえば、C#だけで処理を書くよりも、PowerShellコマンドを呼び出したほうが簡単に実装できる場面があります。Get-Processでプロセス一覧を取得したり、Get-Serviceでサービス状態を確認したり、既存の.ps1ファイルをC#アプリから実行したりするケースです。

一方で、C#からPowerShellを実行する場合は、単にコマンドを起動するだけでは不十分です。実行結果の取得、エラー判定、文字化け対策、実行ポリシー、管理者権限、コマンドインジェクション対策などを考慮する必要があります。

この記事では、C#からPowerShellを実行する方法について、Process.Startを使う方法とSystem.Management.Automationを使う方法の両方を解説します。コマンド実行、結果取得、エラー処理、ps1ファイル実行、セキュリティ対策、よくあるトラブルへの対処法まで、実務で使いやすい形で紹介します。

1. C#からPowerShellを実行する前に知っておくべき基本

C#からPowerShellを実行する方法はいくつかあります。代表的なのは、外部プロセスとしてpowershell.exepwsh.exeを起動する方法と、PowerShell SDKを使ってC#のコード内からPowerShellランタイムを扱う方法です。

どちらを選ぶべきかは、実行したい内容やアプリケーションの構成によって変わります。まずは、C#からPowerShellを実行する目的と、主な実装方法の違いを理解しておきましょう。

1-1. C#からPowerShellを実行したい主なケース

C#からPowerShellを実行したい場面としては、次のようなケースがあります。

Windowsの管理情報を取得したい場合、PowerShellは非常に便利です。プロセス、サービス、イベントログ、ネットワーク設定、ファイル情報などを簡単なコマンドで取得できます。

既存のPowerShellスクリプトをC#アプリケーションから呼び出したい場合もあります。すでに運用で使っている.ps1ファイルがある場合、同じ処理をC#で書き直すより、C#からスクリプトを実行したほうが効率的です。

また、管理ツールや社内向けアプリケーションで、ユーザー操作に応じてPowerShellコマンドを実行したいケースもあります。たとえば、サービスの起動・停止、ログファイルの収集、フォルダ作成、権限確認などです。

ただし、PowerShellは強力な反面、使い方を誤るとセキュリティリスクにつながります。特に、ユーザー入力をそのままコマンド文字列に連結する実装は避けるべきです。

1-2. Process.StartとSystem.Management.Automationの違い

C#からPowerShellを実行する代表的な方法は、次の2つです。

1つ目は、System.Diagnostics.Processを使ってpowershell.exeまたはpwsh.exeを外部プロセスとして起動する方法です。

C#
using System.Diagnostics;

var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -Command \"Get-Process\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

using var process = Process.Start(psi);
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();

この方法はシンプルで、既存のPowerShellコマンドや.ps1ファイルをそのまま実行しやすいのが特徴です。外部プロセスとして実行されるため、C#アプリケーション本体とPowerShellの実行環境を分離しやすいメリットもあります。

2つ目は、System.Management.Automationを使う方法です。これはPowerShell SDKを利用し、C#コード内でPowerShellコマンドを構築・実行します。

C#
using System.Management.Automation;

using var ps = PowerShell.Create();
ps.AddCommand("Get-Process");

var results = ps.Invoke();

foreach (var item in results)
{
Console.WriteLine(item.ToString());
}

この方法では、文字列でコマンド全体を組み立てるのではなく、AddCommandAddParameterを使って安全にコマンドを構成できます。また、結果を文字列ではなくPSObjectとして扱えるため、より構造化された処理が可能です。

簡単にまとめると、外部コマンドとして実行したい場合はProcess.Start、C#アプリ内でPowerShellを細かく制御したい場合はSystem.Management.Automationが向いています。

1-3. コマンド実行・結果取得・エラー処理で必要になる考え方

C#からPowerShellを実行するときは、少なくとも次の3つを意識する必要があります。

まず、コマンドをどのように渡すかです。-Commandで直接コマンドを渡すのか、-File.ps1ファイルを指定するのかによって書き方が変わります。

次に、実行結果をどのように取得するかです。PowerShellの標準出力をC#側で取得するには、RedirectStandardOutput = trueを設定し、StandardOutput.ReadToEnd()などで読み取ります。

最後に、エラーをどのように判定するかです。PowerShellコマンドが失敗しても、標準出力だけを見ていると原因が分からない場合があります。標準エラー、ExitCode、例外、ログを組み合わせて確認することが重要です。

特に実務では、成功時の結果だけでなく、失敗時に原因を追跡できる実装が重要です。標準エラーを取得し、終了コードを確認し、必要に応じてログに出力するようにしておきましょう。

1-4. Windows PowerShellとPowerShell 7の違いに注意する

C#からPowerShellを実行する際は、どのPowerShellを起動するかにも注意が必要です。

Windowsに標準で搭載されている従来のPowerShellは、通常powershell.exeとして実行します。一方、PowerShell 7以降はpwsh.exeとして実行します。

C#
FileName = "powershell.exe"; // Windows PowerShell
C#
FileName = "pwsh.exe"; // PowerShell 7以降

Windows PowerShellとPowerShell 7では、利用できるモジュール、文字コード、互換性、実行環境が異なる場合があります。特に、古いWindows専用モジュールを使う場合はWindows PowerShell、クロスプラットフォーム対応や新しい機能を使いたい場合はPowerShell 7を選ぶことが多いです。

また、サーバーや実行環境によってはpwsh.exeがインストールされていない場合もあります。C#アプリケーションでPowerShell 7を前提にする場合は、実行環境にPowerShell 7が存在するか確認しておきましょう。

2. C#からPowerShellコマンドを実行する方法

C#からPowerShellコマンドを実行する基本的な方法は、ProcessStartInfoを使ってPowerShellの実行ファイルを起動することです。

ここでは、powershell.exeを使ってコマンドを実行する方法を中心に解説します。PowerShell 7を使う場合は、FileNamepwsh.exeに変更してください。

2-1. ProcessStartInfoを使ってpowershell.exeを起動する

ProcessStartInfoを使うと、実行ファイル名、引数、標準出力のリダイレクト、ウィンドウ表示の有無などを細かく指定できます。

基本形は次のとおりです。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -Command \"Get-Date\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = Process.Start(psi);

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

Console.WriteLine("Output:");
Console.WriteLine(output);

Console.WriteLine("Error:");
Console.WriteLine(error);

Console.WriteLine($"ExitCode: {process.ExitCode}");
}
}

FileNameにはPowerShellの実行ファイルを指定します。ArgumentsにはPowerShellへ渡す引数を指定します。

-NoProfileは、ユーザープロファイルを読み込まずにPowerShellを起動する指定です。環境依存の影響を減らせるため、C#から実行する場合は付けておくことをおすすめします。

UseShellExecute = falseは、標準出力や標準エラーをリダイレクトするために必要です。RedirectStandardOutputRedirectStandardErrorを使う場合は必ずfalseにします。

2-2. 単純なPowerShellコマンドを実行するサンプルコード

単純なPowerShellコマンドをC#から実行する例として、現在日時を取得するGet-Dateを実行してみます。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
string command = "Get-Date";

var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = $"-NoProfile -Command \"{command}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = Process.Start(psi);
string result = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

if (process.ExitCode == 0)
{
Console.WriteLine(result);
}
else
{
Console.WriteLine("PowerShellの実行に失敗しました。");
Console.WriteLine(error);
}
}
}

このコードでは、PowerShellのGet-Dateコマンドを実行し、その結果をC#側で文字列として受け取っています。

ただし、コマンド文字列をそのままArgumentsに埋め込む方法は、引数に空白や引用符が含まれる場合に注意が必要です。また、外部入力を含める場合はコマンドインジェクション対策が必要です。

2-3. 引数を指定してPowerShellを実行する方法

PowerShellコマンドに引数を渡す場合は、引用符の扱いに注意します。

たとえば、指定したサービス名の状態を取得する場合は次のように書けます。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
string serviceName = "wuauserv";

var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = $"-NoProfile -Command \"Get-Service -Name '{serviceName}'\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = Process.Start(psi);
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

Console.WriteLine(output);

if (!string.IsNullOrWhiteSpace(error))
{
Console.WriteLine("Error:");
Console.WriteLine(error);
}
}
}

ただし、このように文字列連結でコマンドを組み立てる場合、serviceNameが外部入力であれば危険です。

可能であれば、.NET 5以降で利用できるProcessStartInfo.ArgumentListを使って、引数を分けて渡す方法を検討しましょう。

C#
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

psi.ArgumentList.Add("-NoProfile");
psi.ArgumentList.Add("-Command");
psi.ArgumentList.Add("Get-Service -Name 'wuauserv'");

ArgumentListを使うと、OSに渡す引数を分離して指定できるため、引用符の扱いが比較的安全になります。

2-4. 実行ウィンドウを非表示にする設定

Windowsアプリケーションやバックグラウンド処理からPowerShellを実行する場合、PowerShellのコンソールウィンドウを表示したくないことがあります。

その場合は、次のように設定します。

C#
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -Command \"Get-Date\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};

重要なのは、UseShellExecute = falseCreateNoWindow = trueです。

CreateNoWindow = trueを指定すると、新しいウィンドウを作成せずにプロセスを実行できます。さらにWindowStyle = ProcessWindowStyle.Hiddenを指定することで、ウィンドウを非表示にしたい意図を明確にできます。

ただし、管理者権限で昇格実行する場合など、UseShellExecute = trueが必要になる場面では、標準出力のリダイレクトと両立できない点に注意してください。

2-5. ExecutionPolicyを考慮して実行する方法

PowerShellスクリプトを実行する場合、実行ポリシーによって.ps1ファイルが実行できないことがあります。

一時的に現在のプロセスだけ実行ポリシーを回避したい場合は、-ExecutionPolicy Bypassを指定する方法があります。

C#
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -ExecutionPolicy Bypass -File \"C:\\Scripts\\sample.ps1\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

-ExecutionPolicy Bypassは、PowerShellプロセス単位で指定できます。システム全体の設定を変更するより影響範囲を限定しやすい方法です。

ただし、実行ポリシーはセキュリティ境界そのものではないとはいえ、無条件にBypassを使う実装は避けるべきです。本番環境では、スクリプトの配置場所、署名、実行権限、改ざん対策を含めて設計する必要があります。

3. C#でPowerShellの実行結果を取得する方法

PowerShellの実行結果をC#で取得するには、標準出力をリダイレクトします。標準出力は、PowerShellコマンドが通常の結果として出力する内容です。

一方、エラー情報は標準エラーに出力されることがあります。実行結果を正しく扱うには、標準出力と標準エラーの両方を取得することが重要です。

3-1. 標準出力を取得する基本コード

標準出力を取得するには、RedirectStandardOutput = trueを設定し、process.StandardOutput.ReadToEnd()で読み取ります。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -Command \"Get-Process | Select-Object -First 5\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = Process.Start(psi);

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

Console.WriteLine("標準出力:");
Console.WriteLine(output);

Console.WriteLine("標準エラー:");
Console.WriteLine(error);
}
}

このコードでは、PowerShellでGet-Processを実行し、先頭5件の結果をC#側で取得しています。

標準出力を取得する場合は、UseShellExecute = falseが必須です。UseShellExecute = trueのままだと、標準出力をリダイレクトできません。

3-2. 日本語が文字化けする場合の対処法

C#からPowerShellを実行したとき、日本語の出力が文字化けすることがあります。これは、PowerShell側の出力エンコードとC#側の読み取りエンコードが合っていない場合に発生します。

対処法として、StandardOutputEncodingStandardErrorEncodingを指定します。

C#
using System;
using System.Diagnostics;
using System.Text;

class Program
{
static void Main()
{
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -Command \"Write-Output '日本語のテスト'\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
CreateNoWindow = true
};

using var process = Process.Start(psi);

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

Console.WriteLine(output);
Console.WriteLine(error);
}
}

PowerShell側でもUTF-8を明示したい場合は、次のように[Console]::OutputEncodingを設定する方法があります。

C#
string command = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Write-Output '日本語のテスト'";

そして、C#側でもEncoding.UTF8で読み取ります。

C#
Arguments = $"-NoProfile -Command \"{command}\"";

Windows PowerShellとPowerShell 7では、既定のエンコード挙動が異なることがあります。文字化けが起きた場合は、PowerShell側の出力エンコードとC#側の読み取りエンコードを合わせることを優先して確認しましょう。

3-3. 実行完了まで待機して結果を受け取る方法

PowerShellの実行が完了する前に結果を取得しようとすると、出力が途中までしか取得できない場合があります。

同期的に実行完了を待つ場合は、WaitForExit()を使います。

C#
using var process = Process.Start(psi);

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

ただし、標準出力と標準エラーに大量の出力がある場合、読み取り方によってはデッドロックが発生する可能性があります。

より安全に実装するには、標準出力と標準エラーを非同期で読み取る方法を検討します。特に、長時間実行されるスクリプトや大量のログを出力する処理では、非同期読み取りが有効です。

3-4. 非同期でPowerShellの実行結果を取得する方法

非同期でPowerShellの実行結果を取得する場合は、ReadToEndAsync()WaitForExitAsync()を使います。

C#
using System;
using System.Diagnostics;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -Command \"Get-Process | Select-Object -First 10\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = new Process
{
StartInfo = psi
};

process.Start();

Task<string> outputTask = process.StandardOutput.ReadToEndAsync();
Task<string> errorTask = process.StandardError.ReadToEndAsync();

await process.WaitForExitAsync();

string output = await outputTask;
string error = await errorTask;

Console.WriteLine("Output:");
Console.WriteLine(output);

Console.WriteLine("Error:");
Console.WriteLine(error);

Console.WriteLine($"ExitCode: {process.ExitCode}");
}
}

非同期で読み取ることで、UIアプリケーションのフリーズを防ぎやすくなります。WPFやWindows Formsなどのデスクトップアプリでは、PowerShellの実行を同期的に待つと画面が固まることがあるため、非同期処理を使うのがおすすめです。

3-5. 戻り値とExitCodeを確認する方法

C#からPowerShellを実行した後は、ExitCodeを確認して成功・失敗を判定します。

C#
if (process.ExitCode == 0)
{
Console.WriteLine("成功しました。");
}
else
{
Console.WriteLine($"失敗しました。ExitCode: {process.ExitCode}");
Console.WriteLine(error);
}

一般的に、ExitCode0であれば成功、0以外であれば失敗として扱います。

ただし、PowerShellコマンドの内容によっては、エラーが発生しても終了コードが期待どおりにならない場合があります。特に、PowerShell内部でエラーが発生してもスクリプトが継続する場合、C#側では成功扱いに見えることがあります。

エラー時に確実に失敗として扱いたい場合は、PowerShell側で$ErrorActionPreference = 'Stop'を指定したり、例外発生時にexit 1するようにスクリプトを設計したりするとよいでしょう。

PowerShell
$ErrorActionPreference = 'Stop'

try {
Get-Item "C:\not-exists.txt"
exit 0
}
catch {
Write-Error $_
exit 1
}

4. C#でPowerShellのエラーを取得・判定する方法

PowerShell実行時のエラーを正しく扱うには、標準エラー、例外、終了コードの3つを確認することが重要です。

標準出力だけを取得していると、エラーの原因が分からないまま処理が失敗することがあります。C#からPowerShellを実行する実装では、標準エラーも必ず取得するようにしましょう。

4-1. 標準エラー出力を取得する方法

標準エラーを取得するには、RedirectStandardError = trueを設定し、StandardError.ReadToEnd()で読み取ります。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -Command \"Get-Item 'C:\\not-exists.txt'\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = Process.Start(psi);

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

Console.WriteLine("Output:");
Console.WriteLine(output);

Console.WriteLine("Error:");
Console.WriteLine(error);

Console.WriteLine($"ExitCode: {process.ExitCode}");
}
}

存在しないファイルを取得しようとしているため、標準エラーにエラーメッセージが出力されます。

実務では、標準エラーが空でない場合はログに残す、画面に表示する、例外として扱うなど、用途に応じて処理しましょう。

4-2. PowerShell実行時の例外をtry-catchで処理する

Process.Start自体が失敗する場合もあります。たとえば、powershell.exeが見つからない、実行ファイルへのアクセス権がない、引数が不正でプロセスを開始できない場合などです。

このようなケースは、try-catchで捕捉します。

C#
try
{
using var process = Process.Start(psi);

if (process == null)
{
throw new InvalidOperationException("PowerShellプロセスを開始できませんでした。");
}

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

Console.WriteLine(output);

if (process.ExitCode != 0)
{
Console.WriteLine($"PowerShellがエラー終了しました。ExitCode: {process.ExitCode}");
Console.WriteLine(error);
}
}
catch (Exception ex)
{
Console.WriteLine("PowerShellの実行中に例外が発生しました。");
Console.WriteLine(ex.Message);
}

ここで注意したいのは、try-catchで捕捉できるのは主にC#側の例外だという点です。PowerShellコマンド内部のエラーは、標準エラーや終了コードとして扱われることが多いため、try-catchだけでは不十分です。

4-3. ExitCodeで成功・失敗を判定する方法

PowerShellの実行結果を判定するときは、ExitCodeを確認します。

C#
bool success = process.ExitCode == 0;

if (success)
{
Console.WriteLine("PowerShellコマンドは正常終了しました。");
}
else
{
Console.WriteLine("PowerShellコマンドは異常終了しました。");
Console.WriteLine($"ExitCode: {process.ExitCode}");
Console.WriteLine(error);
}

ただし、PowerShellの非終端エラーは、スクリプト全体の終了コードに反映されない場合があります。

たとえば、Get-Itemで存在しないファイルを指定しても、その後の処理が継続されることがあります。このような場合は、PowerShell側でエラーを終端エラーとして扱う設定が有効です。

PowerShell
$ErrorActionPreference = 'Stop'

C#から実行するコマンドに含める場合は、次のように書けます。

C#
string command = "$ErrorActionPreference = 'Stop'; Get-Item 'C:\\not-exists.txt'";

さらに、失敗時に明示的にexit 1を返すようにすると、C#側で判定しやすくなります。

4-4. エラーメッセージが取得できない場合の確認ポイント

PowerShell実行時にエラーが発生しているはずなのに、C#側でエラーメッセージが取得できない場合は、次の点を確認してください。

まず、RedirectStandardError = trueが設定されているか確認します。これがfalseだと、C#側で標準エラーを取得できません。

次に、UseShellExecute = falseになっているか確認します。標準出力や標準エラーをリダイレクトするには、UseShellExecutefalseにする必要があります。

また、PowerShellコマンドがエラーを標準エラーではなく標準出力に出している場合もあります。スクリプト内でWrite-OutputWrite-Hostを使っている場合、エラーとして取得できないことがあります。

PowerShell側で例外を握りつぶしていないかも確認しましょう。try-catchの中でエラーを出力せずに処理を終了していると、C#側では原因が分かりません。

PowerShell
try {
# 何らかの処理
}
catch {
Write-Error $_
exit 1
}

このように、PowerShell側でもエラーを明示的に出力し、終了コードを設定することが重要です。

4-5. ログ出力して原因調査しやすくする方法

本番環境では、PowerShellの実行結果とエラーをログに残すことをおすすめします。

C#
using System;
using System.Diagnostics;
using System.IO;

class Program
{
static void Main()
{
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -Command \"Get-Item 'C:\\not-exists.txt'\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = Process.Start(psi);

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

string log = $@"
[{DateTime.Now}]
Command: {psi.FileName} {psi.Arguments}
ExitCode: {process.ExitCode}

Output:
{output}

Error:
{error}
";

File.AppendAllText("powershell-execution.log", log);
}
}

ログには、実行日時、実行したコマンド、終了コード、標準出力、標準エラーを残しておくと原因調査がしやすくなります。

ただし、ログにパスワード、アクセストークン、個人情報などの機密情報を出力しないように注意してください。

5. PowerShellスクリプトファイルをC#から実行する方法

C#からPowerShellを実行する場合、コマンドを直接渡すだけでなく、.ps1ファイルを指定して実行することもできます。

既存のPowerShellスクリプトを再利用したい場合や、複雑な処理をPowerShell側に分離したい場合は、.ps1ファイルを実行する方法が便利です。

5-1. ps1ファイルを指定して実行する基本コード

.ps1ファイルを実行するには、PowerShellの-Fileオプションを使います。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
string scriptPath = @"C:\Scripts\sample.ps1";

var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = $"-NoProfile -ExecutionPolicy Bypass -File \"{scriptPath}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = Process.Start(psi);

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

Console.WriteLine(output);

if (process.ExitCode != 0)
{
Console.WriteLine("スクリプト実行エラー:");
Console.WriteLine(error);
}
}
}

-Fileの後に実行したい.ps1ファイルのパスを指定します。パスに空白が含まれる可能性があるため、ダブルクォートで囲んでいます。

5-2. スクリプトに引数を渡す方法

PowerShellスクリプトに引数を渡す場合は、.ps1ファイルの後ろにパラメーターを指定します。

たとえば、次のようなスクリプトがあるとします。

PowerShell
param(
[string]$Name
)

Write-Output "Hello, $Name"

C#からこのスクリプトにNameを渡す場合は、次のようにします。

C#
string scriptPath = @"C:\Scripts\hello.ps1";
string name = "Taro";

var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

psi.ArgumentList.Add("-NoProfile");
psi.ArgumentList.Add("-ExecutionPolicy");
psi.ArgumentList.Add("Bypass");
psi.ArgumentList.Add("-File");
psi.ArgumentList.Add(scriptPath);
psi.ArgumentList.Add("-Name");
psi.ArgumentList.Add(name);

ArgumentListを使うと、パスや引数に空白が含まれていても比較的安全に扱えます。

Argumentsに文字列として組み立てることもできますが、引用符の扱いやエスケープが複雑になりやすいため、可能であればArgumentListを使うことをおすすめします。

5-3. パスに空白がある場合の書き方

スクリプトのパスに空白が含まれる場合は、必ず引用符で囲みます。

C#
string scriptPath = @"C:\My Scripts\sample script.ps1";

var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = $"-NoProfile -ExecutionPolicy Bypass -File \"{scriptPath}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

ただし、より安全なのはArgumentListを使う方法です。

C#
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

psi.ArgumentList.Add("-NoProfile");
psi.ArgumentList.Add("-ExecutionPolicy");
psi.ArgumentList.Add("Bypass");
psi.ArgumentList.Add("-File");
psi.ArgumentList.Add(@"C:\My Scripts\sample script.ps1");

ArgumentListを使えば、引数を個別に指定できるため、スペースを含むパスでも扱いやすくなります。

5-4. 管理者権限が必要なスクリプトを実行する方法

管理者権限が必要なスクリプトを実行する場合は、Verb = "runas"を指定して昇格実行できます。

C#
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -ExecutionPolicy Bypass -File \"C:\\Scripts\\admin.ps1\"",
UseShellExecute = true,
Verb = "runas"
};

Process.Start(psi);

Verb = "runas"を指定すると、UACの確認画面が表示されます。

ただし、この方法には重要な注意点があります。UseShellExecute = trueにする必要があるため、標準出力や標準エラーをリダイレクトできません。つまり、管理者権限で実行しながら、同時にC#側で実行結果を直接取得することは難しくなります。

管理者権限が必要な処理の結果を取得したい場合は、ログファイルに出力する、タスクスケジューラを利用する、管理者権限でC#アプリ自体を起動するなど、別の設計を検討しましょう。

5-5. 実行ポリシーでps1ファイルが動かない場合の対処法

.ps1ファイルが実行できない場合、PowerShellの実行ポリシーが原因になっていることがあります。

一時的に現在のプロセスだけ許可する場合は、次のように-ExecutionPolicy Bypassを指定します。

C#
Arguments = "-NoProfile -ExecutionPolicy Bypass -File \"C:\\Scripts\\sample.ps1\"";

また、スクリプトファイルがインターネットからダウンロードされた場合、ブロック属性が付いていることがあります。その場合は、PowerShellで次のように解除できます。

PowerShell
Unblock-File -Path "C:\Scripts\sample.ps1"

ただし、本番環境で常にBypassを指定するのは慎重に判断してください。信頼できないスクリプトを実行できてしまう設計は危険です。スクリプトの配置先を制限し、改ざんされないようにアクセス権を設定し、必要に応じて署名付きスクリプトを利用しましょう。

6. System.Management.Automationを使ってPowerShellを実行する方法

Process.Startを使う方法は手軽ですが、PowerShellコマンドを文字列として組み立てる必要があり、引数の扱いやエラー処理が複雑になることがあります。

よりC#らしくPowerShellを扱いたい場合は、System.Management.Automationを使う方法があります。この方法では、PowerShellコマンドをC#コード内で構造的に組み立て、結果をPSObjectとして取得できます。

6-1. Microsoft.PowerShell.SDKを導入する方法

System.Management.Automationを使うには、NuGetでMicrosoft.PowerShell.SDKを追加します。

.NET CLIを使う場合は、次のコマンドで追加できます。

Bash
dotnet add package Microsoft.PowerShell.SDK

Visual Studioを使っている場合は、NuGetパッケージマネージャーからMicrosoft.PowerShell.SDKを検索してインストールします。

インストール後、C#コードで次の名前空間を利用できます。

C#
using System.Management.Automation;

この方法は、外部プロセスとしてpowershell.exeを起動するのではなく、PowerShell SDKを通じてコマンドを実行します。結果をオブジェクトとして扱えるため、単純な文字列処理よりも堅牢な実装にしやすい点がメリットです。

6-2. PowerShell.Createでコマンドを実行する基本コード

PowerShell.Create()を使うと、C#からPowerShellの実行パイプラインを作成できます。

C#
using System;
using System.Management.Automation;

class Program
{
static void Main()
{
using var ps = PowerShell.Create();

ps.AddCommand("Get-Date");

var results = ps.Invoke();

foreach (var result in results)
{
Console.WriteLine(result);
}
}
}

このコードでは、Get-Dateコマンドを実行し、その結果をC#側で表示しています。

Process.Startと異なり、PowerShellコマンドの結果はPSObjectとして返されます。文字列として扱いたい場合はToString()を使えますが、オブジェクトのプロパティを取り出すこともできます。

6-3. AddCommandとAddParameterで安全に実行する方法

System.Management.Automationを使う大きなメリットは、AddCommandAddParameterでコマンドと引数を分けて指定できることです。

C#
using System;
using System.Management.Automation;

class Program
{
static void Main()
{
string serviceName = "wuauserv";

using var ps = PowerShell.Create();

ps.AddCommand("Get-Service")
.AddParameter("Name", serviceName);

var results = ps.Invoke();

foreach (var result in results)
{
Console.WriteLine(result);
}

if (ps.HadErrors)
{
foreach (var error in ps.Streams.Error)
{
Console.WriteLine(error.ToString());
}
}
}
}

文字列として"Get-Service -Name wuauserv"のように組み立てるのではなく、コマンド名とパラメーターを分離して指定しています。

この書き方は、引数に空白や特殊文字が含まれる場合でも扱いやすく、コマンドインジェクションのリスクを下げやすい実装です。

6-4. 実行結果をPSObjectとして取得する方法

System.Management.Automationを使うと、PowerShellの実行結果をPSObjectとして取得できます。

たとえば、サービス名と状態を取得する場合は次のように書けます。

C#
using System;
using System.Management.Automation;

class Program
{
static void Main()
{
using var ps = PowerShell.Create();

ps.AddCommand("Get-Service")
.AddParameter("Name", "wuauserv");

var results = ps.Invoke();

foreach (var item in results)
{
var name = item.Properties["Name"]?.Value;
var status = item.Properties["Status"]?.Value;
var displayName = item.Properties["DisplayName"]?.Value;

Console.WriteLine($"Name: {name}");
Console.WriteLine($"Status: {status}");
Console.WriteLine($"DisplayName: {displayName}");
}
}
}

PSObjectのプロパティにアクセスすることで、文字列をパースする必要がなくなります。

Process.Startで標準出力を取得する場合、PowerShellの表示形式に依存して文字列を解析することがあります。しかし、PSObjectを使えば、プロパティ単位で値を扱えるため、より安定した実装が可能です。

6-5. エラー情報をStreams.Errorから取得する方法

System.Management.Automationでは、エラー情報をStreams.Errorから取得できます。

C#
using System;
using System.Management.Automation;

class Program
{
static void Main()
{
using var ps = PowerShell.Create();

ps.AddCommand("Get-Item")
.AddParameter("Path", @"C:\not-exists.txt");

var results = ps.Invoke();

if (ps.HadErrors)
{
Console.WriteLine("PowerShellエラーが発生しました。");

foreach (var error in ps.Streams.Error)
{
Console.WriteLine(error.Exception.Message);
Console.WriteLine(error.CategoryInfo);
Console.WriteLine(error.FullyQualifiedErrorId);
}
}
else
{
foreach (var result in results)
{
Console.WriteLine(result);
}
}
}
}

Streams.Errorには、PowerShellのエラーレコードが格納されます。エラーメッセージだけでなく、カテゴリ情報やエラーIDも取得できるため、原因調査に役立ちます。

6-6. Process.Startとの使い分け

Process.StartSystem.Management.Automationは、用途に応じて使い分けるのが現実的です。

既存の.ps1ファイルをそのまま実行したい場合や、PowerShellを別プロセスとして分離したい場合は、Process.Startが向いています。実装が分かりやすく、外部コマンドを起動する感覚で利用できます。

一方、C#コード内でPowerShellコマンドを安全に組み立てたい場合や、結果をオブジェクトとして扱いたい場合は、System.Management.Automationが向いています。

また、セキュリティ面では、コマンド文字列を連結しがちなProcess.Startよりも、AddCommandAddParameterで構造化できるSDK方式のほうが安全に実装しやすいです。

ただし、SDK方式はNuGetパッケージの追加や実行環境との互換性を考慮する必要があります。シンプルな用途ならProcess.Start、複雑な連携ならSDK方式という基準で選ぶとよいでしょう。

7. C#からPowerShellを実行するときのセキュリティ対策

PowerShellはWindows管理に強力な機能を提供しますが、その分、セキュリティリスクも大きくなります。

C#からPowerShellを実行する場合、特に注意すべきなのがコマンドインジェクションです。ユーザー入力をそのままPowerShellコマンドに連結すると、意図しないコマンドを実行される可能性があります。

7-1. ユーザー入力をそのままコマンドに渡さない

最も避けるべき実装は、ユーザー入力をそのままコマンド文字列に連結することです。

C#
string userInput = textBox.Text;
string command = "Get-Item " + userInput;

このような実装では、ユーザーが悪意のある文字列を入力した場合、想定外のコマンドが実行される可能性があります。

たとえば、ファイルパスを入力させるつもりでも、PowerShellの構文として解釈される文字列が混入すると危険です。

ユーザー入力を扱う場合は、入力値の検証、許可リストによる制限、パラメーター分離、権限の最小化を行いましょう。

7-2. コマンドインジェクションを防ぐ書き方

コマンドインジェクションを防ぐには、可能な限り文字列連結でコマンドを組み立てないことが重要です。

System.Management.Automationを使える場合は、AddCommandAddParameterを使います。

C#
using var ps = PowerShell.Create();

ps.AddCommand("Get-Item")
.AddParameter("Path", userInput);

このように、コマンドとパラメーターを分けて指定することで、ユーザー入力をコマンドの一部として解釈させにくくできます。

ProcessStartInfoを使う場合も、.NETArgumentListを使って引数を分けると安全性が高まります。

C#
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

psi.ArgumentList.Add("-NoProfile");
psi.ArgumentList.Add("-File");
psi.ArgumentList.Add(scriptPath);
psi.ArgumentList.Add("-Path");
psi.ArgumentList.Add(userInput);

さらに、入力値は許可リストで検証しましょう。たとえば、サービス名を指定させる場合は、実行可能なサービス名をあらかじめ限定するのが安全です。

7-3. 管理者権限で実行する場合の注意点

管理者権限でPowerShellを実行すると、システム設定の変更、サービス制御、ファイル削除など、影響の大きい操作が可能になります。

そのため、管理者権限で実行する場合は、実行できるコマンドを厳しく制限する必要があります。

ユーザーが自由にPowerShellコマンドを入力できる画面を作り、それを管理者権限で実行するような実装は非常に危険です。管理者権限で実行する処理は、アプリケーション側で決めた固定処理に限定し、ユーザー入力はパラメーターとして安全に扱うべきです。

また、C#アプリ自体を管理者権限で起動する場合、アプリ内のすべての処理が高い権限で動作します。必要最小限の範囲だけ昇格する設計を検討しましょう。

7-4. 実行ポリシー変更のリスク

PowerShellの実行ポリシーをBypassUnrestrictedに変更すると、スクリプトの実行制限が緩くなります。

C#から.ps1ファイルを実行するために、安易にシステム全体の実行ポリシーを変更するのは避けましょう。

PowerShell
Set-ExecutionPolicy Unrestricted

このような変更は、他のスクリプト実行にも影響します。

C#から実行する場合は、必要に応じてプロセス単位で-ExecutionPolicy Bypassを指定するほうが影響範囲を限定できます。

C#
psi.ArgumentList.Add("-ExecutionPolicy");
psi.ArgumentList.Add("Bypass");

ただし、これも無条件に使うべきではありません。信頼できるスクリプトだけを実行する、スクリプトの改ざんを防ぐ、実行権限を制限するなどの対策と組み合わせてください。

7-5. 本番環境で避けるべき実装例

本番環境では、次のような実装は避けるべきです。

C#
string command = Console.ReadLine();
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-Command \"" + command + "\"",
UseShellExecute = false
};
Process.Start(psi);

この実装では、ユーザーが入力した任意のPowerShellコマンドを実行できてしまいます。特に管理者権限で動作している場合は非常に危険です。

また、次のようにユーザー入力をパスとして受け取る場合も注意が必要です。

C#
string path = userInput;
string command = $"Remove-Item {path} -Recurse";

ファイル削除などの危険な操作では、パスの検証、対象ディレクトリの制限、確認処理、ログ記録が必要です。

安全な実装にするには、ユーザーに自由なコマンドを入力させるのではなく、アプリケーション側で許可した操作だけを実行できるように設計しましょう。

8. C#からPowerShellが実行できないときの原因と対処法

C#からPowerShellを実行できない場合、原因はさまざまです。実行ファイルが見つからない、実行ポリシーにブロックされている、権限が不足している、標準出力の取得方法が間違っている、文字コードが合っていないなどが考えられます。

ここでは、よくある原因と対処法を整理します。

8-1. powershell.exeが見つからない

powershell.exeが見つからない場合、FileNameの指定が間違っている可能性があります。

通常、Windows PowerShellは次の場所にあります。

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe

明示的にフルパスを指定することもできます。

C#
FileName = @"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe";

PowerShell 7を使う場合は、pwsh.exeを指定します。

C#
FileName = "pwsh.exe";

ただし、PowerShell 7がインストールされていない環境ではpwsh.exeは見つかりません。必要に応じて、アプリ起動時に存在確認を行いましょう。

C#
if (!File.Exists(@"C:\Program Files\PowerShell\7\pwsh.exe"))
{
Console.WriteLine("PowerShell 7が見つかりません。");
}

8-2. ps1ファイルの実行が許可されていない

.ps1ファイルが実行できない場合は、実行ポリシーが原因の可能性があります。

エラーメッセージに「running scripts is disabled」や「スクリプトの実行が無効」といった内容が含まれている場合は、実行ポリシーを確認します。

PowerShell
Get-ExecutionPolicy

C#から一時的に回避する場合は、次のように指定します。

C#
Arguments = "-NoProfile -ExecutionPolicy Bypass -File \"C:\\Scripts\\sample.ps1\"";

また、ダウンロードしたスクリプトの場合はブロック解除が必要なことがあります。

PowerShell
Unblock-File -Path "C:\Scripts\sample.ps1"

本番環境では、実行ポリシーを下げるだけでなく、信頼できるスクリプトのみ実行できる設計にしましょう。

8-3. 権限不足でコマンドが失敗する

サービスの操作、システムフォルダへの書き込み、レジストリ変更などは、通常ユーザー権限では失敗する場合があります。

この場合、標準エラーに「Access is denied」や「アクセスが拒否されました」といったメッセージが出力されます。

管理者権限が必要な場合は、C#アプリを管理者として起動するか、PowerShellプロセスを昇格実行します。

C#
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -File \"C:\\Scripts\\admin.ps1\"",
UseShellExecute = true,
Verb = "runas"
};

ただし、昇格実行では標準出力のリダイレクトが難しくなるため、実行結果はログファイルに出力するなどの工夫が必要です。

8-4. 標準出力・標準エラーが取得できない

標準出力や標準エラーが取得できない場合は、設定を確認してください。

C#
UseShellExecute = false;
RedirectStandardOutput = true;
RedirectStandardError = true;

この3つの設定が重要です。UseShellExecute = trueになっていると、標準出力や標準エラーをC#側で取得できません。

また、PowerShell側でWrite-Hostを使っている場合、期待どおりに標準出力として取得できないことがあります。C#で結果を取得したい場合は、Write-Outputを使うほうが扱いやすいです。

PowerShell
Write-Output "結果を返します"

スクリプトの戻り値をC#で利用したい場合は、PowerShell側の出力方法も見直しましょう。

8-5. 文字化けやエンコードの問題が発生する

日本語が文字化けする場合は、C#側とPowerShell側のエンコード設定を確認します。

C#側では、次のように設定できます。

C#
StandardOutputEncoding = Encoding.UTF8;
StandardErrorEncoding = Encoding.UTF8;

PowerShell側では、次のように出力エンコードを指定できます。

PowerShell
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

C#から実行する場合は、コマンドの先頭に含めることもできます。

C#
string command = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Write-Output '日本語テスト'";

PowerShell 7ではUTF-8を扱いやすくなっていますが、Windows PowerShellでは環境によって文字化けしやすいことがあります。文字化けが発生した場合は、PowerShellの種類、コンソールのコードページ、C#側の読み取りエンコードを確認しましょう。

8-6. 実行が終わらず処理が止まる

C#からPowerShellを実行したとき、処理が終わらず止まってしまうことがあります。

原因としては、PowerShell側で入力待ちになっている、長時間処理が実行されている、標準出力や標準エラーの読み取りでデッドロックが発生している、などが考えられます。

対策として、タイムアウト処理を入れましょう。

C#
using var process = Process.Start(psi);

if (!process.WaitForExit(30000))
{
process.Kill();
Console.WriteLine("PowerShellの実行がタイムアウトしました。");
}

また、大量の出力がある場合は、標準出力と標準エラーを非同期で読み取ることをおすすめします。

PowerShell側で確認プロンプトが出るコマンドを実行している場合は、-Confirm:$falseを付けるなど、入力待ちにならないように設計することも重要です。

9. 用途別の実装サンプル

ここからは、C#からPowerShellを実行する具体的な用途別サンプルを紹介します。

プロセス一覧の取得、サービス状態の取得、ファイル操作、リモートPCへの実行、ログ出力など、実務でよく使うパターンを確認していきましょう。

9-1. Get-Processの結果をC#で取得する

Get-Processを実行して、プロセス一覧を取得するサンプルです。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -Command \"Get-Process | Select-Object -First 5\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = Process.Start(psi);

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

if (process.ExitCode == 0)
{
Console.WriteLine(output);
}
else
{
Console.WriteLine(error);
}
}
}

単純に表示結果を取得したいだけであれば、このように標準出力を取得するだけでも利用できます。

ただし、プロセス名やIDを個別に扱いたい場合は、System.Management.Automationを使ってPSObjectとして取得するほうが便利です。

9-2. サービスの状態を取得する

Windowsサービスの状態を取得する例です。

C#
using System;
using System.Management.Automation;

class Program
{
static void Main()
{
using var ps = PowerShell.Create();

ps.AddCommand("Get-Service")
.AddParameter("Name", "wuauserv");

var results = ps.Invoke();

foreach (var item in results)
{
var name = item.Properties["Name"]?.Value;
var status = item.Properties["Status"]?.Value;
var displayName = item.Properties["DisplayName"]?.Value;

Console.WriteLine($"Name: {name}");
Console.WriteLine($"Status: {status}");
Console.WriteLine($"DisplayName: {displayName}");
}

if (ps.HadErrors)
{
foreach (var error in ps.Streams.Error)
{
Console.WriteLine(error.ToString());
}
}
}
}

サービス名や状態をプロパティとして取得できるため、画面表示や条件分岐に使いやすい実装です。

9-3. ファイル操作コマンドを実行する

PowerShellでフォルダを作成する例です。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
string folderPath = @"C:\Temp\SampleFolder";

var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

psi.ArgumentList.Add("-NoProfile");
psi.ArgumentList.Add("-Command");
psi.ArgumentList.Add("New-Item -ItemType Directory -Force -Path $args[0]");
psi.ArgumentList.Add(folderPath);

using var process = Process.Start(psi);

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

Console.WriteLine(output);

if (process.ExitCode != 0)
{
Console.WriteLine(error);
}
}
}

ファイル操作では、パスに空白や特殊文字が含まれることが多いため、文字列連結には注意が必要です。可能であれば、SDK方式のAddParameterを使うか、引数を分離して渡す方法を検討してください。

9-4. リモートPCに対してPowerShellを実行する

リモートPCに対してPowerShellを実行する場合は、PowerShell Remotingを利用します。

たとえば、Invoke-Commandを使うと、リモートPC上でコマンドを実行できます。

C#
using System;
using System.Diagnostics;

class Program
{
static void Main()
{
string computerName = "RemotePC01";

var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

psi.ArgumentList.Add("-NoProfile");
psi.ArgumentList.Add("-Command");
psi.ArgumentList.Add($"Invoke-Command -ComputerName '{computerName}' -ScriptBlock {{ Get-Service -Name wuauserv }}");

using var process = Process.Start(psi);

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

Console.WriteLine(output);

if (process.ExitCode != 0)
{
Console.WriteLine(error);
}
}
}

リモート実行を行うには、対象PC側でPowerShell Remotingが有効になっている必要があります。また、認証、ファイアウォール、権限、ネットワーク設定も関係します。

リモートPC名をユーザー入力から受け取る場合は、許可されたPC名だけを受け付けるなど、安全性を確保しましょう。

9-5. 実行結果を画面やログに表示する

PowerShellの実行結果を画面に表示しつつ、ログにも残す例です。

C#
using System;
using System.Diagnostics;
using System.IO;

class Program
{
static void Main()
{
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -Command \"Get-Date\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};

using var process = Process.Start(psi);

string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

process.WaitForExit();

string message = $@"
実行日時: {DateTime.Now}
ExitCode: {process.ExitCode}
Output:
{output}
Error:
{error}
";

Console.WriteLine(message);
File.AppendAllText("powershell-log.txt", message);
}
}

ログを残すことで、ユーザー環境で問題が発生したときに原因を追跡しやすくなります。

ただし、ログには機密情報を出力しないように注意してください。PowerShellコマンドに認証情報やトークンを含める場合は、ログ出力時にマスクするなどの対策が必要です。

10. C#からPowerShellを実行する実装のベストプラクティス

C#からPowerShellを実行する実装では、動けばよいという考え方では不十分です。実務で安定して使うには、実行方法の選定、エラー処理、セキュリティ、タイムアウト、ログ設計まで考慮する必要があります。

ここでは、実装時に意識したいベストプラクティスをまとめます。

10-1. 目的に応じてProcess.StartとSDKを選ぶ

単純にPowerShellコマンドや.ps1ファイルを実行したい場合は、Process.Startが手軽です。

一方、C#コード内でPowerShellコマンドを構造的に組み立てたい場合や、結果をオブジェクトとして扱いたい場合は、System.Management.Automationを使うのがおすすめです。

判断基準は次のようになります。

Process.Startが向いているのは、外部プロセスとしてPowerShellを実行したい場合、既存の.ps1ファイルを呼び出したい場合、実装をシンプルにしたい場合です。

System.Management.Automationが向いているのは、結果をPSObjectとして扱いたい場合、AddParameterで安全に引数を渡したい場合、C#アプリ内でPowerShell実行を細かく制御したい場合です。

10-2. 標準出力・標準エラー・ExitCodeを必ず確認する

PowerShellを実行したら、標準出力だけでなく、標準エラーとExitCodeも確認しましょう。

C#
if (process.ExitCode == 0)
{
// 成功
}
else
{
// 失敗
Console.WriteLine(error);
}

標準出力には正常な結果、標準エラーにはエラー内容、ExitCodeにはプロセスとしての終了状態が入ります。

この3つを確認することで、失敗時の原因を把握しやすくなります。

10-3. 引数は安全に組み立てる

PowerShellコマンドを文字列連結で組み立てると、引用符の問題やコマンドインジェクションのリスクが発生します。

ProcessStartInfoを使う場合は、できるだけArgumentListを使いましょう。

C#
psi.ArgumentList.Add("-File");
psi.ArgumentList.Add(scriptPath);
psi.ArgumentList.Add("-Name");
psi.ArgumentList.Add(name);

System.Management.Automationを使う場合は、AddCommandAddParameterを使います。

C#
ps.AddCommand("Get-Service")
.AddParameter("Name", serviceName);

特に、ユーザー入力を含む場合は、入力値の検証と許可リストによる制限を必ず検討してください。

10-4. タイムアウト処理を入れる

PowerShellコマンドが想定外に長時間実行された場合、C#アプリケーション全体が停止したように見えることがあります。

そのため、タイムアウト処理を入れておくことが重要です。

C#
using var process = Process.Start(psi);

if (!process.WaitForExit(60000))
{
process.Kill();
throw new TimeoutException("PowerShellの実行がタイムアウトしました。");
}

非同期処理を使う場合も、CancellationTokenなどを組み合わせてキャンセルできる設計にしておくと安全です。

長時間実行が想定されるスクリプトでは、進捗ログを出力する、処理を分割する、タイムアウト時間を設定ファイルで変更できるようにするなどの工夫も有効です。

10-5. エラー時に原因を追跡できるログを残す

本番環境では、エラーが発生したときに原因を追跡できるログが重要です。

ログには、次の情報を残すと調査しやすくなります。

実行日時、実行対象のスクリプト、引数、終了コード、標準出力、標準エラー、例外メッセージなどです。

ただし、ログに機密情報を残さないように注意してください。パスワード、トークン、個人情報、接続文字列などは、マスクまたは出力対象外にする必要があります。

安全で調査しやすいログを残すことで、開発環境では再現しないトラブルにも対応しやすくなります。

まとめ

C#からPowerShellを実行するには、主にProcess.Startを使う方法と、System.Management.Automationを使う方法があります。

Process.Startは、powershell.exepwsh.exeを外部プロセスとして起動する方法です。既存のPowerShellコマンドや.ps1ファイルを実行しやすく、シンプルな実装に向いています。標準出力や標準エラーを取得するには、UseShellExecute = falseRedirectStandardOutput = trueRedirectStandardError = trueを設定します。

一方、System.Management.Automationは、PowerShell SDKを利用してC#コード内からPowerShellを実行する方法です。AddCommandAddParameterを使って安全にコマンドを構築でき、実行結果をPSObjectとして取得できます。結果を構造化して扱いたい場合や、コマンドインジェクション対策を重視する場合に適しています。

どちらの方法を使う場合でも、標準出力、標準エラー、ExitCodeの確認は重要です。また、日本語の文字化け対策、実行ポリシー、管理者権限、タイムアウト、ログ出力も忘れずに実装しましょう。

C#からPowerShellを実行する処理は便利ですが、強力な操作ができる分、セキュリティ面の配慮が欠かせません。ユーザー入力をそのままコマンドに渡さず、引数を安全に扱い、必要最小限の権限で実行することが大切です。

目的に応じてProcess.StartSystem.Management.Automationを使い分ければ、C#アプリケーションからPowerShellの機能を安全かつ効率的に活用できます。