Search code examples
powershellpowershell-7.0output-formatting

Is There a Way to Cause Powershell to Use a Particular Format for a Function's Output?


I wish to suggest (perhaps enforce, but I am not firm on the semantics yet) a particular format for the output of a PowerShell function.

about_Format.ps1xml (versioned for PowerShell 7.1) says this: 'Beginning in PowerShell 6, the default views are defined in PowerShell source code. The Format.ps1xml files from PowerShell 5.1 and earlier versions don't exist in PowerShell 6 and later versions.'. The article then goes on to explain how Format.ps1xml files can be used to change the display of objects, etc etc. This is not very explicit: 'don't exist' -ne 'cannot exist'...

This begs several questions:

  1. Although they 'don't exist', can Format.ps1xml files be created/used in versions of PowerShell greater than 5.1?
  2. Whether they can or not, is there some better practice for suggesting to PowerShell how a certain function should format returned data? Note that inherent in 'suggest' is that the pipeline nature of PowerShell's output must be preserved: the user must still be able to pipe the output of the function to Format-List or ForEach-Object etc..

For example, the Get-ADUser cmdlet returns objects formatted by Format-List. If I write a function called Search-ADUser that calls Get-ADUser internally and returns some of those objects, the output will also be formatted as a list. Piping the output to Format-Table before returning it does not satisfy my requirements, because the output will then not be treated as separate objects in a pipeline.

Example code:

function Search-ADUser {
  param (
    $Name,
    [ValidateNotNullOrEmpty()][string[]]$Properties = @('Enabled', 'SamAccountName', 'Name', 'emailAddress', 'proxyAddresses')
  )
  return Get-ADUser -Filter ('name -like "*{0}*"' -F $Name) -Properties $Properties | Select-Object $Properties
}

The best answers should address both questions, although the second is more salient.

Unacceptable answers include suggestions that the function should not enforce a format, and/or that the user should pipe the output of the function to their formatter of choice. That is a very subjective stance, and whether it is held by the majority or not is irrelevant to the question.

I searched force function format #powershell-7.0 before posting, but none of the search results appeared to be relevant.


Solution

  • Although they 'don't exist', can Format.ps1xml files be created/used in versions of PowerShell greater than 5.1?

    • Yes; in fact any third-party code must use them to define custom formatting.

      • That *.ps1xml files are invariably needed for such definitions is unfortunate; GitHub issue #7845 asks for an in-memory, API-based alternative (which for type data already exists, via the Update-TypeData cmdlet).

      • However, in the context of modules, the need for files is less inconvenient, as they can be bundled with the module and are automatically loaded when referenced from the module manifest's FormatsToProcess entry.

    • It is only the formatting data that ships with PowerShell that is now hardcoded into the PowerShell (Core) executable, presumably for performance reasons.

    is there some better practice for suggesting to PowerShell how a certain function should format returned data?

    The lack of an API-based way to define formatting data requires the following approach:

    • Determine the full name of the .NET type(s) to which the formatting should apply.

      • If it is [pscustomobject] instances that the formatting should apply to, you need to (a) choose a unique (virtual) type name and (b) assign it to the [pscustomobject] instances via PowerShell's ETS (Extended Type System); e.g.:

        • For [pscustomobject] instances created by the Select-Object cmdlet:

          # Assign virtual type name "MyVirtualType" to the objects output
          # by Select-Object
          Get-ChildItem *.txt | Select-Object Name, Length | ForEach-Object {
            $_.pstypenames.Insert(0, 'MyVirtualType'); $_
          }
          
        • For [pscustomobject] literals, specify the type name via a PSTypeName entry:

          [pscustomobject] @{
            PSTypeName = 'MyVirtualType'
            foo = 1
            bar = 2
          }
          
    • Create a *.ps1mxl file for that type and load it into every session.

      • If the commands that rely on this formatting data are defined in a module, you can incorporate the file into your module so that it is automatically automatically when the module is imported.

      • For help on authoring such files, see:

    GitHub proposal #10463 asks for a greatly simplified experience, along the lines of supporting extended [OutputType()] attributes that specify the desired formatting.


    Applied to your sample function:

    • The following function creates a (temporary) *.ps1xml file for its output type on demand, on the first call in the session, so as to ensure that (implicit) Format-Table formatting is applied, for all 5 properties (by default, 5 or more properties result in (implicit) Format-List formatting).

      • As you can see, creating the XML for the formatting definitions is verbose and cumbersome, even without additional settings, such as column width and alignment.

      • A better, but more elaborate solution would be to package your function in a module into whose folder you can place the *.ps1mxl file (e.g., SearchAdUserResult.Format.ps1xml) and then instruct PowerShell to load the file on module import, via the FormatsToProcess key in the module manifest (*.psd1) - e.g., FormatsToProcess = 'SearchAdUserResult.Format.ps1xml'

    • Note that you could alternatively create the *.ps1mxl file directly for the Microsoft.ActiveDirectory.Management.ADUser instances that Get-ADUser outputs, but doing so would apply the formatting session-wide, to any command that emits such objects.

    function Search-ADUser {
      param (
        $Name,
        [ValidateNotNullOrEmpty()][string[]]$Properties = @('Enabled', 'SamAccountName', 'Name', 'emailAddress', 'proxyAddresses')
      )
    
      # The self-chosen ETS type name.
      $etsTypeName = 'SearchAdUserResult'
    
      # Create the formatting data on demand.
      if (-not (Get-FormatData -ErrorAction Ignore $etsTypeName)) {
    
        # Create a temporary file with formatting definitions to pass to 
        # Update-FormatData below.
        $tempFile = Join-Path ([IO.Path]::GetTempPath()) "$etsTypeName.Format.ps1xml"
    
        # Define a table view with all 5 properties.
        @"
    <Configuration>
    <ViewDefinitions>
        <View>
          <Name>$etsTypeName</Name>
          <ViewSelectedBy>
            <TypeName>$etsTypeName</TypeName>
          </ViewSelectedBy>
          <TableControl>
            <TableRowEntries>
              <TableRowEntry>
                <TableColumnItems>
                  <TableColumnItem>
                    <PropertyName>Enabled</PropertyName>
                  </TableColumnItem>
                  <TableColumnItem>
                    <PropertyName>SamAccountName</PropertyName>
                  </TableColumnItem>
                  <TableColumnItem>
                    <PropertyName>Name</PropertyName>
                  </TableColumnItem>
                  <TableColumnItem>
                    <PropertyName>emailAddress</PropertyName>
                  </TableColumnItem>
                  <TableColumnItem>
                    <PropertyName>proxyAddresses</PropertyName>
                  </TableColumnItem>
                </TableColumnItems>
              </TableRowEntry>
            </TableRowEntries>
          </TableControl>
        </View>
      </ViewDefinitions>
    </Configuration>
    "@ > $tempFile
    
        # Load the formatting data into the current session.
        Update-FormatData -AppendPath $tempFile
    
        # Clean up.
        Remove-Item $tempFile
      }
    
      # Call Get-ADUser and assign the self-chosen ETS type name to the the output.
      # Note: To test this with a custom-object literal, use the following instead of the Get-ADUser call:
      #      [pscustomobject] @{ Enabled = $true; SamAccountName = 'jdoe'; Name = 'Jane Doe'; emailAddress = '[email protected]'; proxyAddresses = '[email protected]' }
      Get-ADUser -Filter ('name -like "*{0}*"' -F $Name) -Properties $Properties | Select-Object $Properties | ForEach-Object {
         $_.pstypenames.Insert(0, $etsTypeName); $_
      }
    
    }
    

    You'll then see the desired tabular output based on the format data; e.g.:

    Enabled SamAccountName Name     emailAddress     proxyAddresses
    ------- -------------- ----     ------------     --------------
    True    jdoe           Jane Doe [email protected] [email protected]