C#から外部プログラムを制御する際、最も頻繁に利用されるのがバッチファイル(.bat)の実行です。

Windows環境のシステム自動化において、既存のスクリプト資産をC#から呼び出す手法は、開発効率を大幅に向上させます。

本記事では、System.Diagnostics.Processクラスを使用して、安全かつ効率的にバッチファイルを制御する方法について、コード例を交えながら詳しく解説します。

初心者から中級者まで、実戦で使える非同期処理や標準出力の取得、エラーハンドリングのポイントを網羅しました。

C#でバッチファイルを実行する基本構造

C#で外部プロセスを起動するには、System.Diagnostics名前空間に含まれるProcessクラスを使用します。

最も単純な実行方法は、Process.Startメソッドにバッチファイルのパスを渡すことです。

しかし、実際のアプリケーション開発では、単に起動するだけでなく、実行結果の取得や終了待ちの制御が必要になることがほとんどです。

そこで重要になるのが、ProcessStartInfoクラスを用いた詳細なプロパティ設定です。

ProcessStartInfoクラスの役割

ProcessStartInfoは、プロセスを起動する際の構成情報を定義するためのクラスです。

バッチファイルを実行する際には、主に以下のプロパティを設定します。

プロパティ名内容
FileName実行するファイル(バッチファイル)のパスを指定します。
Argumentsバッチファイルに渡す引数を指定します。
UseShellExecuteOSのシェルを使用するかどうかを切り替えます。
CreateNoWindowコンソールウィンドウを表示せずに実行するかどうかを制御します。
WorkingDirectoryプロセスの作業ディレクトリを指定します。

特に、UseShellExecuteをfalseに設定することは非常に重要です。

これをfalseに設定することで、標準入出力のリダイレクトが可能になり、C#側でプログラムの出力を受け取れるようになります。

同期処理によるバッチファイルの実行

まずは、最も基本的な同期処理での実行方法を見ていきましょう。

同期処理とは、バッチファイルの処理が終わるまで、呼び出し元のC#プログラムが停止(待機)する方式です。

小規模なツールや、バッチの結果を待ってから次の処理に進む必要がある場合に適しています。

C#
using System;
using System.Diagnostics;

class Program
{
    static void Main()
    {
        // 実行するバッチファイルの情報を設定
        ProcessStartInfo startInfo = new ProcessStartInfo();
        startInfo.FileName = "sample.bat"; // 実行するファイル名
        startInfo.CreateNoWindow = true;   // ウィンドウを表示しない
        startInfo.UseShellExecute = false; // シェルを使用しない

        try
        {
            // プロセスの開始
            using (Process process = Process.Start(startInfo))
            {
                // プロセスの終了まで待機
                process.WaitForExit();

                // 終了コードの取得
                int exitCode = process.ExitCode;
                Console.WriteLine($"バッチ処理が終了しました。終了コード: {exitCode}");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"エラーが発生しました: {ex.Message}");
        }
    }
}

このコードでは、WaitForExit()メソッドにより、バッチファイルが完了するまでメインスレッドがブロックされます。

GUIアプリケーション(WinFormsやWPF)でこのコードを実行すると、処理中に画面がフリーズしたような状態になるため注意が必要です。

標準出力と標準エラー出力の取得

バッチファイル内でechoコマンドなどによって出力された内容を、C#側で文字列として取得したい場合があります。

これには、RedirectStandardOutputプロパティを有効にします。

同様に、エラー内容を取得したい場合はRedirectStandardErrorを有効にします。

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

class Program
{
    static void Main()
    {
        ProcessStartInfo startInfo = new ProcessStartInfo();
        startInfo.FileName = "test_script.bat";
        startInfo.UseShellExecute = false;
        startInfo.RedirectStandardOutput = true;
        startInfo.RedirectStandardError = true;
        startInfo.CreateNoWindow = true;
        
        // 日本語環境での文字化けを防ぐため、Shift-JISを指定することが多い
        startInfo.StandardOutputEncoding = Encoding.GetEncoding("Shift_JIS");

        using (Process process = Process.Start(startInfo))
        {
            // 標準出力の読み取り
            string output = process.StandardOutput.ReadToEnd();
            // エラー出力の読み取り
            string error = process.StandardError.ReadToEnd();

            process.WaitForExit();

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

            if (!string.IsNullOrEmpty(error))
            {
                Console.WriteLine("--- エラー出力 ---");
                Console.WriteLine(error);
            }
        }
    }
}
実行結果
--- 標準出力 ---
2026/05/22 10:00:00 バッチ処理を開始しました。
処理を完了しました。

バッチ処理が終了しました。終了コード: 0

注意点として、StandardOutput.ReadToEnd()を呼び出すタイミングが挙げられます。

大きなデータを出力するバッチファイルの場合、バッファが一杯になり、プロセスがデッドロック(停止)してしまう可能性があります。

これを防ぐためには、非同期読み取りイベントを使用するか、適切な順序で待機を行う必要があります。

非同期処理による効率的な実行

現代的なC#開発では、UIをブロックさせないためにasync/awaitを活用した非同期実行が推奨されます。

Processクラス自体は古いAPIであるため、そのままではawaitできませんが、TaskCompletionSourceを使用することでラップできます。

また、.NET 5以降であれば、よりスマートに非同期制御を行うことが可能です。

Taskを用いた終了待機

以下の例では、プロセスの終了を非同期に待機する方法を示します。

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

public async Task RunBatchFileAsync(string fileName)
{
    ProcessStartInfo startInfo = new ProcessStartInfo
    {
        FileName = fileName,
        UseShellExecute = false,
        CreateNoWindow = true
    };

    using (Process process = new Process { StartInfo = startInfo, EnableRaisingEvents = true })
    {
        // 終了を待機するためのTaskCompletionSource
        var tcs = new TaskCompletionSource<int>();

        process.Exited += (sender, args) =>
        {
            tcs.SetResult(process.ExitCode);
        };

        process.Start();

        // 終了まで非同期に待機(UIスレッドを解放する)
        int exitCode = await tcs.Task;
        Console.WriteLine($"非同期実行完了。終了コード: {exitCode}");
    }
}

このように実装することで、長時間のバッチ処理中でも、アプリケーションの他の操作を妨げることがありません。

特に、複数のバッチファイルを並列で実行したい場合にこの手法は非常に強力です。

実戦的なトラブルシューティングと対策

C#からバッチファイルを呼び出す際、開発者がよく直面する問題がいくつかあります。

これらを事前に把握しておくことで、デバッグ時間を大幅に短縮できます。

1. 文字化けへの対応

Windowsのコマンドプロンプトは、デフォルトでShift-JIS(CP932)を使用します。

一方、C#の標準(特に.NET Core以降)はUTF-8が基本です。

出力を受け取る際に文字化けが発生する場合は、先述したStandardOutputEncodingプロパティに適切なエンコーディングを指定してください。

2. 作業ディレクトリ(カレントディレクトリ)の問題

バッチファイル内で相対パスを使用している場合、C#から起動した際のカレントディレクトリが意図しない場所になっていることがあります。

ProcessStartInfo.WorkingDirectoryを使用して、明示的にバッチファイルのあるフォルダを作業ディレクトリとして設定することを強くお勧めします。

3. スペースを含むパスの処理

ファイルパスにスペースが含まれている場合、引数として正しく認識されないことがあります。

パス全体をダブルクォーテーションで囲むように文字列を作成する必要があります。

例: startInfo.Arguments = $"\"C:\\Path With Space\\script.bat\" arg1";

管理者権限での実行方法

システム設定を変更するバッチファイルなどは、管理者権限が必要な場合があります。

C#から管理者として実行するには、Verbプロパティに"runas"を指定します。

ただし、この方法はUseShellExecute = trueである必要があるため、標準出力のリダイレクトとは併用できない点に注意が必要です。

C#
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.FileName = "admin_task.bat";
startInfo.Verb = "runas"; // 管理者として実行
startInfo.UseShellExecute = true;

try
{
    Process.Start(startInfo);
}
catch (System.ComponentModel.Win32Exception)
{
    // ユーザーがUACで「いいえ」を選択した場合の処理
    Console.WriteLine("管理者権限の承認がキャンセルされました。");
}

まとめ

C#でバッチファイルを実行する際は、ProcessクラスとProcessStartInfoクラスの組み合わせを理解することが基本となります。

単純な実行であれば数行のコードで済みますが、実用的なアプリケーションにおいては、非同期処理による待機や、標準出力のリダイレクト、エンコーディングの調整が不可欠です。

特にUseShellExecuteの設定や、作業ディレクトリの指定は、バグを防ぐための重要なポイントです。

本記事で解説した手法を活用し、外部スクリプトと連携する堅牢なC#プログラムを構築してください。

バッチファイルは古い技術と思われがちですが、適切に活用することでWindows環境での運用の自動化を強力にサポートしてくれるはずです。