PowerShell メモ1: パイプライン

目次

概要

PowerShell について個人的なまとめを書いておく。PowerShell の基本そのものは他の文献におまかせする。 なお、この記事のコードは PowerShell 7.3 で確認している。

PowerShell は仕事を楽にできる

2017年頃から、PowerShell は Windows 10 の デフォルトのシェルになったにもかかわらず、未だに多くの方が「独自拡張されたコマンドプロンプト」ぐらいにしか思っていないらしい。コマンドプロンプトと DOS 形式のバッチファイルは、MS-DOS から Windows に至る 20 年近く標準搭載されてきたため、「不便だけどタダで使える」「自動実行するための秘伝のタレ」として根付いてしまっているようだ。

Linux や Mac には、歴史は長いがそれなりに高機能な shell (主にbash) があるので、Windows 環境はシステム系や開発系で虐げられてきたのだと思う。

私が「PowerShell はどうも使えそうだ」と思ったのは、VMware 社の vSphere PowerCLI を知ってからである。それまで ESXi に登録された VM 一覧を取得するためには、vSphere Perl SDK でスクリプトを書く必要があり面倒だった。最近の流れで行けば、vSphere Python SDK が登場してもおかしくないのに、なぜか PowerShell 製。VMware 社 SDK シリーズ一押し感があるので試してみたら、思いのほか便利だった。

vSphere PowerCLI で VM 操作をしてみる

まずは ESXi または vCenter に接続:

Connect-VIServer -Server "サーバホスト名" -User "ユーザー" -Password "パスワード" -Force

VM一覧を表示:

Get-VM

VM一覧をCSVファイルに出力:

Get-VM | Export-Csv vmlist.csv

電源が入っている VM のみ、VM 名でソートして CSV ファイル出力:

Get-VM | where PowerState -eq "PoweredOn" | sort Name | Export-Csv vmlist-sorted.csv

見ての通り直感的に読めるし、データの加工も簡単になる。ここでは触れないが、特定条件の VM だけ抽出してシャットダウンや起動、利用メモリの集計なども割と簡単に実装できる。

PowerShell とパイプ処理 (本題)

PowerShell パイプ処理は他のスクリプト言語にはない特徴があり、これを意識すると少ない行数でわかりやすく処理が書ける。他のシェルとどう違うのか、比較してみる。

Unix系 (Linux/Mac含む) shell でのパイプ

コマンド間の接続はすべてテキスト情報になり、出力でテキストに変換し、入力でテキストをパースして取りこむ。複雑な処理になると awk や sed、あるいは perl を駆使して正規表現などでゴリゴリと加工する必要がある。

この方式の弱点は、前段の commandA の出力仕様(テキストフォーマット)が変わると途端に動かなくなることである。 例えば、前段の日付出力が 「Jan 19 2023」を想定していたら、アップデートで日本語ロケール処理が加わり「2023年1月19日」に変わっていたことがあった。

PowerShell のパイプ処理のイメージ

PowerShell では、「shell のコマンドに該当するもの」を コマンドレット (cmdlet) と呼ぶ。コマンドレット間は PSCustomObject という共通オブジェクト型の配列で渡される。

PSCustomObject は、JSON やレジストリのような複雑なツリーデータ形式を表現できる。各データはそれぞれキー名と値を持ち、個別の型も持っているため、浮動小数点型や日付型、バイナリデータも正確に保持したまま次のコマンドレットに引き渡すことができる。受け側のコマンドレットでは文字列をパースする必要がないので、本質的な処理だけを書けばいい。

PowerShell と型

PSCustomObject の説明入る前に、PowerShellで 意識すべきことを挙げる。

  • 変数名の前には必ず $ を付ける。型は意識しない。
    • Perl は型によって先頭文字 ($ @ %) が異なる
    • Python は先頭文字は付けない
    • Ruby は型によって付けたり付けなかったり
    • bash は代入するときは付けず、参照するときに $ を付ける
  • 大抵の値は「型を持ったオブジェクト」になる
  • 変数に値を代入すると型が確定する
  • 異なる型の演算は、自動的によろしく型変換される
  • 変数名だけを書くと、オブジェクトの中身が自動整形してよろしく出力される
    • デバッグがとても楽

型の違いを見てみる

型を調べるには、変数名やオブジェクトに .GetType() を付ければ良い。

PS C:\> $a = 1   #整数
PS C:\> $b = 1.55   #浮動小数点数
PS C:\> $c = "This is a pen!"  #文字列

PS C:\> $a.GetType()
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType

PS C:\> $b.GetType()
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Double                                   System.ValueType

PS C:\> $c.GetType()
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

値は「自動的に型を持つ」という特性が分かりやすい例:

次のように変数に代入せずとも計算できる。PowerShell は簡単な電卓になる。

PS C:\> 10 + 200
210
PS C:\> (10 + 30) / 2
20
PS C:\> "Hello! " + "World " + 2023
Hello! World 2023

型の異なる演算はどうなるのか

よろしく型を合わせてくれる。

PS C:\> $x = $a + $b  # 整数 + 浮動小数点数
PS C:\> $x            # 結果表示
2.55
PS C:\> $x.GetType()  # 型を表示 → Double (浮動小数点数)
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Double                                   System.ValueType
PS C:\> $y = $c + $a  # 文字列 + 整数
PS C:\> $y            # 結果表示
This is a pen!1
PS C:\> $y.GetType()  # 型を表示 → String (文字列)
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

PSCustomObject とは

書籍「Windows PowerShell クックブック」(オライリー/2008年初版) には、PSCustomObject は出てこない。本文中には対象バージョンの記載がないが、年代的に PowerShell 1.0 (2006年リリース) だと思われる。この頃の説明では「パイプは .NET のオブジェクトで渡される」と記載がある。

PowerShell 2.0 (2009年) では PSObject という型が登場し、PowerShell 3.0 (2012年) で、PSCustomObject が追加された。PowerShell 7.0 でも PSObject は型として指定できるが、パフォーマンスやメモリ使用量の問題から現在では PSCustomObject が推奨となっている。

(もう絶滅したはずの) Windows Server 2008 / Windows 7 標準搭載の PowerShell 2.0 は、PSCustomObject は使えないので注意。

PSCustomObject を作る

PSCustomObject は、普通のハッシュテーブルのように 「キー」=「値」 の形式で利用できる。

自分で PSCustomObject を作る、今どきの簡単な書き方:

(1) シンプルなプロパティ構造

$pcInfo = [PSCustomObject]@{
    Name = "PC0001"
    HardwareManufacturer = "Lenovo"
    HardwareModelName = "ThinkPad"
    HardwareSerialNumber = "PN00XXXX"
    OSManufacturer = "Microsoft"
    OSProduct = "Windows 11 Pro"
    OSVersion = "22H2"
    OSBuildNo = "22621.1265"   
}

(2) 多重構造で表現

$pcInfoTree = [PSCustomObject]@{
    Name = "PC0001"
    Hardware = [PSCustomObject]@{
        Manufacturer = "Lenovo"
        ModelName = "ThinkPad"
        SerialNumber = "PN00XXXX"
    }
    OS = [PSCustomObject]@{
        Manufacturer = "Microsoft"
        Product = "Windows 11 Pro"
        Version = "22H2"
        BuildNo = "22621.1265"
    }
}

(1) の結果

❯ $pcInfo

Name                 : PC0001
HardwareManufacturer : Lenovo
HardwareModelName    : ThinkPad
HardwareSerialNumber : PN00XXXX
OSManufacturer       : Microsoft
OSProduct            : Windows 11 Pro
OSVersion            : 22H2
OSBuildNo            : 22621.1265

(2) の結果

$変数名.キー名 で中身を参照できる

❯ $pcInfoTree

Name   Hardware                                                          OS
----   --------                                                          --
PC0001 @{Manufacturer=Lenovo; ModelName=ThinkPad; SerialNumber=PN00XXXX} @{Manufacturer=Microso…

❯ $pcInfoTree.Hardware

Manufacturer ModelName SerialNumber
------------ --------- ------------
Lenovo       ThinkPad  PN00XXXX

❯ $pcInfoTree.OS

Manufacturer Product        Version BuildNo
------------ -------        ------- -------
Microsoft    Windows 11 Pro 22H2    22621.1265

(2) の方が一見整理されているように見えるが、データ更新や参照、ループ処理が複雑になるので、必要でなければ (1) のように平たい構造を推奨する。

多重構造が便利な例

例えば PC にインストールされている複数のアプリ情報を列挙したい場合は、配列をぶら下げる方がよい。

$pcInfo = [PSCustomObject]@{
    Name = "PC0001"
    #....
    InstalledApps = @(
        [PSCustomObject]@{
            Manufacturer = "Microsoft"
            Name = "Visual Studio Code"
            Version = "1.75.1"
        },
        [PSCustomObject]@{
            Manufacturer = "Mozilla Foundation"
            Name = "Firefox"
            Version = "110.0 (64bit)"
        }
    )
}

結果

❯ $pcInfo

Name   InstalledApps
----   -------------
PC0001 {@{Manufacturer=Microsoft; Name=Visual Studio Code; Version=1.75.1}, @{Manufacturer=Mozi…

❯ $pcInfo.InstalledApps

Manufacturer       Name               Version
------------       ----               -------
Microsoft          Visual Studio Code 1.75.1
Mozilla Foundation Firefox            110.0 (64bit)

JSON との相性

Web 系のデータ交換でよく使われる JSON (JavaScript Object Notation) 形式は、PSCustomObject の内部表現に近く、下記のように変換できる。

PSCustom Object から JSON への変換:

❯ $pcInfo | ConvertTo-Json
{
  "Name": "PC0001",
  "InstalledApps": [
    {
      "Manufacturer": "Microsoft",
      "Name": "Visual Studio Code",
      "Version": "1.75.1"
    },
    {
      "Manufacturer": "Mozilla Foundation",
      "Name": "Firefox",
      "Version": "110.0 (64bit)"
    }
  ]
}

インデント不要なデータ交換専用であれば -Compress オプションを使う。

❯ $pcInfo | ConvertTo-Json -Compress
{"Name":"PC0001","InstalledApps":[{"Manufacturer":"Microsoft","Name":"Visual Studio Code","Version":"1.75.1"},{"Manufacturer":"Mozilla Foundation","Name":"Firefox","Version":"110.0 (64bit)"}]}

JSON から PSCustomObject へ変換

Web API 等から JSON 形式データを持ってきて PowerShell で処理しやすくするには、丸ごと PSCustomObject に入れてしまうのがよい。まずは適当な JSON 文字列データを変数に取得しておく。

PowerShell で curl 的なデータ取得をするには Invoke-WebRequest を使う。

JSONデータ取得の例: 気象庁の東京地方の気象情報

Invoke-WebRequest https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json

Invoke-WebRequest は、HTTP レスポンス全体を PSCustomObject に変換して返す。

StatusCode        : 200
StatusDescription : OK
Content           : [{"publishingOffice":"気象庁","reportDatetime":"2023-02-18T17:00:00+09:00","
                    timeSeries":[{"timeDefines":["2023-02-18T17:00:00+09:00","2023-02-19T00:00:0
                    0+09:00","2023-02-20T00:00:00+09:00"],"areas":[{"ar…
RawContent        : HTTP/1.1 200 OK
                    Server: Apache
                    Accept-Ranges: bytes
                    X-Content-Type-Options: nosniff
                    Access-Control-Allow-Origin: *
                    Access-Control-Allow-Headers: X-Requested-With
                    Cache-Control: max-age=23
                    Date:…
Headers           : {[Server, System.String[]], [Accept-Ranges, System.String[]], [X-Content-Typ
                    e-Options, System.String[]], [Access-Control-Allow-Origin, System.String[]]…
                    }
Images            : {}
InputFields       : {}
Links             : {}
RawContentLength  : 6742
RelationLink      : {}

JSON データは、キー “Content” に含まれているため、これを取り出す。※エラーハンドリングはないものとする。

$weatherJson = (Invoke-WebRequest https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json).Content

JSON はこんな感じ。(読みづらいためエディタで整形済み)

[
    {
        "publishingOffice": "気象庁",
        "reportDatetime": "2023-02-18T17:00:00+09:00",
        "timeSeries": [
            {
                "timeDefines": [
                    "2023-02-18T17:00:00+09:00",
                    "2023-02-19T00:00:00+09:00",
                    "2023-02-20T00:00:00+09:00"
                ],
                "areas": [
                    {
                        "area": {
                            "name": "東京地方",
                            "code": "130010"
                        },
                        "weatherCodes": [
                            "200",
                            "202",
                            "101"
                        ],
                        "weathers": [
                            "くもり 所により 夜遅く 雨",
                            "くもり 後 晴れ 明け方 一時 雨",
                            "晴れ 時々 くもり"
                        ],
                        "winds": [
                            "南西の風 23区西部 では 南西の風 やや強く",
                            "南西の風 後 北の風 23区西部 では 南西の風 強く",
                            "北の風 後 やや強く"
(以下略)

JSONデータを直接 PSCustomObject に入れるには、ConvertFrom-Json を使う。ConvertFrom-Json は、String 型オブジェクトをパイプで受け取り、PSCustomObject に変換している。

$wetherData = (Invoke-WebRequest https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json).Content | ConvertFrom-Json

明日と明後日の東京地方の天気予報

気象庁のデータを PSCustomObject 型の変数に取りこむところまでできた。東京地方の予報だけを抜き出して PSCustomObject に流し込んでみる。

ファイル名を Get-Weather.ps1 としてスクリプト作成。

# 東京エリア
$areaCode = "130000"
# 東京地方
$subAreaCode = "130010"
$url = "https://www.jma.go.jp/bosai/forecast/data/forecast/${areaCode}.json"

$weatherData = (Invoke-WebRequest $url).Content | ConvertFrom-Json
# 直近3日間のデータ
foreach ($i in 0..2) {
    foreach ($areaData in $weatherData[0].timeSeries[0].areas) {
        if ($areaData.area.code -eq $subAreaCode) {
            [PSCustomObject]@{
                areaName = $areaData.area.name
                timeDefine = $weatherData[0].timeSeries[0].timeDefines[$i]
                weather = $areaData.weathers[$i]
                wind = $areaData.winds[$i]
                wave = $areaData.waves[$i]
            }
        }
    }
}

結果

❯ .\Get-Weather.ps1

areaName   : 東京地方
timeDefine : 2023/02/18 17:00:00
weather    : くもり 所により 夜遅く 雨
wind       : 南西の風 23区西部 では 南西の風 やや強く
wave       : 0.5メートル 後 1.5メートル

areaName   : 東京地方
timeDefine : 2023/02/19 0:00:00
weather    : くもり 後 晴れ 明け方 一時 雨
wind       : 南西の風 後 北の風 23区西部 では 南西の風 強く
wave       : 2メートル 後 1メートル

areaName   : 東京地方
timeDefine : 2023/02/20 0:00:00
weather    : 晴れ 時々 くもり
wind       : 北の風 後 やや強く
wave       : 0.5メートル 後 1メートル

気象庁のオリジナル JSON データには、他にも最低・最高気温の時系列予想などが含まれていて、すべて PSCustomObject に取りこまれているので、見たいデータだけを取り出して HTML に変換したり、Slack へ投稿するなど、いくらでも応用できる。

Excel 連携

PSCustomObject は CSV ファイルに簡単に変換できるが、文字列変換されてしまうので、情報が落ちることになる。PowerShell にモジュールを追加すると、Excel ファイルの扱いが簡単にできるようになる。

Excel 形式 (.xlsx) は CSV 形式と違って下記の特徴を持つ。

  • セルごとに独立したデータなので、CSV 区切り文字の「,」(コンマ) 等がデータ内に混入してもエスケープする必要がない
  • .xlsx 形式は UTF-8 での表現が規定されていて、文字化けの可能性がほぼない
    • CSV の場合は UTF-8 の BOM を付与するか、読み込み側で UTF-8 エンコーディングを意図的に指定する必要がある
  • セル単位で「型」を持つ。数値・文字列・日付等のデータとして正確に保持できる
  • .xlsx 形式は、Office Open XML Workbook ECMA-376 / ISO/IEC 29500-1:2016 として国際規格化されている

ImportExcel (Doug Finke氏 作成) という PowerShell モジュールをインストールすることで、.xlsx 形式と PSCustomObject 間のインポートとエクスポートが可能になる。とても便利なモジュールなので最後に紹介しておく。

ImportExcel のインストール

Install-Module -Name ImportExcel

実行例

例) 先ほど抽出した東京地方の天気予報を Excel ファイルに出力

.\Get-Weather.ps1 | Export-Excel processes.xlsx

結果: ※フォントは変更しています

その他応用

Export-Excel で業務運用で使用している例を紹介しておく。

  • 冒頭に紹介した VMware ESXi サーバの VM 一覧を Excel ファイルに直接出力
  • Active Directory のユーザー一覧を Excel に出力
  • Azure のユーザーごとの権限リストを Excel に出力

一度 Excel にしてしまえば、PowerShell を直接障るのが困難なメンバーにも、データとして渡すことができる。抽出から出力までを自動化できるため、面倒な繰り返し業務も減らすことができて助かっている。

参考