⚠️ Note: Auto-translated Article
This page was generated by AI-assisted automatic translation from the original Japanese article.

Overview

These are my personal notes on PowerShell. For the basics of PowerShell itself, I’ll leave that to other references. The code in this article was verified on PowerShell 7.3.

PowerShell Can Make Work Easier

Even though PowerShell has been the default shell on Windows 10 since around 2017, many people still seem to think of it as little more than “an enhanced Command Prompt.” The Command Prompt and DOS-style batch files shipped as standard for nearly 20 years from MS-DOS through to Windows, so they’ve become entrenched as “inconvenient but free” or a “secret recipe for automation.”

Linux and Mac have had shells (mainly bash) that, while old, are reasonably capable — so Windows environments have historically been second-class citizens in system administration and development.

My own “PowerShell might actually be useful” moment came when I discovered VMware’s vSphere PowerCLI. Previously, getting a list of VMs registered in ESXi required writing scripts with the vSphere Perl SDK, which was tedious. You’d think a vSphere Python SDK would have appeared by now, but it’s somehow PowerShell-based. VMware seems to push it as their flagship SDK, so I gave it a try and found it surprisingly convenient.

Trying VM Operations with vSphere PowerCLI

First, connect to ESXi or vCenter:

Connect-VIServer -Server "server-hostname" -User "username" -Password "password" -Force

Display VM list:

Get-VM

Export VM list to CSV file:

Get-VM | Export-Csv vmlist.csv

Export only powered-on VMs, sorted by name, to CSV:

Get-VM | Where-Object PowerState -eq "PoweredOn" | Sort-Object Name | Export-Csv "vmlist-sorted.csv"

As you can see, it’s intuitively readable and data manipulation becomes easy. Not covered here, but extracting only VMs matching certain criteria for shutdown/startup, or aggregating memory usage, are all relatively straightforward to implement.

PowerShell and Pipeline Processing (The Main Topic)

PowerShell pipelines have characteristics not found in other scripting languages — being aware of this allows you to write clear, concise processing with fewer lines. Let’s compare how it differs from other shells.

Pipelines in Unix-style (Linux/Mac) Shells

Unix-style shell pipeline

All connections between commands become text — output is converted to text, and input is parsed from text. For complex processing, you end up wrestling with awk, sed, or perl to handle it with regular expressions.

The weakness of this approach is that if the output format of the preceding commandA changes, things break immediately. For example: if a date output previously produced “Jan 19 2023” and an update introduced Japanese locale handling that changed it to “2023年1月19日”, suddenly nothing works.

How PowerShell Pipelines Work

In PowerShell, the equivalent of shell commands are called cmdlets. Cmdlets pass data between each other as arrays of a common object type called PSCustomObject.

PowerShell pipeline

PSCustomObject can represent complex tree data formats like JSON or the registry. Each piece of data has a key name and a value, and each has its own type — so floating-point values, dates, and binary data are all passed to the next cmdlet with their types preserved intact. The receiving cmdlet doesn’t need to parse strings, so it can focus purely on its own logic.

PowerShell and Types

Before explaining PSCustomObject, a few things to be aware of in PowerShell:

  • Variable names always start with $. Types don’t need to be declared.
    • In Perl, the leading character ($ @ %) differs by type
    • In Python, no leading character
    • In Ruby, sometimes present, sometimes not, depending on the type
    • In bash, no $ when assigning, but $ required when referencing
  • Most values become “typed objects”
  • The type is determined when a value is assigned to a variable
  • Operations on different types are automatically coerced as needed
  • Writing a variable name alone automatically formats and outputs its contents
    • Makes debugging very easy

Looking at Type Differences

Use .GetType() on a variable or object to check its type.

PS C:\> $a = 1   # integer
PS C:\> $b = 1.55   # floating point
PS C:\> $c = "This is a pen!"  # string

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

A clear example of “values automatically have types”:

You can calculate without assigning to a variable. PowerShell makes a great quick calculator.

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

What Happens When Types Differ in an Operation?

Types are coerced automatically.

PS C:\> $x = $a + $b  # integer + float
PS C:\> $x            # display result
2.55
PS C:\> $x.GetType()  # display type → Double (floating point)
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Double                                   System.ValueType
PS C:\> $y = $c + $a  # string + integer
PS C:\> $y            # display result
This is a pen!1
PS C:\> $y.GetType()  # display type → String
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

What Is PSCustomObject?

The book Windows PowerShell Cookbook (O’Reilly, first edition 2008) doesn’t mention PSCustomObject. The covered version isn’t stated explicitly, but given the era it was likely PowerShell 1.0 (released 2006). The description at the time says “pipe passes .NET objects.”

PowerShell 2.0 (2009) introduced the PSObject type, and PowerShell 3.0 (2012) added PSCustomObject. PSObject is still specifiable as a type in PowerShell 7.0, but due to performance and memory concerns, PSCustomObject is now recommended.

Note: the (presumably extinct by now) PowerShell 2.0 shipped with Windows Server 2008 / Windows 7 does not support PSCustomObject.

Creating a PSCustomObject

PSCustomObject can be used like an ordinary hash table with “key” = “value” pairs.

The modern, concise way to create a PSCustomObject yourself:

(1) Simple flat property structure

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

(2) Nested structure

$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"
    }
}

Result of (1):

❯ $pcInfo

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

Result of (2):

※ Contents can be referenced with $variableName.keyName

❯ $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) looks more organized at first glance, but data updates, lookups, and loop processing become more complex, so unless necessary, the flat structure of (1) is recommended.

When Nested Structures Are Useful

For example, if you want to enumerate multiple apps installed on a PC, hanging an array off the object works well.

$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)"
        }
    )
}

Result:

❯ $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)

Compatibility with JSON

JSON (JavaScript Object Notation), widely used for web data exchange, is close to PSCustomObject’s internal representation and can be converted as follows.

PSCustomObject to 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)"
    }
  ]
}

For compact data exchange without indentation, use the -Compress option:

❯ $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)"}]}

Converting JSON to PSCustomObject

To pull JSON data from a Web API and process it easily in PowerShell, the best approach is to load the whole thing into a PSCustomObject. First, fetch the JSON string into a variable.

To perform curl-like data fetching in PowerShell, use Invoke-WebRequest.

Example of fetching JSON: Tokyo area weather forecast from the Japan Meteorological Agency

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

Invoke-WebRequest converts the entire HTTP response into a PSCustomObject and returns it.

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      : {}

The JSON data is in the “Content” key, so extract it. ※ No error handling here.

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

The JSON looks like this (formatted in an editor for readability):

[
    {
        "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": [
                            "くもり 所により 夜遅く 雨",
                            "くもり 後 晴れ 明け方 一時 雨",
                            "晴れ 時々 くもり"
                        ],
...

To load JSON data directly into a PSCustomObject, use ConvertFrom-Json. It receives a String object via the pipeline and converts it to PSCustomObject.

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

2024-12-29 Update

Using Invoke-RestMethod makes the above roundabout operation a one-liner. It loads the JSON response directly into a PSCustomObject.

$weatherData = Invoke-RestMethod "https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json"

Tokyo Area Weather Forecast for Tomorrow and the Day After

We’ve now loaded the JMA data into a PSCustomObject. Let’s extract just the Tokyo area forecast and feed it into another PSCustomObject.

Save as Get-Weather.ps1:

# Tokyo area
$areaCode = "130000"
# Tokyo sub-area
$subAreaCode = "130010"
#$url = "https://www.jma.go.jp/bosai/forecast/data/forecast/${areaCode}.json"

#$weatherData = (Invoke-WebRequest $url).Content | ConvertFrom-Json
$weatherData = Invoke-RestMethod $url

# Data for the next 3 days
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]
            }
        }
    }
}

Result:

❯ .\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メートル

The original JMA JSON data contains much more, including time-series forecasts of min/max temperatures — all loaded into the PSCustomObject. You can extract whatever data you want, convert it to HTML, post it to Slack, or apply it in any number of ways.

Excel Integration

PSCustomObject can easily be converted to CSV, but that converts everything to strings and loses type information. Adding a module to PowerShell makes Excel file handling straightforward.

Excel (.xlsx) format has the following advantages over CSV:

  • Each cell holds independent data, so commas (,) or other delimiters within data don’t need escaping
  • The .xlsx format specifies UTF-8 encoding, so character encoding issues are virtually non-existent
    • CSV requires either a UTF-8 BOM or intentional UTF-8 encoding specification on the reader side
  • Each cell holds a “type” — numbers, strings, dates, etc. are preserved accurately
  • The .xlsx format is an international standard: Office Open XML Workbook ECMA-376 / ISO/IEC 29500-1:2016

Installing the PowerShell module ImportExcel (by Doug Finke) enables importing and exporting between .xlsx format and PSCustomObject. It’s an extremely useful module, so I’ll introduce it at the end.

Installing ImportExcel

Install-Module -Name ImportExcel

Usage Example

Example: Export the Tokyo weather forecast extracted earlier to an Excel file

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

Result: ※ Font has been changed

Exporting PSCustomObject to Excel

Other Applications

Some examples of Export-Excel in real operational use:

  • Export the VMware ESXi VM list mentioned at the start directly to an Excel file
  • Export Active Directory user lists to Excel
  • Export per-user permission lists from Azure to Excel

Once in Excel, data can be handed off to team members who aren’t comfortable with PowerShell directly. Automating the extraction-to-output pipeline eliminates tedious repetitive tasks — very helpful.

References