1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「PowerShellの出力は正規表現でパースすればいけます」と言っていた後輩が、自分がスクレイピングしていたのはオブジェクトの「影」だったと知った話

1
Posted at

導入

私はSasaki。Windowsクライアントの設計レビューと改修を長くやっている、少し口うるさい先輩だ。

ある日の午後、後輩の Go(22歳)が、管理ツールのデモを見せてくれた。
C# の GUI からサーバーのサービス一覧を取って表示する、社内向けの小さなツールだ。

「これ、中で powershell.exe を起動して Get-Service の標準出力を読んでるんです。
出力は正規表現でパースすればいけます。もう動いてますし」

実際、Go のマシンでは動いていた。
だが翌週、検証サーバーに置いた途端に報告が来た。

  • サービス名が途中で ... になって、別サービスとして扱われる
  • 列の位置がズレて、Status に DisplayName の切れ端が入る
  • たまに行が丸ごと欠ける

Go のコードは、Get-Service表形式の画面出力Substring と正規表現で切り出していた。
つまり Go は、自社のツールから、自社のシェルの画面を スクレイピング していたことになる。

そして画面というやつは、コンソール幅が変わるだけで平気でレイアウトを変える。

何が起きていたのか

PowerShell の出力は、本来テキストではない。

Get-Process の結果はプロセスの オブジェクト で、Get-Service の結果はサービスの オブジェクト だ。
コンソールに表が出るのは、最後の最後に表示用フォーマッタが人間向けに整形しているからにすぎない。

Go がやっていたのは、こういう流れだ。

PowerShell の出力(オブジェクト)
  ↓ 表示用に整形される(列幅はコンソール依存、長い値は "..." に省略)
文字列
  ↓ Split / 正規表現 / Substring
C# の値(たまに壊れている)

この方法は 表示形式に依存 する。列幅、ロケール、改行、値の中の空白や区切り文字。
どれか 1 つ変わるだけで、パーサーごと壊れる。Go のツールが検証サーバーで壊れたのは、ネットワークでも権限でもなく、コンソールの都合だった。

「えぇ……じゃあどうすれば」

オブジェクトのまま受け取ればいい。そのための SDK がある。

そこで読んでもらった記事

私は Go に、次の記事を送った。

C#(CSharp)でPowerShellを実行して、オブジェクトとして受け取る方法
https://comcomponent.com/blog/2026/06/08/001-csharp-run-powershell-receive-objects/

「正規表現を直す前にこれを読め。
パーサーを強化する話じゃない。パーサーを不要にする 話だ」

夕方、Go が言った。

「……Get-Service の結果って、最初からオブジェクトだったんですね。
わざわざテキストに落としてたのは、俺のほうだった」

そういうことだ。

PowerShell SDK でオブジェクトのまま受け取る

C# から PowerShell を呼ぶ方法は大きく 2 つある。

方法 特徴 向いている場面
ProcessStartInfopowershell.exe / pwsh.exe を起動 標準出力を文字列として読む 既存バッチの単純実行、ログを残すだけの処理
System.Management.Automation.PowerShell(PowerShell SDK) 結果を PSObject として受け取れる C# 側で結果を加工する処理、管理ツール、業務アプリ

結果を C# で使うなら後者だ。NuGet で SDK を入れる。

dotnet add package Microsoft.PowerShell.SDK --version 7.4.16

(SDK のバージョンは対象 .NET に合わせて選び、固定しておく。.NET 8 なら 7.4 系が使いやすい)

最小コードはこうなる。

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Management.Automation;

using PowerShell ps = PowerShell.Create();

Collection<PSObject> results = ps
    .AddCommand("Get-Process")
    .AddParameter("Id", Environment.ProcessId)
    .Invoke();

foreach (PSObject item in results)
{
    if (item.BaseObject is Process process)
    {
        Console.WriteLine($"{process.Id}: {process.ProcessName}");
    }
}

Invoke() の戻り値は文字列ではなく Collection<PSObject>
PSObject は PowerShell の出力値を包むラッパーで、BaseObject を見れば元の .NET オブジェクト(この例なら System.Diagnostics.Process)がそのまま取れる。

正規表現の出番は、最初から無い。

パイプラインも C# から組める

AddCommand を続けて呼ぶと、前のコマンドの出力が次へ渡る。つまりパイプラインになる。

Collection<PSObject> rows = ps
    .AddCommand("Get-Process")
    .AddCommand("Sort-Object")
        .AddParameter("Property", "CPU")
        .AddParameter("Descending", true)
    .AddCommand("Select-Object")
        .AddParameter("First", 10)
        .AddParameter("Property", new[] { "Name", "Id", "CPU" })
    .Invoke();

foreach (PSObject row in rows)
{
    Console.WriteLine($"{row.Properties["Id"]?.Value}: {row.Properties["Name"]?.Value}");
}

これは PowerShell で書くこれと等価だ。

Get-Process | Sort-Object -Property CPU -Descending |
  Select-Object -First 10 -Property Name, Id, CPU

値の取り出し方は 2 通りあって、使い分けはこうなる。

取り出し方 使う場面
BaseObject コマンドが返した元の .NET オブジェクトをそのまま使う
Properties["列名"] Select-Object[pscustomobject] で整形した列を読む

そして 1 つ、絶対のルールがある。

C# に渡す前に Format-Table を使わない。
Format-Table を通すと、結果はサービスのオブジェクトではなく「表示用のフォーマット情報」に化ける。Go が画面でスクレイピングしていたアレが、オブジェクトの世界に出張してくるわけだ。

画面で人間が見る   → Format-Table / Format-List
C# で後続処理に使う → Select-Object / [pscustomobject]

Goがやらかしかけた第二の罠

直している途中で、Go のコードにもう 1 つ見つけた。検索パスをユーザーが指定できる機能だ。

// Goが書いていたコード(避ける例)
string script = $"Get-ChildItem -Path '{userInputPath}'";
ps.AddScript(script).Invoke();

「何かまずいですか? ちゃんとクォートしてますよ」

まずい。ユーザー入力を PowerShell のコードとして 文字列連結している。
入力にクォートやサブ式が混ざれば、入力が「値」ではなく「コード」として解釈される余地が生まれる。シェルに対する SQL インジェクションと同じ構図だ。

値は AddParameter で渡す。

Collection<PSObject> files = ps
    .AddCommand("Get-ChildItem")
    .AddParameter("Path", userInputPath)
    .AddParameter("File", true)
    .Invoke();

AddParameter で渡した値は、コード文字列に連結されるのではなく パラメーター値 として扱われる。

書き方 使いどころ
AddCommand / AddParameter C# から安全にコマンドを組み立てる。基本これ
AddScript 固定の短いスクリプト、既存スクリプトの読み込み
文字列連結した AddScript 原則避ける

エラーは戻り値の外を流れている

もう 1 つ、Go が見落としていたのがエラーだ。
PowerShell では出力とエラーは 別ストリーム なので、Invoke() の戻り値だけ見ていると、失敗が静かに素通りする。

Collection<PSObject> output = ps
    .AddCommand("Get-Item")
    .AddParameter("Path", @"C:\no-such-file.txt")
    .Invoke();

if (ps.HadErrors)
{
    foreach (ErrorRecord error in ps.Streams.Error)
    {
        Console.WriteLine($"Error: {error.Exception.Message}");
    }
}

「一部失敗しても一覧は出したい」管理ツールなら、こうしてエラーストリームを回収して表示する。
「失敗したら全体を止めたい」なら、AddParameter("ErrorAction", "Stop") を付けて RuntimeException を catch し、例外として扱う。どちらにするかはツールの性格で決める。決めずに握りつぶすのだけが無い。

Goがどう直したか

  • powershell.exe の起動と標準出力のパースを全部捨て、PowerShell SDK に置き換えた
  • 正規表現パーサーを削除した。Select-Object で列を絞り、Properties で読むようにした
  • PSObject は PowerShell との境界でだけ扱い、すぐに C# の record に変換して内部に渡すようにした
  • ユーザー入力の埋め込み AddScript をやめ、AddCommand / AddParameter に統一した
  • HadErrorsStreams.Error を確認し、失敗をログに残して画面にも出すようにした

「パーサーのテスト、20本くらい書いてたんですよ。
列幅が変わるケース、... に省略されるケース、って。全部要らなくなりました

壊れやすいものを頑張ってテストするより、壊れようがない形にするほうが早い。

以後、C#×PowerShell のレビューで見る表

観点 確認すること
受け取り方 標準出力の文字列パースをしていないか。SDK で PSObject として受けているか
Format C# に渡す前に Format-Table / Format-List を使っていないか
入力 ユーザー入力を AddScript の文字列に連結していないか。AddParameter
境界 PSObject がアプリ内部まで漏れていないか。早めに C# の型へ変換しているか
エラー HadErrors / Streams.Error を見ているか。方針(続行か停止か)を決めているか
環境 実行ユーザーの権限、必要モジュールの有無を確認したか

まとめ

  • PowerShell の出力はテキストではなく、最初からオブジェクト
  • 標準出力の正規表現パースは「表示のスクレイピング」であり、列幅や省略で壊れる
  • PowerShell SDK を使えば Collection<PSObject> として受け取れて、パーサー自体が不要になる
  • C# に渡すなら Format-Table ではなく Select-Object / [pscustomobject]
  • ユーザー入力は AddScript への文字列連結ではなく AddParameter で渡す
  • エラーは別ストリーム。Invoke() の戻り値だけ見ていると失敗を見落とす

頑丈なパーサーは要らない。
パースしなくていい形で受け取る のが、いちばん頑丈だ。

PSObjectrecord 変換のヘルパー実装、繰り返し実行用ラッパーの作り方、SDK バージョンの選び方、権限・32bit/64bit・モジュール配置・UI スレッド・配布サイズといった実務の注意点まで含めた詳細版はこちら。

https://comcomponent.com/blog/2026/06/08/001-csharp-run-powershell-receive-objects/

(了)

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?