question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Consistently document a scalar -InputObject parameter as an implementation detail or make item-by-item processing cmdlets explicitly iterate over collections

See original GitHub issue

Updated based on feedback from @PetSerAl to clarify Category B.

-InputObject <psobject> and -InputObject <object> parameters bind values that are collections as-is to the parameter variable. Unless the cmdlet explicitly checks for collection-valued input and iterates over it, the collection is processed as itself, as a single object.

The core cmdlets that have such a parameter can be categorized as follows:

ConvertTo-Json is the only cmdlet where the parameter is [object]-typed rather than [psobject]-typed - it is unclear to me why.
Register-Object is not covered below, because its -InputObject parameter doesn’t actually pipeline-bind.

  • Category A: A few cmdlets do make a useful distinction between a collection passed as a whole via -InputObject vs. item iteration via the pipeline, notably Get-Member.
  • For the majority of cmdlets, processing a collection as a whole is pointless, and these cmdlets currently fall into the two remaining categories:
    • Category B: If a collection is passed via -InputObject, they explicitly iterate over the collection, though potentially differently than with pipeline input.
      • For such cmdlets, using the pipeline and using -InputObject is effectively equivalent, but only for flat collections.
    • Category C: If a collection is passed via -InputObject, that collection is processed as a single object, which, given the premise, doesn’t make sense.
      • For such cmdlets, the -InputObject parameter should be documented as not for direct use, it being a mere implementation detail that facilitates pipeline input.
        • The documentation of some of these cmdlets already mentions the pitfall of using -InputObject, but by no means all.
      • Alternatively, these cmdlets could be modified to perform explicit iteration, as the cmdlets in the previous category do. While this would technically be a breaking change, it probably falls into Bucket 3: Unlikely Grey Area

Here’s my attempt at mapping the cmdlets that ship with PowerShell to these categories:

  • Category A: OK: Useful distinction between pipeline input and explicit -InputObject use:

    • Add-Member
    • Export-Clixml
    • Get-Member
    • Trace-Command
  • Category B: SOMEWHAT PROBLEMATIC: No effective distinction between pipeline input and explicit -InputObject use for flat collections, but behavior differs with nested ones:

    • Format-Custom
    • Format-List
    • Format-Table
    • Format-Wide
    • Out-Host
    • Out-String
    • Out-File
    • Join-String
    • Set-Content / Add-Content
  • Category C: PROBLEMATIC: Distinction between pipeline input and explicit -InputObject use, with -InputObject input performing no enumeration, making it useless:

    • ConvertTo-Csv
    • ConvertTo-Html
    • ConvertTo-Xml
    • Export-Csv
    • ForEach-Object
    • Format-Hex
    • Get-Unique
    • Group-Object
    • Invoke-Command
    • Measure-Command
    • Measure-Object
    • Select-Object
    • Select-String
    • Sort-Object
    • Start-Job
    • Where-Object

Among the Category C cmdlets, the help topics of the following contain misleading information:

To give a concrete example of the problematic current documentation, the Export-Csv help topic currently states:

-InputObject Specifies the objects to export as CSV strings. Enter a variable that contains the objects or type a command or expression that gets the objects. You can also pipe objects to Export-CSV.

See #3865 - which @BrucePay rightly closed as by-design - for what happens when you believe the documentation (which I did).

As stated, given how this cmdlet currently works, the documentation should effectively say something like:

-InputObject is an auxiliary parameter that enables pipeline input. Don’t use this parameter directly, use the pipeline instead.

Of course, the alternative is to change the behavior to perform explicit iteration, as stated.

Generally, though, when implementing item-by-item-processing cmdlets, -InputObject <psobject> is inherently problematic:

  • With pipeline input, it conveniently iterates for you via the Process block
  • But with explicit -InputObject use, you get no useful behavior by default and your choices are:
    • Document your parameter along the lines of “Nothing to see here. Use the pipeline instead”.
    • Explicitly iterate over the variable in your Process block (at which point you may as well declare your parameter explicitly as array-valued, -InputObject <psobject[]>).

Here’s the full matrix of cmdlets and their behavior from which the categorization above was obtained. The source code is further below, which also contains direct links to the help topics.

Note: True values in column IsSame only indicate equivalence for flat collections.

Cmdlet           IsSame HelpClaimsIsSame ShouldBeSame FailsWithArrayInputObject Comment                                                                        
------           ------ ---------------- ------------ ------------------------- -------                                                                        
Add-Member        False             True        False                     False                                                                                
ConvertTo-Csv     False             True         True                     False                                                                                
ConvertTo-Html    False            False         True                     False                                                                                
ConvertTo-Xml     False             True         True                     False                                                                                
Export-Clixml     False             True        False                     False                                                                                
Export-Csv        False             True         True                     False                                                                                
ForEach-Object    False            False         True                     False                                                                                
Format-Custom      True             True         True                     False                                                                                
Format-Hex        False             True         True                      True Help merely contains a placeholder for the -InputObject description (as of 2...
Format-List        True             True         True                     False                                                                                
Format-Table       True             True         True                     False                                                                                
Format-Wide        True             True         True                     False                                                                                
Get-Member        False            False        False                     False                                                                                
Get-Unique        False            False         True                     False                                                                                
Group-Object      False            False         True                     False                                                                                
Invoke-Command    False             True         True                     False                                                                                
Measure-Command   False             True         True                     False                                                                                
Measure-Object    False            False         True                     False                                                                                
Out-Default        True             True         True                     False Help just states (as of 26 May 2017): "Accepts input to the cmdlet."           
Out-File          False             True         True                     False                                                                                
Out-Host           True             True         True                     False                                                                                
Out-Null                                                                  False Output behavior by definition doesn't apply.                                   
Out-String         True             True         True                     False                                                                                
Select-Object     False            False         True                     False                                                                                
Select-String     False            False         True                     False                                                                                
Set-ItemProperty                                                          False Couldn't figure out -InputObject use. -InputObject description uses *singlua...
Sort-Object       False            False         True                     False                                                                                
Start-Job         False             True         True                     False Hard-coded result (test too complex to model here)                             
Trace-Command     False             True        False                     False Help states "You can enter a variable that represents the input that the exp...
Where-Object      False            False         True                     False                                                                                

Test-function source code

function Test-InputObjectParam {

  [CmdletBinding()]
  param()

  set-strictmode -version 1


  $inCollCustObj = [System.Collections.Generic.List[pscustomobject]] ([pscustomobject] @{ foo = 1; bar = 2 }, [pscustomobject] @{ foo = 3; bar = 4 })
  $inCollStr = 'one', 'two', 'three'

  $tempFiles = ($tempFile1 = New-TemporaryFile), ($tempFile2 = New-TemporaryFile) 

  # Use this command to scaffold the hashtable of tests below.
  <#
    Get-Command -Type cmdlet -ParameterName inputobject | ? { 
      $_.Parameters.InputObject.ParameterType -in [psobject], [object] -and $_.Parameters.InputObject.ParameterSets.Values.ValueFromPipeline 
    } | % { "'$($_.Name)' = @{`n  ShouldbeSame = `n}`n" }
  #>

  # !! Those cmdlets that DO unwrap a collection passed to -InputObject
  # !! accept only a *single* collection.
  # !! Something like -InputObject (1, 2), (3,4) still causes different output.

  $tests = [ordered] @{

    'Add-Member' = @{
      ShouldBeSame = $False
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Add-Member.md'
      Params = @{ NotePropertyMembers = @{ 'addedProp' = 666 }; PassThru = $True }
      Test = { ($inColl.psobject.Properties).Name -notcontains 'addedProp' }
    }

    'ConvertTo-Csv' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/ConvertTo-Csv.md'
      Params = @{}
    }

    'ConvertTo-Html' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $False
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/ConvertTo-Html.md'
      HelpOK = $False
      Params = @{}
    }

    'ConvertTo-Xml' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/ConvertTo-Xml.md'
      Params = @{ As = 'String' }
    }

    'Export-Clixml' = @{
      ShouldBeSame = $False
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Export-Clixml.md'
      Params = @{ LiteralPath = $null }
    }

    'Export-Csv' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Export-Csv.md'
      Params = @{ LiteralPath = $null }
    }

    'ForEach-Object' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $False
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Core/ForEach-Object.md'
      Params = @{ Process = { "[$_]" } }
    }

    'Format-Custom' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Format-Custom.md'
      Params = @{}
    }

    'Format-Hex' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Format-Hex.md'
      Params = @{}
      UseStrings = $True
      Comment = 'Help merely contains a placeholder for the -InputObject description (as of 26 May 2017): "{{Fill InputObject Description}}."'
    }

    'Format-List' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Format-List.md'
      Params = @{}
    }

    'Format-Table' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Format-Table.md'
      Params = @{}
    }

    'Format-Wide' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Format-Wide.md'
      Params = @{}
    }

    'Get-Member' = @{
      ShouldBeSame = $False #
      HelpClaimsIsSame = $False
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Get-Member.md'
      Params = @{}
    }

    'Get-Unique' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $False
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Get-Unique.md'
      Params = @{}
    }

    'Group-Object' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $False
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Group-Object.md'
      Params = @{}
      UseStrings = $true
    }

    'Invoke-Command' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Core/Invoke-Command.md'
      Params = @{ ScriptBlock = { $Input | Measure-Object | % Count } }
    }

    'Measure-Command' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Measure-Command.md'
      Params = @{ Expression = { $_.GetType().Name | Write-Information -InformationVariable res } }
      UseResVariable = $True
    }

    'Measure-Object' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $False
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Measure-Object.md'
      Params = @{}
      Test = { $res1.Count -eq $res2.Count }
    }

    # Note: Output from this cmdlet's test commands won't be suppressed below.
    'Out-Default' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Out-Default.md'
      Params = @{ OutVariable = 'res' }
      UseResVariable = $True
      Comment = 'Help just states (as of 26 May 2017): "Accepts input to the cmdlet."'
    }

    'Out-File' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Out-File.md'
      Params = @{ LiteralPath = $null }
    }

    # Note: Output from this cmdlet's test commands won't be suppressed below.
    'Out-Host' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Core/Out-Host.md'
      Params = @{ OutVariable = 'res' }
      UseResVariable = $True
    }

    'Out-Null' = @{
      Skip = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Core/Out-Null.md'    
      Comment = 'Output behavior by definition doesn''t apply.'
    }

    'Out-String' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Out-String.md'    
      Params = @{ Stream = $True }
    }

    'Select-Object' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $False
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Select-Object.md'
      Params = @{ Last = 1 }
    }

    'Select-String' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $False
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Select-String.md'
      Params = @{ Pattern = 'n' }
      UseStrings = $true
    }

    'Set-ItemProperty' = @{
      Skip = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Management/Set-ItemProperty.md'
      Comment = 'Couldn''t figure out -InputObject use. -InputObject description uses *singluar* and seems incorrect.'
    }

    'Sort-Object' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $False
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Sort-Object.md'
      Params = @{ Descending = $True }
      UseStrings = $true
    }

    # Manually determined results:
    # $coll = 1, 2
    # Receive-Job -Wait -AutoRemoveJob (Start-Job { $Input | Measure-Object } -Input $coll) # -> 1
    # Receive-Job -Wait -AutoRemoveJob ($coll | Start-Job { $Input | Measure-Object }) # -> 2
    'Start-Job' = @{
      Skip = $True
      ShouldBeSame = $True
      HardcodedIsSame = $False
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Core/Start-Job.md'
      Params = @{ LiteralPath = $null }
      UseStrings = $True
      Comment = 'Hard-coded result (test too complex to model here)'
    }

    'Trace-Command' = @{
      ShouldBeSame = $False
      HelpClaimsIsSame = $True
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Utility/Trace-Command.md'
      Params = @{ Name = 'type*'; Expression = { $Input | Measure-Object | % Count } }
      Comment = 'Help states "You can enter a variable that represents the input that the expression accepts, or pass an object through the pipeline."'
    }

    'Where-Object' = @{
      ShouldBeSame = $True
      HelpClaimsIsSame = $False
      HelpSourceUrl = 'https://github.com/PowerShell/PowerShell-Docs/blob/staging/reference/6/Microsoft.PowerShell.Core/Where-Object.md'
      Params = @{ FilterScript = { 'one' -eq $_ } }
      UseStrings = $True
    }

  }


  foreach ($cmdlet in $tests.keys) {
    $test = $tests[$cmdlet]
    $htParams = $test.Params
    if (-not $test.Skip) {
      $usesOutFiles = $htParams.ContainsKey('LiteralPath')
      $inColl = if ($test.UseStrings) { $inCollStr } else { $inCollCustObj }
      if ($usesOutFiles) { $htParams.LiteralPath = $tempFile1 }
      $res1 = & $cmdlet -InputObject $inColl @htParams -EA SilentlyContinue -EV err
      if ($test.UseResVariable) { $res1 = $res }
      if ($usesOutFiles) { $htParams.LiteralPath = $tempFile2 }
      $res2 = $inColl | & $cmdlet @htParams
      if ($test.UseResVariable) { $res2 = $res }
    }
    [pscustomobject] @{
      Cmdlet = $cmdlet
      IsSame = if ($test.Skip) {
                $test.HardcodedIsSame
              } elseif ($err -or $null -eq $res1) {
                $False
              } elseif ($test.Test) {
                & $test.Test $res1, $res1
              } else {
                if ($usesOutFiles) {
                    (Get-Content -Raw $tempFile1) -eq (Get-Content -Raw $tempFile2)
                } else {
                  (Compare-Object -SyncWindow 0 $res1 $res2).Count -eq 0
                }
              }
      HelpClaimsIsSame = $test.HelpClaimsIsSame
      ShouldBeSame = $test.ShouldBeSame
      FailsWithArrayInputObject = $err.Count -gt 0
      Comment = $test.Comment
      HelpSourceUrl = $test.HelpSourceUrl
    }
    if ($test.Skip) {
      Write-Verbose "SKIPPED: =============== $cmdlet"
    } elseif ($usesOutFiles) {
      Write-Verbose "*1: -InputObject* =============== $cmdlet"
      Write-Verbose (Get-Content -Raw $tempFile1)
      Write-Verbose "*2: Pipeline*     =============== $cmdlet"
      Write-Verbose (Get-Content -Raw $tempFile2)
    } else {
      Write-Verbose "*1: -InputObject* ================ $cmdlet"
      Write-Verbose ($res1 | Out-String)
      Write-Verbose "*2: Pipeline*     ================ $cmdlet"
      Write-Verbose ($res2 | Out-String)
    } 
  }

  Remove-Item -ea Ignore $tempFiles

}

' --- Those where pipeline input and -InputObject use are equivalent:'
Test-InputObjectParam | ? { $_.IsSame -and $_.ShouldBeSame } | % cmdlet


' --- Those where the distinction between pipeline input and -InputObject makes sense:'
Test-InputObjectParam | ? { $False -eq $_.ShouldBeSame -and $false -eq $_.IsSame } | % cmdlet


' --- Those where there is a distinction between pipeline input and -InputObject, but it makes no sense:'
Test-InputObjectParam | ? { $_.ShouldBeSame -and $false -eq $_.IsSame } | % cmdlet

' --- Those where there is a distinction between pipeline input and -InputObject, but it makes no sense, and the help topics don''t clarify that:'
Test-InputObjectParam | ? { $_.ShouldBeSame -and $false -eq $_.IsSame -and $_.HelpClaimsIsSame } | % cmdlet

Environment data

PowerShell Core v6.0.0-beta.3 on macOS 10.12.5
PowerShell Core v6.0.0-beta.3 on Ubuntu 16.04.1 LTS
PowerShell Core v6.0.0-beta.3 on Microsoft Windows 10 Pro (64-bit; v10.0.14393)
Windows PowerShell v5.1.15063.413 on Microsoft Windows 10 Pro (64-bit; v10.0.15063)

Issue Analytics

  • State:open
  • Created 6 years ago
  • Reactions:6
  • Comments:16 (10 by maintainers)

github_iconTop GitHub Comments

2reactions
vexx32commented, Jan 9, 2019

@mklement0 in terms of the implementation differences with cmdlets or advanced functions that attempt to account for both input methods, I believe there is an ExpectingInput value that can be used to determine if the cmdlet is being used in a pipeline capacity, which might help to properly mirror the implementations, rather than blindly iterating over whatever happens to be stored in InputObject at the time.

1reaction
vexx32commented, Jan 14, 2019

As I mention here, it might make sense to simply have -InputObject parameters be designated purely for pipeline use. In such a case, perhaps it would make the most sense to separate pipeline from regular use via parameter sets in most cases, potentially even declaring an entire parameter set as only usable for the pipeline?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Looping over a pipeline parameter - what is the point?
In the first one, the foreach loop isn't necessary, because pipeline input gets unrolled automatically and passed one-by-one to the process ...
Read more >
Understanding C# foreach Internals and Custom Iterators ...
This interface is critical because implementing the methods of IEnumerable<T> is the minimum needed to support iterating over a collection.
Read more >
Should I accept empty collections in my methods that ...
Transforming an empty collection has an obvious outcome: the empty collection. (You may even save some garbage by returning the parameter itself ...
Read more >
Powershell Loop Through Array: A Detailed Guide
PowerShell arrays are versatile data structures used in scripts for storing, manipulating, and iterating over a collection of items.
Read more >
Processing a Series of Items with Iterators
An iterator is responsible for the logic of iterating over each item and determining when the sequence has finished. When you use iterators,...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found