System.Diagnostics.Processを用いてコマンドラインを実行する

System.Diagnostics.Processを利用するとプロセスを新たに実行したり、すでに実行中のプロセスに対してなにか処理を行うことができます。

例としてgit pullをC#プログラム上から実行してみます。

まずはプロセスを実行する準備を行います。System.Diagnostics.ProcessをnewしてProcess.StartInfoに情報を埋めていきます。

var process = new System.Diagnostics.Process();
// 作業ディレクトリの指定。指定しないとEnvironment.CurrentDirectoryになる
process.StartInfo.WorkingDirectory = "/tmp/git-repository";
process.StartInfo.FileName = "git";
process.StartInfo.Arguments = "pull";
// OSのシェルを利用するかどうか
process.StartInfo.UseShellExecute = false;
// Windowを生成しない
process.StartInfo.CreateNoWindow = true;
// 上記をfalseにした場合標準入力、標準出力、標準エラー出力
// をストリームにリダイレクトすることができる
// こちらは用途に応じて
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;

StartInfo.WorkingDirectoryはそのプロセスを実行するディレクトリになります。 指定しない場合はEnvironment.CurrentDirectoryになります。 このCurrentDirectoryはプログラムを実行する状況によって異なる可能性があるため、場合によっては明示的に指定するほうが良いです。

StartInfo.UseShellExecuteはOSのシェルを経由してプロセスを実行するかを指定します。外部プロセスでコマンドを実行したい場合はこれをfalseにします。 大抵のコマンドラインアプリケーションの場合はウインドウを生成することはないので念の為StartInfo.CreateNoWindowもあわせて指定しておきます。(もちろんウインドウアプリケーションを起動する場合は別です。)

StartInfo.FileNameは実行するプログラムのファイルパス、StartInfo.Argumentsはそのコマンドラインの引数です。

StartInfo.RedirectStandardOutputは、通常そのまま標準出力に出力される文字列をProcess.StandardOutputへリダイレクトします。 主に標準出力をプログラムで扱いたい場合や、標準出力へ出力したくない場合などに利用できるでしょう。

// 現在標準出力にリダイレクトされてる文字列を最後まで取得する
var stdOut = await process.StandardOutput.ReadToEndAsync();

ちなみにユーザー名パスワードを指定するプロパティも存在します。適宜利用しましょう。

StartInfoに実行情報を埋めたらいよいよプロセスを実行します。

// プロセスのスタート
process.Start();
// プロセスの実行完了待ち
await _process.WaitForExitAsync(token);

Process.Start()を呼び出すとプロセスが実行されます。そのままプロセスの終了を待ちたい場合は、Process.WaitForExitを実行し終了を待ちます。ちなみに非同期版のProcess.WaitForExitAsyncも存在します。

実行後にそのプロセスの終了コードを取得したい場合はProcess.ExitCodeで取得できます。

最後に、自作のメモ管理コマンドのmemo-cliで利用したGitコマンドを実行するクラスを示します。

private class GitCommand : System.IDisposable
{
    public const int SuccessExitCode = 0;
    public GitCommand(CommandConfig config, string arguments)
    {
        _process = new System.Diagnostics.Process();
        _process.StartInfo.WorkingDirectory = config.HomeDirectory.FullName;
        _process.StartInfo.FileName = config.GitPath;
        _process.StartInfo.Arguments = arguments;
        _process.StartInfo.CreateNoWindow = true;
        _process.StartInfo.UseShellExecute = false;
        _process.StartInfo.RedirectStandardInput = true;
        _process.StartInfo.RedirectStandardOutput = true;
        _process.StartInfo.RedirectStandardError = true;
    }

    public async Task<int> Execute(CancellationToken token)
    {
        _process.Start();
        await _process.WaitForExitAsync(token);
        return _process.ExitCode;
    }

    public async Task<string> CollectStandardError()
    {
        return await _process.StandardError.ReadToEndAsync();
    }

    public void Dispose()
    {
        _process?.Dispose();
    }

    private System.Diagnostics.Process _process = null;
}

このクラスを用いてpullを実行する例を示します。(ちなみにUseColorは独自のコマンドライン装飾クラスです)

private async Task<bool> Pull(CancellationToken token)
{
    using var git = new GitCommand(CommandConfig, "pull");
    switch (await git.Execute(token))
    {
        case GitCommand.SuccessExitCode:
            return true;
        default:
            using (var _ = new UseColor(System.ConsoleColor.Red))
            {
                await Output.WriteLineAsync("Failed to execute pull command following reason.");
            }

            using (var _ = new UseColor(System.ConsoleColor.Yellow))
            {
                await Output.WriteLineAsync(await git.CollectStandardError());
            }
            return false;
    }
}

Reference