Parametrization of a pipe with a function - powershell

I'm discovering powershell with some joy and some frustation, and I'm currently hitting a wall with respect to my powershell abilities.
I'd like to create a pipe function to summarize member types of a collection of objects.
Without a function, the working code looks like this:
get-process |
foreach { get-member -inputobject $_ } |
foreach { "[$($_.membertype)]$($_.name)" } |
group-object |
sort count, name
Now, my current attempt for a function achieving this is:
function get-membersummary {
process {
get-member -inputobject $_ |
foreach { "[$($_.membertype)]$($_.name)" } |
group-object |
sort count, name
}
}
it should be used like this:
&$anything | get-membersummary
The obvious problem is that the code in the "process" statement is called for each element, which means that the grouping is performed on each item. What I would like is the first result of the whole two lines to be subjected to the grouping.
Is it possible to achieve this in powershell without using an array variable that would induce memory inefficiency ?
Also, I'm pretty sure that this question as already been asked, but I couldn't find the correct words to express it.

You can use SteppablePipeline, but for sorting you have to collect whole input before start sorting it, so using array here hardly induce much more "memory inefficiency" than you already have.
function get-membersummary {
begin {
$Pipeline={
&foreach { get-member -inputobject $_ } |
foreach { "[$($_.membertype)]$($_.name)" } |
group-object|
sort count, name
}.GetSteppablePipeline()
$Pipeline.Begin($MyInvocation.ExpectingInput,$ExecutionContext)
}
process {
if($MyInvocation.ExpectingInput){
$Pipeline.Process($_)
}else{
$Pipeline.Process()
}
}
end {
$Pipeline.End()
$Pipeline.Dispose()
}
}

You can't group and sort until all data is received, so you need to wait until the end {} block in the function. Two examples:
#process using pipeline, but wait until end to group and sort
function get-membersummary {
begin { $res = #() }
process {
$res += Get-Member -inputobject $_ |
ForEach-Object { "[$($_.MemberType)]$($_.Name)" }
}
end {
$res | Group-Object | Sort-Object Count, Name | Select-Object Count, Name
}
}
#do everything after all objects have arrived
function get-membersummary2 {
end {
#in process { }, $input is the object in the pipeline. in end { } it is a collection of all the objects.
$input | % {
Get-Member -InputObject $_ |
ForEach-Object { "[$($_.MemberType)]$($_.Name)" }
} | Group-Object | Sort-Object Count, Name | Select-Object Count, Name
}
}
Get-Process | get-membersummary | ft -AutoSize
#Get-Process | get-membersummary2 | ft -AutoSize
Output:
Count Name
----- ----
75 [AliasProperty]Handles
75 [AliasProperty]Name
75 [AliasProperty]NPM
75 [AliasProperty]PM
75 [AliasProperty]VM
75 [AliasProperty]WS
75 [Event]Disposed
75 [Event]ErrorDataReceived
75 [Event]Exited
75 [Event]OutputDataReceived
....
On a general note, you should avoid using Group-Object and Sort-Object in functions as they break the flow of the pipeline. Select-Object (which I added this time) should also be avoided because it destroys the original objects. I understand the choice in this scenario, but be careful not the use them too much. You could have written a function or filter to process the objects, and then call group and sort manually when needed, like:
filter get-membersummary3 {
$_ |
Get-Member |
ForEach-Object { "[$($_.MemberType)]$($_.Name)" }
}
Get-Process | get-membersummary3 | Group-Object | Sort-Object Count, Name | Select-Object Count, Name

Related

powershell winform searchbox shows results incorrect [duplicate]

I'm using Powershell to set up IIS bindings on a web server, and having a problem with the following code:
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
if ($serverIps.length -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
$primaryIp = $serverIps[0]
$secondaryIp = $serverIps[1]
If there's 2+ IPs on the server, fine - Powershell returns an array, and I can query the array length and extract the first and second addresses just fine.
Problem is - if there's only one IP, Powershell doesn't return a one-element array, it returns the IP address (as a string, like "192.168.0.100") - the string has a .length property, it's greater than 1, so the test passes, and I end up with the first two characters in the string, instead of the first two IP addresses in the collection.
How can I either force Powershell to return a one-element collection, or alternatively determine whether the returned "thing" is an object rather than a collection?
Define the variable as an array in one of two ways...
Wrap your piped commands in parentheses with an # at the beginning:
$serverIps = #(gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort)
Specify the data type of the variable as an array:
[array]$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
Or, check the data type of the variable...
IF ($ServerIps -isnot [array])
{ <error message> }
ELSE
{ <proceed> }
Force the result to an Array so you could have a Count property. Single objects (scalar) do not have a Count property. Strings have a length property so you might get false results, use the Count property:
if (#($serverIps).Count -le 1)...
By the way, instead of using a wildcard that can also match strings, use the -as operator:
[array]$serverIps = gwmi Win32_NetworkAdapterConfiguration -filter "IPEnabled=TRUE" | Select-Object -ExpandProperty IPAddress | Where-Object {($_ -as [ipaddress]).AddressFamily -eq 'InterNetwork'}
You can either add a comma(,) before return list like return ,$list or cast it [Array] or [YourType[]] at where you tend to use the list.
If you declare the variable as an array ahead of time, you can add elements to it - even if it is just one...
This should work...
$serverIps = #()
gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort | ForEach-Object{$serverIps += $_}
You can use Measure-Object to get the actual object count, without resorting to an object's Count property.
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
if (($serverIps | Measure).Count -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
Return as a referenced object, so it never converted while passing.
return #{ Value = #("single data") }
I had this problem passing an array to an Azure deployment template. If there was one object, PowerShell "converted" it to a string. In the example below, $a is returned from a function that gets VM objected according to the value of a tag. I pass the $a to the New-AzureRmResourceGroupDeployment cmdlet by wrapping it in #(). Like so:
$TemplateParameterObject=#{
VMObject=#($a)
}
New-AzureRmResourceGroupDeployment -ResourceGroupName $RG -Name "TestVmByRole" -Mode Incremental -DeploymentDebugLogLevel All -TemplateFile $templatePath -TemplateParameterObject $TemplateParameterObject -verbose
VMObject is one of the template's parameters.
Might not be the most technical / robust way to do it, but it's enough for Azure.
Update
Well the above did work. I've tried all the above and some, but the only way I have managed to pass $vmObject as an array, compatible with the deployment template, with one element is as follows (I expect MS have been playing again (this was a report and fixed bug in 2015)):
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Web.Extensions")
foreach($vmObject in $vmObjects)
{
#$vmTemplateObject = $vmObject
$asJson = (ConvertTo-Json -InputObject $vmObject -Depth 10 -Verbose) #-replace '\s',''
$DeserializedJson = (New-Object -TypeName System.Web.Script.Serialization.JavaScriptSerializer -Property #{MaxJsonLength=67108864}).DeserializeObject($asJson)
}
$vmObjects is the output of Get-AzureRmVM.
I pass $DeserializedJson to the deployment template' parameter (of type array).
For reference, the lovely error New-AzureRmResourceGroupDeployment throws is
"The template output '{output_name}' is not valid: The language expression property 'Microsoft.WindowsAzure.ResourceStack.Frontdoor.Expression.Expressions.JTokenExpression'
can't be evaluated.."
There is a way to deal with your situation. Leave most of you code as-is, just change the way to deal with the $serverIps object. This code can deal with $null, only one item, and many items.
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
# Always use ".Count" instead of ".Length".
# This works on $null, only one item, or many items.
if ($serverIps.Count -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
# Always use foreach on a array-possible object, so that
# you don't have deal with this issue anymore.
$serverIps | foreach {
# The $serverIps could be $null. Even $null can loop once.
# So we need to skip the $null condition.
if ($_ -ne $null) {
# Get the index of the array.
# The #($serverIps) make sure it must be an array.
$idx = #($serverIps).IndexOf($item)
if ($idx -eq 0) { $primaryIp = $_ }
if ($idx -eq 1) { $secondaryIp = $_ }
}
}
In PowerShell Core, there is a .Count property exists on every objects. In Windows PowerShell, there are "almost" every object has an .Count property.

Trouble filtering a Nested Hashtable inside a Switch

I've built a WPF form with 2 Combo Boxes, both filled with queries from this Nested Hashtable.
cboAgencies fills when the script runs and the form loads. cboOffices based on the selection made in cboAgencies and lists the office locations specific to the selected Agency.
Or, it's supposed to.
Here's the relevant code:
$cboAgencies.Add_SelectionChanged({
$Script:selectedAgency = $cboAgencies.SelectedItem.ToString()
$Script:cboOffices.Items.Clear()
$Script:selectedAdmin = $null
Switch($Script:selectedAgency) {
subdivision1 {
$Script:selectedAdmin = 'division2'
$Script:arr_Offices = ($Script:hash_AgencyOffices[$Script:selectedAdmin][$Script:selectedAgency].Keys | Sort-Object)
$Script:arr_Offices | ForEach-Object {
$Script:cboOffices.Items.Add($_) | Out-Null
}
break
}
subdivision2 {
$Script:selectedAdmin = 'division2'
$Script:arr_Offices = ($Script:hash_AgencyOffices[$Script:selectedAdmin][$Script:selectedAgency].Keys | Sort-Object)
$Script:arr_Offices | ForEach-Object {
$Script:cboOffices.Items.Add($_) | Out-Null
}
break
}
division1 {
$Script:selectedAdmin = 'division1'
$Script:arr_Offices = ($Script:hash_AgencyOffices[$Script:selectedAdmin][$Script:selectedAgency].Keys | Sort-Object)
$Script:arr_Offices | ForEach-Object {
$Script:cboOffices.Items.Add($_) | Out-Null
}
break
}
}
})
Note: The code is anonymized. There are no spaces in the real division or subdivision names in the hashtable. Just in case that is a point of concern.
Running this code using . .\script.ps1 I query the various variables and find that the following variables are all correctly:
$cboAgencies.SelectedItem (and $Script:cboAgencies.SelectedItem)
$selectedAgency (and $Script:selectedAgency)
$selectedAdmin (and $Script:selectedAdmin)
But $arr_Offices is empty, meaning that the problem is happening in this line:
$Script:arr_Offices = ($Script:hash_AgencyOffices[$Script:selectedAdmin][$Script:selectedAgency].Keys | Sort-Object)
I've further confirmed this by placing this outside the Switch function:
$Script:arr_Offices = ($Script:hash_AgencyOffices['division2']['subdivision1'] | Sort-Object)
$Script:arr_Offices | ForEach-Object {
$Script:cboOffices.Items.Add($_) | Out-Null
}
And $arr_Offices and $cboOffices fill just fine.
What have I done wrong here?
From your linked question, $script:hash_AgencyOffices[$Script:selectedAdmin][$Script:se‌​lectedAgency] results in an array of strings, it will not have a .Keys property.
Your data structure is "Root hashtable which has keys, their values are hashtables, which have keys, their values are arrays which directly have content".
So you can use AgencyOffices['division2'].Keys and AgencyOffices['division2']['subdivision1'], but not AgencyOffices['division2']['subdivision1'].Keys.

powershell prevent duplicate object keys

This is a follow up to this question
If I have 2 json files
file1.json
{
"foo": {
"honk": 42
}
}
file2.json
{
"foo": {
"honk": 9000,
"toot": 9000
}
}
And I create an object using ConvertFrom-Json
$bar = #(Get-ChildItem . -Filter *.json -Recurse | Get-Content -Raw |ConvertFrom-Json)
Powershell will happily take both, and overwrite foo.
foo
---
#{honk=42}
#{honk=9000; toot=9000}
The contents of $bar.foo are merged
$bar.foo
honk
----
42
9000
How can I error if importing duplicate objects?
Each JSON file is imported as a separate object, so there's nothing overwritten really. You just get a list of objects.
To throw an error when you get multiple objects with the same top-level property you can group the objects by property name and throw an error if you get a count >1.
$bar | Group-Object { $_.PSObject.Properties.Name } |
Where-Object { $_.Count -gt 1 } |
ForEach-Object { throw "Duplicate object $($_.Name)" }
When importing to an array, every object is unique. In this example it isn't ideal to leave the objects in an array, since there is no way to predictably iterate over them, since some objects might contain multiple keys.
{
"foo": 42
}
vs
{
"bar": 9000,
"buzz": 9000
}
This will cause heartache when trying to loop through all objects.
Instead, I took all array items and combined them into 1 powershell object. Since powershell objects are basically hashes, and hashes by design must have all keys unique, powershell will automatically error if overwriting a key.
function Load-Servers {
$allObjects = #(
Get-ChildItem '.\servers' -Filter *.json -Recurse | Get-Content -Raw | ConvertFrom-Json
)
$object = New-Object PSObject
Foreach ($o in $allObjects) {
$o.psobject.members | ? {$_.Membertype -eq "noteproperty" } | %{$object | add-member $_.Name $_.Value }
}
return $object
}

Compare-Object - Separate side columns

Is it possible to display the results of a PowerShell Compare-Object in two columns showing the differences of reference vs difference objects?
For example using my current cmdline:
Compare-Object $Base $Test
Gives:
InputObject SideIndicator
987654 =>
555555 <=
123456 <=
In reality the list is rather long. For easier data reading is it possible to format the data like so:
Base Test
555555 987654
123456
So each column shows which elements exist in that object vs the other.
For bonus points it would be fantastic to have a count in the column header like so:
Base(2) Test(1)
555555 987654
123456
Possible? Sure. Feasible? Not so much. PowerShell wasn't really built for creating this kind of tabular output. What you can do is collect the differences in a hashtable as nested arrays by input file:
$ht = #{}
Compare-Object $Base $Test | ForEach-Object {
$value = $_.InputObject
switch ($_.SideIndicator) {
'=>' { $ht['Test'] += #($value) }
'<=' { $ht['Base'] += #($value) }
}
}
then transpose the hashtable:
$cnt = $ht.Values |
ForEach-Object { $_.Count } |
Sort-Object |
Select-Object -Last 1
$keys = $ht.Keys | Sort-Object
0..($cnt-1) | ForEach-Object {
$props = [ordered]#{}
foreach ($key in $keys) {
$props[$key] = $ht[$key][$_]
}
New-Object -Type PSObject -Property $props
} | Format-Table -AutoSize
To include the item count in the header name change $props[$key] to $props["$key($($ht[$key].Count))"].

How can I force Powershell to return an array when a call only returns one object?

I'm using Powershell to set up IIS bindings on a web server, and having a problem with the following code:
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
if ($serverIps.length -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
$primaryIp = $serverIps[0]
$secondaryIp = $serverIps[1]
If there's 2+ IPs on the server, fine - Powershell returns an array, and I can query the array length and extract the first and second addresses just fine.
Problem is - if there's only one IP, Powershell doesn't return a one-element array, it returns the IP address (as a string, like "192.168.0.100") - the string has a .length property, it's greater than 1, so the test passes, and I end up with the first two characters in the string, instead of the first two IP addresses in the collection.
How can I either force Powershell to return a one-element collection, or alternatively determine whether the returned "thing" is an object rather than a collection?
Define the variable as an array in one of two ways...
Wrap your piped commands in parentheses with an # at the beginning:
$serverIps = #(gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort)
Specify the data type of the variable as an array:
[array]$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
Or, check the data type of the variable...
IF ($ServerIps -isnot [array])
{ <error message> }
ELSE
{ <proceed> }
Force the result to an Array so you could have a Count property. Single objects (scalar) do not have a Count property. Strings have a length property so you might get false results, use the Count property:
if (#($serverIps).Count -le 1)...
By the way, instead of using a wildcard that can also match strings, use the -as operator:
[array]$serverIps = gwmi Win32_NetworkAdapterConfiguration -filter "IPEnabled=TRUE" | Select-Object -ExpandProperty IPAddress | Where-Object {($_ -as [ipaddress]).AddressFamily -eq 'InterNetwork'}
You can either add a comma(,) before return list like return ,$list or cast it [Array] or [YourType[]] at where you tend to use the list.
If you declare the variable as an array ahead of time, you can add elements to it - even if it is just one...
This should work...
$serverIps = #()
gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort | ForEach-Object{$serverIps += $_}
You can use Measure-Object to get the actual object count, without resorting to an object's Count property.
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
if (($serverIps | Measure).Count -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
Return as a referenced object, so it never converted while passing.
return #{ Value = #("single data") }
I had this problem passing an array to an Azure deployment template. If there was one object, PowerShell "converted" it to a string. In the example below, $a is returned from a function that gets VM objected according to the value of a tag. I pass the $a to the New-AzureRmResourceGroupDeployment cmdlet by wrapping it in #(). Like so:
$TemplateParameterObject=#{
VMObject=#($a)
}
New-AzureRmResourceGroupDeployment -ResourceGroupName $RG -Name "TestVmByRole" -Mode Incremental -DeploymentDebugLogLevel All -TemplateFile $templatePath -TemplateParameterObject $TemplateParameterObject -verbose
VMObject is one of the template's parameters.
Might not be the most technical / robust way to do it, but it's enough for Azure.
Update
Well the above did work. I've tried all the above and some, but the only way I have managed to pass $vmObject as an array, compatible with the deployment template, with one element is as follows (I expect MS have been playing again (this was a report and fixed bug in 2015)):
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Web.Extensions")
foreach($vmObject in $vmObjects)
{
#$vmTemplateObject = $vmObject
$asJson = (ConvertTo-Json -InputObject $vmObject -Depth 10 -Verbose) #-replace '\s',''
$DeserializedJson = (New-Object -TypeName System.Web.Script.Serialization.JavaScriptSerializer -Property #{MaxJsonLength=67108864}).DeserializeObject($asJson)
}
$vmObjects is the output of Get-AzureRmVM.
I pass $DeserializedJson to the deployment template' parameter (of type array).
For reference, the lovely error New-AzureRmResourceGroupDeployment throws is
"The template output '{output_name}' is not valid: The language expression property 'Microsoft.WindowsAzure.ResourceStack.Frontdoor.Expression.Expressions.JTokenExpression'
can't be evaluated.."
There is a way to deal with your situation. Leave most of you code as-is, just change the way to deal with the $serverIps object. This code can deal with $null, only one item, and many items.
$serverIps = gwmi Win32_NetworkAdapterConfiguration
| Where { $_.IPAddress }
| Select -Expand IPAddress
| Where { $_ -like '*.*.*.*' }
| Sort
# Always use ".Count" instead of ".Length".
# This works on $null, only one item, or many items.
if ($serverIps.Count -le 1) {
Write-Host "You need at least 2 IP addresses for this to work!"
exit
}
# Always use foreach on a array-possible object, so that
# you don't have deal with this issue anymore.
$serverIps | foreach {
# The $serverIps could be $null. Even $null can loop once.
# So we need to skip the $null condition.
if ($_ -ne $null) {
# Get the index of the array.
# The #($serverIps) make sure it must be an array.
$idx = #($serverIps).IndexOf($item)
if ($idx -eq 0) { $primaryIp = $_ }
if ($idx -eq 1) { $secondaryIp = $_ }
}
}
In PowerShell Core, there is a .Count property exists on every objects. In Windows PowerShell, there are "almost" every object has an .Count property.