-
Notifications
You must be signed in to change notification settings - Fork 7.7k
Description
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, notablyGet-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.
- For such cmdlets, using the pipeline and using
- 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.
- The documentation of some of these cmdlets already mentions the pitfall of using
- 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
- For such cmdlets, the
- Category B: If a collection is passed via
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:
ConvertTo-Csv
ConvertTo-Xml
Export-Csv
Format-Hex
Invoke-Command
Measure-Command
- but see Fix the description of the -InputObject parameter for Measure-Command MicrosoftDocs/PowerShell-Docs#2140Start-Job
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 = @{}
UseStrin
8000
gs = $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)