はい、承知いたしました。C#からコマンドを実行する方法について、約5000語の詳細な解説記事を記述します。
C#でコマンドを実行する方法を徹底解説【サンプルコード付き】 – 同期・非同期、出力取得、エラー処理まで
はじめに
ソフトウェア開発において、外部のコマンドラインツールやスクリプトを実行する必要に迫られることは少なくありません。例えば、画像処理のために外部の画像変換ツールを呼び出したり、システム情報を取得するためにOSのコマンドを実行したり、あるいは他のプログラムとの連携のために特定の実行ファイルを起動したりする場合などです。
C#/.NET Framework/.NET Core 環境では、System.Diagnostics.Process
クラスを使用することで、これらの外部プロセスを簡単に起動し、制御することができます。しかし、単に起動するだけでなく、実行結果(標準出力、標準エラー出力)を取得したり、エラーが発生した場合に対処したり、あるいは非同期で実行してアプリケーションの応答性を保ったりと、様々な要件が出てきます。
本記事では、C#から外部コマンドを実行するための基本的な方法から、同期実行と非同期実行の違い、標準出力・標準エラー出力の取得方法、エラーハンドリング、そしてセキュリティやクロスプラットフォーム開発における注意点に至るまで、System.Diagnostics.Process
クラスを使いこなすための知識を網羅的に解説します。豊富なサンプルコードと共に、実践的なスキルを習得することを目指します。
この解説を通じて、読者の皆様がC#アプリケーションから外部プロセスを安全かつ効率的に操作できるようになることを願っています。
C#におけるコマンド実行の基本:System.Diagnostics.Process
クラス
C#で外部の実行ファイルやコマンドを起動する際に中心となるのが、System.Diagnostics
名前空間に属する Process
クラスです。このクラスは、ローカルまたはリモートのプロセスを開始、停止、制御、および監視するための機能を提供します。
Process
クラスとは
Process
クラスは、現在実行中のプロセスや新しく起動するプロセスを表すために使用されます。このクラスのインスタンスを通じて、プロセスの開始や終了、優先度の設定、関連するスレッドの操作、および標準入力/出力/エラー ストリームへのアクセスなどが行えます。
ProcessStartInfo
クラスとは
Process
クラスのインスタンスを生成し、Start()
メソッドを呼び出すことでプロセスを起動しますが、どのようにプロセスを起動するか(どの実行ファイルを、どのような引数で、どのような環境で)を詳細に設定するために使用されるのが ProcessStartInfo
クラスです。
ProcessStartInfo
オブジェクトには、以下のような重要なプロパティがあります。
FileName
: 実行するプログラムまたはコマンドのパスを指定します。環境変数 PATH に含まれる実行ファイルであれば、ファイル名だけでもかまいません。Arguments
: 実行ファイルに渡すコマンドライン引数を指定します。WorkingDirectory
: プロセスが実行される作業ディレクトリを指定します。省略した場合、通常はアプリケーションの現在の作業ディレクトリになります。UseShellExecute
: プロセスをOSのシェル(Windowsではcmd.exeやPowerShell、Linux/macOSではbashなど)を使用して起動するかどうかを指定します。この設定は、後述する出力リダイレクトやウィンドウ制御などに影響します。デフォルトはtrue
です。RedirectStandardOutput
: 標準出力をリダイレクトするかどうかを指定します。true
に設定すると、プロセスの標準出力はProcess
オブジェクトのStandardOutput
ストリームから読み取れるようになります。このプロパティをtrue
にする場合、UseShellExecute
はfalse
に設定する必要があります。RedirectStandardError
: 標準エラー出力をリダイレクトするかどうかを指定します。true
に設定すると、プロセスの標準エラー出力はProcess
オブjectのStandardError
ストリームから読み取れるようになります。このプロパティをtrue
にする場合、UseShellExecute
はfalse
に設定する必要があります。CreateNoWindow
: 新しいプロセスのためにウィンドウを作成するかどうかを指定します。true
に設定すると、ウィンドウは作成されません。コンソールアプリケーションをバックグラウンドで実行したい場合などに使用します。WindowStyle
: 新しいプロセスのウィンドウの初期状態(通常、最小化、最大化、非表示など)を指定します。CreateNoWindow
がtrue
の場合は無視されます。EnvironmentVariables
: プロセスに渡す環境変数のコレクションです。親プロセスの環境変数を引き継ぎますが、ここで追加または変更することもできます。
これらのプロパティを適切に設定することで、様々な要件に応じたプロセスの起動が可能になります。
最も基本的なコマンド実行(同期・出力無視)
まずは、最もシンプルなコマンド実行方法から見ていきましょう。これは、単に外部プログラムを起動するだけで、その出力や終了を待つ必要がない場合に使用できます。
コード例:notepad.exe を起動する (Windows)
“`csharp
using System;
using System.Diagnostics;
public class BasicProcessExample
{
public static void Main(string[] args)
{
try
{
// ProcessStartInfo オブジェクトを作成
ProcessStartInfo startInfo = new ProcessStartInfo();
// 実行するファイル名を指定 (Windowsのメモ帳)
startInfo.FileName = "notepad.exe";
// 引数は指定しない場合は省略可能
// startInfo.Arguments = "";
// シェルを使って起動するかどうか (デフォルトは true)
// true の場合、OSのファイル関連付けやPATH環境変数が利用される
startInfo.UseShellExecute = true;
// 新しい Process オブジェクトを作成し、設定情報を渡す
Process process = new Process();
process.StartInfo = startInfo;
// プロセスを起動する
process.Start();
Console.WriteLine("notepad.exe が起動しました。");
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
}
}
“`
コード解説
using System.Diagnostics;
を追加して、Process
クラスを利用できるようにします。ProcessStartInfo
オブジェクトstartInfo
を作成します。これがプロセス起動のための設定情報となります。startInfo.FileName
に実行したいプログラムのファイル名を指定します。ここではWindowsのメモ帳 (notepad.exe
) を指定しています。もし実行ファイルが環境変数 PATH に登録されていれば、フルパスでなくてもかまいません。フルパスで指定する方が確実な場合が多いです。startInfo.UseShellExecute = true;
と設定しています。これは、OSのシェル(コマンドプロンプトなど)を介してプロセスを起動することを意味します。true
にすると、ファイル名の解決(PATH環境変数の利用)や、ファイル関連付けに基づいたプログラムの起動(例: .txt ファイルを指定するとメモ帳で開く)などが可能になります。また、通常は新しいコンソールウィンドウが表示されます。出力のリダイレクトを行わない場合は、通常true
で問題ありません。Process
オブジェクトprocess
を作成し、先ほど作成したstartInfo
オブジェクトをprocess.StartInfo
プロパティに設定します。process.Start()
メソッドを呼び出して、設定に基づいたプロセスを起動します。このメソッドは、プロセスが正常に起動できた場合にtrue
を返します。起動に失敗した場合は例外が発生します。- 起動に成功した場合は、プロセスはバックグラウンドで実行され続けます。このコード例では、起動した直後にコンソールにメッセージを表示して終了します。
この基本的な方法は、単に外部アプリケーションを起動したいだけで、その完了を待ったり、出力を処理したりする必要がない場合に適しています。
引数付きコマンドの実行
多くのコマンドラインツールは、動作を制御するためにコマンドライン引数を受け取ります。ProcessStartInfo
の Arguments
プロパティを使用することで、実行するプログラムに引数を渡すことができます。
引数は、プログラム名に続けてスペース区切りで指定するのが一般的です。複数の引数がある場合もスペースで区切ります。引数にスペースが含まれる場合は、ダブルクォーテーション ("
) で囲む必要があります。
コード例:echo コマンドを実行する (Windows)
Windowsの echo
コマンドは、引数として渡された文字列を標準出力に出力します。
“`csharp
using System;
using System.Diagnostics;
public class ArgumentsExampleWindows
{
public static void Main(string[] args)
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo();
// 実行するコマンド (cmd.exe をシェルとして使う)
// UseShellExecute = true のため、ファイル名解決はシェルが行う
startInfo.FileName = "cmd.exe";
// /c オプションはコマンド実行後に cmd.exe を終了させる
// echo コマンドとその引数を指定
startInfo.Arguments = "/c echo Hello, Command Line!";
// シェルを使用 (デフォルト true)
startInfo.UseShellExecute = true;
// ウィンドウ表示スタイルを指定することも可能
// startInfo.WindowStyle = ProcessWindowStyle.Normal;
Process process = new Process();
process.StartInfo = startInfo;
Console.WriteLine($"コマンド実行中: {startInfo.FileName} {startInfo.Arguments}");
// プロセスを起動
// この例では出力を取得しないため、同期的に終了を待つ必要はないが、
// cmd.exe /c はすぐに終了するので Start() だけで十分
process.Start();
Console.WriteLine("コマンドが実行されました。");
// 注意:UseShellExecute = true の場合、StandardOutput/Error はリダイレクトできない
// よって、ここでは出力を取得する方法は使用できない
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
}
}
“`
コード例:echo コマンドを実行する (Linux/macOS)
Linux/macOSでは、直接 echo
コマンドを指定できます。
“`csharp
using System;
using System.Diagnostics;
public class ArgumentsExampleLinux
{
public static void Main(string[] args)
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo();
// 実行するコマンド (Linux/macOSの echo コマンド)
// PATH 環境変数に含まれていることが前提
startInfo.FileName = "echo";
// echo コマンドの引数を指定
startInfo.Arguments = "Hello, Command Line!";
// シェルを使用 (デフォルト true)
startInfo.UseShellExecute = true; // Linux/macOSでも有効
Process process = new Process();
process.StartInfo = startInfo;
Console.WriteLine($"コマンド実行中: {startInfo.FileName} {startInfo.Arguments}");
// プロセスを起動
process.Start();
Console.WriteLine("コマンドが実行されました。");
// UseShellExecute = true のため、出力取得はこの方法ではできない
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
}
}
“`
上記の例はどちらも UseShellExecute = true
を使用しているため、コマンドの出力を直接C#コード内で取得することはできません。出力が必要な場合は、後述する出力リダイレクトの方法を使用する必要があります(その場合は UseShellExecute = false
となります)。
引数の指定方法と注意点
Arguments
プロパティに渡す文字列は、OSが実行ファイルと引数を解釈する方法に従う必要があります。通常はスペース区切りですが、引数にスペースが含まれる場合は"
で囲む必要があります。- 例えば、
"C:\My Documents\My File.txt"
というパスを引数として渡したい場合、Arguments
文字列は"
C:\My Documents\My File.txt"
のように指定します。C#の文字列リテラル内でダブルクォーテーションを使う場合は、エスケープするか@
文字列リテラルを使用します。例えば、"\"C:\\My Documents\\My File.txt\""
または@"""C:\My Documents\My File.txt"""
のようになります。 - ユーザーからの入力を引数として使用する場合は、セキュリティ上のリスク(コマンドインジェクション)が発生する可能性があります。これは後述のセキュリティセクションで詳しく説明します。
作業ディレクトリの指定
プロセスが実行される際の現在のディレクトリ(作業ディレクトリ)を指定したい場合があります。例えば、特定のスクリプトファイルが他のファイルを参照する場合などです。ProcessStartInfo
の WorkingDirectory
プロパティを使用します。
コード例:特定のディレクトリでコマンドを実行する
“`csharp
using System;
using System.Diagnostics;
using System.IO; // Path クラスのために必要
public class WorkingDirectoryExample
{
public static void Main(string[] args)
{
try
{
// 一時的に作業ディレクトリとして使用するフォルダを作成 (例)
string tempDir = Path.Combine(Path.GetTempPath(), “MyProcessWorkingDir”);
if (!Directory.Exists(tempDir))
{
Directory.CreateDirectory(tempDir);
}
Console.WriteLine($”一時作業ディレクトリ: {tempDir}”);
ProcessStartInfo startInfo = new ProcessStartInfo();
// 実行するコマンド (Windows: dir, Linux/macOS: ls)
// ディレクトリの内容一覧を表示するコマンド
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
{
startInfo.FileName = "cmd.exe";
startInfo.Arguments = "/c dir"; // Windows の dir コマンドを実行
}
else
{
startInfo.FileName = "ls"; // Linux/macOS の ls コマンドを実行
startInfo.Arguments = ""; // ls は引数なしで現在のディレクトリを一覧
}
// 作業ディレクトリを指定
startInfo.WorkingDirectory = tempDir;
// シェルを使用 (デフォルト true)
startInfo.UseShellExecute = true;
Process process = new Process();
process.StartInfo = startInfo;
Console.WriteLine($"コマンド実行中 ({startInfo.WorkingDirectory}): {startInfo.FileName} {startInfo.Arguments}");
process.Start();
Console.WriteLine("コマンドが実行されました。");
// 例: 作成した一時ディレクトリをクリーンアップ
// System.Threading.Thread.Sleep(1000); // コマンド終了を待つ場合は WaitForExit などが必要
// Directory.Delete(tempDir, true); // 要件に応じて
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
}
}
“`
この例では、コマンド(Windowsなら dir
、Linux/macOSなら ls
)が、指定された WorkingDirectory
内で実行されます。これにより、コマンドが特定のファイルやディレクトリにアクセスする際に、フルパスではなく相対パスを使用できるようになります。
コマンド出力の取得(同期的な方法)
外部コマンドを実行する目的の一つに、そのコマンドが標準出力や標準エラー出力に出力した内容を取得したいというケースがあります。例えば、git
コマンドの実行結果を取得したり、ping
コマンドの応答時間を確認したりする場合です。
Process
クラスでは、これらの出力をC#アプリケーション内でリダイレクトして読み取ることができます。
標準出力・標準エラー出力のリダイレクト
出力のリダイレクトを有効にするには、ProcessStartInfo
オブジェクトの以下のプロパティを設定します。
RedirectStandardOutput = true;
RedirectStandardError = true;
非常に重要な注意点として、これらのリダイレクトを有効にする場合は、必ず UseShellExecute
プロパティを false
に設定する必要があります。 UseShellExecute = true
の場合、プロセスはOSのシェルによって起動され、そのシェルの出力ストリームはC#アプリケーションからは直接制御できないためです。
UseShellExecute = false
に設定すると、プロセスはシェルを介さずに直接起動されます。これにより、C#アプリケーションはプロセスの標準入力、標準出力、標準エラー出力のストリームを直接操作できるようになります。
同期的な出力の読み取り
リダイレクトを有効にした後、プロセスの標準出力 (StandardOutput
プロパティ) および標準エラー出力 (StandardError
プロパティ) からデータを読み取ることができます。
同期的に読み取る場合、StandardOutput.ReadToEnd()
や StandardError.ReadToEnd()
メソッドを使用します。これらのメソッドは、プロセスがすべての出力を終えて終了するまで、またはストリームが閉じられるまでブロックします。
同期読み取りにおけるデッドロックの危険性
同期的に出力ストリームを読み取る際に、デッドロックが発生する可能性があります。これは特に、出力データ量が大きい場合に起こりやすいです。
デッドロックは、以下の条件が同時に満たされる場合に発生する可能性があります。
- 親プロセス(C#アプリケーション)が子プロセス(実行したコマンド)の終了を
process.WaitForExit()
などで待っている。 - 子プロセスが標準出力や標準エラー出力に大量のデータを書き出している。
- OSのパイプバッファがいっぱいになり、子プロセスがそれ以上データを書き込めずブロックしている。
- 親プロセスが子プロセスの終了を待っているため、出力バッファからデータを読み取ることができず、子プロセスもブロックしたまま親プロセスの読み取りを待つ形になり、双方が互いを待ち合って停止してしまう。
このデッドロックを避けるためには、親プロセスが子プロセスの終了を待つ前に、子プロセスの出力ストリームからデータを読み取る必要があります。あるいは、出力ストリームの読み取りとプロセスの終了待機を並行して行う必要があります。
最も簡単なデッドロック回避策は、ReadToEnd()
を呼び出す前に process.WaitForExit()
を呼び出さないことです。しかし、これだけでは不十分な場合があります。確実なデッドロック回避策は、出力ストリームの読み取りを別のスレッドで行うか、後述する非同期イベント駆動方式で出力データを処理することです。
ただし、出力データ量が非常に少ないことが確実なコマンド(例: echo
)であれば、同期的な ReadToEnd()
でデッドロックが発生する可能性は低いですが、安全のためには非同期方式を推奨します。
ここではまず、デッドロックが発生しにくい、比較的短い出力を持つコマンドでの同期出力取得例を示します。
コード例:同期的にコマンドの出力を取得する (Windows: dir)
“`csharp
using System;
using System.Diagnostics;
using System.IO; // Path クラスのために必要
public class SyncOutputExampleWindows
{
public static void Main(string[] args)
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo();
// 実行するコマンド (Windows の dir コマンド)
// /c オプションは cmd.exe を終了させる
startInfo.FileName = "cmd.exe";
startInfo.Arguments = "/c dir";
// シェルを使用しない設定!
// これがないと出力のリダイレクトはできません
startInfo.UseShellExecute = false;
// 標準出力と標準エラー出力をリダイレクトする
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
// ウィンドウを作成しない (バックグラウンド実行のように)
startInfo.CreateNoWindow = true;
Process process = new Process();
process.StartInfo = startInfo;
Console.WriteLine($"コマンド実行中 (UseShellExecute=false): {startInfo.FileName} {startInfo.Arguments}");
// プロセスを起動
process.Start();
// ★重要★ デッドロック回避のため、WaitForExit() の前に ReadToEnd() を実行しない
// または ReadToEnd() を実行中に別のスレッドで WaitForExit() を実行する必要がある。
// ここでは、出力がそれほど大きくないコマンドを想定し、
// まず出力を完全に読み取ってから終了を待つ(または待たない)方法をとる。
// ReadToEnd() はプロセスが終了するまでブロックするので、
// プロセス終了前に ReadToEnd() を実行すれば、終了待ちは別途必須ではない場合が多い。
// 標準出力を全て読み取る
string standardOutput = process.StandardOutput.ReadToEnd();
// 標準エラー出力を全て読み取る
string standardError = process.StandardError.ReadToEnd();
// プロセスの終了を待つ (任意だが、終了コード取得のためには必要)
// ReadToEnd() がプロセス終了まで待つため、多くの場合不要または高速に終了する
// process.WaitForExit(); // ここで待つとデッドロックの可能性が高まる
// プロセスの終了コードを取得 (WaitForExit() 後に有効)
// int exitCode = process.ExitCode; // ReadToEnd() 後に取得可能
Console.WriteLine("\n--- 標準出力 ---");
Console.WriteLine(standardOutput);
Console.WriteLine("\n--- 標準エラー出力 ---");
Console.WriteLine(standardError);
// Console.WriteLine($"\n終了コード: {exitCode}"); // 取得した場合
// リソースを解放する
process.Close();
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
}
}
“`
コード例:同期的にコマンドの出力を取得する (Linux/macOS: ls)
“`csharp
using System;
using System.Diagnostics;
using System.IO; // Path クラスのために必要
public class SyncOutputExampleLinux
{
public static void Main(string[] args)
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo();
// 実行するコマンド (Linux/macOS の ls コマンド)
startInfo.FileName = "ls";
startInfo.Arguments = "-l"; // 詳細表示オプション
// シェルを使用しない設定!
startInfo.UseShellExecute = false;
// 標準出力と標準エラー出力をリダイレクトする
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
// ウィンドウを作成しない (コンソールアプリのため影響は少ないが慣例的に)
startInfo.CreateNoWindow = true;
Process process = new Process();
process.StartInfo = startInfo;
Console.WriteLine($"コマンド実行中 (UseShellExecute=false): {startInfo.FileName} {startInfo.Arguments}");
// プロセスを起動
process.Start();
// 標準出力を全て読み取る
string standardOutput = process.StandardOutput.ReadToEnd();
// 標準エラー出力を全て読み取る
string standardError = process.StandardError.ReadToEnd();
// プロセスの終了を待つ(デッドロックの可能性を考慮し、出力読み取り後に推奨)
// process.WaitForExit(); // ReadToEnd() がブロックするため多くの場合不要または高速
// プロセスの終了コードを取得 (WaitForExit() 後に有効)
// int exitCode = process.ExitCode; // ReadToEnd() 後に取得可能
Console.WriteLine("\n--- 標準出力 ---");
Console.WriteLine(standardOutput);
Console.WriteLine("\n--- 標準エラー出力 ---");
Console.WriteLine(standardError);
// Console.WriteLine($"\n終了コード: {exitCode}"); // 取得した場合
// リソースを解放する
process.Close();
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
}
}
“`
コード解説:デッドロック回避のベストプラクティス(同期版)
上記の同期的な出力取得例では、デッドロックが発生しないように process.StandardOutput.ReadToEnd()
と process.StandardError.ReadToEnd()
を process.WaitForExit()
より先に呼び出しています。
ReadToEnd()
メソッドは、対応するストリームがクローズされるまで(つまり、子プロセスが終了するまで)ブロックします。そのため、ReadToEnd()
を呼び出すこと自体が子プロセスの終了を待つ役割を果たします。子プロセスが終了すれば、出力バッファへの書き込みも止まり、パイプがクローズされるため、ReadToEnd()
は正常に完了します。
ただし、これはあくまで「出力がそれほど膨大でなく、出力バッファが枯渇する前にプロセスが自然に終了する」という前提に基づいています。もし子プロセスが非常に大量のデータを標準出力/エラー出力に書き続け、かつ終了しないような場合(例:無限ループでログを吐き続ける)、親プロセスは ReadToEnd()
でブロックしたままになり、子プロセスも出力バッファ満杯でブロックし、結局デッドロックと同じ状況に陥る可能性があります。
したがって、出力データ量が大きい可能性のあるコマンドや、実行時間が予測できないコマンドの場合は、非同期的な出力取得方法を使用することを強く推奨します。
非同期コマンド実行と出力取得
同期実行、特に同期的な出力読み取りは、呼び出し元のスレッドをブロックしてしまいます。これは、GUIアプリケーションのUIスレッドで行うと、アプリケーションがフリーズする原因となります。また、大量の出力が発生する場合のデッドロックのリスクも無視できません。
これらの問題を解決するためには、非同期的なコマンド実行と出力取得が有効です。Process
クラスは、出力データが利用可能になったときにイベントを発生させる仕組みを提供しています。
なぜ非同期が必要か? 同期実行の限界
- UIフリーズ: GUIアプリケーションで同期実行を行うと、コマンドの完了までUIが応答しなくなり、ユーザーエクスペリエンスが悪化します。
- デッドロック: 大量の出力が発生するコマンドを同期的に実行し、かつ終了を待つ場合に、デッドロックのリスクが高まります。
- 進行状況の表示: 同期実行では、コマンドが実行中にどの程度進んでいるかといった中間的な情報を取得するのが困難です。
非同期実行とイベント駆動型の出力処理を用いることで、これらの問題を回避できます。
非同期実行のための設定
非同期でコマンドを実行し、出力イベントを受け取るには、以下の設定が必要です。
ProcessStartInfo.UseShellExecute = false;
(出力リダイレクトに必須)ProcessStartInfo.RedirectStandardOutput = true;
ProcessStartInfo.RedirectStandardError = true;
Process.EnableRaisingEvents = true;
(これはProcess
オブジェクトのプロパティであり、ProcessStartInfo
ではないことに注意) – これをtrue
にすることで、Exited
,OutputDataReceived
,ErrorDataReceived
などのイベントが発生するようになります。
出力データ受信イベント (OutputDataReceived
, ErrorDataReceived
)
Process.EnableRaisingEvents = true
に設定すると、以下のイベントを購読(サブスクライブ)できるようになります。
OutputDataReceived
: 標準出力に新しいデータ行が書き込まれるたびに発生します。ErrorDataReceived
: 標準エラー出力に新しいデータ行が書き込まれるたびに発生します。
これらのイベントハンドラは、DataReceivedEventArgs
という引数を受け取ります。この引数の Data
プロパティに、読み取られたデータ行(文字列)が含まれています。データがない場合 (Data
が null
) は、ストリームの終わりを示します。
非同期読み取りの開始 (BeginOutputReadLine
, BeginErrorReadLine
)
イベントを設定しただけでは、非同期での読み取りは始まりません。Process.Start()
の呼び出し後、以下のメソッドを呼び出すことで、それぞれのストリームからの非同期読み取りを開始します。
process.BeginOutputReadLine();
process.BeginErrorReadLine();
これらのメソッドは、内部的にスレッドプールなどを使用してバックグラウンドで出力ストリームを監視し、新しいデータ行が見つかるたびに適切なイベントを発生させます。
非同期実行の終了待機とリソース解放
非同期で実行を開始した場合でも、コマンドの完了を待って、その終了コードを確認したい場合があります。これには、同期実行と同様に process.WaitForExit()
メソッドを使用します。
ただし、非同期出力読み取りを使用している場合は、WaitForExit()
を呼び出す前に、非同期読み取りが完了していることを確認するか、または非同期読み取りをキャンセル(後述)する必要があります。 最も一般的な方法は、非同期イベントハンドラがすべて処理され、出力ストリームが閉じられた後に WaitForExit()
を呼び出すことです。幸い、WaitForExit()
は非同期読み取りが終了するのを自動的に待つように設計されています(ただし、タイムアウトを指定しない場合)。
非同期読み取りを明示的に停止したい場合は、process.CancelOutputRead()
および process.CancelErrorRead()
メソッドを使用します。
プロセスが終了し、出力ストリームの読み取りも完了したら、process.Close()
または process.Dispose()
を呼び出してリソースを解放することを忘れないでください。using
ステートメントを使用するのが最も簡単で確実な方法です。
コード例:非同期でコマンドを実行し、出力をイベントで取得する (Windows: ping)
Windowsの ping
コマンドは、連続して出力を生成するため、非同期処理に適しています。
“`csharp
using System;
using System.Diagnostics;
using System.Text; // Encoding のために必要
using System.Threading.Tasks; // Task.Delay のために必要
public class AsyncProcessExampleWindows
{
public static async Task Main(string[] args)
{
// ping コマンドの実行対象ホスト
string targetHost = “google.com”;
using (Process process = new Process())
{
try
{
process.StartInfo.FileName = "ping";
// -n 4 オプションで4回だけ実行 (無限ループにならないように)
process.StartInfo.Arguments = $"-n 4 {targetHost}";
process.StartInfo.UseShellExecute = false; // 出力リダイレクトに必須
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true; // ウィンドウを非表示に
// ★非同期イベントを有効にする★
process.EnableRaisingEvents = true;
// 標準出力データ受信イベントハンドラを追加
process.OutputDataReceived += Process_OutputDataReceived;
// 標準エラー出力データ受信イベントハンドラを追加
process.ErrorDataReceived += Process_ErrorDataReceived;
Console.WriteLine($"非同期コマンド実行中: {process.StartInfo.FileName} {process.StartInfo.Arguments}");
// プロセスを起動
process.Start();
// ★非同期出力読み取りを開始★
process.BeginOutputReadLine();
process.BeginErrorReadLine(); // エラー出力も非同期で読み取る
// プロセスが終了するまで非同期で待機
// 非同期出力読み取りも自動的に終了を待つ
await Task.Run(() => process.WaitForExit());
Console.WriteLine("\nコマンド実行が完了しました。");
// 終了コードを取得
int exitCode = process.ExitCode;
Console.WriteLine($"終了コード: {exitCode}");
// イベントハンドラの解除 (必須ではないが、丁寧にリソースを解放する場合)
process.OutputDataReceived -= Process_OutputDataReceived;
process.ErrorDataReceived -= Process_ErrorDataReceived;
// process.Dispose() は using ステートメントがやってくれる
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
} // using ブロックを抜けると process.Dispose() が呼ばれる
}
// 標準出力データ受信イベントハンドラ
private static void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
// Data が null の場合はストリームの終わり
if (e.Data != null)
{
Console.WriteLine($"[STDOUT] {e.Data}");
}
}
// 標準エラー出力データ受信イベントハンドラ
private static void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
// Data が null の場合はストリームの終わり
if (e.Data != null)
{
Console.WriteLine($"[STDERR] {e.Data}");
}
}
}
“`
コード例:非同期でコマンドを実行し、出力をイベントで取得する (Linux/macOS: ping)
Linux/macOSの ping
コマンドは、オプションが少し異なります。
“`csharp
using System;
using System.Diagnostics;
using System.Text; // Encoding のために必要
using System.Threading.Tasks; // Task.Delay のために必要
public class AsyncProcessExampleLinux
{
public static async Task Main(string[] args)
{
// ping コマンドの実行対象ホスト
string targetHost = “google.com”;
using (Process process = new Process())
{
try
{
process.StartInfo.FileName = "ping";
// -c 4 オプションで4回だけ実行
process.StartInfo.Arguments = $"-c 4 {targetHost}";
process.StartInfo.UseShellExecute = false; // 出力リダイレクトに必須
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true; // コンソールアプリのため影響は少ない
// ★非同期イベントを有効にする★
process.EnableRaisingEvents = true;
// 標準出力データ受信イベントハンドラを追加
process.OutputDataReceived += Process_OutputDataReceived;
// 標準エラー出力データ受信イベントハンドラを追加
process.ErrorDataReceived += Process_ErrorDataReceived;
Console.WriteLine($"非同期コマンド実行中: {process.StartInfo.FileName} {process.StartInfo.Arguments}");
// プロセスを起動
process.Start();
// ★非同期出力読み取りを開始★
process.BeginOutputReadLine();
process.BeginErrorReadLine(); // エラー出力も非同期で読み取る
// プロセスが終了するまで非同期で待機
await Task.Run(() => process.WaitForExit());
Console.WriteLine("\nコマンド実行が完了しました。");
// 終了コードを取得
int exitCode = process.ExitCode;
Console.WriteLine($"終了コード: {exitCode}");
// イベントハンドラの解除
process.OutputDataReceived -= Process_OutputDataReceived;
process.ErrorDataReceived -= Process_ErrorDataReceived;
// process.Dispose() は using ステートメントがやってくれる
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
} // using ブロックを抜けると process.Dispose() が呼ばれる
}
// 標準出力データ受信イベントハンドラ
private static void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data != null)
{
Console.WriteLine($"[STDOUT] {e.Data}");
}
}
// 標準エラー出力データ受信イベントハンドラ
private static void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data != null)
{
Console.WriteLine($"[STDERR] {e.Data}");
}
}
}
“`
コード解説:イベントハンドラ、非同期開始、終了待機
using System.Diagnostics;
,using System.Text;
,using System.Threading.Tasks;
を追加します。Main
メソッドをasync Task
にすることで、await
キーワードを使用可能にし、非同期処理を記述しやすくします。using (Process process = new Process())
のようにusing
ステートメントを使用することで、プロセス終了後のリソース解放を確実にします。process.StartInfo
の設定で、UseShellExecute = false;
,RedirectStandardOutput = true;
,RedirectStandardError = true;
,CreateNoWindow = true;
としています。これらは非同期出力読み取りのために必須です。process.EnableRaisingEvents = true;
を設定することで、OutputDataReceived
などのイベントが発生するようにします。process.OutputDataReceived += Process_OutputDataReceived;
およびprocess.ErrorDataReceived += Process_ErrorDataReceived;
で、それぞれのイベントが発生したときに呼び出されるメソッド(イベントハンドラ)を登録します。process.Start()
でプロセスを起動します。process.BeginOutputReadLine();
およびprocess.BeginErrorReadLine();
を呼び出すことで、非同期での出力読み取りを開始します。この呼び出しの後、新しいデータ行が出力されるたびに、登録したイベントハンドラが別スレッドで実行されます。await Task.Run(() => process.WaitForExit());
は、プロセスが終了するまで非同期的に待機する一般的なパターンです。WaitForExit()
自体はブロッキングメソッドですが、Task.Run
の中で実行し、それをawait
することで、呼び出し元のスレッド(この場合はMain
メソッドのスレッド)をブロックせずに待つことができます。WaitForExit()
は、非同期出力読み取りが完全に完了するまで待ってから終了します。- イベントハンドラ
Process_OutputDataReceived
とProcess_ErrorDataReceived
は、DataReceivedEventArgs e
を受け取ります。e.Data
には読み取られた出力行が含まれます。e.Data
がnull
になるのは、対応するストリームが閉じられた(つまり、子プロセスが終了した)ことを意味します。
この非同期方式は、長時間実行されるコマンドや、大量の出力を生成するコマンドを扱う場合に非常に有効です。UIアプリケーションであれば、UIスレッドをブロックすることなくバックグラウンドでコマンドを実行し、イベントハンドラ内でUIに進行状況や出力を表示するといった処理が可能になります。
コマンド実行の中止 (Process.Kill()
)
非同期で実行しているコマンドを途中で強制終了させたい場合もあります。これには process.Kill()
メソッドを使用します。
csharp
// プロセスを起動して非同期で実行を開始した後...
// 例: ユーザーが「キャンセル」ボタンを押したときなど
if (!process.HasExited) // プロセスがまだ実行中か確認
{
Console.WriteLine("コマンド実行を中断します...");
process.Kill(); // プロセスを強制終了
Console.WriteLine("コマンドが中断されました。");
}
Kill()
メソッドは、プロセスを強制的に終了させます。これは通常、graceful shut down (正常終了) ではありません。プロセスによってはリソースの解放が不完全になるなどの副作用がある可能性があるため、可能な限りコマンド自身に終了させるための信号(例:標準入力への特定の文字列書き込みなど)を送る方が望ましい場合もあります。しかし、一般的な外部コマンドに対しては Kill()
が最も確実な中止方法です。
Kill()
を呼び出した後、プロセスは終了状態になります。その後の WaitForExit()
はすぐに完了し、ExitCode
は通常、OSによって異なる特別な値(例: -1 または非ゼロの値)になります。非同期出力読み取りも、プロセス終了に伴い自動的に終了します。
エラーハンドリングとコマンドの終了コード
外部コマンドを実行する際には、様々なエラーが発生する可能性があります。例えば、指定した実行ファイルが見つからない、権限がない、あるいはコマンド自体が内部的に失敗した場合などです。これらのエラーに適切に対処することは、堅牢なアプリケーションを構築する上で重要です。
コマンド実行時の例外の種類と処理
process.Start()
メソッドを呼び出した際に発生する可能性のある主な例外には、以下のようなものがあります。
System.ComponentModel.Win32Exception
: プロセス起動に失敗した場合の一般的な例外です。ファイルが見つからない、権限不足、指定した引数が不正などのOSレベルのエラーが原因となることが多いです。例外のNativeErrorCode
やMessage
プロパティから詳細な情報を得られる場合があります。System.IO.FileNotFoundException
: 指定したFileName
の実行ファイルが見つからない場合に発生します(特にUseShellExecute = false
の場合)。
これらの例外は、try-catch
ブロックを使用して捕捉し、適切なエラー処理を行う必要があります。
“`csharp
try
{
using (Process process = new Process())
{
process.StartInfo.FileName = “non_existent_command.exe”; // 存在しないコマンド
process.StartInfo.UseShellExecute = false;
process.Start();
// ... 実行待機や出力取得 ...
}
}
catch (FileNotFoundException ex)
{
Console.WriteLine($”エラー: 実行ファイル ‘{ex.FileName}’ が見つかりません。パスを確認してください。”);
}
catch (System.ComponentModel.Win32Exception ex)
{
Console.WriteLine($”プロセス起動エラー ({ex.NativeErrorCode}): {ex.Message}”);
// 例: 権限不足の場合など
if (ex.NativeErrorCode == 5) // ERROR_ACCESS_DENIED
{
Console.WriteLine(“エラー: 権限が不足しています。管理者として実行する必要があるかもしれません。”);
}
}
catch (Exception ex)
{
Console.WriteLine($”予期しないエラーが発生しました: {ex.Message}”);
}
“`
Process.ExitCode
の確認
process.Start()
が成功し、プロセスが正常に実行されたとしても、コマンド自体が内部的な処理で失敗する場合があります。多くのコマンドラインツールは、その実行結果を示すために終了コード (Exit Code) を返します。
- 慣習として、終了コード
0
はコマンドが正常に完了したことを意味します。 - 非ゼロの終了コードは、通常、何らかのエラーや警告が発生したことを示します。具体的な非ゼロの終了コードが何を意味するかは、実行したコマンドやOSによって異なります。コマンドのドキュメントを確認する必要があります。
Process
オブジェクトの ExitCode
プロパティを使用することで、コマンドの終了コードを取得できます。このプロパティは、プロセスが終了した後に有効になります。同期実行であれば WaitForExit()
の後、非同期実行であれば WaitForExit()
または Exited
イベント発生後にアクセスします。
コード例:例外処理と終了コードの確認
“`csharp
using System;
using System.Diagnostics;
using System.IO; // Path クラスのために必要
using System.ComponentModel; // Win32Exception のために必要
public class ErrorHandlingExample
{
public static void Main(string[] args)
{
// 存在するがエラーを返す可能性のあるコマンドを想定 (例: ファイルを移動するがファイルが存在しない)
// ここでは例として、存在しないファイルに対して delete コマンドを実行してみる (Windows)
// Linux/macOS の場合は rm コマンドなどで同様のテストが可能
string command;
string arguments;
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
{
command = "cmd.exe";
// 存在しないファイル 'non_existent_file.txt' を削除しようとする
arguments = "/c del non_existent_file.txt";
}
else
{
command = "rm";
// 存在しないファイル 'non_existent_file.txt' を削除しようとする
arguments = "non_existent_file.txt";
}
using (Process process = new Process())
{
try
{
process.StartInfo.FileName = command;
process.StartInfo.Arguments = arguments;
process.StartInfo.UseShellExecute = false; // 出力リダイレクトのため
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
Console.WriteLine($"コマンド実行中: {process.StartInfo.FileName} {process.StartInfo.Arguments}");
process.Start();
// 非同期で出力とエラーを読み取る (デッドロック回避)
string standardOutput = process.StandardOutput.ReadToEnd();
string standardError = process.StandardError.ReadToEnd();
// プロセスが終了するまで待機
process.WaitForExit();
// 終了コードを確認
int exitCode = process.ExitCode;
Console.WriteLine("\n--- 標準出力 ---");
Console.WriteLine(standardOutput);
Console.WriteLine("\n--- 標準エラー出力 ---");
Console.WriteLine(standardError);
Console.WriteLine($"\nコマンド終了コード: {exitCode}");
if (exitCode == 0)
{
Console.WriteLine("コマンドは正常に完了しました。");
}
else
{
Console.WriteLine("コマンドはエラーまたは警告と共に完了しました。終了コードを確認してください。");
// 具体的な終了コードの意味はコマンドのドキュメントを参照
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) && exitCode == 1)
{
Console.WriteLine("(Windows del コマンドの場合、ファイルが見つからないなどのエラーで通常終了コード 1 を返します)");
}
else if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux) || System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX))
{
// Linux/macOS の rm コマンドは、ファイルが存在しない場合など非ゼロを返す
Console.WriteLine("(Linux/macOS rm コマンドの場合、ファイルが見つからないなどのエラーで通常非ゼロの終了コードを返します)");
}
}
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"エラー: 実行ファイル '{ex.FileName}' が見つかりません。パスを確認してください。");
}
catch (Win32Exception ex)
{
Console.WriteLine($"プロセス起動エラー ({ex.NativeErrorCode}): {ex.Message}");
// 例: 権限不足の場合
if (ex.NativeErrorCode == 5) // ERROR_ACCESS_DENIED (Windows)
{
Console.WriteLine("エラー: 権限が不足しています。管理者として実行する必要があるかもしれません。");
}
}
catch (Exception ex)
{
Console.WriteLine($"予期しないエラーが発生しました: {ex.Message}");
}
}
}
}
“`
この例では、存在しないファイルを削除しようとするコマンドを実行しています。これにより、コマンド自体は起動できますが、内部処理でエラーが発生し、非ゼロの終了コードを返すことが期待されます。try-catch
で起動時の例外を捕捉しつつ、起動に成功した場合は WaitForExit()
後に ExitCode
を確認してコマンド内部のエラーを判定しています。
高度な利用方法と応用例
Process
クラスは、基本的なコマンド実行だけでなく、より詳細な制御や様々な応用が可能です。
シェルを利用する場合 (UseShellExecute = true
) としない場合 (UseShellExecute = false
)
この設定は、プロセス起動の挙動に大きな違いをもたらす重要なプロパティです。
-
UseShellExecute = true
(デフォルト):- OSのシェル(Windowsではcmd.exe/PowerShell、Linux/macOSでは通常bashなど)を介してプロセスを起動します。
- メリット:
- 実行ファイルのパス解決に環境変数
PATH
が利用されます。 - ファイルの関連付けが利用できます(例:
.txt
ファイルを指定するとメモ帳で開く)。 - シェルが提供する機能(パイプ
|
、リダイレクト>
,<
など)をArguments
文字列に含めることで利用できます(ただし引数のエスケープが複雑になる)。 - 特定のURLを開く、といったOSの機能に依存した起動が可能です。
- 通常、新しいウィンドウが開きます。
- 実行ファイルのパス解決に環境変数
- デメリット:
- 標準出力/標準エラー出力のリダイレクトができません。
- プロセスの環境変数を直接制御するのが難しい場合があります。
- 権限昇格(管理者権限での実行)が必要な場合、UACプロンプトが表示されます(ShellExecuteの機能)。
- セキュリティ上のリスク(後述)がやや高まります。
-
UseShellExecute = false
:- シェルを介さず、OSのAPIを直接呼び出してプロセスを起動します。
- メリット:
- 標準出力/標準エラー出力のリダイレクトが可能です。
CreateNoWindow
やWindowStyle
プロパティによるウィンドウ表示のより詳細な制御が可能です(ウィンドウを完全に非表示にするなど)。EnvironmentVariables
プロパティによる環境変数の設定が可能です。- ユーザー資格情報を指定して別のユーザーとして実行する設定が可能です。
FileName
に指定するパスは、原則としてフルパスまたは現在の作業ディレクトリからの相対パスである必要があります(PATH環境変数はデフォルトでは使用されません)。
- デメリット:
- ファイル関連付けは利用できません。
- シェルの特殊文字(
|
,>
,<
など)はそのままでは解釈されません。これらを利用するには、明示的にcmd.exe
(Windows) やbash
/sh
(Linux/macOS) をFileName
に指定し、コマンド全体を引数として渡す必要があります。 - 権限昇格は
Verb
プロパティ(UseShellExecute = true
の場合にのみ有効)を使用する方法では行えず、別途処理が必要です。
使い分け:
- 外部プログラムを起動してユーザーに見せたい、ファイル関連付けを利用したい、またはPATH環境変数に依存したい場合は、
UseShellExecute = true
を使用します。 - コマンドの出力をプログラム内で処理したい、バックグラウンドでウィンドウなしで実行したい、環境変数を細かく制御したい、またはデッドロックのリスクを避けたい場合は、
UseShellExecute = false
を使用します。特に、本記事で解説した出力リダイレクトや非同期処理を行う場合は、必ずUseShellExecute = false
に設定する必要があります。
環境変数の設定
ProcessStartInfo
の EnvironmentVariables
コレクションを使用すると、子プロセスに引き渡される環境変数をカスタマイズできます。デフォルトでは親プロセス(C#アプリケーション)の環境変数を全て引き継ぎますが、ここで特定の変数を追加・変更・削除できます。
コード例:環境変数を設定してコマンドを実行する
Windowsで set
または Linux/macOSで env
コマンドを使って環境変数を確認する例です。
“`csharp
using System;
using System.Diagnostics;
using System.Collections.Specialized; // StringDictionary のために必要
public class EnvironmentVariablesExample
{
public static void Main(string[] args)
{
using (Process process = new Process())
{
try
{
string command;
string arguments;
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
{
command = "cmd.exe";
arguments = "/c set MY_TEST_VAR"; // 指定した環境変数のみ表示
}
else
{
command = "env";
arguments = "grep MY_TEST_VAR"; // パイプで grep に渡して表示
}
process.StartInfo.FileName = command;
process.StartInfo.Arguments = arguments;
process.StartInfo.UseShellExecute = false; // 環境変数制御には false を推奨 (必須ではないが挙動がより予測可能)
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
// 環境変数を設定・追加する
// EnvironmentVariables プロパティは StringDictionary 型
process.StartInfo.EnvironmentVariables["MY_TEST_VAR"] = "Hello from C#!";
process.StartInfo.EnvironmentVariables["ANOTHER_VAR"] = "Another Value";
// 既存の変数を削除する場合は Remove() を使う
Console.WriteLine($"コマンド実行中: {process.StartInfo.FileName} {process.StartInfo.Arguments}");
Console.WriteLine($"環境変数 MY_TEST_VAR を '{process.StartInfo.EnvironmentVariables["MY_TEST_VAR"]}' に設定して実行します。");
process.Start();
string standardOutput = process.StandardOutput.ReadToEnd();
string standardError = process.StandardError.ReadToEnd();
process.WaitForExit();
Console.WriteLine("\n--- コマンド出力 ---");
Console.WriteLine(standardOutput);
Console.WriteLine(standardError); // エラー出力がある場合
Console.WriteLine($"終了コード: {process.ExitCode}");
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
}
}
}
“`
この例では、子プロセスでのみ有効な新しい環境変数 MY_TEST_VAR
を設定しています。子プロセスは親プロセスの環境変数を引き継ぎますが、ここで設定した値はそれを上書きします。
ユーザー資格情報での実行 (セキュリティ上の注意点)
特定のユーザーとしてコマンドを実行したい場合は、ProcessStartInfo
の以下のプロパティを使用します。
UserName
: ユーザー名を指定します。Password
: ユーザーのパスワードをSystem.Security.SecureString
オブジェクトとして指定します。Domain
: ドメインユーザーの場合はドメイン名を指定します。UseShellExecute
はfalse
にする必要があります。
“`csharp
using System;
using System.Diagnostics;
using System.Security; // SecureString のために必要
public class RunAsUserExample
{
public static void Main(string[] args)
{
// 注: このコードは例であり、パスワードの扱いには十分注意が必要です。
// 本番環境では、パスワードをハードコードしたり、安全でない方法で取得したりしないでください。
string domain = "YOUR_DOMAIN"; // ドメインユーザーの場合
string userName = "YOUR_USERNAME";
string password = "YOUR_PASSWORD"; // ここに平文で書くのは危険!
// パスワードを SecureString に変換 (安全な方法とは言えませんが例として)
SecureString securePassword = new SecureString();
foreach (char c in password)
{
securePassword.AppendChar(c);
}
securePassword.MakeReadOnly(); // 読み取り専用にする
using (Process process = new Process())
{
try
{
process.StartInfo.FileName = "cmd.exe"; // または他のコマンド
process.StartInfo.Arguments = "/c echo Running as %USERNAME% on %USERDOMAIN%";
process.StartInfo.UseShellExecute = false; // ユーザー指定に必須
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.CreateNoWindow = true;
// ユーザー資格情報を設定
process.StartInfo.UserName = userName;
process.StartInfo.Password = securePassword;
process.StartInfo.Domain = domain; // ドメインユーザーの場合のみ設定
// LoadUserProfile = true; // ユーザープロファイルを読み込むか (必要に応じて)
Console.WriteLine($"コマンド実行中 (ユーザー: {userName})...");
process.Start();
string standardOutput = process.StandardOutput.ReadToEnd();
process.WaitForExit();
Console.WriteLine("\n--- コマンド出力 ---");
Console.WriteLine(standardOutput);
Console.WriteLine($"終了コード: {process.ExitCode}");
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
finally
{
// SecureString を解放する
securePassword.Dispose();
}
}
}
}
“`
セキュリティに関する注意点: ユーザー資格情報をコード中に記述したり、安全でない方法で取得したりすることは絶対避けるべきです。本番環境でこのような要件がある場合は、Windowsサービスや他のセキュアな方法(例: Windows Credential Manager、環境変数への依存ではなく安全な設定ファイルなど)でのパスワード管理を検討してください。また、この機能は通常、管理者権限が必要になります。
ウィンドウ表示の制御 (CreateNoWindow
, WindowStyle
)
ProcessStartInfo
の CreateNoWindow
と WindowStyle
プロパティを使用して、起動するプロセスのウィンドウ表示を制御できます。これらは UseShellExecute = false
の場合に特に効果的です。
CreateNoWindow = true
: ウィンドウを作成しません。コンソールアプリケーションを完全にバックグラウンドで実行したい場合などに使用します。通常、RedirectStandardOutput
などと組み合わせて使用します。WindowStyle
:ProcessWindowStyle
列挙体(Normal
,Minimized
,Maximized
,Hidden
)を指定します。CreateNoWindow = true
の場合は無視されます。Hidden
にすると、ウィンドウは作成されますが画面には表示されません。
コード例:ウィンドウを非表示にして実行する
“`csharp
using System;
using System.Diagnostics;
public class HiddenWindowExample
{
public static void Main(string[] args)
{
using (Process process = new Process())
{
try
{
// ウィンドウを持つコマンド (Windows: notepad, Linux/macOS: gedit/nanoなど)
string command;
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
{
command = “notepad.exe”;
}
else
{
// Linux/macOS の場合、GUIエディタなどを非表示で起動するのは通常意図しない挙動になる可能性
// コンソールコマンドを使う方が適切
command = “ls”; // 例としてコンソールコマンドを使用
}
process.StartInfo.FileName = command;
process.StartInfo.Arguments = ""; // 引数なし
// ★ウィンドウを作成しない★
process.StartInfo.CreateNoWindow = true;
// UseShellExecute = false と組み合わせることが多い
// UseShellExecute = true でも CreateNoWindow は有効だが、挙動がOSやシェルに依存する場合がある
// 出力をリダイレクトしない場合は UseShellExecute = true でも可
process.StartInfo.UseShellExecute = false; // 例として false に設定
// 標準出力もリダイレクトしておくと、非表示で実行しつつ結果を取得できる
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
Console.WriteLine($"コマンド実行中 (ウィンドウ非表示): {process.StartInfo.FileName}");
process.Start();
// 非表示で実行しつつ、出力を取得して表示
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
Console.WriteLine("\n--- 出力 ---");
Console.WriteLine(output);
Console.WriteLine(error);
Console.WriteLine($"終了コード: {process.ExitCode}");
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
}
}
}
“`
この例では、通常ウィンドウが表示されるコマンド(Windowsのnotepadなど、ただしLinux/macOSではコンソールコマンドのlsで代用)を CreateNoWindow = true
と UseShellExecute = false
の設定で実行しています。これにより、コマンドはバックグラウンドで実行され、ユーザーはウィンドウを目にすることなくその結果をプログラム内で取得できます。
複数のコマンドをパイプで繋ぐ
OSのシェルでは、パイプ演算子 |
を使ってあるコマンドの標準出力を別のコマンドの標準入力に繋ぐことができます。例えば dir | find "exe"
(Windows) や ls -l | grep ".txt"
(Linux/macOS) のように使用します。
Process
クラスでこれを実現するには、明示的にシェルを起動し、パイプを含むコマンドライン全体をシェルの引数として渡す必要があります。この場合、UseShellExecute
は false
に設定してシェルの出力をリダイレクトする必要があります。
コード例:パイプ処理 (Windows)
cmd.exe /c "command1 | command2"
の形式を使用します。
“`csharp
using System;
using System.Diagnostics;
public class PipeExampleWindows
{
public static void Main(string[] args)
{
using (Process process = new Process())
{
try
{
process.StartInfo.FileName = “cmd.exe”;
// /c “dir | find \”exe\””
// パイプを含むコマンドライン全体を cmd.exe の引数として渡す
// 引数内のダブルクォーテーションはエスケープが必要
process.StartInfo.Arguments = “/c \”dir | find \”\”exe\”\”\””;
process.StartInfo.UseShellExecute = false; // 出力リダイレクトに必須
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.CreateNoWindow = true;
Console.WriteLine($"コマンド実行中: {process.StartInfo.FileName} {process.StartInfo.Arguments}");
process.Start();
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
Console.WriteLine("\n--- 標準出力 (dir | find \"exe\") ---");
Console.WriteLine(output);
Console.WriteLine($"終了コード: {process.ExitCode}");
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
}
}
}
“`
コード例:パイプ処理 (Linux/macOS)
bash -c "command1 | command2"
の形式を使用します。
“`csharp
using System;
using System.Diagnostics;
public class PipeExampleLinux
{
public static void Main(string[] args)
{
using (Process process = new Process())
{
try
{
process.StartInfo.FileName = “/bin/bash”; // または /bin/sh
// -c “ls -l | grep \”.txt\””
// パイプを含むコマンドライン全体を bash の引数として渡す
// 引数内のダブルクォーテーションや他の特殊文字は適切にエスケープが必要
process.StartInfo.Arguments = “-c \”ls -l | grep \\”\.txt\\”\””;
process.StartInfo.UseShellExecute = false; // 出力リダイレクトに必須
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.CreateNoWindow = true;
Console.WriteLine($"コマンド実行中: {process.StartInfo.FileName} {process.StartInfo.Arguments}");
process.Start();
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
Console.WriteLine("\n--- 標準出力 (ls -l | grep \".txt\") ---");
Console.WriteLine(output);
Console.WriteLine($"終了コード: {process.ExitCode}");
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
}
}
}
“`
パイプを含むコマンドライン文字列のエスケープは複雑になりがちです。特にユーザーからの入力を含む場合は、セキュリティ上のリスクが高まるため、十分な注意が必要です。可能であれば、パイプを使用せずに、C#コード内で一つ目のコマンドの出力を受け取り、それを処理してから二つ目のコマンドに渡す、といった方法を検討する方が安全で制御しやすくなります。
長時間コマンドの扱いとタイムアウト処理
実行に時間がかかるコマンドを同期的に実行すると、アプリケーションがその間ブロックされます。非同期実行と出力イベントを使用すればブロックは回避できますが、コマンドが予期せずハングアップしたり、完了にあまりに時間がかかったりする場合に、それを検知して強制終了させたいことがあります。
Process.WaitForExit()
にはタイムアウトを指定できるオーバーロード (WaitForExit(int milliseconds)
) がありますが、これはプロセスの終了自体にタイムアウトを設定するものであり、出力ストリームの読み取りはタイムアウトさせません。 そのため、大量の出力でデッドロックが発生している状況では、このタイムアウトは役に立ちません。
より確実なタイムアウト処理を行うには、独自のタイマーを使用するか、非同期実行と組み合わせてタイムアウト時に process.Kill()
を呼び出す、といった方法を実装する必要があります。
コード例:タイムアウト処理付き同期実行(簡易版)
この例は同期実行ですが、別スレッドでタイムアウトを監視してプロセスを強制終了させます。
“`csharp
using System;
using System.Diagnostics;
using System.Threading; // Thread.Sleep のために必要
using System.Threading.Tasks; // Task.Run のために必要
public class TimeoutExample
{
public static async Task Main(string[] args)
{
// 実行に時間がかかる可能性のあるコマンド (例: Windows: ping google.com -t, Linux: ping google.com)
// あるいは単純に Sleep コマンドなど
string command;
string arguments;
int timeoutMilliseconds = 5000; // タイムアウト時間 (5秒)
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
{
command = "ping";
// -t で継続的に ping を打つ (Ctrl+C で停止するまで)
// この例ではタイムアウトさせるため無限pingを使用
arguments = "google.com -t";
}
else
{
command = "ping";
// オプションなしだと継続的 (Ctrl+C で停止するまで)
arguments = "google.com";
}
using (Process process = new Process())
{
try
{
process.StartInfo.FileName = command;
process.StartInfo.Arguments = arguments;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.EnableRaisingEvents = true; // イベントも使う場合は true
Console.WriteLine($"コマンド実行中 (最大 {timeoutMilliseconds}ms): {process.StartInfo.FileName} {process.StartInfo.Arguments}");
process.Start();
// 非同期出力読み取りを開始 (タイムアウト後も出力を処理するため)
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// プロセス終了を待つタスクと、タイムアウトタスクを作成
Task processTask = Task.Run(() => {
process.WaitForExit();
Console.WriteLine("プロセスが終了しました。");
});
Task timeoutTask = Task.Delay(timeoutMilliseconds);
// どちらかのタスクが完了するのを待つ
Task finishedTask = await Task.WhenAny(processTask, timeoutTask);
if (finishedTask == timeoutTask)
{
// タイムアウトが発生した場合
Console.WriteLine($"\n**タイムアウト ({timeoutMilliseconds}ms) に達しました。プロセスを強制終了します。**");
if (!process.HasExited)
{
process.Kill(); // プロセスを強制終了
}
}
else
{
// プロセスがタイムアウト前に終了した場合
Console.WriteLine("\nプロセスはタイムアウト前に完了しました。");
}
// 非同期出力読み取りの完了を待つ (Kill() 後もまだデータが残っている可能性)
// WaitForExit() は Kill() 後はすぐに完了するが、イベント処理スレッドがまだデータを処理中かもしれない
// 明示的に非同期読み取りをキャンセルして待つことも可能だが、通常は Kill() + WaitForExit() で十分
// process.CancelOutputRead();
// process.CancelErrorRead();
// プロセス終了を完全に待つ (Killされた場合もすぐ終わる)
await Task.Run(() => process.WaitForExit());
// 出力を収集 (非同期で取得したものはイベントハンドラで処理済み)
// ここでは ReadToEnd は使わない(非同期読み取りを開始しているため)
int exitCode = process.ExitCode; // プロセス終了後に取得可能
Console.WriteLine($"最終的な終了コード: {exitCode}");
// イベントハンドラ解除
process.OutputDataReceived -= Process_OutputDataReceived;
process.ErrorDataReceived -= Process_ErrorDataReceived;
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
} // using ブロック
}
// 非同期出力受信イベントハンドラ (前述のコードと同じ)
private static void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data != null)
{
Console.WriteLine($"[STDOUT] {e.Data}");
}
}
private static void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data != null)
{
Console.WriteLine($"[STDERR] {e.Data}");
}
}
}
“`
この例では、Task.WhenAny
を使用して、プロセスが終了するタスクとタイムアウト用の遅延タスクのどちらかが先に完了するのを待っています。タイムアウトタスクが先に完了した場合、process.Kill()
を呼び出してプロセスを強制終了させています。非同期出力読み取りを併用することで、タイムアウトが発生した場合でも、それまでにコマンドが出力した内容をイベントハンドラで受け取ることができます。
クロスプラットフォーム開発における注意点
.NET Core/.NET 5 以降はクロスプラットフォームで動作します。C#からコマンドを実行する際も、WindowsだけでなくLinuxやmacOSでも同じコードが利用できます。しかし、OS間での違いには注意が必要です。
- コマンド名: 実行ファイルやコマンド名はOSによって異なります(例:
notepad.exe
vsnano
、dir
vsls
、cmd.exe
vsbash
)。OSを判定して適切なコマンド名を使用する必要があります。System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform()
メソッドが便利です。 - パス: ファイルパスの区切り文字はWindowsでは
\
、Unix系OSでは/
です。System.IO.Path.Combine()
を使用すると、実行環境に応じた適切なパス文字列を生成できます。 - シェル: デフォルトのシェルや、シェルに渡す引数の書式(例: Windowsの
/c
vs Unix系の-c
)が異なります。パイプ処理などをシェルに依存して行う場合は、実行環境のシェルを考慮したコマンド文字列を構築する必要があります。 - 終了コード: 成功が
0
であることは共通ですが、非ゼロのエラーコードの意味はOSやコマンドによって異なります。
例として、前述のコード例では System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform()
を使用してWindowsとそれ以外でコマンドを切り替えるようにしています。
よくある問題とトラブルシューティング
C#からコマンドを実行する際に遭遇しやすい問題とその解決策をまとめます。
- “ファイルが見つかりません” エラー:
UseShellExecute = false
の場合:FileName
に指定したパスが間違っているか、実行ファイルがそのパスに存在しません。フルパスを指定するか、WorkingDirectory
を正しく設定してください。PATH環境変数による解決は原則行われません。UseShellExecute = true
の場合:FileName
に指定した実行ファイルがPATH環境変数に含まれていない可能性があります。フルパスを指定するか、PATH環境変数を確認してください。または、OSのファイル関連付けが壊れている可能性もあります。
- デッドロックの発生:
- 同期的な出力読み取り (
ReadToEnd
) とWaitForExit
を同じスレッドでかつ不適切な順序で行っている場合に発生しやすいです。 - 解決策: 非同期出力読み取り(イベント駆動方式)を使用することを強く推奨します。同期的に行う場合は、
ReadToEnd
を呼び出す前にWaitForExit
を呼び出さないようにし、出力が非常に多い可能性のあるコマンドでは同期方式を避けてください。
- 同期的な出力読み取り (
- 出力が一部しか取得できない、またはイベントが発生しない:
UseShellExecute = true
の場合、出力はリダイレクトできません。必ずUseShellExecute = false
に設定してください。- 非同期出力読み取りの場合、
EnableRaisingEvents = true
に設定し、BeginOutputReadLine()
/BeginErrorReadLine()
を呼び出す必要があります。 - 非同期イベントハンドラが実行される前にプロセスが終了してしまうと、一部またはすべての出力が失われる可能性があります。
WaitForExit()
は非同期読み取り完了を待つように設計されていますが、念のため終了前にBeginOutputReadLine()
などを呼び出していることを確認してください。 - 出力ストリームのエンコーディングがデフォルトと異なる場合、文字化けや読み取りエラーが発生することがあります。
ProcessStartInfo.StandardOutputEncoding
やStandardErrorEncoding
プロパティでエンコーディングを指定してください。
- 権限不足:
- 管理者権限が必要なコマンドを実行する場合、権限不足で起動できないことがあります。アプリケーション自体を管理者として実行するか、Windowsの場合は
UseShellExecute = true
とstartInfo.Verb = "runas";
を組み合わせてUACプロンプトを表示させる方法があります(ただし、この方法では出力リダイレクトはできません)。
- 管理者権限が必要なコマンドを実行する場合、権限不足で起動できないことがあります。アプリケーション自体を管理者として実行するか、Windowsの場合は
- 引数のエスケープ問題:
- 引数にスペース、ダブルクォーテーション、またはシェルの特殊文字(
|
,>
,<
,&
など)が含まれる場合、正しくエスケープしないと意図しない挙動になります。特にUseShellExecute = true
でシェル機能を利用する場合や、UseShellExecute = false
でcmd.exe
やbash
にコマンドライン全体を引数として渡す場合に複雑になります。 - 解決策:
Arguments
プロパティに渡す文字列は、実行されるプロセス(またはそれを起動するシェル)の引数解析規則に従う必要があります。一般的にはダブルクォーテーションで囲み、内部のダブルクォーテーションはエスケープします(Windowscmd.exe
の場合は""
、Unix系シェルでは\"
など)。ユーザー入力を使用する場合は、後述のセキュリティ対策が必須です。
- 引数にスペース、ダブルクォーテーション、またはシェルの特殊文字(
- クロスプラットフォームでのコマンド互換性:
- コマンド名や引数のオプションはOSによって異なります。実行するOSを判定して適切なコマンドと引数を組み立てるロジックが必要です。
C#からのコマンド実行におけるセキュリティ上の考慮事項
外部コマンドを実行する機能は非常に強力である反面、不適切に使用するとセキュリティ上の脆弱性を招く可能性があります。特に注意すべきは「コマンドインジェクション」のリスクです。
コマンドインジェクションの危険性とその対策
アプリケーションがユーザーからの入力(例: テキストフィールドに入力されたファイル名、パスなど)を基にコマンドライン引数を生成し、それを適切に処理せずに外部コマンドに渡してしまうと、悪意のあるユーザーが注入したコマンドや引数が意図せず実行されてしまう可能性があります。
例:
アプリケーションが Process.Start("cmd.exe", "/c del " + userInput);
のようにユーザー入力 userInput
を使用してファイル削除コマンドを組み立てているとします。
ユーザーが my_file.txt & format C:
のような入力をした場合、組み立てられたコマンドは cmd.exe /c del my_file.txt & format C:
となり、もしアプリケーションが管理者権限で実行されていたら、ユーザーは my_file.txt
の削除に加えて、システムドライブをフォーマットするコマンドまで実行できてしまう可能性があります。
対策:
- ユーザー入力の検証と無害化: ユーザー入力をコマンドライン引数として使用する前に、厳密な検証(許容される文字、形式、長さなど)を行い、危険な文字やパターンを除去またはエスケープします。許可リスト方式で安全な入力のみを受け付けるのが最も安全です。
- 引数を直接渡す (
Arguments
プロパティの利用):ProcessStartInfo.Arguments
プロパティに引数を文字列として設定する場合、OSやシェルの引数解析規則に従って正しくエスケープすることが重要です。ただし、シェルの特殊文字(|
,>
,<
,&
,;
,&&
,||
など)が含まれる入力をそのまま渡すと、シェルがそれをコマンドの区切りやリダイレクトとして解釈してしまう危険性があります。 - シェルを経由しない (
UseShellExecute = false
): コマンドインジェクションのリスクを低減するためには、可能な限りUseShellExecute = false
を使用し、シェルの解釈を回避することを検討してください。この場合、FileName
に実行ファイルのフルパスを指定し、Arguments
には引数のみを文字列として渡します。シェルの特殊文字を含む複雑なコマンドラインが必要な場合は、明示的にシェル (cmd.exe
やbash
) を起動し、-c
オプションなどでコマンドライン全体を引数として渡す方法を取りますが、この場合も引数のエスケープには十分注意が必要です。 -
引数を配列として渡す (.NET Core / .NET 5+): .NET Core 3.0 以降では、
ProcessStartInfo
に引数を文字列配列として渡すためのArgumentList
プロパティが追加されました。これを使用すると、フレームワークが自動的に引数を正しくエスケープするため、引数のエスケープミスによるコマンドインジェクションのリスクを大幅に減らすことができます。これは引数を直接渡すより安全な方法です。ただし、シェルの特殊文字(パイプなど)は引数として解釈されるため、シェル機能を利用したい場合はやはり明示的にシェルを起動する必要があります。“`csharp
// .NET Core 3.0+ で ArgumentList を使用する例
using System.Diagnostics;
using System;public class ArgumentListExample
{
public static void Main(string[] args)
{
string userInput = “file with spaces & quotes.txt”; // ユーザーからの入力と想定using (Process process = new Process()) { process.StartInfo.FileName = "notepad.exe"; // または他のコマンド process.StartInfo.UseShellExecute = false; // ★ArgumentList を使用★ // 各要素が1つの引数として扱われ、フレームワークが自動的にエスケープ process.StartInfo.ArgumentList.Add(userInput); process.StartInfo.ArgumentList.Add("another_arg"); Console.WriteLine($"コマンド実行中: {process.StartInfo.FileName} {string.Join(" ", process.StartInfo.ArgumentList)}"); // 注: string.Join はデバッグ表示用。実際の引数渡しのエスケープは ArgumentList が行う。 process.Start(); // ... 待機など ... } }
}
“`
最小権限の原則
コマンドを実行するアプリケーションには、必要な最小限の権限のみを与えるように設計します。管理者権限が必要ない場合は、管理者権限で実行しないようにします。これにより、仮にコマンドインジェクションなどの脆弱性が悪用された場合でも、被害を最小限に抑えることができます。
信頼できないソースからのコマンド実行を避ける
信頼できない外部ソース(ネットワーク越しに受け取ったデータなど)から取得した文字列をそのままコマンド名や引数として使用することは絶対に避けてください。実行するコマンドは、事前に定義された安全なリストの中から選択するか、厳格な検証とサニタイズ処理を行った上で使用します。
まとめ
本記事では、C#から外部コマンドを実行するための System.Diagnostics.Process
クラスの様々な使い方を解説しました。
- 基本的な実行:
ProcessStartInfo
を設定し、process.Start()
を呼び出す最もシンプルな方法。 - 引数と作業ディレクトリ:
Arguments
やWorkingDirectory
プロパティで実行環境をカスタマイズ。 - 出力取得(同期):
RedirectStandardOutput
/Error
とUseShellExecute = false
を設定し、ReadToEnd
で出力を取得。ただしデッドロックに注意。 - 非同期実行と出力取得:
EnableRaisingEvents = true
、BeginOutputReadLine
/ErrorReadLine
を使用し、イベントハンドラで出力を取得。GUIアプリケーションや長時間コマンドに最適。 - エラーハンドリング:
try-catch
で起動時の例外を捕捉し、ExitCode
でコマンド内部のエラーを確認。 - 高度な利用:
UseShellExecute
の使い分け、環境変数、ユーザー指定実行、ウィンドウ制御、パイプ処理、タイムアウト処理、クロスプラットフォーム対応など。 - セキュリティ: コマンドインジェクションのリスクとその対策(ユーザー入力の検証、
UseShellExecute = false
、`.NET Core+ ArgumentList の利用など)。
どの方法を選択するかは、実行したいコマンドの性質、出力の有無と量、実行時間、アプリケーションの種類(GUIかCUIか)、必要な制御レベル、およびセキュリティ要件によって異なります。
一般的には、以下のガイドラインが推奨されます。
- コマンドの出力を取得する必要がある場合は、必ず
UseShellExecute = false
に設定します。 - 長時間実行されるコマンドや、大量の出力を生成する可能性があるコマンド、あるいはGUIアプリケーションで実行する場合は、非同期実行とイベント駆動型の出力処理を使用します。
- ユーザー入力を引数として使用する場合は、厳重な検証とサニタイズを行うか、
.NET Core+ の ArgumentList
を使用してコマンドインジェクションのリスクを最小限に抑えます。 - クロスプラットフォーム対応が必要な場合は、OSごとのコマンド名や引数の違いを考慮したロジックを実装します。
System.Diagnostics.Process
クラスは、C#アプリケーションがOSレベルの機能や外部ツールと連携するための非常に強力で柔軟な手段を提供します。本記事で解説した様々な側面を理解し、適切に活用することで、より高機能で堅牢なアプリケーションを開発できるようになるでしょう。