Get parents path name and join them from a PSObject with recursivity - powershell

This is my JSON example :
{
"Logging": {
"IncludeScopes": toto,
"LogLevel": {
"Default": "test",
"System": "1234",
}
},
"App": {
"Instr": "new"
},
}
It can include a random depth.
I've used the Convertfrom-Json cmdlet to have my PSObject Variable
My objective is to create a new hashtable with joined path and ":" , and with the values name.
This is the final result needed:
Name : Value:
Logging:IncludeScopes toto
Logging:LogLevel:Default test
Logging:LogLevel:System 1234
App:Instr new
I need too use the recursivity to be compatible with all JSON example (eq: if they got more parents, more childs by parents,...).
Thanks

There is a Powershell function ConvertTo-FlatObject I found on GitHub that you may use for this.
Function ConvertTo-FlatObject {
<#
.SYNOPSIS
Flatten an object to simplify discovery of data
.DESCRIPTION
Flatten an object. This function will take an object, and flatten the properties using their full path into a single object with one layer of properties.
You can use this to flatten XML, JSON, and other arbitrary objects.
This can simplify initial exploration and discovery of data returned by APIs, interfaces, and other technologies.
NOTE:
Use tools like Get-Member, Select-Object, and Show-Object to further explore objects.
This function does not handle certain data types well. It was original designed to expand XML and JSON.
.PARAMETER InputObject
Object to flatten
.PARAMETER Exclude
Exclude any nodes in this list. Accepts wildcards.
Example:
-Exclude price, title
.PARAMETER ExcludeDefault
Exclude default properties for sub objects. True by default.
This simplifies views of many objects (e.g. XML) but may exclude data for others (e.g. if flattening a process, ProcessThread properties will be excluded)
.PARAMETER Include
Include only leaves in this list. Accepts wildcards.
Example:
-Include Author, Title
.PARAMETER Value
Include only leaves with values like these arguments. Accepts wildcards.
.PARAMETER MaxDepth
Stop recursion at this depth.
.INPUTS
Any object
.OUTPUTS
System.Management.Automation.PSCustomObject
.EXAMPLE
#Pull unanswered PowerShell questions from StackExchange, Flatten the data to date a feel for the schema
Invoke-RestMethod "https://api.stackexchange.com/2.0/questions/unanswered?order=desc&sort=activity&tagged=powershell&pagesize=10&site=stackoverflow" |
ConvertTo-FlatObject -Include Title, Link, View_Count
$object.items[0].owner.link : http://stackoverflow.com/users/1946412/julealgon
$object.items[0].view_count : 7
$object.items[0].link : http://stackoverflow.com/questions/26910789/is-it-possible-to-reuse-a-param-block-across-multiple-functions
$object.items[0].title : Is it possible to reuse a 'param' block across multiple functions?
$object.items[1].owner.link : http://stackoverflow.com/users/4248278/nitin-tyagi
$object.items[1].view_count : 8
$object.items[1].link : http://stackoverflow.com/questions/26909879/use-powershell-to-retreive-activated-features-for-sharepoint-2010
$object.items[1].title : Use powershell to retreive Activated features for sharepoint 2010
...
.EXAMPLE
#Set up some XML to work with
$object = [xml]'
<catalog>
<book id="bk101">
<author>Gambardella, Matthew</author>
<title>XML Developers Guide</title>
<genre>Computer</genre>
<price>44.95</price>
</book>
<book id="bk102">
<author>Ralls, Kim</author>
<title>Midnight Rain</title>
<genre>Fantasy</genre>
<price>5.95</price>
</book>
</catalog>'
#Call the flatten command against this XML
ConvertTo-FlatObject $object -Include Author, Title, Price
#Result is a flattened object with the full path to the node, using $object as the root.
#Only leaf properties we specified are included (author,title,price)
$object.catalog.book[0].author : Gambardella, Matthew
$object.catalog.book[0].title : XML Developers Guide
$object.catalog.book[0].price : 44.95
$object.catalog.book[1].author : Ralls, Kim
$object.catalog.book[1].title : Midnight Rain
$object.catalog.book[1].price : 5.95
#Invoking the property names should return their data if the orginal object is in $object:
$object.catalog.book[1].price
5.95
$object.catalog.book[0].title
XML Developers Guide
.EXAMPLE
#Set up some XML to work with
[xml]'<catalog>
<book id="bk101">
<author>Gambardella, Matthew</author>
<title>XML Developers Guide</title>
<genre>Computer</genre>
<price>44.95</price>
</book>
<book id="bk102">
<author>Ralls, Kim</author>
<title>Midnight Rain</title>
<genre>Fantasy</genre>
<price>5.95</price>
</book>
</catalog>' |
ConvertTo-FlatObject -exclude price, title, id
Result is a flattened object with the full path to the node, using XML as the root. Price and title are excluded.
$Object.catalog : catalog
$Object.catalog.book : {book, book}
$object.catalog.book[0].author : Gambardella, Matthew
$object.catalog.book[0].genre : Computer
$object.catalog.book[1].author : Ralls, Kim
$object.catalog.book[1].genre : Fantasy
.EXAMPLE
#Set up some XML to work with
[xml]'<catalog>
<book id="bk101">
<author>Gambardella, Matthew</author>
<title>XML Developers Guide</title>
<genre>Computer</genre>
<price>44.95</price>
</book>
<book id="bk102">
<author>Ralls, Kim</author>
<title>Midnight Rain</title>
<genre>Fantasy</genre>
<price>5.95</price>
</book>
</catalog>' |
ConvertTo-FlatObject -Value XML*, Fantasy
Result is a flattened object filtered by leaves that matched XML* or Fantasy
$Object.catalog.book[0].title : XML Developers Guide
$Object.catalog.book[1].genre : Fantasy
.EXAMPLE
#Get a single process with all props, flatten this object. Don't exclude default properties
Get-Process | select -first 1 -skip 10 -Property * | ConvertTo-FlatObject -ExcludeDefault $false
#NOTE - There will likely be bugs for certain complex objects like this.
For example, $Object.StartInfo.Verbs.SyncRoot.SyncRoot... will loop until we hit MaxDepth. (Note: SyncRoot is now addressed individually)
.NOTES
I have trouble with algorithms. If you have a better way to handle this, please let me know!
.FUNCTIONALITY
General Command
#>
[cmdletbinding()]
param(
[parameter( Mandatory = $True,
ValueFromPipeline = $True)]
[PSObject[]]$InputObject,
[string[]]$Exclude = "",
[bool]$ExcludeDefault = $True,
[string[]]$Include = $null,
[string[]]$Value = $null,
[int]$MaxDepth = 10
)
Begin
{
#region FUNCTIONS
#Before adding a property, verify that it matches a Like comparison to strings in $Include...
Function IsIn-Include {
param($prop)
if(-not $Include) {$True}
else {
foreach($Inc in $Include)
{
if($Prop -like $Inc)
{
$True
}
}
}
}
#Before adding a value, verify that it matches a Like comparison to strings in $Value...
Function IsIn-Value {
param($val)
if(-not $Value) {$True}
else {
foreach($string in $Value)
{
if($val -like $string)
{
$True
}
}
}
}
Function Get-Exclude {
[cmdletbinding()]
param($obj)
#Exclude default props if specified, and anything the user specified. Thanks to Jaykul for the hint on [type]!
if($ExcludeDefault)
{
Try
{
$DefaultTypeProps = #( $obj.gettype().GetProperties() | Select -ExpandProperty Name -ErrorAction Stop )
if($DefaultTypeProps.count -gt 0)
{
Write-Verbose "Excluding default properties for $($obj.gettype().Fullname):`n$($DefaultTypeProps | Out-String)"
}
}
Catch
{
Write-Verbose "Failed to extract properties from $($obj.gettype().Fullname): $_"
$DefaultTypeProps = #()
}
}
#( $Exclude + $DefaultTypeProps ) | Select -Unique
}
#Function to recurse the Object, add properties to object
Function Recurse-Object {
[cmdletbinding()]
param(
$Object,
[string[]]$path = '$Object',
[psobject]$Output,
$depth = 0
)
# Handle initial call
Write-Verbose "Working in path $Path at depth $depth"
Write-Debug "Recurse Object called with PSBoundParameters:`n$($PSBoundParameters | Out-String)"
$Depth++
#Exclude default props if specified, and anything the user specified.
$ExcludeProps = #( Get-Exclude $object )
#Get the children we care about, and their names
$Children = $object.psobject.properties | Where {$ExcludeProps -notcontains $_.Name }
Write-Debug "Working on properties:`n$($Children | select -ExpandProperty Name | Out-String)"
#Loop through the children properties.
foreach($Child in #($Children))
{
$ChildName = $Child.Name
$ChildValue = $Child.Value
Write-Debug "Working on property $ChildName with value $($ChildValue | Out-String)"
# Handle special characters...
if($ChildName -match '[^a-zA-Z0-9_]')
{
$FriendlyChildName = "'$ChildName'"
}
else
{
$FriendlyChildName = $ChildName
}
#Add the property.
if((IsIn-Include $ChildName) -and (IsIn-Value $ChildValue) -and $Depth -le $MaxDepth)
{
$ThisPath = #( $Path + $FriendlyChildName ) -join "."
$Output | Add-Member -MemberType NoteProperty -Name $ThisPath -Value $ChildValue
Write-Verbose "Adding member '$ThisPath'"
}
#Handle null...
if($ChildValue -eq $null)
{
Write-Verbose "Skipping NULL $ChildName"
continue
}
#Handle evil looping. Will likely need to expand this. Any thoughts on a better approach?
if(
(
$ChildValue.GetType() -eq $Object.GetType() -and
$ChildValue -is [datetime]
) -or
(
$ChildName -eq "SyncRoot" -and
-not $ChildValue
)
)
{
Write-Verbose "Skipping $ChildName with type $($ChildValue.GetType().fullname)"
continue
}
#Check for arrays by checking object type (this is a fix for arrays with 1 object) otherwise check the count of objects
if (($ChildValue.GetType()).basetype.Name -eq "Array") {
$IsArray = $true
}
else {
$IsArray = #($ChildValue).count -gt 1
}
$count = 0
#Set up the path to this node and the data...
$CurrentPath = #( $Path + $FriendlyChildName ) -join "."
#Exclude default props if specified, and anything the user specified.
$ExcludeProps = #( Get-Exclude $ChildValue )
#Get the children's children we care about, and their names. Also look for signs of a hashtable like type
$ChildrensChildren = $ChildValue.psobject.properties | Where {$ExcludeProps -notcontains $_.Name }
$HashKeys = if($ChildValue.Keys -notlike $null -and $ChildValue.Values)
{
$ChildValue.Keys
}
else
{
$null
}
Write-Debug "Found children's children $($ChildrensChildren | select -ExpandProperty Name | Out-String)"
#If we aren't at max depth or a leaf...
if(
(#($ChildrensChildren).count -ne 0 -or $HashKeys) -and
$Depth -lt $MaxDepth
)
{
#This handles hashtables. But it won't recurse...
if($HashKeys)
{
Write-Verbose "Working on hashtable $CurrentPath"
foreach($key in $HashKeys)
{
Write-Verbose "Adding value from hashtable $CurrentPath['$key']"
$Output | Add-Member -MemberType NoteProperty -name "$CurrentPath['$key']" -value $ChildValue["$key"]
$Output = Recurse-Object -Object $ChildValue["$key"] -Path "$CurrentPath['$key']" -Output $Output -depth $depth
}
}
#Sub children? Recurse!
else
{
if($IsArray)
{
foreach($item in #($ChildValue))
{
Write-Verbose "Recursing through array node '$CurrentPath'"
$Output = Recurse-Object -Object $item -Path "$CurrentPath[$count]" -Output $Output -depth $depth
$Count++
}
}
else
{
Write-Verbose "Recursing through node '$CurrentPath'"
$Output = Recurse-Object -Object $ChildValue -Path $CurrentPath -Output $Output -depth $depth
}
}
}
}
$Output
}
#endregion FUNCTIONS
}
Process
{
Foreach($Object in $InputObject)
{
#Flatten the XML and write it to the pipeline
Recurse-Object -Object $Object -Output $( New-Object -TypeName PSObject )
}
}
}
For your purposes it can be used like this:
$json = #"
{
"Logging": {
"IncludeScopes": "toto",
"LogLevel": {
"Default": "test",
"System": "1234"
}
},
"App": {
"Instr": "new"
}
}
"#
$object = ConvertTo-FlatObject -InputObject ($json | ConvertFrom-Json) -MaxDepth 100
$hash = [Ordered]#{}
foreach ($item in $object.PSObject.Properties) {
if (!($item.Value -is [PSCustomObject])) {
$name = ($item.Name -replace '^\$Object.','' -replace '\.', ':')
$hash[$name] = $item.Value
}
}
$hash | Format-Table -AutoSize
and the output will be:
Name Value
---- -----
Logging:IncludeScopes toto
Logging:LogLevel:Default test
Logging:LogLevel:System 1234
App:Instr new

Related

Powershell.I want to add a object to my array $computers but it said that the index is out of bounds

I want to add a object to my array $computers
but it said that the index is out of bounds
function ajouterperipherique ($id, $emplacement, $type) {
$computers = import-csv -path "C:\Temp\Peripherique.csv"
echo $computers
$newObject = [pscustomobject]#{
idObject = $id
EmplacementObject = $emplacement
TypeObject=$type
}
for ($i = 0; $i -lt $computers.Count; $i++) {
if ($i +1 -eq $computers.count) {
$computers[$computers.count+1]=$newObject
}
}
Write-Host ($newObject | Format-List | Out-String)
}
ajouterperipherique "GLADIATOR" "ordinateur" "Statique"
Here is the solution proposed by Lee_Dailey:
$csvPath='C:\Temp\Peripherique.csv'
function ajouterperipherique {
param(
[string]$ID,
[string]$Emplacement,
[string]$Type,
[string]$Path
)
if(-not(Test-Path $Path) -or -not [IO.Path]::GetExtension($Path) -eq '.csv')
{
throw 'File doest not exist or is not a Csv...'
}
[pscustomobject]#{
Identifiant = $id
Type = $type
Emplacement = $emplacement
}|Export-Csv $Path -NoTypeInformation -Append
}
ajouterperipherique -ID "GLADIATOR" -Emplacement "ordinateur" -Type "Statique" -Path $csvPath
A few tips, as pointed out in comments, you shouldn't really use or you should try to avoid Write-Host whenever possible.
You shouldn't really hardcode paths inside your functions, since they're meant to be re-used, hardcoding information you know can change in the future is never a good idea.
You might also wanna consider setting your parameters as Mandatory, parameters are somewhat important in Powershell and can make your life easier. I recommend reading this article if you're thinking of creating more functions in the future: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-7.1

SharePoint PowerShell list filter

hoping someone can guide on the code below:
*$CreatedDate = (Get-Date $CreatedDateTime)
$AddedDateTime = (Get-Date $CreatedDate.AddMinutes(190))
Write-Host "CD:" $CreatedDate
Write-Host "AD:" $AddedDateTime
$ItemsColl = $ReviewList.Items | where {($_['Created'] -ge $CreatedDate) -and ($_['Created'] -le $AddedDateTime)}*
this is suppose to return everything within a date range, i pass on created date, add 190 minutes to that date, and the return should be everything from a workflow task list that was created as a task.
this does bring some but is missing most of the items in the list, i was hoping for an opinion to see if there is any obvious mistakes or maybe a new way of doing this.
Thanks in advance
ok so here is the scenario, when someone sends a document for approval, this creates a new version of document (uk Time), then a task is created by system account (Swiss Time +1hr from uk). I have a function that exports all the metadata for each version of the document into a csv format. where i'm struggling is filtering the reviewer list where all the tasks for each reviewer are stored.
i have two functions in my script GetMetadata and GetReviews.
Get metadata gets all metadata for each versions of the document, where a status changes to review in progress it suggests that the document has gone for a review so therefore we have to look into reviews list to find this item and see who the review was and what the outcome is, however the document could have been reviews multiple times and therefore i need to pass in document name, and datetime of created to filter the list, but date and time of created in review is 1hr and so ahead.
Script:
# ******* Variables Section ******************
#Define these variables
$WebURL="http://site"
$ModuleList = "Library With Versions"
$ReportFile = "D:\Migration\Test\CSV\ItemsForum.csv"
$ReviewList = "Review Tasks"
# *********************************************
#delete file if exists
If (Test-Path $ReportFile)
{
Remove-Item $ReportFile
}
#Get the Web and List
$Web = Get-SPWeb $WebURL
$ReviewList = $web.Lists.TryGetList($ReviewList)
#Get reviewers from review list
function GetReviews($DocumentName, $CreatedDateTime)
{
$CreatedDate = Get-Date $CreatedDateTime
#add 80 minutes, this would be plus one hr to swiss time plus account for time it takes to create item
$AddedCreatedList = Get-Date $CreatedDate.AddMinutes(80)
#filter and get items where document name matches workflow related item (this works but it's missing a lot of items as er image)
$ItemsColl = $ReviewList.Items | where {$_['WorkflowLink'] -match $DocumentName -and $_['Created'] -ge $CreatedDate -and $_['Created'] -le $AddedCreatedList}
#$ItemsColl = $ReviewList.Items
Write-Host "Item Count: " $ItemsColl.Count
$itemIds = ""
if($ItemsColl.Count -gt 0)
{
foreach ($item in $ItemsColl)
{
Write-Host $relatedItemUrl
#$relatedItem = $relatedItemUrl -split ','
#$rit = $relatedItem[1].Trim()
$itemIds += $item.id
$itemIds += ";"
}
Write-Host $itemIds
$itemIds = $itemIds.TrimEnd(";")
}
return $itemIds
}
function GetMetadata{
$List = $web.Lists.TryGetList($ModuleList)
#Check if list exists
if($List -ne $null)
{
#Get all list items
$ItemsColl = $List.Items | where {$_['Module'] -match $ModuleName}
#add headings
Add-Content -Path $ReportFile -Value "Item ID, Status, File Name + Version, Created, Review ID"
#Loop through each item
foreach ($item in $ItemsColl)
{
if ($item['Module'])
{
#Write-Host $item['Module']
ForEach($version in $item.Versions)
{
#Add version label to file in format: [Filename]_v[version#].[extension]
$filesplit = $version['Name'].split(".")
$fullname = $filesplit[0]
$fileext = $filesplit[1]
#$FullFileName = $fullname+"_v"+$version.VersionLabel+"."+$fileext
$FullFileName ="V_"+$version.VersionLabel+" " + $version['Name']
#Get Modified By
$user = $version["Editor"]
$userObj = New-Object Microsoft.SharePoint.SPFieldUserValue($web, $user)
$modifiedBy = $userObj.User.DisplayName
#get and format modified date
$ModifiedDate = ($version['Modified'] -as [datetime]).DateTime
$ModifiedDateF = Get-Date $ModifiedDate -format "yyyy-MM-ddTHH:mm:ss.fffZ"
#Write-Host $ModifiedDateF
#get and format created date
$CreatedDate = ($version.Created -as [datetime]).DateTime
$CreatedDateF = Get-Date $CreatedDate -format "yyyy-MM-ddTHH:mm:ss.fffZ"
$ReviwerID = ""
#get reviewers
#if status matches an item get review list id
if($version['WFStatus'] -eq 'Review in progress'){
#pass document name and created
$ReviwerID = GetReviews $version['Name'].TrimEnd('.docx') $version.Created
}
#add data
$VersionData = "$($item.id),$($version['WFStatus']),$($FullFileName),$($CreatedDateF),$($ReviwerID)"
#Write to report
Add-Content -Path $ReportFile -Value $VersionData
}
}
}
}
}
GetMetadata
Write-Host "Version history has been exported successfully!"
Output:
Ouput
Sample test script by SharePoint PowerShell, if your issue exists, provide more details for your script.
if ((Get-PSSnapin "Microsoft.SharePoint.PowerShell" -ErrorAction SilentlyContinue) -eq $null) {
Add-PSSnapin "Microsoft.SharePoint.PowerShell"
}
$sourceWebURL = "http://sp:12001"
$sourceListName = "TestList"
$CreatedDate = Get-Date
$AddedDateTime = Get-Date $CreatedDate.AddMinutes(190)
$spSourceWeb = Get-SPWeb $sourceWebURL
$ReviewList = $spSourceWeb.Lists[$sourceListName]
$ItemsColl = $ReviewList.Items | where {($_['Created'] -ge $CreatedDate.AddMinutes(-10)) -and ($_['Created'] -le $AddedDateTime)}
Write-Host "CD:" $CreatedDate.AddMinutes(-10)
Write-Host "AD:" $AddedDateTime
$ItemsColl | ForEach-Object {
Write-Host $_['ID']
Write-Host $_['Title']
}
Update:
You could use PowerGUI editor to debug your script, the tool would help you find out most of logic issue.

How do I preserve Enums as 'Strings' in ConvertTo-Json cmdlet? [duplicate]

For "Get-Msoldomain" powershell command-let I get the below output (lets call it Output#1) where Name, Status and Authentication are the property names and below are their respective values.
Name Status Authentication
myemail.onmicrosoft.com Verified Managed
When I use the command with "ConvertTo-Json" like below
GetMsolDomain |ConvertTo-Json
I get the below output (lets call it Output#2) in Json Format.
{
"ExtensionData": {
},
"Authentication": 0,
"Capabilities": 5,
"IsDefault": true,
"IsInitial": true,
"Name": "myemail.onmicrosoft.com",
"RootDomain": null,
"Status": 1,
"VerificationMethod": 1
}
However, the problem is, that if you notice the Status property in both the outputs, it's different. Same happens for VerificationMethod property. Without using the ConvertTo-JSon Powershell gives the Text, and with using ConvertTo-Json it gives the integer.
When I give the below command
get-msoldomain |Select-object #{Name='Status';Expression={"$($_.Status)"}}|ConvertTo-json
I get the output as
{
"Status": "Verified"
}
However, I want something so that I don't have to specify any specific property name for it to be converted , the way I am specifying above as
Select-object #{Name='Status';Expression={"$($_.Status)"}}
This line is transforming only the Status Property and not the VerificationMethod property because that is what I am providing as input .
Question: Is there something generic that I can give to the "ConvertTo-Json" commandlet, so that It returns ALL the Enum properties as Texts and not Integers, without explicitly naming them, so that I get something like below as the output:
{
"ExtensionData": {
},
"Authentication": 0,
"Capabilities": 5,
"IsDefault": true,
"IsInitial": true,
"Name": "myemail.onmicrosoft.com",
"RootDomain": null,
"Status": "Verified",
"VerificationMethod": "DnsRecord"
}
Well, if you don't mind to take a little trip :) you can convert it to CSV which will force the string output, then re-convert it back from CSV to PS Object, then finally back to Json.
Like this:
Get-MsolDomain | ConvertTo-Csv | ConvertFrom-Csv | ConvertTo-Json
If you need to keep the original Types instead of converting it all to string see mklement0 helpful answer...
PowerShell Core (PowerShell versions 6 and above) offers a simple solution via ConvertTo-Json's -EnumsAsStrings switch.
GetMsolDomain | ConvertTo-Json -EnumsAsStrings # PS *Core* (v6+) only
Unfortunately, this switch isn't supported in Windows PowerShell.
Avshalom's answer provides a quick workaround that comes with a big caveat, however: All property values are invariably converted to strings in the process, which is generally undesirable (e.g., the Authentication property's numeric value of 0 would turn into string '0').
Here's a more generic workaround based on a filter function that recursively introspects the input objects and outputs ordered hashtables that reflect the input properties with enumeration values converted to strings and all other values passed through, which you can then pass to ConvertTo-Json:
Filter ConvertTo-EnumsAsStrings ([int] $Depth = 2, [int] $CurrDepth = 0) {
if ($_ -is [enum]) { # enum value -> convert to symbolic name as string
$_.ToString()
} elseif ($null -eq $_ -or $_.GetType().IsPrimitive -or $_ -is [string] -or $_ -is [decimal] -or $_ -is [datetime] -or $_ -is [datetimeoffset]) {
$_
} elseif ($_ -is [Collections.IEnumerable] -and $_ -isnot [Collections.IDictionary]) { # enumerable (other than a dictionary)
, ($_ | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth+1))
} else { # non-primitive type or dictionary (hashtable) -> recurse on properties / entries
if ($CurrDepth -gt $Depth) { # depth exceeded -> return .ToString() representation
Write-Warning "Recursion depth $Depth exceeded - reverting to .ToString() representations."
"$_"
} else {
$oht = [ordered] #{}
foreach ($prop in $(if ($_ -is [Collections.IDictionary]) { $_.GetEnumerator() } else { $_.psobject.properties })) {
if ($prop.Value -is [Collections.IEnumerable] -and $prop.Value -isnot [Collections.IDictionary] -and $prop.Value -isnot [string]) {
$oht[$prop.Name] = #($prop.Value | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth+1))
} else {
$oht[$prop.Name] = $prop.Value | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth+1)
}
}
$oht
}
}
}
Caveat: As with ConvertTo-Json, the recursion depth (-Depth) is limited to 2 by default, to prevent infinite recursion / excessively large output (as you would get with types such as [System.IO.FileInfo] via Get-ChildItem, for instance). Similarly, values that exceed the implied or specified depth are represented by their .ToString() value. Use -Depth explicitly to control the recursion depth.
Example call:
PS> [pscustomobject] #{ p1 = [platformId]::Unix; p2 = 'hi'; p3 = 1; p4 = $true } |
ConvertTo-EnumsAsStrings -Depth 2 |
ConvertTo-Json
{
"p1": "Unix", # Enum value [platformId]::Unix represented as string.
"p2": "hi", # Other types of values were left as-is.
"p3": 1,
"p4": true
}
Note: -Depth 2 isn't necessary here, given that 2 is the default value (and given that the input has depth 0), but it is shown here as a reminder that you may want to control it explicitly.
If you want to implement custom representations for additional types, such as [datetime], [datetimoffset] (using the ISO 8601-compatible .NET round-trip date-time string format, o, as PowerShell (Core) v6+ automatically does), as well as [timespan], [version], [guid] and [ipaddress], see Brett's helpful variation of this answer.
I needed to serialize pwsh objects to JSON, and was not able to use the -EnumsAsStrings parameter of ConvertTo-Json, as my code is running on psv5. As I encountered infinite loops while using #mklement0's code Editor's note: since fixed., I rewrote it. My revised code also deals with the serialization of some other types such as dates, serializing them into the ISO 8601 format, which is generally the accepted way to represent dates in JSON. Feel free to use this, and let me know if you encounter any issues.
Filter ConvertTo-EnumsAsStrings ([int] $Depth = 10, [int] $CurrDepth = 0) {
if ($CurrDepth -gt $Depth) {
Write-Error "Recursion exceeded depth limit of $Depth"
return $null
}
Switch ($_) {
{ $_ -is [enum] -or $_ -is [version] -or $_ -is [IPAddress] -or $_ -is [Guid] } {
$_.ToString()
}
{ $_ -is [datetimeoffset] } {
$_.UtcDateTime.ToString('o')
}
{ $_ -is [datetime] } {
$_.ToUniversalTime().ToString('o')
}
{ $_ -is [timespan] } {
$_.TotalSeconds
}
{ $null -eq $_ -or $_.GetType().IsPrimitive -or $_ -is [string] -or $_ -is [decimal] } {
$_
}
{ $_ -is [hashtable] } {
$ht = [ordered]#{}
$_.GetEnumerator() | ForEach-Object {
$ht[$_.Key] = ($_.Value | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth + 1))
}
if ($ht.Keys.Count) {
$ht
}
}
{ $_ -is [pscustomobject] } {
$ht = [ordered]#{}
$_.PSObject.Properties | ForEach-Object {
if ($_.MemberType -eq 'NoteProperty') {
Switch ($_) {
{ $_.Value -is [array] -and $_.Value.Count -eq 0 } {
$ht[$_.Name] = #()
}
{ $_.Value -is [hashtable] -and $_.Value.Keys.Count -eq 0 } {
$ht[$_.Name] = #{}
}
Default {
$ht[$_.Name] = ($_.Value | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth + 1))
}
}
}
}
if ($ht.Keys.Count) {
$ht
}
}
Default {
Write-Error "Type not supported: $($_.GetType().ToString())"
}
}
}

PowerShell Is there a way to get proper order of properties in Select-Object so that each object has order?

Following code lists all AD users.
$Domain.DomainUsersFullList = Get-ADUser -Server $Domain -ResultPageSize 500000 -Filter * -Properties *, "msDS-UserPasswordExpiryTimeComputed" | Select * -ExcludeProperty *Certificate, PropertyNames, *Properties, PropertyCount, Certificates
$($Domain.DomainUsersFullList[0]).PSObject.Properties.Name[12]
$($Domain.DomainUsersFullList[10]).PSObject.Properties.Name[12]
It seems that order returned by PSObject.Properties.Name can be different. Is there a way to order properties without exclusively telling Select-Object the order you want them in?
Just for the sake of why I need this:
https://github.com/EvotecIT/PSWriteWord - I wrote a function Add-WordTable that takes any $Object and puts this into Word document. No need to parse objects yourself. Just pass it to function and it will be put into Word document.
I am now working on same thing for:
https://github.com/EvotecIT/PSWriteExcel - which has function Add-ExcelWorksheetData that does exactly the same as above with one exception .. since it's Excel you don't have column limit. So with 100 columns I was/am getting wrong order per each row. Which makes no sense.
While in case of WORD document I didn't notice this issue because I never added more then 10 columns, with Excel and 100 columns I was getting wrong order. Below is an example of this:
Here is the method that does the conversion:
function Format-PSTableConvertType2 {
[CmdletBinding()]
param(
$Object,
[switch] $SkipTitles,
[string[]] $ExcludeProperty,
[switch] $NoAliasOrScriptProperties,
[switch] $DisplayPropertySet
)
[int] $Run = 0
$Array = New-ArrayList
$Titles = New-ArrayList
if ($NoAliasOrScriptProperties) {$PropertyType = 'AliasProperty', 'ScriptProperty' } else {$PropertyType = ''}
Write-Verbose "Format-PSTableConvertType2 - Option 2 - NoAliasOrScriptProperties: $NoAliasOrScriptProperties"
foreach ($O in $Object) {
$ArrayValues = New-ArrayList
if ($DisplayPropertySet -and $O.psStandardmembers.DefaultDisplayPropertySet.ReferencedPropertyNames) {
$ObjectProperties = $O.psStandardmembers.DefaultDisplayPropertySet.ReferencedPropertyNames.Where( { $ExcludeProperty -notcontains $_ } ) #.Name
} else {
$ObjectProperties = $O.PSObject.Properties.Where( { $PropertyType -notcontains $_.MemberType -and $ExcludeProperty -notcontains $_.Name } ).Name
}
#$ObjectProperties = $O.PSObject.Properties
foreach ($Name in $ObjectProperties) {
if ($Run -eq 0 -and -not $SkipTitle) { Add-ToArray -List $Titles -Element $Name }
Add-ToArray -List $ArrayValues -Element $O.$Name
}
if ($Run -eq 0 -and -not $SkipTitle) {Add-ToArray -List ($Array) -Element $Titles }
Add-ToArray -List $Array -Element $ArrayValues
$Run++
}
return , $Array
}
It essentially converts object into Array of Arrays. Which then makes it trivial to just loop thru rows / columns.
Now it's important that while generally I could probably make Get-AdUser display only values I want in proper order I am working on general use modules (PSWriteWord/PSWriteExcel) and I want people to pass any object to it and not have to care about it too much.
Unless anyone has a better option:
$SpecialData = $Domain.DomainUsersFullList | Select-Object $($Domain.DomainUsersFullList[0]).PSObject.Properties.Name
$($Domain.DomainUsersFullList[0]).PSObject.Properties.Name[12]
$($Domain.DomainUsersFullList[10]).PSObject.Properties.Name[12]
$($SpecialData[0] | Select-Object $($Domain.DomainUsersFullList[0]).PSObject.Properties.Name).PSObject.Properties.Name[12]
$($SpecialData[10] | Select-Object $($Domain.DomainUsersFullList[0]).PSObject.Properties.Name).PSObject.Properties.Name[12]
Basically what this does is copy the order of 1st element and applies same order to each and every new line. This ensures that each object will return properties in same order as 1st element.
Final implementation:
function Format-PSTableConvertType2 {
[CmdletBinding()]
param(
$Object,
[switch] $SkipTitles,
[string[]] $ExcludeProperty,
[switch] $NoAliasOrScriptProperties,
[switch] $DisplayPropertySet,
$OverwriteHeaders
)
#[int] $Run = 0
$Array = New-ArrayList
$Titles = New-ArrayList
if ($NoAliasOrScriptProperties) {$PropertyType = 'AliasProperty', 'ScriptProperty' } else {$PropertyType = ''}
Write-Verbose "Format-PSTableConvertType2 - Option 2 - NoAliasOrScriptProperties: $NoAliasOrScriptProperties"
# Get Titles first (to make sure order is correct for all rows)
if ($OverwriteHeaders) {
$Titles = $OverwriteHeaders
} else {
foreach ($O in $Object) {
if ($DisplayPropertySet -and $O.psStandardmembers.DefaultDisplayPropertySet.ReferencedPropertyNames) {
$ObjectProperties = $O.psStandardmembers.DefaultDisplayPropertySet.ReferencedPropertyNames.Where( { $ExcludeProperty -notcontains $_ } ) #.Name
} else {
$ObjectProperties = $O.PSObject.Properties.Where( { $PropertyType -notcontains $_.MemberType -and $ExcludeProperty -notcontains $_.Name } ).Name
}
foreach ($Name in $ObjectProperties) {
Add-ToArray -List $Titles -Element $Name
}
break
}
# Add Titles to Array (if not -SkipTitles)
if (-not $SkipTitle) {
Add-ToArray -List $Array -Element $Titles
}
}
# Extract data (based on Title)
foreach ($O in $Object) {
$ArrayValues = New-ArrayList
foreach ($Name in $Titles) {
Add-ToArray -List $ArrayValues -Element $O.$Name
}
Add-ToArray -List $Array -Element $ArrayValues
}
return , $Array
}

ConvertTo-CSV -UseCulture ignores current thread's culture

Question
Is it possible to force PowerShell to export to CSV in French format when run in a Windows Session with en-GB culture?
More Info
I'm hoping to export some data to CSV using the French culture rules (i.e. CSV's delimiter set to semicolon, but also with numbers using commas for decimal places, and other cultural formatting differences; so just using the -Delimiter parameter is not sufficient).
I came up with the below code (based on https://stackoverflow.com/a/7052955/361842)
function Set-Culture
{
[CmdletBinding(DefaultParameterSetName='ByCode')]
param (
[Parameter(Mandatory,ParameterSetName='ByCode',Position=1)]
[string] $CultureCode
,
[Parameter(Mandatory,ParameterSetName='ByCulture',Position=1)]
[System.Globalization.CultureInfo] $Culture
)
begin {
[System.Globalization.CultureInfo] $Culture = [System.Globalization.CultureInfo]::GetCultureInfo($CultureCode)
}
process {
[System.Threading.Thread]::CurrentThread.CurrentUICulture = $Culture
[System.Threading.Thread]::CurrentThread.CurrentCulture = $Culture
}
}
function Invoke-CommandInCulture {
[CmdletBinding()]
param (
[Parameter(Mandatory,ParameterSetName='ByCode',Position=1)]
[string]$CultureCode
,
[Parameter(Mandatory,Position=2)]
[ScriptBlock]$Code
)
process {
$OriginalCulture = Get-Culture
try
{
Set-Culture $CultureCode
Write-Verbose (Get-Culture) #this always returns en-GB
Invoke-Command -ScriptBlock $Code
}
finally
{
Set-Culture $OriginalCulture
}
}
}
The following code implies that this method works:
Invoke-CommandInCulture -CultureCode 'fr' -Code {
[System.Threading.Thread]::CurrentThread.CurrentUICulture
[System.Threading.Thread]::CurrentThread.CurrentCulture
} #shows that the command's thread's culture is French
Invoke-CommandInCulture -CultureCode 'fr' -Code {
get-date
} #returns the current date in French
However PowerShell has it's own idea of what's going on
Invoke-CommandInCulture -CultureCode 'fr' -Code {
get-culture
"PSCulture: $PSCulture"
"PSUICulture: $PSUICulture"
} #returns my default (en-GB) culture; not the thread's culture
And this impacts the logic for converting to CSV:
Invoke-CommandInCulture -CultureCode 'fr' -Code {
get-process | ConvertTo-CSV -UseCulture
} #again, uses my default culture's formatting rules; not the FR ones
Workaround #1: Custom Function to Convert Values to Strings in Given Culture
Here's a workaround solution; converting each field to a string using the given culture, then converting the string values to a CSV:
function ConvertTo-SpecifiedCulture {
[CmdletBinding()]
param (
[Parameter(Mandatory,ValueFromPipeline)]
[PSObject]$InputObject
,
[Parameter(Mandatory)]
[string]$CultureCode
,
[Parameter(ParameterSetName='DefaultParameter', Position=0)]
[System.Object[]]$Property
)
begin {
[System.Globalization.CultureInfo] $Culture = [System.Globalization.CultureInfo]::GetCultureInfo($CultureCode)
}
process {
if($Property -eq $null) {$Property = $InputObject.PSStandardMembers.DefaultDisplayPropertySet.ReferencedPropertyNames}
$Result = new-object -TypeName PSObject -Property #{}
$Property | %{
$Result | Add-Member -MemberType NoteProperty -Name $_ -Value ($InputObject."$_").ToString($Culture)
}
$Result
}
}
Get-Process | select -first 2 | ConvertTo-SpecifiedCulture -CultureCode 'fr' | ConvertTo-CSV -Delimiter ';'
Workaround #2: Override Current Culture to Match Target Culture
Another option is to change the settings of the current culture so that it shares those of the required culture. This feels more hacky; though depending on scenario may work out cleaner/more practical than the above.
e.g. to use FR's number format we just update the current culture's number format to match FR's:
$(Get-Culture).NumberFormat = ([System.Globalization.CultureInfo]'FR').NumberFormat
...and we can do likewise for the remaining (settable) properties:
function Set-CurrentCulture {
[CmdletBinding()]
param (
[string]$CultureCode
)
begin {
$Global:FakedCurrentCulture = $CultureCode #in case we need a reference to the current culture's name
[System.Globalization.CultureInfo]$NewCulture = [System.Globalization.CultureInfo]::GetCultureInfo($CultureCode)
[System.Globalization.CultureInfo]$ReferenceToCurrentCulture = Get-Culture
Write-Verbose "Switching Defintion to $($NewCulture.Name)"
}
process {
#NB: At time of writing, the only settable properties are NumberFormatInfo & DateTimeFormatInfo
$ReferenceToCurrentCulture.psobject.properties | ?{$_.isSettable} | %{
$propertyName = $_.Name
write-verbose "Setting property $propertyName"
write-verbose "- from: $($ReferenceToCurrentCulture."$propertyName")"
write-verbose "- to: $($NewCulture."$propertyName")"
$ReferenceToCurrentCulture."$propertyName" = $NewCulture."$propertyName"
}
#ListSeparator
$ReferenceToCurrentCulture.TextInfo.psobject.properties | ?{$_.isSettable} | %{
$propertyName = $_.Name
write-verbose "Setting property TextInfo.$propertyName"
write-verbose "- from: $($ReferenceToCurrentCulture.TextInfo."$propertyName")"
write-verbose "- to: $($NewCulture.TextInfo."$propertyName")"
$ReferenceToCurrentCulture.TextInfo."$propertyName" = $NewCulture."$propertyName"
}
#for some reason this errors
#Localized, TwoDigitYearMax
<#
$ReferenceToCurrentCulture.Calendar.psobject.properties | ?{$_.isSettable} | %{
$propertyName = $_.Name
write-verbose "Setting property Calendar.$propertyName"
write-verbose "- from: $($ReferenceToCurrentCulture.Calendar."$propertyName")"
write-verbose "- to: $($NewCulture.Calendar."$propertyName")"
$ReferenceToCurrentCulture.Calendar."$propertyName" = $NewCulture."$propertyName"
}
#>
}
}
function Reset-CurrentCulture {
[CmdletBinding()]
param ()
process {
Set-CurrentCulture -CultureCode ((get-culture).Name)
}
}
function Test-It {
[CmdletBinding()]
param ()
begin {
write-verbose "Current Culture: $Global:FakedCurrentCulture"
}
process {
1..5 | %{
New-Object -TypeName PSObject -Property #{
Integer = $_
String = "Hello $_"
Numeric = 2.139 * $_
Money = (2.139 * $_).ToString('c')
Date = (Get-Date).AddDays($_)
}
} | ConvertTo-Csv -NoTypeInformation
}
}
Set-CurrentCulture 'fr' -Verbose
Test-It
Set-CurrentCulture 'en-GB' -Verbose
Test-It
Set-CurrentCulture 'en-US' -Verbose
Test-It
Set-CurrentCulture 'ar-DZ' -Verbose
Test-It
Reset-CurrentCulture -Verbose
Test-It
We could potentially go further and look at overwriting the read only properties (this is possible: https://learn-powershell.net/2016/06/27/quick-hits-writing-to-a-read-only-property/)... but this already feels very nasty; so I'm not going to go there as the above was sufficient for my needs.