I'm attempting to add a custom column to the table output of some objects retrieved from our MECM instance.
The following code shows the precise format of the code, only gathering data from Get-Process instead.
$types = #{
"chrome" = "This is Chrome"
"winlogon" = "This is winlogon"
}
$procs = Get-Process | Select Name,Id | Where { ($_.Name -eq "winlogon") -or ($_.Name -eq "chrome") }
$procsCustom = $procs | Select Name,Id,#{
Name = "TestColumn"
Expression = {
$name = $_.Name
$types.$name
}
}
$procsCustom | Format-Table
This code functions as expected:
Name Id TestColumn
---- -- ----------
chrome 12428 This is Chrome
chrome 12448 This is Chrome
chrome 12460 This is Chrome
winlogon 880 This is winlogon
winlogon 5076 This is winlogon
When I do the same thing for my actual code:
$refreshTypes = #{
1 = "Manual Update Only"
2 = "Scheduled Updates Only"
4 = "Incremental Updates Only"
6 = "Incremental and Scheduled Updates"
}
$colls = Get-CMCollection | Where { ($_.RefreshType -eq 4) -or ($_.RefreshType -eq 6) }
$collsCustom = $colls | Select Name,RefreshType,#{
Name = "RefreshTypeFriendly"
Expression = {
$type = $_.RefreshType
$refreshTypes.$type
}
}
$collsCustom | Format-Table
The custom column is not populated:
Name RefreshType RefreshTypeFriendly
---- ----------- -------------------
Collection 1 6
Collection 2 4
The following code shows that $_.RefreshType is being parsed correctly:
$collsCustom = $colls | Select Name,RefreshType,#{
Name = "RefreshTypeFriendly"
Expression = {
$_.RefreshType
}
}
$collsCustom | Format-Table
Name RefreshType RefreshTypeFriendly
---- ---------- -------------------
Collection 1 6 6
Collection 2 4 4
The following code forces a fake value for $type in the expression scriptblock and shows that the rest of the code works:
$collsCustom = $colls | Select Name,RefreshType,#{
Name = "RefreshTypeFriendly"
Expression = {
$type = 1
$refreshTypes.$type
}
}
$collsCustom | Format-Table
Name RefreshType RefreshTypeFriendly
---- ----------- -------------------
Collection 1 6 Manual Update Only
Colleciton 2 4 Manual Update Only
So why in the world does the intended code output nothing in the custom column? Given the examples I've provided above, I'm stumped.
FWIW I've also tried using the array syntax ($refreshTypes[$type]) instead of the object property syntax ($refreshTypes.$type) but I get the same behavior.
Thanks for your time.
Environment:
Win10 x64 20H2
Powershell 5.1
Looks like your type isn't being interpreted as an int. Try casting $type to Int
Try this
Expression = {
[int]$type = $_.RefreshType
$refreshTypes.$type
}
Related
When we're trying to export data to other functions via the pipeline, we observe some strange behavior in PowerShell.
Example code:
$Array = #()
$Obj1 = [PSCustomObject]#{
Member1 = 'First'
Member2 = 'Second'
}
$Obj2 = [PSCustomObject]#{
Member1 = 'First'
Member2 = 'Second'
Member3 = 'Third'
}
$Array = $Obj1, $Obj2
$Array | Out-GridView -Title 'Not showing Member3'
$Array = $Obj2, $Obj1
$Array | Out-GridView -Title 'All members correctly displayed'
In the example above you can see that when the first object only contains 2 properties, the Out-GridView CmdLet (and others) only show 2 properties, even though the second object has 3 properties. However, when the first object in the array has 3 properties it does display them all correctly.
Is there a way around this? Because it's not possible to predict up front how many properties on an object there will be and if the object with the most properties will be the first one in the array.
I had the same experience once and created the following reusable 'Union' function:
# 2021-08-25 Removed Union function
Usage:
$Obj1, $Obj2 | Union | Out-GridView -Title 'Showing all members'
It is also supposed to work with complex objects. Some standard cmdlets output multiple object types at once and if you view them (e.g. Out-GridView) or dump them in a file (e.g. Export-Csv) you might miss a lot of properties. Take as another example:
Get-WmiObject -Namespace root/hp/instrumentedBIOS -Class hp_biosSetting | Union | Export-Csv ".\HPBIOS.csv"
Added 2014-09-19:
Maybe this is already between the lines in the comments $Array | Select * | … will not resolve the issue but specifically selecting the properties $Array | Select Member1, Member2, Member3 | … does.
Besides, although in most cases the Union function will work, there are some exceptions to that as it will only align the first object with the rest.
Consider the following object:
$List = #(
New-Object PSObject -Property #{Id = 2}
New-Object PSObject -Property #{Id = 1}
New-Object PSObject -Property #{Id = 3; Name = "Test"}
)
If you Union this object everything appears to be fine and if you e.g. ExportTo-CSV and work with the export .csv file from then on you will never have any issue.
$List | Union
Id Name
-- ----
2
1
3 Test
Still there is a catch as only the first object is aligned. If you e.g. sort the result on Id (Sort Id) or take just the last 2 (Select -Last 2) entries, the Name is not listed because the second object doesn’t contain the Name property:
$List | Union | Sort Id
Id
--
1
2
3
Therefor I have rewritten the Union-Object (Alias Union) function`):
Union-Object
# 2021-08-25 Removed Union-Object function
Syntax:
$Array | Union | Out-GridView -Title 'All members correctly displayed'
Update 2021-08-25
Based on az1d helpful feedback on an error caused by equal property names with different casing, I have created a new UnifyProperties function.
(I will no longer use the name UnionObject for his)
function UnifyProperties {
$Names = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
$InputCollected = #($Input)
$InputCollected.ForEach({
foreach ($Name in $_.psobject.Properties.Name) { $Null = $Names.Add($Name) }
})
$inputCollected | Select-Object #($Names)
}
Usage:
[pscustomobject] #{ one = 1; two = 2; three = 3 },
[pscustomobject] #{ ONE = 10; THREE = 30; FOUR = 4 } |
UnifyProperties
one two three FOUR
--- --- ----- ----
1 2 3
10 30 4
See also: #13906 Add -UnifyProperties parameter to Select-Object
I want to generate the following table:
AAA BBB CCC
--- --- ---
10 10 10
10 10 10
10 10 10
10 10 10
10 10 10
So I write the following code using a foreach loop to generate the column names:
$property = #('AAA', 'BBB', 'CCC') | foreach {
#{ name = $_; expression = { 10 } }
}
#(1..5) | select -Property $property
But I get the following error saying the name is not a string:
select : The "name" key has a type, System.Management.Automation.PSObject, that is not valid; expected type is System.String.
At line:4 char:11
+ #(1..5) | select -Property $property
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Select-Object], NotSupportedException
+ FullyQualifiedErrorId : DictionaryKeyIllegalValue2,Microsoft.PowerShell.Commands.SelectObjectCommand
To get the code work, I have to convert the $_ to string like below:
$property = #('AAA', 'BBB', 'CCC') | foreach {
#{ name = [string]$_; expression = { 10 } }
}
#(1..5) | select -Property $property
Or like below:
$property = #('AAA', 'BBB', 'CCC') | foreach {
#{ name = $_; expression = { 10 } }
}
$property | foreach { $_.name = [string]$_.name }
#(1..5) | select -Property $property
The question is: the $_ is already a string. Why do I have to convert it to string again? And why select thinks that the name is PSObject?
To confirm that it's already a string, I write the following code to print the type of name:
$property = #('AAA', 'BBB', 'CCC') | foreach {
#{ name = $_; expression = { 10 } }
}
$property | foreach { $_.name.GetType() }
The following result confirms that it's already a string:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
True True String System.Object
True True String System.Object
I know that there are many other easier ways to generate the table. But I want to understand why I have to convert a string to string to make the code work, and why select doesn't think that the string is a string. For what it's worth, my $PSVersionTable.PSVersion is:
Major Minor Build Revision
----- ----- ----- --------
5 1 18362 1474
You're seeing the unfortunate effects of incidental, normally invisible [psobject] wrappers PowerShell uses behind the scenes.
In your case, because the input strings are supplied via the pipeline, they get wrapped in and stored as [psobject] instances in your hashtables, which is the cause of the problem.
The workaround - which is neither obvious nor should it be necessary - is to discard the wrapper by accessing .psobject.BaseObject:
$property = 'AAA', 'BBB', 'CCC' | ForEach-Object {
#{ name = $_.psobject.BaseObject; expression = { 10 } }
}
1..5 | select -Property $property
Note:
In your case, a simpler alternative to .psobject.BaseObject (see the conceptual about_Intrinsic Members help topic) would have been to call .ToString(), given that you want a string.
To test a given value / variable for the presence of such a wrapper, use -is [psobject]; with your original code, the following yields $true, for instance:
$property[0].name -is [psobject]
Note, however, that this test is meaningless for [pscustomobject] instances, where it is always $true (custom objects are in essence [psobject] instances without a .NET base objects - they only have dynamic properties).
That the normally invisible [psobject] wrappers situationally, obscurely result in behavioral differences is arguably a bug and the subject of GitHub issue #5579.
Simpler and faster alternative, using the .ForEach() array method:
$property = ('AAA', 'BBB', 'CCC').ForEach({
#{ name = $_; expression = { 10 } }
})
1..5 | select -Property $property
Unlike the pipeline, the .ForEach() method does not wrap $_ in [psobject], so the problem doesn't arise and no workaround is needed.
Using the method is also faster, although note that, unlike the pipeline, it must collect all its input in memory up front (clearly not a problem with an array literal).
I've been working on this for a little while, basically all I'm doing is grabbing the licensing from our clients' Office 365 tenant and displaying it in a more readable manner since Office 365 Powershell doesn't output subscription names in a common name. This works perfectly when every subscription is in my CSV of subscription names, however on occasions where the SKU is not in my list (brand new or legacy offerings) the licensing table doesn't populate correctly because it can't find the friendlyname in my CSV (licensing quantities don't match the subscription name because of a blank record when it failed to find the SKU).
What I'm trying to have it do is display the skupartnumber in place of the friendlyname in the event that the subscription is not in my CSV instead of breaking the output. The first snippet below is my current working script that only works if the SKU is in my CSV, the one below it is the my best attempt at trying some input validation but I just can't get it to work. Everything displays correctly except the Subscription column which is blank (I also notice that it takes about 5x as long to run as normal), I would greatly appreciate any assistance offered; thanks!
Works as long as subscription is in my CSV:
$sku = Get-MsolAccountSku | select-object skupartnumber,ActiveUnits,suspendedUnits,ConsumedUnits | sort-object -property skupartnumber
$skudata = import-csv -Header friendlyname,skupartnumber "C:\PShell\cspcatalogalphabet.csv" | where-object {$sku.skupartnumber -eq $_.skupartnumber} | sort-object -property skupartnumber
$result = for ($n = 0; $n -lt #($skudata).Count; $n++) {
[PsCustomObject]#{
Subscription = #($skudata.friendlyname)[$n]
Active = $sku.ActiveUnits[$n]
Suspended = $sku.SuspendedUnits[$n]
Assigned = $sku.ConsumedUnits[$n]
}
}
$result | Format-Table -AutoSize
# Output:
Subscription Active Suspended Assigned
------------ ------ --------- --------
Microsoft Flow Free 10000 0 1
Power Bi (Free) 1000000 0 1
Microsoft Teams Exploratory 100 0 6
My best attempt at input validation which results in no data being read into $skulist.Subscription:
$sku = Get-MsolAccountSku | select-object skupartnumber,ActiveUnits,suspendedUnits,ConsumedUnits
$skulist = import-csv -Header friendlyname,skupartnumber "C:\PShell\cspcatalogalphabet.csv"
$skuname = for ($c = 0; $c -lt #($sku).count; $c++) {
if ($sku.skupartnumber[$c] -in $skulist.skupartnumber) {
[PsCustomObject]#{
Subscription = $skulist.friendlyname | where-object {$sku.skupartnumber[$c] -eq $skulist.skupartnumber}
}
}
else {
[PSCustomObject]#{
Subscription = #($sku.skupartnumber)[$c]
}
}
}
$table = for ($n = 0; $n -lt #($sku).Count; $n++) {
[PsCustomObject]#{
Subscription = #($skuname.Subscription)[$n]
Active = $sku.ActiveUnits[$n]
Suspended = $sku.SuspendedUnits[$n]
Assigned = $sku.ConsumedUnits[$n]
}
}
$table | format-table -AutoSize
# Output:
Subscription Active Suspended Assigned
------------ ------ --------- --------
10000 0 1
1000000 0 1
100 0 6
# An example of data I am grabbing from our clients' accounts:
$sku = Get-MsolAccountSku | select-object skupartnumber,ActiveUnits,suspendedUnits,ConsumedUnits
$sku
# Output:
SkuPartNumber ActiveUnits SuspendedUnits ConsumedUnits
------------- ----------- -------------- -------------
FLOW_FREE 10000 0 1
POWER_BI_STANDARD 1000000 0 1
TEAMS_EXPLORATORY 100 0 6
Instead of relying on the two arrays being aligned - that is, index $n in $sku must correspond to the item at index $n in $skulist for you code to work - you'll want to be able to resolve a value in $skulist based on the actual $SKU.SkuPartNumber value instead.
So how does one do that?!
Feed your $skulist into a [hashtable] instead:
$skulist = #{}
Import-Csv -Header friendlyname,skupartnumber "C:\PShell\cspcatalogalphabet.csv" |ForEach-Object {
$skulist[$_.skupartnumber] = $_.friendlyname
}
And then iterate over $skudata like this (notice there's no need for a for(;;) loop anymore, we don't need to care about array alignment!):
foreach($skuEntry in $sku){
[pscustomobject]#{
Subscription = if($skuList.ContainsKey($skuEntry.SKUPartNumber){$skuList[$skuEntry.SKUPartNumber]}else{$skuEntry.SKUPartNumber})
Active = $skuEntry.ActiveUnits
Suspended = $skuEntry.SuspendedUnits
Assigned = $skuEntry.ConsumedUnits
}
}
Below is the code I'm using get data from the output of two commands, I then put them into two separate array's. When I combine the arrays the output looks how I would expect, but when I do select and try to output, it has gaps and not formatted correct. How get I get this to output nice to a csv file?
Example code:
$a = get-agentserver
$NewCSV = $a | ForEach-Object {
New-Object PSObject -Prop #{
'Client Name' = ($_."Name" -Split '\.(?!\d)')[0]
'Policy Type' = $_."AgentServerType"
'Backup State' = $_."BackupStatus"
'logon' = $_."LogonAccountTestStatus"
'account' = $_."LogonAccount"
}
} | Select "Client Name","Policy Type","Backup State","logon","account"
#$NewCSV
$l = foreach($i in $a.name){
get-definition -agentserver $i}
$l | convertto-csv | out-file t1.csv
$m = import-csv t1.csv | select agentserver,name,selectionsummary
$defcsv = $m | foreach-object{
new-object psobject -prop #{
'Policy Name' = $_.Name
'Backup Selection' = $_.selectionsummary
}
} | select "Policy Name","Backup Selection"
#$defcsv
$hope = $NewCSV + $defcsv
$hope2 = $hope | select "Client Name","Policy Name","Policy Type","Backup Selection"
$hope2
Ex output $hope(that look right to me)
Client Name : Name
Policy Type : Ndmp
Backup State : Unknown
logon : Succeeded
account : ndmp_user
Policy Name : Diff Bakcup
Backup Selection : COMMON, D: (Partial)
Ex output of $hope2(which is killing me how to fix)
Client Name Policy Name Policy Type Backup Selection
----------- ----------- ----------- ----------------
Name Windows
Name Windows
Name Windows
Name Windows
Name Windows
Name Ndmp
Diff Bakcup - Name ,... COMMON, D: (Partial)
ArchiveJob_Backup_to_Tape Name \e$ (Partial), ...
Diff Bakcup - Name ,... COMMON, D: (Partial)
ArchiveJob_Backup_to_Tape Name\e$ (Partial), ...
Diff Bakcup - Name ,... COMMON, D: (Partial)
Name Backup BLR_Pro... /root_vdm/IN-BLR400-FS-C...
I have cleaned up my code and tried to put my command outputs into one variable and iterate through it in one go, which looks much nicer, but the output result in the same as above in my $hope2 output. It is leaving a big gap under two of the header "Policy Name" and "Backup Selection". Is there a way to use regex to remove those particular spaces only under those two columns in Powershell?
This is the new code I am running using
$agentserver = get-agentserver
$agentserver | convertto-csv | select-object -skip 2 | out-file t2.csv
$agentserver = import-csv t2.csv -Header server,id,type,accountstate,logonaccount
$budefinition = foreach($i in $a.name){
get-backupdefinition -agentserver $i}
$budefinition | convertto-csv | out-file t1.csv
$converted_budef = import-csv t1.csv | select agentserver,name,selectionsummary
$a = $agentserver + $converted_budef
$NewCSV = $a | ForEach-Object {
New-Object PSObject -Prop #{
'Client Name' = ($_."server" -Split '\.(?!\d)')[0]
'Policy Type' = $_."type"
'Backup State' = $_."BackupStatus"
'logon' = $_."LogonAccountTestStatus"
'account' = $_."LogonAccount"
'Policy Name' = ($_.Name -replace ","," ")
'Backup Selection' = ($_.selectionsummary -replace ","," ")
}
} | Select "Client Name","Policy Name","Policy Type","Backup Selection"
$NewCSV
Example of what I am trying to accomplish would look like this, that I can then use the export-csv and have a nice csv doc.
Client Name Policy Name Policy Type Backup Selection
----------- ----------- ----------- ----------------
NAME Diff Bakup Windows Common D
NAME Archive Ndmp /root_vdm/
After doing $NewCSV | fl I get a output of two separate list as shown below and I need them to all be in one. Any ideas how to fix it in my code above?
Client Name : Name
Policy Name :
Policy Type : Ndmp
Backup Selection :
Client Name :
Policy Name : Diff Bakcup
Policy Type :
Backup Selection : COMMON D: (Partial)
I have a hashtable in PowerShell that looks like this:
$table = #{
1 = 3;
2 = 3;
5 = 6;
10 = 12;
30 = 3
}
I need to replace all "3" values with "4".
Is there a nice and clean way to do this without iterating over each pair and writing each one to a new hashtable?
Could the action with the same data be done easier if I'd use some other .NET collection class?
This throws exception that "Collection was modified":
$table.GetEnumerator() | ? {$_.Value -eq 3} | % { $table[$_.Key]=4 }
This adds another "Values" member to the object and breaks it:
$table.Values = $table.Values -replace 3,4
You can't modify the table while iterating over it, so do the iteration first and then do the updates. Just split your pipeline in two:
PS>$change = $table.GetEnumerator() | ? {$_.Value -eq 3}
PS>$change | % { $table[$_.Key]=4 }
PS>$table
Name Value
---- -----
30 4
10 12
5 6
2 4
1 4
The above answer didn't work for me and I couldn't fit this as a comment. The above single-lined answer didn't do anything. I am trying to change a single value to "Off" based on my hashtable.Key aka Name. Notice where I wrote $(backtick) is supposed to be a literal backtick, but it was messing up the code block. Here is my hashtable that is pulled from .\BeepVariables.txt.
Name Value
---- ----
$varAwareCaseSound "On"
$varEmailSound "On"
function SetHash2([string]$keyword, [string]$value){
$hash = #{}
$hash = (get-content .\BeepVariables.txt).replace(";"," $(backtick) n") | ConvertFrom-StringData
#($hash.GetEnumerator()) | ?{$_.key -like "*$keyword*"} | %{$hash[$_.value]=$value}
$hash
}
SetHash2 "aware" '"Off"'