Optimize PowerShell code to avoid calling the cmdlet multiple times inside calculated properties?

Viewed 65

I am looking to optimize the code below to avoid calling the same command twice under the calculated properties.

https://docs.microsoft.com/en-us/powershell/module/az.compute/get-azvm

https://docs.microsoft.com/en-us/powershell/module/az.compute/get-azvmsize

Get-AzVM | Select-Object-Object Name,
    @{ l = 'osdiskingb'; e = { ($_.StorageProfile.OsDisk.DiskSizeGB) } }, `
    @{ l = 'memory'; e = { $size = $_.HardwareProfile.VmSize; Get-AzVMSize -vmname $_.Name -ResourceGroupName $_.ResourceGroupName | Where-Object { $_.name -eq $size } | Select-Object -expand MemoryInMB } }, `
    @{ l = 'cpu'; e = { $size = $_.HardwareProfile.VmSize; Get-AzVMSize -vmname $_.Name -ResourceGroupName $_.ResourceGroupName | Where-Object { $_.name -eq $size } | Select-Object -expand NumberOfCores } }, `
    @{ l = 'nic'; e = { $_.NetworkProfile.NetworkInterfaces.id.split('/') | Select-Object -Last 1 } }, `
    @{ l = 'ip'; e = { $nic = $_.NetworkProfile.NetworkInterfaces.id.split('/') | Select-Object -Last 1; Get-AzNetworkInterface -Name $nic | Select-Object -expand ipconfigurations | Select-Object -expand privateipaddress } }

The script above works for pulling various different Azure VMs.

What can I try next?

2 Answers

Note:

  • This answer addresses the question as asked, in the context of Select-Object and calculated properties.

  • For a ForEach-Object-based alternative that uses explicit construction of [pscustomobject] instances, see zett42's helpful answer.


While the script blocks of calculated properties are executed in sequence, for each input object, they each run in their own child scope relative to the caller, which complicates sharing state between them.

However, you can simply create a variable whose value you want to share in the parent scope, which in the simplest case inside a script is the $script: scope, as the following simplified example shows (which uses a call to Get-Date in lieu of a call to Azure cmdlet as an example of a call you do not want to repeat):

# Share the result of the `Get-Date` call between calculated properties.
'foo' | Select-Object `
  @{ n='Month'; e = { $script:dt = Get-Date; $dt.Month } },
  @{ n='Year'; e = { $dt.Year } }

Output:

Month Year
----- ----
    8 2022

This proves that the $script:-scoped $dt variable was successfully used in the second calculated property.

If you want to reliably target the parent scope, which may differ from the $script: scope if you're running inside a nested function call, for instance, replace $script:dt = Get-Date with
Set-Variable -Scope 1 dt (Get-Date)

Note:

  • That script blocks of calculated properties as well as delay-bind script blocks run in a child scope may be surprising, given that it contrasts with the behavior of script blocks passed to ForEach-Object and Where-Object, for instance - for a discussion, see GitHub issue #7157.

This might not exactly answer your original question, but you might consider dropping calculated properties when the code becomes too complicated. Instead, use a [pscustomobject]@{…} literal in a ForEach-Object script block. This way you can move common code out of the properties to the begin of the script block.

Get-AzVM | ForEach-Object {
    $size = $_.HardwareProfile.VmSize
    $vmsize = Get-AzVMSize -vmname $_.Name -ResourceGroupName $_.ResourceGroupName | Where-Object { $_.name -eq $size }
    $nic = $_.NetworkProfile.NetworkInterfaces.id.split('/') | Select-Object -Last 1

    # Implicitly outputs an object with the given properties
    [pscustomobject]@{
        Name       = $_.Name
        osdiskingb = $_.StorageProfile.OsDisk.DiskSizeGB
        memory     = $vmsize.MemoryInMB
        cpu        = $vmsize.NumberOfCores
        nic        = $nic
        ip         = (Get-AzNetworkInterface -Name $nic).ipconfigurations.privateipaddress  
    } 
}

On a side note, SomeCommand | Select-Object -Expand PropertyName isn't very efficient and can be replaced by member access, as I did for the ip property. The key is to enclose the command in parentheses.

Related