しばたテックブログ

気分で書いている技術ブログです。

PowerShellのCountプロパティについてあれやこれや (2018年10月版)

3年ほど前に

blog.shibata.tech

というエントリを書いたのですが、その後新たに分かったことや新しいバージョンでの変更も発生したため、内容を更新してリライトしました。

便利なCountプロパティ

たとえばコマンドを実行して出力した結果の件数を取得したい場合、

([コマンド]).Count

とするとその件数を取得することができます。

具体例として以下の様にコマンドを実行するとC:\Program Files配下にあるEXEファイルの総数を取得することができます。

# Countプロパティを使うと、評価した結果(System.IO.FileInfo[])の要素数を取得することができます。
PS C:\> (dir -Path "C:\Program Files" -File -Filter "*.exe" -Recurse).Count
815

Countプロパティの実体

System.Array.Count?

このCount、要はコマンドを実行した結果(この場合はSystem.IO.FileInfo[])の要素数を取得しているだけなのですが、しかしながら、.NETの配列(System.Array)にはLengthプロパティはあれどもCountプロパティはありません。

それでもPowerShellではCountプロパティを利用することができています。

バージョン間の差異

この謎めいたCountプロパティはPowerShellのバージョンによってその裏にある実体が異なり、その挙動も異なります。

大きくはPowerShell 2.0までとPowerShell 3.0以降に分かれ、加えてPowerShell Core 6.0で若干の仕様変更が入り現在に至っています。

以降、バージョンごとの実体について触れていきます。

PowerShell 2.0までCountプロパティの実体

PowerShell 2.0までのCountプロパティの実体はSystem.Arrayに追加された型データになります。

型データとは.NETの型にPowerShell独自のデータを付与できる機能になります。
詳細については牟田口先生の以下のエントリを参照してください。

winscript.jp

型データの定義が記載されているC:\Windows\System32\WindowsPowerShell\v1.0\types.ps1xmlを見るとCountプロパティは以下の様に定義されており、System.Array.Lengthに対するAliasPropertyとなっていることがわかります。

# C:\Windows\System32\WindowsPowerShell\v1.0\types.ps1xmlより抜粋
    <Type>
        <Name>System.Array</Name>
        <Members>
            <AliasProperty>
                <Name>Count</Name>
                <ReferencedMemberName>Length</ReferencedMemberName>
            </AliasProperty>
        </Members>
    </Type>

Get-Memberで空の配列@()のメンバを確認してみるとこんな結果を返します。

PS C:\> Get-Member -InputObject @() -View Extended

   TypeName: System.Object[]

Name  MemberType    Definition
----  ----------    ----------
Count AliasProperty Count = Length

この型データにより本来System.Arrayには存在しないCountプロパティを利用することができます。

制限事項

PowerShell 2.0までのCountプロパティはSystem.Arrayに対するものなので配列以外では使うことができません。

例えば以下の様なコードは$nullを返し、配列以外ではCountプロパティが存在しないことがわかります。

# PowerShell 2.0
PS C:\> $PSVersionTable.PSVersion

Major  Minor  Build  Revision
-----  -----  -----  --------
2      0      -1     -1

PS C:\> (123).Count
PS C:\>

配列でないオブジェクトに対してCountプロパティを使いたいときは配列化してやる必要があります。

# PowerShell 2.0
# 要素数1の配列化してCountプロパティを取得
PS C:\> @(123).Count
1

PowerShell 3.0 ~ 5.1のCountプロパティの実体

PowerShell 2.0では配列にしかCountプロパティが使えませんでしたが、PowerShell 3.0からは配列でないスカラ値や$nullに対してもCountプロパティが使える様に機能が拡張されています。

# PowerShell 3.0 以降
PS C:\> $PSVersionTable.PSVersion

Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      17763  1

# スカラ値のCountは 1
PS C:\> (123).Count
1

# $nullのCountは 0
PS C:\> ($null).Count
0

この変更については公にはWindows PowerShell Blogのこの記事で触れられる程度の情報しかなく、機能の実体については謎でした。

しかしながらPowerShellがオープンソースとなったため実際にソースを確認してみたところ、このCountプロパティは動的オブジェクトのメンバーフォールバックとして実装されていることが分かりました。

PowerShellは3.0ではランタイムが.NET 4ベースで刷新され、PowerShellすべてのオブジェクトの基本となるPSObjectDLRを使った動的オブジェクトとして定義されています。
これによりこれまでリフレクションによって行われていたメソッドやプロパティの解決が高速化され、PowerShell全体のパフォーマンス改善につながっています。

そしてDLRでは、主にエラーハンドリングのために、メソッドやプロパティの解決に失敗したときの処理を定義するフォールバック機能が提供されており、Countプロパティにおいては、

  1. オブジェクトのCount(またはLength)プロパティを取得しようとする

  2. Count(またはLength)プロパティが存在しないオブジェクトの場合、DLRのフォールバック(FallbackGetMember)が呼び出される

  3. フォールバック処理内で元のオブジェクトの種類に応じて1または0を返す

といった動作をしています。

(これがすべてではありませんが)ソースコードとしてはこのあたりの処理が参考になります。

参考として以下にフォールバックの一部を列挙しておきます。

//
// PowerShell 6.0時点の Binders.cs より一部引用
// 

/// <summary>
/// Return the binding result when no property exists.
/// </summary>
private DynamicMetaObject PropertyDoesntExist(DynamicMetaObject target, BindingRestrictions restrictions)
{

    // ・・・ 省略 ・・・

    // As part of our effort to hide how a command can return a singleton or array, we want to allow people to iterate
    // over singletons with the foreach statement (which has worked since V1) and a for loop, for example:
    //     for ($i = 0; $i -lt $x.Length; $i++) { $x[$i] }
    // If $x is a singleton, we want to return 1 for the length so code like this works correctly.
    // We do not want this magic to show up in Get-Member output, tab completion, intellisense, etc.
    if (Name.Equals("Length", StringComparison.OrdinalIgnoreCase) || Name.Equals("Count", StringComparison.OrdinalIgnoreCase))
    {
        // $null.Count should be 0, anything else should be 1
        var resultCount = PSObject.Base(target.Value) == null ? 0 : 1;
        return new DynamicMetaObject(
            Expression.Condition(
                Compiler.IsStrictMode(2),
                ThrowPropertyNotFoundStrict(),
                ExpressionCache.Constant(resultCount).Cast(typeof(object))), restrictions);
    }

    // ・・・ 省略 ・・・

}

ちなみに配列に対しては従来通り型データによるAliasPropertyが使われています。

制限事項

PowerShell 3.0で配列以外にもCountプロパティが使える様になったのですが、ぎたぱそ先生の以下のエントリによるとPScustomObjectに対してはCountプロパティは$nullを返してしまいます。

tech.guitarrapc.com

実際に試してみると確かに$nullを返します。

# PowerShell 3.0 ~ 6.0
PS C:\> $PSVersionTable.PSVersion

Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      17763  1

PS C:\> ([PScustomObject]@{hoge = "hoge"}).Count
PS C:\>

このためPScustomObjectに対してCountプロパティを使用する際は注意が必要です。

PowerShell 6.0以降のCountプロパティの実体

PowerShell Core 6.0においてもCountプロパティの実体が動的オブジェクトのメンバーフォールバックであることは変わらないのですが、いくつか挙動が変更されています。

AliasPropertyの削除 (PowerShell Core 6.0より)

最初に説明したSystem.Array.Lengthに対するAliasPropertyですが、これはPowerShell Core 6.0で削除されました。

理由としてはこのAliasPropertyが無くても問題がない*1点と、ConvertTo-Json等の処理で不要なCountプロパティが露出してしまう問題(#3222)があったため削除されています。

github.com

PSCustomObjectに対するCount (PowerShell Core 6.1より)

前項で触れたPScustomObjectに対するCountですが、PowerShell Core 6.1で挙動が改善されPScustomObjectでもCountプロパティが取得できる様に改善されています。

PS C:\> $PSVersionTable.PSVersion

Major  Minor  Patch  PreReleaseLabel BuildLabel
-----  -----  -----  --------------- ----------
6      1      0

PS C:\> ([PScustomObject]@{hoge = "hoge"}).Count
1

【補足】PowerShell 4.0のWhere、ForEachメソッドについて

最後にちょっとだけおまけを。

PowerShell 4.0から配列などのコレクションオブジェクトに対してWhere()およびForEach()メソッドが使える様に機能拡張されています。

# PowerShell 4.0以降
PS C:\> @(1,2,3).Where({$_ -eq 2})
2

PS C:\> @(1,2,3).ForEach({$_ * 2})
2
4
6

これらのメソッドも動的オブジェクトのメンバーフォールバックとして実装されいます。
ソースコードとしてはこのあたりが参考になると思いますので興味のある方はご覧ください。

*1:前項で説明を端折りましたが、動的オブジェクトのメンバーフォールバックは配列の場合も考慮されいます

Windows Server 2019のServer Core App Compatibility Feature on Demandを試す

Windows Server 2019で新たに導入されたApp Compatibility Feature on Demand(FoD)は、基本的にCUIのみであるServer Coreに対して後入れでいくつかのGUI管理ツールを導入できる機能になります。

Insider previewの時点で既にムッシュが試しており、リリース版でも結果的には変わらない内容となりましたが、せっかく試したのでエントリとして残しておきます。

FoDをセットアップする

公式の手順はこちらになります。

docs.microsoft.com

最初にWindows Server 2019 Server Coreを普通にセットアップし、その後専用のISO媒体からDISMを使いFoDの機能を追加する形になります。

Server Coreのセットアップ

ここの手順は端折ります。
適当にServer Coreのセットアップをして管理者ユーザーがログインできる状態にしてください。

f:id:stknohg:20181004121229p:plain

エディションはStandard、Datacenterどちらでも構いません。

FoD ISOイメージの取得

先述の通りFoDをセットアップするには専用のISOイメージが必要です。
このイメージの入手先についてはドキュメントにきちんと明記されておらず残念な感じになっています。
(そのうち明記されるとは思いますが...)

とりあえず私はVisual Studioサブスクリプション(MSDNサブスクリプション)からダウンロードすることができたのでそのイメージを利用しています。

f:id:stknohg:20181004121254p:plain

FoD ISOイメージのマウント

このISOイメージをOS内でマウントします。

公式の手順としてはネットワークドライブなどに配置したISOファイルをPowerShellのMount-DiskImageコマンドレットを使ってマウントする手順が紹介されています。

本エントリでは公式の手順に加えてマウントしたボリューム、およびドライブレターの取得方法も提示しておきます。

# ISOイメージのマウント
$image = Mount-DiskImage -ImagePath '[FoD ISOのパス].iso' -PassThru

# マウントしたボリューム情報を取得
Get-Volume -DiskImage $image

# ドライブレターを取得する場合
(Get-Volume -DiskImage $image).DriveLetter

FoDをセットアップする

DISMを使い先ほどマウントしたドライブをソースに指定して機能を追加します。
公式の手順ではDISM.exeを使っていましたが、せっかく最初でPowerShellを使っているので本エントリではAdd-WindowsCapabilityを使った手順を紹介します。

Add-WindowsCapability -Online -Name 'ServerCore.AppCompatibility~~~~0.0.1.0' -Source [マウントしたドライブレター]:\ -LimitAccess

# 公式の手順だとこんな感じ
# DISM /Online /Add-Capability /CapabilityName:"ServerCore.AppCompatibility~~~~0.0.1.0" /Source:[マウントしたドライブレター]:\ /LimitAccess

実行例)

f:id:stknohg:20181004121311p:plain

機能の追加後は再起動が要求されるので、サーバーを再起動すれば完了です。

Restart-Computer

【補足】Internet Explorerの追加方法

FoDを使ってInternet Explorerを利用するにはさらにもうひと機能追加する必要があります。

Add-WindowsCapability -Online -Name 'Browser.InternetExplorer~~~~0.0.11.0' -Source [マウントしたドライブレター]:\  -LimitAccess

こちらもインストール後に再起動が必要です。

なお、iexplore.exeへのパスは追加されないので、IEを起動する際は

"C:\Program Files\internet explorer\iexplore.exe"

の様にフルパスで指定してやる必要があります。

FoDを試してみる

公式には

  • Microsoft Management Console (mmc.exe)
  • イベントビューアー (Eventvwr.msc)
  • パフォーマンスモニター (PerfMon.exe)
  • リソースモニター (Resmon.exe)
  • Device Manager (Devmgmt.msc)
  • エクスプローラー (Explorer.exe)
  • PowerShell ISE (Powershell_ISE.exe)
  • フェイルオーバークラスターマネージャー (CluAdmin.msc)

が利用できると謳われています。

ただ、MMCに関してはデスクトップ版の全機能ではなく、上記以外だと

  • ディスクの管理 (diskmgmt.msc)
  • ローカルグループポリシーエディター (gpedit.msc)
  • ローカルユーザーとグループ (lusrmgr)
  • ポリシーの結果セット (rsop.msc)
  • ローカルセキュリティポリシー (secpol.msc)
  • セキュリティが強化されたWindows Defenderファイアウォール(WF.msc)

のみ使える様です。

加えてざっと調べたところ、

  • コントロールパネル (control.exe)
  • クラスター更新対応 (ClusterUpdateUI.exe )
  • 色の管理 (colorcpl.exe)
  • FAX送付状エディター (FXSCOVER.exe)
  • ハードウェア追加ウィザード (hdwwiz.exe)
  • IExperssウィザード (iexpress.exe (非推奨機能))

の起動を確認できました。
(機能がきちんと利用可能かまでは調べてません...)

以下に適当にスクリーンショットを張っていきます。

f:id:stknohg:20181004124043p:plain

f:id:stknohg:20181004124049p:plain

f:id:stknohg:20181004124055p:plain

f:id:stknohg:20181004124102p:plain

f:id:stknohg:20181004124109p:plain

f:id:stknohg:20181004124116p:plain

f:id:stknohg:20181004124133p:plain

f:id:stknohg:20181004124141p:plain

f:id:stknohg:20181004124146p:plain

f:id:stknohg:20181004124156p:plain

f:id:stknohg:20181004124202p:plain

f:id:stknohg:20181004124209p:plain

f:id:stknohg:20181004124216p:plain

f:id:stknohg:20181004124222p:plain

PowerShell CoreのWindows PowerShell互換性とWindowsCompatibilityモジュールについて

先日リリースされたPowerShell Core 6.1の新機能一覧に、以下の様にWindows PowerShellに対する互換性について記載されています。

On Windows, the .NET team shipped the Windows Compatibility Pack for .NET Core, a set of assemblies that add a number of removed APIs back to .NET Core on Windows.

We've added the Windows Compatibility Pack to PowerShell Core 6.1 release so that any modules or scripts that use these APIs can rely on them being available.

The Windows Compatibility Pack enables PowerShell Core to use more than 1900 cmdlets that ship with Windows 10 October 2018 Update and Windows Server 2019.

Windows PowerShellに対する互換性について

この内容にあるとおりPowerShell Core 6.1にはWindows Compatibility Pack for .NET Coreのアセンブリが取り込まれており、もうすぐリリース予定のWindows 10 October 2018 Update(RS5, 1809)とWindows Server 2019においては1900以上の(Windows PowerShell向け)コマンドレットがPowerShell Coreでも利用可能になるそうです。

これらの新しいOSにおいては、これまで機能は追加されたものの一切設定がされていなかったモジュールのPSEditionパラメーターが設定され、この内容にCoreが含まれているモジュールはPowerShell Core 6.1(以降)から呼び出すことが可能です。

例としてWindows Server 2019 Insider preview build 17744の環境にPowerShell Core 6.1をインストールして

Get-Module -ListAvailable -PSEdition Desktop

を呼び出しみると下図の様にC:\Windows\system32\WindowsPowerShell\v1.0\Modules配下にあるWindows PowerShell向けの各種モジュールが利用可能になっていることが確認できます。

f:id:stknohg:20180930203209p:plain

(PSEditionの欄がCore,Desk(top)とCoreも追加されている点に注目)

このため例えばNetAdapterモジュールにあるGet-NetAdapterコマンドレットもふつうに呼び出して利用することが可能です。

f:id:stknohg:20180930203229p:plain

ちなみに、新しいOSであってもPowerShell Core 6.0ではダメですので注意してください。

f:id:stknohg:20180930203301p:plain

WindowsCompatibilityモジュールについて

新しいOSであればPowerShell CoreとWindows PowerShellの互換性が大幅に増したことはわかりましたが、残念ながら古いOSにおいてはPowerShell Core 6.1(以降)でも互換性は従来のまま変わりません。

この問題を解消するためにPowerShell TeamによってWindowsCompatibilityというモジュールが提供されています。

github.com

このモジュールはもともと前項の互換性の向上で対応しきれない部分を補完するために作られた様なのですが、古いOSにおける互換性の向上にも使うことができ、こちらの用途の方か多く使われそうな雰囲気を感じます。


【2018/11/16追記】

本日このモジュールのVer.1.0.0がGAされ、PowerShell Team Blogでより詳細な情報が発表されました。

blogs.msdn.microsoft.com

こちらの内容も参考になりますのでぜひご覧ください。

【追記ここまで】

インストール

このモジュールはPowerShell Galleryからインストール可能です。

Install-Module WindowsCompatibility -Scope CurrentUser

使い方

このモジュールの基本的な使い方は、Import-WinModuleコマンドで利用したいWindows PowerShell向けのモジュールを読み込むとそのモジュールの機能が利用可能になります。

例として先述のNetAdapterモジュールにあるGet-NetAdapterコマンドレットを使いたい場合は、

# Import-WinModuleでWindows PowerShell向けのモジュールを読み込む
Import-WinModule NetAdapter

# あとは使いたい機能を利用する
Get-NetAdapter

f:id:stknohg:20180930211039p:plain

(一部結果の表示がおかしくなっているが、プロパティの値は取得できている)

この他にWindows PowerShellのランタイム上でスクリプトブロックを実行してその結果を返すInvoke-WinCommandといった機能もあります。

# Windows PowerShell上でスクリプトブロックを実行
Invoke-WinCommand -ScriptBlock { $PSVersionTable }

f:id:stknohg:20180930211157p:plain

ほかにもまだいくつか機能はあるのですが、本エントリではこのくらいにしておきます。
細かい説明説明はQuick Start Guideを参照してください。

補足

先ほどの例でもそうでしたが、Import-WinModule使ってインポートしたモジュールは完全に互換があるわけではなく、一部表示などに問題がでる様です。
もしこの様な不具合が出た場合はInvoke-WinCommandを使う方が良いでしょう。

# 例えば表示がおかしい場合はInvoke-WinCommand内で調整して対応することが出来る
# ※対応方法はケースバイケースであるが...
Invoke-WinCommand { Get-NetAdapter | Out-String }

f:id:stknohg:20180930211834p:plain

WindowsCompatibilityモジュールの仕組み

このモジュールの仕組みについてですが、仕組みそのものは割と単純です。

ローカルホストのWindows PowerShellに対してリモートセッションをひとつ張り、PowerShell Remotingの機能を使いWindows PowerShell側の機能を利用しています。

このため、このモジュールの使用中は専用のPSSessionが一つ存在しています。

f:id:stknohg:20180930205622p:plain

そして内部処理を簡単にコードで表すと以下の様な感じになります。

# WindowsCompatibilityモジュールの基本イメージ

# ローカルホストのWindows PowerShellに対してリモートセッションを張る
$session = New-PSSession -ComputerName localhost -ConfigurationName 'Microsoft.PowerShell' -EnableNetworkAccess

# Import-WinModuleの処理イメージ
Import-Module NetAdapter -PSSession $session
Get-NetAdapter

# Invoke-WinCommandの処理イメージ
Invoke-Command -ScriptBlock { $PSVersionTable } -Session $session

最後に

ざっとこんな感じです。

PowerShell CoreのWindows PowerShellに対する互換性は日々向上していますので最新のPowerShell Coreをどんどん使っていくのが良いのではないかと思います。