PowerShell: What is the point of ForEach-Object with InputObject? - powershell

The documentation for ForEach-object says "When you use the InputObject parameter with ForEach-Object, instead of piping command results to ForEach-Object, the InputObject value is treated as a single object." This behavior can easily be observed directly:
PS C:\WINDOWS\system32> ForEach-Object -InputObject #(1, 2, 3) {write-host $_}
1 2 3
This seems weird. What is the point of a "ForEach" if there is no "each" to do "for" on? Is there really no way to get ForEach-object to act directly on the individual elements of an array without piping? if not, it seems that ForEach-Object with InputObject is completely useless. Is there something I don't understand about that?

In the case of ForEach-Object, or any cmdlet designed to operate on a collection, using the -InputObject as a direct parameter doesn't make sense because the cmdlet is designed to operate on a collection, which needs to be unrolled and processed one element at a time. However, I would also not call the parameter "useless" because it still needs to be defined so it can be set to allow input via the pipeline.
Why is it this way?
-InputObject is, by convention, a generic parameter name for what should be considered to be pipeline input. It's a parameter with [Parameter(ValueFromPipeline = $true)] set to it, and as such is better suited to take input from the pipeline rather passed as a direct argument. The main drawback of passing it in as a direct argument is that the collection is not guaranteed to be unwrapped, and may exhibit some other behavior that may not be intended. From the about_pipelines page linked to above:
When you pipe multiple objects to a command, PowerShell sends the objects to the command one at a time. When you use a command parameter, the objects are sent as a single array object. This minor difference has significant consequences.
To explain the above quote in different words, passing in a collection (e.g. an array or a list) through the pipeline will automatically unroll the collection and pass it to the next command in the pipeline one at a time. The cmdlet does not unroll -InputObject itself, the data is delivered one element at a time. This is why you might see problems when passing a collection to the -InputObject parameter directly - because the cmdlet is probably not designed to unroll a collection itself, it expects each collection element to be handed to it in a piecemeal fashion.
Consider the following example:
# Array of hashes with a common key
$myHash = #{name = 'Alex'}, #{name='Bob'}, #{name = 'Sarah'}
# This works as intended
$myHash | Where-Object { $_.name -match 'alex' }
The above code outputs the following as expected:
Name Value
---- -----
name Alex
But if you pass the hash as InputArgument directly like this:
Where-Object -InputObject $myHash { $_.name -match 'alex' }
It returns the whole collection, because -InputObject was never unrolled as it is when passed in via the pipeline, but in this context $_.name -match 'alex' still returns true. In other words, when providing a collection as a direct parameter to -InputObject, it's treated as a single object rather than executing each time against each element in the collection. This can also give the appearance of working as expected when checking for a false condition against that data set:
Where-Object -InputObject $myHash { $_.name -match 'frodo' }
which ends up returning nothing, because even in this context frodo is not the value of any of the name keys in the collection of hashes.
In short, if something expects the input to be passed in as pipeline input, it's usually, if not always, a safer bet to do it that way, especially when passing in a collection. However, if you are working with a non-collection, then there is likely no issue if you opt to use the -InputObject parameter directly.

Bender the Greatest's helpful answer explains the current behavior well.
For the vast majority of cmdlets, direct use of the -InputObject parameter is indeed pointless and the parameter should be considered an implementation detail whose sole purpose is to facilitate pipeline input.
There are exceptions, however, such as the Get-Member cmdlet, where direct use of -InputObject allows you to inspect the type of a collection itself, whereas providing that collection via the pipeline would report information about its elements' types.
Given how things currently work, it is quite unfortunate that the -InputObject features so prominently in most cmdlets' help topics, alongside "real" parameters, and does not frame the issue with enough clarity (as of this writing): The description should clearly convey the message "Don't use this parameter directly, use the pipeline instead".
This GitHub issue provides an categorized overview of which cmdlets process direct -InputObject arguments how.
Taking a step back:
While technically a breaking change, it would make sense for -InputObject parameters (or any pipeline-binding parameter) to by default accept and enumerate collections even when they're passed by direct argument rather than via the pipeline, in a manner that is transparent to the implementing command.
This would put direct-argument input on par with pipeline input, with the added benefit of the former resulting in faster processing of already-in-memory collections.

Related

PowerShell Hashtable - how to select property

I need to get the value of an environment variable from a kubernetes pod. I have my values listed in a hash table.
I call
$hash["service-testurl"].spec.template.spec.containers.env
And it returns a table:
name value
---- -----
ADDR https://test.com
TOKEN 123456789
CERT_PATH public-certs/test
ENVIRONMENT dev
I need to get https://test.com into a variable in my ps1 script, but i'm not sure how to get this value. (consider that for each deployment the url will be different, like abc.com, def.com, ghj.com... so i can't filter by the name test.com)
I was looking for something like $hash["service-testurl"].spec.template.spec.containers.env.name["ADDR"].value
Running $hash["service-testurl"].spec.template.spec.containers.env.PSTypeNames returns
System.Object[]
System.Array
System.Object
To complement your own effective solution:
Even though your display output of $hash["service-testurl"].spec.template.spec.containers.env looks like the representation of a (single) hashtable, the value is actually:
(a) an array, as your diagnostic output with .pstypenames demonstrates,
(b) whose elements are (presumably) [pscustomobject] instances that each have a a .name and a .value property (an easy way to tell is that the display output's column headers are name and value, whereas with hashtables they would be Name and Value).
Leaving aside that the identifier ADDR is a property value rather than a property / key name in your case, you fundamentally cannot use key-based index notation (['ADDR']) on an array - that generally only works on a (single) hashtable (or, more generally, dictionary).[1]
In your case, you need to find the array element whose .name property value is 'ADDR', which then allows you to return its .value property value.
For collections already in memory, the intrinsic .Where() method (as used in your own solution) is a more efficient - and more flexible - alternative to filtering a collection via the Where-Object cmdlet.
It will often not matter in practice, but you can optimize a .Where() call to stop filtering once the first match has been found, if you expect or are only interested in one match:
$hash["service-testurl"].spec.template.spec.containers.env.Where(
{ $_.name -eq 'ADDR' },
'First'
).value
Note that .Where() always returns an array-like collection, even if only a single value is matched - see this answer for details. As such, the .value property access is attempted on that collection, which, however, still works as intended, courtesy of the PowerShell feature known as member-access enumeration.
Note how using (...) around the arguments is now a syntactic necessity.
While with only a single argument - the filter script block ({ ... }) - you can get away with not using (...) - .Where{ $_.name -eq 'ADDR' } as shorthand for .Where({ $_.name -eq 'ADDR' }) - omitting the (...) is problematic for two reasons:
Given that the Where-Object cmdlet can also be referred to as Where (via a built-in alias), the two command forms could be confused, and given that Where-Object allows and is typically used with a space separating the command name from its script-block argument (e.g, 1..3 | Where { $_ -eq 2 }, it is tempting to also try to use a space with the .Where() method, which does not work:
# !! BROKEN, due to space before "{"
(1..3).Where { $_ -eq 2 }
If you add another argument later, you need to remember to use (...)
[1] The fact that key-based index notation does not work with member-access enumeration, i.e. doesn't work on an array of hashtables (only dot notation does, which PowerShell supports for hashtables too) could be considered an inconsistency; e.g. #( #{ foo=1 } ).foo works (dot notation), but #( #{ foo=1 } )['foo'] does not, due to the array wrapper.
However, this inconsistency was declared to be by design - see GitHub issue #17514.
I was able to do it with something similar that #iRon proposed:
$hash["service-testurl"].spec.template.spec.containers.env.where{$_.name -eq 'ADDR'}.value
Thanks!

Powershell output PSCustomObject blocks output hashtable [duplicate]

I'm learning PowerShell and a vast number of articles I read strongly discourages the use of write-host telling me it's "bad practice" and almost always, the output can be displayed in another way.
So, I'm taking the advice and try to avoid use of write-host. One suggestion I found was to use write-output instead. As far as I understand, this puts everything in a pipeline, and the output is executed at the end of the script (?).
However, I have problems outputting what I want. This example demonstrates the issue:
$properties = #{'OSBuild'="910ef01.2.8779";
'OSVersion'="CustomOS 3";
'BIOSSerial'="A5FT-XT2H-5A4B-X9NM"}
$object = New-Object –TypeName PSObject –Prop $properties
Write-output $object
$properties = #{'Site'="SQL site";
'Server'="SQL Server";
'Database'="SQL Database"}
$object = New-Object –TypeName PSObject –Prop $properties
Write-Output $object
This way I get a nice output of the first object displaying the OS data, but the second object containing the SQL data is never displayed. I've tried renaming the variable names, and a bunch of other different stuff, but no luck.
While troubleshooting this problem, I found similar problems with suggestions to just replace write-output with write-host. This gets me very confused. Why are some people strongly discouraging write-host, while other people encourage it?
And how exactly do I output these two objects in a fashionably manner? I do not fully understand the pipeline mechanism of write-output.
Just to clarify: the problem is only a display problem:
When outputting to the console, if the first object is table-formatted (if Format-Table is applied, which happens implicitly in your case), the display columns are locked in based on that first object's properties.
Since your second output object shares no properties with the first one, it contributes nothing to the table display and is therefore effectively invisible.
By contrast, if you programmatically process the script's output - assign it to a variable or send its output through the pipeline to another command - both objects will be there.
See Charlie Joynt's answer for a helpful example of assigning the two output objects to separate variables.
The simplest solution to the display problem is to explicitly format for display each input object individually - see below.
For a given single object inside a script, you can force formatted to-display (to-host) output with Out-Host:
$object | Out-Host # same as: Write-Output $object | Out-Host
Note, however, that this outputs directly and invariably to the console only and the object is then not part of the script's data output (the objects written to the success output stream, the stream with index 1).
In other words: if you try to assign the script's output to a variable or send its output to another command in a pipeline, that object won't be there.
See below for why Out-Host is preferable to Write-Host, and why it's better to avoid Write-Host in most situations.
To apply the technique ad hoc to a given script's output as a whole, so as to make sure you see all output objects, use:
./some-script.ps1 | % { $_ | Out-String } # % is the built-in alias of ForEach-Object
Note that here too you could use Out-Host, but the advantage of using Out-String is that it still allows you to capture the for-display representation in a file, if desired.
Here's a simple helper function (filter) that you can put in your $PROFILE:
# Once defined, you can use: ./some-script.ps1 | Format-Each
Filter Format-Each { $_ | Out-String }
PetSerAl's suggestion - ./some-script.ps1 | Format-List - works in principle too, but it switches the output from the usual table-style output to list-style output, with each property listed on its own line, which may be undesired.
Conversely, however, Format-Each, if an output object is (implicitly) table-formatted, prints a header for each object.
Why Write-Output doesn't help:
Write-Output doesn't help, because it writes to where output objects go by default anyway: the aforementioned success output stream, where data should go.
If the output stream's objets aren't redirected or captured in some form, they are sent to the host by default (typically, the console), where the automatic formatting is applied.
Also, use of Write-Output is rarely necessary, because simply not capturing or redirecting a command or expression implicitly writes to the success stream; another way of putting it:
Write-Output is implied.
Therefore, the following two statements are equivalent:
Write-Output $object # write $object to the success output stream
$object # same; *implicitly* writes $object to the success output stream
Why use of Write-Host is ill-advised, both here and often in general:
Assuming you do know the implications of using Write-Host in general - see below - you could use it for the problem at hand, but Write-Host applies simple .ToString() formatting to its input, which does not give you the nice, multi-line formatting that PowerShell applies by default.
Thus, Out-Host (and Out-String) were used above, because they do apply the same, friendly formatting.
Contrast the following two statements, which print a hash-table ([hashtable]) literal:
# (Optional) use of Write-Output: The friendly, multi-line default formatting is used.
# ... | Out-Host and ... | Out-String would print the same.
PS> Write-Output #{ foo = 1; bar = 'baz' }
Name Value
---- -----
bar baz
foo 1
# Write-Host: The hashtable's *entries* are *individually* stringified
# and the result prints straight to the console.
PS> Write-Host #{ foo = 1; bar = 'baz' }
System.Collections.DictionaryEntry System.Collections.DictionaryEntry
Write-Host did two things here, which resulted in near-useless output:
The [hashtable] instance's entries were enumerated and each entry was individually stringified.
The .ToString() stringification of hash-table entries (key-value pairs) is System.Collections.DictionaryEntry, i.e., simply the type name of the instance.
The primary reasons for avoiding Write-Host in general are:
It outputs directly to the host (console) rather than to PowerShell's success output stream.
As a beginner, you may mistakenly think that Write-Host is for writing results (data), but it isn't.
In bypassing PowerShell's system of streams, Write-Host output cannot be redirected - that is, it can neither be suppressed nor captured (in a file or variable).
That said, starting with PowerShell v5.0, you can now redirect its output via the newly introduced information stream (number 6; e.g., ./some-script.ps1 6>write-host-output.txt); however, that stream is more properly used with the new Write-Information cmdlet.
By contrast, Out-Host output still cannot be redirected.
That leaves just the following legitimate uses of Write-Host:
Creating end-user prompts and colored for-display-only representations:
Your script may have interactive prompts that solicit information from the user; using Write-Host - optionally with coloring via the -ForegroundColor and -BackgroundColor parameters - is appropriate, given that prompt strings should not become part of the script's output and users also provide their input via the host (typically via Read-Host).
Similarly, you can use Write-Host with selective coloring to explicitly create friendlier for-display-only representations.
Quick prototyping: If you want a quick-and-dirty way to write status/diagnostic information directly to the console without interfering with a script's data output.
However, it is generally better to use Write-Verbose and Write-Debug in such cases.
Generally speaking the expectation is for script/functions to return a single "type" of object, often with many instances. For example, Get-Process returns a load of processes, but they all have the same fields. As you'll have seen from the tutorials, etc. you can then pass the output of Get-Process along a pipeline and process the data with subsequent cmdlets.
In your case you are returning two different types of object (i.e. with two different sets of properties). PS outputs the first object, but not the second one (which doesn't match the first) as you discovered. If you were to add extra properties to the first object that match those used in the second one, then you'd see both objects.
Write-Host doesn't care about this sort of stuff. The push-back against using this is mainly to do with (1) it being a lazy way to give feedback about script progress, i.e. use Write-Verbose or Write-Debug instead and (2) it being imperfect when it comes to passing objects along a pipeline, etc.
Clarification on point (2), helpfully raised in the comments to this answer:
Write-Host is not just imperfect with respect to the pipeline /
redirection / output capturing, you simply cannot use it for that in
PSv4 and earlier, and in PSv5+ you have to awkwardly use 6>; also,
Write-Host stringifies with .ToString(), which often produces
unhelpful representations
If your script is really just meant to print data to the console then go ahead and Write-Host.
Alternatively, you can return multiple objects from a script or function. Using return or Write-Output, just return object objects comma-separated. For example:
Test-WriteOutput.ps1
$object1 = [PSCustomObject]#{
OSBuild = "910ef01.2.8779"
OSVersion = "CustomOS 3"
BIOSSerial = "A5FT-XT2H-5A4B-X9NM"
}
$object2 = [PSCustomObject]#{
Site = "SQL site"
Server= "SQL Server"
Database="SQL Database"
}
Write-Output $object1,$object2
The run the script, assigning the output into two variables:
$a,$b = .\Test-WriteOutput.ps1
You'll see that $a is $object1 and $b is $object2.
use write-host, write-output is for pipeline (and by default on console after clear)

How to call "Invoke-MgSubscribeGroup"?

There is a PowerShell cmdlet Invoke-MgSubscribeGroup that I want to call this way:
Invoke-MgSubscribeGroup -GroupId da2d17a7-64a5-43e5-9d95-7b70333dd78c
#{ UserId = "ed3d927d-7999-459f-955d-2afc272bd4d4" }
(Split into multiple lines for better readability)
When calling it, I get en error message:
A positional parameter cannot be found that accepts argument "System.Collections.Hashtable".
Since I'm not that deep into PowerShell I must have some false understanding of how to pass a hashtable, or I have misunderstood the documentation that says:
...To create the parameters described below, construct a hash table containing the appropriate properties...
My question
What is the correct syntax to call the Invoke-MgSubscribeGroup cmdlet and pass the user ID?
To elaborate on your own answer:
PowerShell's syntax diagrams - available locally via Invoke-MgSubscribeGroup -? or Get-Command -Syntax Invoke-MgSubscribeGroup - contain all the relevant information.
That said, they're not easy to read, especially locally, and there's room for future improvement.
Quoting from Invoke-MgSubscribeGroup's documentation:
Invoke-MgSubscribeGroup
-GroupId <String>
# ...
Invoke-MgSubscribeGroup
-InputObject <IGroupsIdentity>
# ...
Each (partial) quote represents a distinct parameter set, i.e. a unique combination of parameters representing a distinct feature group.
That -GroupId and -InputObject are in different parameter sets and are exclusive to each, tells you that you cannot use them both in a given invocation (they way you mistakenly tried), i.e., that they are mutually exclusive.
Additionally, given that the parameter names -GroupId and -InputObject are not enclosed in [...] means that you can only pass named, not positional arguments to them - that is, you must precede an argument to bind to these parameters with the parameter name; e.g, -GroupId foo rather than just foo.
By convention, a parameter named -InputObject is typically used to represent values that can be supplied via the pipeline, as evidenced by the parameter's description stating Accept pipeline input: True - locally, you can see this with either Get-Help -Full Invoke-MgSubscribeGroup or - parameter-specifically - with Get-Help Invoke-MgSubscribeGroup -Parameter InputObject
Often, multiple input objects can only (meaningfully) be supplied via the pipeline; that is, -InputObject is often a mere implementation detail whose purpose is to facilitate pipeline input - see GitHub issue #4242.
GitHub issue #4135 proposes making syntax diagrams directly reflect which parameters accept pipeline input.
What complicates matters with respect to Invoke-MgSubscribeGroup's documentation, specifically (which seems to be very sparse in general):
The help topic contains no examples (which you could normally request locally with Get-Help -Examples Invoke-MgSubscribeGroup)
The data type of the -InputObject parameter, <IGroupsIdentity> (Microsoft.Graph.PowerShell.Models.IGroupsIdentity) doesn't seem to have its own documentation; it is only described in the "Notes" section of the help topic, as accepting a hashtable (#{ ... }), along with a list of the supported entries (keys and value types).
All that said: you could have passed your hashtable via the pipeline, as follows:
#{
UserId = 'ed3d927d-7999-459f-955d-2afc272bd4d4'
GroupId = 'da2d17a7-64a5-43e5-9d95-7b70333dd78c'
} | Invoke-MgSubscribeGroup
The advantage of this approach over passing an argument to -InputObject is that it would allow you to pass multiple hashtables to act on.
After further investigating and reading the documentation again and again, I've found the correct syntax:
Invoke-MgSubscribeGroup -InputObject #{
UserId = "ed3d927d-7999-459f-955d-2afc272bd4d4";
GroupId = "da2d17a7-64a5-43e5-9d95-7b70333dd78c" }
(Split into multiple lines for better readability)
I now face permission issues, but this is out-of-scope for this question, since I've asked for the correct syntax only.

PowerShell: write-output only writes one object

I'm learning PowerShell and a vast number of articles I read strongly discourages the use of write-host telling me it's "bad practice" and almost always, the output can be displayed in another way.
So, I'm taking the advice and try to avoid use of write-host. One suggestion I found was to use write-output instead. As far as I understand, this puts everything in a pipeline, and the output is executed at the end of the script (?).
However, I have problems outputting what I want. This example demonstrates the issue:
$properties = #{'OSBuild'="910ef01.2.8779";
'OSVersion'="CustomOS 3";
'BIOSSerial'="A5FT-XT2H-5A4B-X9NM"}
$object = New-Object –TypeName PSObject –Prop $properties
Write-output $object
$properties = #{'Site'="SQL site";
'Server'="SQL Server";
'Database'="SQL Database"}
$object = New-Object –TypeName PSObject –Prop $properties
Write-Output $object
This way I get a nice output of the first object displaying the OS data, but the second object containing the SQL data is never displayed. I've tried renaming the variable names, and a bunch of other different stuff, but no luck.
While troubleshooting this problem, I found similar problems with suggestions to just replace write-output with write-host. This gets me very confused. Why are some people strongly discouraging write-host, while other people encourage it?
And how exactly do I output these two objects in a fashionably manner? I do not fully understand the pipeline mechanism of write-output.
Just to clarify: the problem is only a display problem:
When outputting to the console, if the first object is table-formatted (if Format-Table is applied, which happens implicitly in your case), the display columns are locked in based on that first object's properties.
Since your second output object shares no properties with the first one, it contributes nothing to the table display and is therefore effectively invisible.
By contrast, if you programmatically process the script's output - assign it to a variable or send its output through the pipeline to another command - both objects will be there.
See Charlie Joynt's answer for a helpful example of assigning the two output objects to separate variables.
The simplest solution to the display problem is to explicitly format for display each input object individually - see below.
For a given single object inside a script, you can force formatted to-display (to-host) output with Out-Host:
$object | Out-Host # same as: Write-Output $object | Out-Host
Note, however, that this outputs directly and invariably to the console only and the object is then not part of the script's data output (the objects written to the success output stream, the stream with index 1).
In other words: if you try to assign the script's output to a variable or send its output to another command in a pipeline, that object won't be there.
See below for why Out-Host is preferable to Write-Host, and why it's better to avoid Write-Host in most situations.
To apply the technique ad hoc to a given script's output as a whole, so as to make sure you see all output objects, use:
./some-script.ps1 | % { $_ | Out-String } # % is the built-in alias of ForEach-Object
Note that here too you could use Out-Host, but the advantage of using Out-String is that it still allows you to capture the for-display representation in a file, if desired.
Here's a simple helper function (filter) that you can put in your $PROFILE:
# Once defined, you can use: ./some-script.ps1 | Format-Each
Filter Format-Each { $_ | Out-String }
PetSerAl's suggestion - ./some-script.ps1 | Format-List - works in principle too, but it switches the output from the usual table-style output to list-style output, with each property listed on its own line, which may be undesired.
Conversely, however, Format-Each, if an output object is (implicitly) table-formatted, prints a header for each object.
Why Write-Output doesn't help:
Write-Output doesn't help, because it writes to where output objects go by default anyway: the aforementioned success output stream, where data should go.
If the output stream's objets aren't redirected or captured in some form, they are sent to the host by default (typically, the console), where the automatic formatting is applied.
Also, use of Write-Output is rarely necessary, because simply not capturing or redirecting a command or expression implicitly writes to the success stream; another way of putting it:
Write-Output is implied.
Therefore, the following two statements are equivalent:
Write-Output $object # write $object to the success output stream
$object # same; *implicitly* writes $object to the success output stream
Why use of Write-Host is ill-advised, both here and often in general:
Assuming you do know the implications of using Write-Host in general - see below - you could use it for the problem at hand, but Write-Host applies simple .ToString() formatting to its input, which does not give you the nice, multi-line formatting that PowerShell applies by default.
Thus, Out-Host (and Out-String) were used above, because they do apply the same, friendly formatting.
Contrast the following two statements, which print a hash-table ([hashtable]) literal:
# (Optional) use of Write-Output: The friendly, multi-line default formatting is used.
# ... | Out-Host and ... | Out-String would print the same.
PS> Write-Output #{ foo = 1; bar = 'baz' }
Name Value
---- -----
bar baz
foo 1
# Write-Host: The hashtable's *entries* are *individually* stringified
# and the result prints straight to the console.
PS> Write-Host #{ foo = 1; bar = 'baz' }
System.Collections.DictionaryEntry System.Collections.DictionaryEntry
Write-Host did two things here, which resulted in near-useless output:
The [hashtable] instance's entries were enumerated and each entry was individually stringified.
The .ToString() stringification of hash-table entries (key-value pairs) is System.Collections.DictionaryEntry, i.e., simply the type name of the instance.
The primary reasons for avoiding Write-Host in general are:
It outputs directly to the host (console) rather than to PowerShell's success output stream.
As a beginner, you may mistakenly think that Write-Host is for writing results (data), but it isn't.
In bypassing PowerShell's system of streams, Write-Host output cannot be redirected - that is, it can neither be suppressed nor captured (in a file or variable).
That said, starting with PowerShell v5.0, you can now redirect its output via the newly introduced information stream (number 6; e.g., ./some-script.ps1 6>write-host-output.txt); however, that stream is more properly used with the new Write-Information cmdlet.
By contrast, Out-Host output still cannot be redirected.
That leaves just the following legitimate uses of Write-Host:
Creating end-user prompts and colored for-display-only representations:
Your script may have interactive prompts that solicit information from the user; using Write-Host - optionally with coloring via the -ForegroundColor and -BackgroundColor parameters - is appropriate, given that prompt strings should not become part of the script's output and users also provide their input via the host (typically via Read-Host).
Similarly, you can use Write-Host with selective coloring to explicitly create friendlier for-display-only representations.
Quick prototyping: If you want a quick-and-dirty way to write status/diagnostic information directly to the console without interfering with a script's data output.
However, it is generally better to use Write-Verbose and Write-Debug in such cases.
Generally speaking the expectation is for script/functions to return a single "type" of object, often with many instances. For example, Get-Process returns a load of processes, but they all have the same fields. As you'll have seen from the tutorials, etc. you can then pass the output of Get-Process along a pipeline and process the data with subsequent cmdlets.
In your case you are returning two different types of object (i.e. with two different sets of properties). PS outputs the first object, but not the second one (which doesn't match the first) as you discovered. If you were to add extra properties to the first object that match those used in the second one, then you'd see both objects.
Write-Host doesn't care about this sort of stuff. The push-back against using this is mainly to do with (1) it being a lazy way to give feedback about script progress, i.e. use Write-Verbose or Write-Debug instead and (2) it being imperfect when it comes to passing objects along a pipeline, etc.
Clarification on point (2), helpfully raised in the comments to this answer:
Write-Host is not just imperfect with respect to the pipeline /
redirection / output capturing, you simply cannot use it for that in
PSv4 and earlier, and in PSv5+ you have to awkwardly use 6>; also,
Write-Host stringifies with .ToString(), which often produces
unhelpful representations
If your script is really just meant to print data to the console then go ahead and Write-Host.
Alternatively, you can return multiple objects from a script or function. Using return or Write-Output, just return object objects comma-separated. For example:
Test-WriteOutput.ps1
$object1 = [PSCustomObject]#{
OSBuild = "910ef01.2.8779"
OSVersion = "CustomOS 3"
BIOSSerial = "A5FT-XT2H-5A4B-X9NM"
}
$object2 = [PSCustomObject]#{
Site = "SQL site"
Server= "SQL Server"
Database="SQL Database"
}
Write-Output $object1,$object2
The run the script, assigning the output into two variables:
$a,$b = .\Test-WriteOutput.ps1
You'll see that $a is $object1 and $b is $object2.
use write-host, write-output is for pipeline (and by default on console after clear)

Is there any difference in order when using pipe and Write-Output

I'm not an expert on powershell but today I came across this line of code (note that Write-Output is only used as an example):
"Foo" | Write-Output
I wonder if it is any different from what I would expect:
Write-Output "Foo"
In effect the two statements should be equivalent:
In Write-Output "Foo", "Foo" is implicitly bound to positional parameter -InputObject, which accepts type PSObject[], i.e., an array of objects of any type.
In "Foo" | Write-Output, by virtue of parameter -InputObject being defined as optionally accepting pipeline input (by value, i.e. whole objects), "Foo" is also bound to -InputObject.
I assume you chose Write-Output as an example, but it's worth nothing that there's rarely a good reason to use that cmdlet explicitly - simply omitting it in your examples would yield the same results.
Furthermore, there are several cmdlets where the two forms are not equivalent, namely those where -InputObject is defined as a scalar (with exceptions); consider the following:
1, 2 | Get-Member # reports [System.Int32]
Get-Member -InputObject 1, 2 # reports [System.Object[]]
1, 2 | Get-Member reports the type members for each element in the input array.
Get-Member -InputObject 1, 2, by contrast, reports the members of the array type itself.
This difference in behavior is intentional and documented: using the parameter (-InputObject) allows inspecting collection types as a whole, whereas using the pipeline allows inspecting a collection's individual elements' types.
Note that there are cmdlets that exhibit the same difference in behavior, even though passing collections as a whole to them doesn't make much sense, such as Export-Csv; in such cases, always use the pipeline - see this GitHub issue for background information.
To determine what parameters of a cmdlet accept pipeline input and thus understand what parameter pipeline input will be bound to:
To see the parameters in the context of the full help topic:
Run Get-Help -Full <cmdlet>; using -Full is crucial.
Search for occurrences of true (, which will match parameters that accept:
pipeline input by value (whole object) only: true (ByValue)
pipeline input by property name only: true (ByPropertyName)
either: true (ByValue, ByPropertyName)
More generally, each parameter description has an Accept pipeline input? line item followed by a Boolean.
To extract just the names and their aliases, data types, and binding characteristics (using Rename-Item as an example):
Get-Help Rename-Item -Parameter * | Where-Object pipelineInput -like 'true*' |
Select-Object Name, Aliases, Type, pipelineInput