Search code examples
powershellpowercli

Add Column to Powershell Display Output


I'm writing a script to audit the disk partition types in our virtual environment. I've got this code that produces the following output:

$vmName = "VM NAME"

$output = Invoke-VMScript -ScriptText {Get-Disk | select Number, @{name='Size (GB)';expr={[int]($_.Size/1GB)}}, PartitionStyle} -VM $vmName -GuestUser $Username -GuestPassword $Password

$output.ScriptOutput | FT -AutoSize

Output:

Number Size (GB) PartitionStyle
------- --------- --------------
      0       160 MBR

This alone is fine, however I also want to add the name of the VM as well, since this will be looping through thousands of VMs. I tried this, but it produced blank output for the ComputerName:

$vmName = "VM NAME"

$output = Invoke-VMScript -ScriptText {Get-Disk | select @{l="ComputerName";e={$vmName}}, Number, @{name='Size (GB)';expr={[int]($_.Size/1GB)}}, PartitionStyle} -VM $vmName -GuestUser $Username -GuestPassword $Password

$output.ScriptOutput | FT -AutoSize

Output:

ComputerName Number Size (GB) PartitionStyle
------------ ------ --------- --------------
                  0       160 MBR           

Any ideas for how I can make this work?

As a bonus, any ideas for how I can add the actual name of the VM in there since the vSphere name may not be the actual name of the machine?


Solution

  • tl;dr

    • Invoke-VMScript -ScriptText accepts only a string containing PowerShell commands, not a script block ({...}).

    • Since there is no separate parameter for passing arguments to the -ScriptText code, the only way to include - invariably stringified - values from the caller's scope is to use string interpolation, as shown in the next section.

      • Notably, the usual script-block-based mechanism of employing the $using: scope in order to embed values from the caller does not work.
    • Passing a script-block literal { ... } to -ScriptText technically works, because on conversion to string the verbatim content between { and } is used, but is best avoided, to avoid conceptual confusion.


    Note:

    • Below is an explanation of your problem and a fix for your original approach.

    • You can bypass your problem if you add the calculated columns via select (Select-Object) on the caller's side (as hinted at by iRon), using the VM property of the object output by Invoke-VMScript, which contains the remote computer name (which Lee_Dailey helped discover):

      $vmName = "VM NAME"
      
      # Only run `Get-Disk` remotely, and run the `select` (`Select-Object`)
      # command *locally*:
      # Note the need to extract the actual script output via the .ScriptOutput
      # property and the use of the .VM property to get the computer name.
      $output = Invoke-VMScript -ScriptText { Get-Disk } -VM $vmName -GuestUser $Username -GuestPassword $Password |
        select -ExpandProperty ScriptOutput -Property VM |
          select @{l="ComputerName";e='VM'}, Number, @{name='Size (GB)';expr={[int]($_.Size/1GB)}}, PartitionStyle  
      

    The command you're passing to Invoke-VMScript via -ScriptText is executed on a different machine, which knows nothing about your local $vmName variable.

    In PowerShell remoting commands such as Invoke-Command, the $using: scope would be the solution: ... | select @{l="ComputerName";e={$using:vmName}}, ...

    However, $using: can only be used in script blocks ({ ... }). While you are trying to pass a script block to Invoke-VMScript -ScriptText, it is actually converted to a string before it is submitted to the VM, because Invoke-VMScript's -ScriptText parameter is [string]-typed.

    Since Invoke-VMScript has no separate parameter for passing arguments from the caller's scope, the only way to include values from the caller's scope is to use string interpolation:

    $vmName = "VM NAME"
    
    $output = Invoke-VMScript -ScriptText @"
      Get-Disk | 
        select @{ l="ComputerName"; e={ '$vmName' } }, 
               Number, 
               @{ name='Size (GB)'; expr={[int](`$_.Size/1GB)} },
               PartitionStyle
    "@ -VM $vmName -GuestUser $Username -GuestPassword $Password
    
    • Note the use of an expandable here-string (@"<newline>...<newline>"@); important: the closing delimiter, "@, must be at the very start of a line (not even whitespace is permitted before it); see this answer for more information about string literals in PowerShell.

    • $vmName is now expanded up front in the caller's scope; to make the resulting e={ ... } script block syntactically valid, the expanded $vmName value must be enclosed in '...'.

    • By contrast, the $ in $_ must be _escaped - as `$- since it must be passed through to the remote machine.


    That said, since you want to determine the actual computer name on that remote computer, you don't even need string interpolation; use $env:COMPUTERNAME:

    $vmName = "VM NAME"
    
    $output = Invoke-VMScript -ScriptText @'
      Get-Disk | 
        select @{ l="ComputerName"; e={ $env:COMPUTERNAME } }, 
               Number, 
               @{ name='Size (GB)'; expr={[int]($_.Size/1GB)} },
               PartitionStyle
    '@ -VM $vmName -GuestUser $Username -GuestPassword $Password
    

    Note that in this case - since no string interpolation is needed - a verbatim (literal) here-string (@'<newline>...<newline>'@) is used, which also means that no escaping of pass-through $ chars. is required.

    When string interpolation isn't needed, you could actually pass the script text via a script block ({ ... }), which simplifies the syntax somewhat (a script block stringifies to its verbatim content between { and }), but since a string is ultimately passed, it is better to use one to begin with, for conceptual clarity.