しばたテックブログ

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

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

Server Core App Compatibility Feature on DemandのInternet Explorerに対する雑感

前のエントリ

blog.shibata.tech

でServer Core App Compatibility Feature on Demand(FoD)を使いServer CoreにInternet Explorer(IE)がインストールできることを述べましたが、本エントリではこの点について思ったことを書いていきます。

そもそもServer CoreにIEは必要なのか?

そもそもServer CoreにIEは必要なのか?

私が本エントリを書こうと思った理由はこの一点に尽きます。

もともとServer Coreはサーバーとして必須でない機能を削ってOS全体の軽量化とセキュリティの強化を図ったものです。

IEの用途は基本的にWEBブラウスで、サーバー上であればソフトウェアのインストーラーなどのリソースのダウンロードに使うくらいしかないでしょう。
従来からリソースのダウンロードにはPowerShellが使えましたし、たかがダウンロードのためだけにセキュリティ的にナイーブな機能であるIEを追加するというのは、個人的には全く割に合わないものだと思っています。

公式にIEを追加すべき動機について一切アナウンスされておらず、いまのところはこの疑問に対する答えを得ることはできません...

一応想像するかぎりでは、

  • IEコンポーネントを内部的に使うアプリケーションとの互換性確保のため?
  • かつてのmodern.ieの様な、テスト用途のIE入りWindows Server Containerを目論んでいる?

などを思いつきましたが、あくまでも私の妄想であり何とも言えません。

Server CoreでのWEBアクセスについて

私の妄想はさておき、Server CoreでのWEBアクセスについての基本的なところとIEを導入することによる挙動の変化について触れておきます。

IEがなくてもWEBアクセスの方法は以下に示す様に十分ありますし、IEが必要なケースがでればFoDで導入するとよいでしょう。

Windows PowerShellの利用

従来のServer CoreではIEが無いため、WEBアクセスは基本的にPowerShellで、Invoke-WebRequestInvoke-RestMethodを使う必要がありました。

これらの機能は内部的にIEコンポーネントを利用しているのですが、IEコンポーネントを利用しないWEBアクセスを行うために-UseBasicParsingというパラメーターがあり、Server Coreではこのパラメーターを指定してやる必要があります。
また、単純にファイルをダウンロードするだけであればこのパラメーターをつけなくてもServer Coreで動作させることは可能です。

# Server Coreでは-UseBasicParsingをつけないとエラーになる
Invoke-WebRequest -Uri 'https://www.yahoo.co.jp/'

f:id:stknohg:20181004160245p:plain

# -UseBasicParsingをつけてエラーを回避
Invoke-WebRequest -Uri 'https://www.yahoo.co.jp/' -UseBasicParsing

f:id:stknohg:20181004160308p:plain

# ファイルのダウンロードだけなら-UseBasicParsingはなくても良い
Invoke-WebRequest -Uri 'https://download.sysinternals.com/files/SysinternalsSuite.zip' -OutFile '.\SysinternalsSuite.zip'

f:id:stknohg:20181004160326p:plain

そして、FoDでIEを追加すると最初の-UseBasicParsingをつけない場合でも動作する様になります。

f:id:stknohg:20181004160341p:plain

これによりフルのWindows Serverと同等の互換性を得ることはできますが、この程度の互換が必要かといわれると疑問ですし、その様なシナリオも皆無と思っています。

curl.exeの利用

Windows Server 2019にはWindowsにポートされたcurl.exeが導入されており、Server Coreでも利用可能です。

f:id:stknohg:20181004160358p:plain

このためPowerShellに頼らなくてもcurlにより強力なWEBアクセスができる様になっています。

curl.exe https://www.yahoo.co.jp

f:id:stknohg:20181004160408p:plain

curl.exe https://download.sysinternals.com/files/SysinternalsSuite.zip -o .\SysinternalsSuite.zip

f:id:stknohg:20181004160418p:plain

PowerShell Coreの利用

こちらはおまけです。

もしWindows PowerShellのWEBアクセスで機能が足りていない部分があるのであれば、PowerShell Coreを追加でインストールするのも一つの解となるかもしれません。
PowerShell CoreのWebCmdletsはIEへの依存が排除されており、また、新しいWEBの仕組みへの対応もいくつかなされています。

インストール自体は手作業で行う必要がありますが、拙作のインストーラーを使えばワンライナーでインストールできます。

blog.shibata.tech

REM Command prompt
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12;iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/stknohg/PSCoreUpdate/master/FirstTimeInstaller/Install-LatestPowerShell.ps1'))"

f:id:stknohg:20181004161620p:plain

f:id:stknohg:20181004161751p:plain

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