ConvertTo-CSV -UseCulture ignores current thread's culture - export-to-csv

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.

Related

Collecting event logs

I want to collect all the event logs since a defined timestamp. Here there is my chunck code:
$StartTime = (Get-Date).AddMinutes(-5)
$rawdata = Get-WinEvent -ListLog *
$eventlogs = #{}
foreach ($record in $rawdata) {
if ($record.LastWriteTime -gt $StartTime) {
$eventlogs[$record.GetHashCode()] = #{
'LogType' = $record.LogType
'Name' = $record.LogName
'Provider' = $record.OwningProviderName
'Path' = $record.LogFilePath
'Mode' = $record.LogMode
'Time' = $record.LastWriteTime
}
}
}
In addition to the above info, how can I retrieve a full extended description of each event log? I would like to avoid parsing each single .evtx file
Best way to do that is to use FilterXml parameter of Get-WinEvent.
You can actually create your filter by creating a filter using Event Viewer:
After that you copy > paste that in PowerShell. Also you will need to escape single quotation marks that surround time '2021-11-23T10:00:00.000Z' should become ''2021-11-23T10:00:00.000Z'' . NB, those are 2 * ', not double quote ".
Of course if you want to pass dates via variable, you can do that by using Get-Date
$filterXml = '
<QueryList>
<Query Id="0" Path="Application">
<Select Path="Application">*[System[TimeCreated[#SystemTime>=''2021-11-23T10:00:00.000Z'' and #SystemTime<=''2021-11-23T14:03:00.999Z'']]]</Select>
</Query>
</QueryList>'
Get-WinEvent –FilterXml $filterXml
Not sure about reading extended descriptions, but you can read the event log with the following. Note logname allows wild cards, thus the *.
$time = (get-date).AddMinutes(-5)
Get-WinEvent –FilterHashtable #{logname='*'; starttime=$time}
I found a solution with a chunk of code like this one I have written:
# get events log
Write-Host -NoNewline "Retrieving events log... "
$logtime = (Get-Date).AddMinutes(-15)
$eventlogs.Clear()
$string = ''
$ErrorActionPreference= 'SilentlyContinue'
foreach ($logtype in ('System', 'Security','Application')) {
$rawdata = Get-WinEvent -FilterHashTable #{LogName=$logtype; StartTime=$logtime}
foreach ($record in $rawdata) {
$logkey = '[' + $record.LogName + ']_'
$logkey += Get-Date $record.TimeCreated -format "yyyy-MM-dd_HH-mm-ss-fff"
$record.Message -match "^(.*)\r+" > $null
if ($matches[1]) {
$string = $matches[1]
$matches[1] = $null
} else {
$record.Message -match "^(.*)\n+" > $null
if ($matches[1]) {
$string = $matches[1]
$matches[1] = $null
} else {
$string = $record.Message
}
}
$eventlogs[$logkey] = #{
'Name' = $record.LogName
'Time' = Get-Date $record.TimeCreated -format "yyyy-MM-dd_HH-mm-ss"
'Id' = $record.Id
'Message' = $string
'Type' = $record.LevelDisplayName
}
}
}
$ErrorActionPreference= 'Inquire'
$logfile = $logpath + '\' + $timestamp + '_EventLogs.csv'
'ID;LOGTYPE;NAME;TIME;MESSAGE' | Out-File $logfile -Encoding UTF8 -Append
foreach ($item in ($eventlogs.Keys | Sort-Object)) {
$new_record = #(
$eventlogs[$item].Id,
$eventlogs[$item].Type,
$eventlogs[$item].Name,
$eventlogs[$item].Time,
$eventlogs[$item].Message
)
$new_string = [system.String]::Join(";", $new_record)
$new_string | Out-File $logfile -Encoding UTF8 -Append
}
Write-Host -ForegroundColor Green 'DONE'
For convenience I will catch only the first line of the message. In order to get this I have adopted the following block:
$record.Message -match "^(.*)\r+" > $null
if ($matches[1]) {
$string = $matches[1]
$matches[1] = $null
} else {
$record.Message -match "^(.*)\n+" > $null
if ($matches[1]) {
$string = $matches[1]
$matches[1] = $null
} else {
$string = $record.Message
}
}
Such solution is due to the fact that some messages use \r\n, others only \n and still others I don't know. This last block does not satisfy me, even if it works.
Simply getting all recent logs, working within the windows api limits. For some reason it still says ProviderName in the headers.
$time = (get-date).AddMinutes(-5)
get-winevent -listlog * | % { Get-WinEvent #{logname=$_.logname;
starttime=$time} -ea 0} | ft -GroupBy logname
ProviderName: Microsoft-Windows-WMI-Activity/Operational
TimeCreated Id LevelDisplayName Message
----------- -- ---------------- -------
11/27/2021 10:52:54 AM 5857 Information Win32_WIN32_TERMINALSERVICE_Prov provider started with result c...
11/27/2021 10:52:54 AM 5857 Information CIMWin32 provider started with result code 0x0. HostProcess = w...
11/27/2021 10:51:59 AM 5857 Information StateMessageProvider provider started with result code 0x0. Hos...
% -parallel works even better in ps 7.
get-winevent -logname *
Get-WinEvent: Log count (458) is exceeded Windows Event Log API limit (256). Adjust filter to return less log names.
$time = (get-date).AddMinutes(-5)
get-winevent -listlog * | % -parallel { Get-WinEvent #{logname=$_.logname;
starttime=$using:time} -ea 0} | ft -GroupBy logname

Powershell Convert a string to datatable

I have the below string which is returned from an Invoke-RestMethod call. How can I transform the results into an object or data table to be able to use it? The first row would be the properties.
I tried the Out-File and Export-CSV first but the only length is added to the file.
Then I tried the format table which did nothing.
Any ideas?
Name,Main No, Sec No
Alex,34.6743,22.7800
Tom,33.8798,21.9098
Tim,34.6743,41.7800
Mark,33.8798,21.9098
The ConvertFrom-Csv cmdlet is smart enough to exclude the empty lines we see in your example.
To output as CSV, simply use:
$data | ConvertFrom-Csv
#output to csv file:
$data | ConvertFrom-Csv | Export-Csv -Path 'D:\test.csv' -NoTypeInformation
Your CSV file will look like this:
"Name","Main No","Sec No"
"Alex","34.6743","22.7800"
"Tom","33.8798","21.9098 "
"Tim","34.6743","41.7800"
"Mark","33.8798","21.9098"
If you want it converted to datatable, you can use below function:
function ConvertTo-DataTable {
[CmdletBinding()]
Param(
[Parameter(Position=0, Mandatory=$true, ValueFromPipeline = $true)]
[PSObject[]]$InputObject
)
Begin {
$dataTable = New-Object System.Data.DataTable
$first = $true
function _GetSafeTypeName($type) {
# internal helper function to return the correct typename for a datatable
$types = #('System.Boolean', 'System.Byte', 'System.SByte', 'System.Char', 'System.Datetime',
'System.TimeSpan', 'System.Decimal', 'System.Double', 'System.Guid', 'System.Single')
$ints = #('System.Int16', 'System.Int32', 'System.Int64')
$uints = #('System.UInt16', 'System.UInt32', 'System.UInt64')
if ($types -contains $type) { return "$type" }
# if the type is Int or UInt, always return the largest variety
if ($ints -contains $type) { return 'System.Int64' }
if ($uints -contains $type) { return 'System.UInt64' }
return 'System.String'
}
}
Process {
foreach ($object in $InputObject) {
$dataRow = $dataTable.NewRow()
foreach($property in $object.PSObject.Properties) {
# read the data type for this property and make sure it is a valid type for a DataTable
$dataType = _GetSafeTypeName $property.TypeNameOfValue
# ensure the property name does not contain invalid characters
$propertyName = $property.Name -replace '[\W\p{Pc}-[,]]', '_' -replace '_+', '_'
if ($first) {
$dataColumn = New-Object System.Data.DataColumn $propertyName, $dataType
$dataTable.Columns.Add($dataColumn)
}
if ($property.Gettype().IsArray -or ($property.TypeNameOfValue -like '*collection*')) {
$dataRow.Item($propertyName) = $property.Value | ConvertTo-XML -As String -NoTypeInformation -Depth 1
}
else {
$value = if ($null -ne $property.Value) { $property.Value } else { [System.DBNull]::Value }
$dataRow.Item($propertyName) = $value -as $dataType
}
}
$dataTable.Rows.Add($dataRow)
$first = $false
}
}
End {
Write-Output #(,($dataTable))
}
}
Then use it like this:
$data | ConvertFrom-Csv | ConvertTo-DataTable
P.S. $data is the result of your Invoke-RestMethod call.

Writing an output on a .txt file on Powershell

I found a little script to get all the local groups and members and it's working perfectly but I need to write the output on PowerShell.
Trap {"Error: $_"; Break;}
function EnumLocalGroup($LocalGroup) {
$Group = [ADSI]"WinNT://$strComputer/$LocalGroup,group"
"`r`n" + "Group: $LocalGroup"
$Members = #($Group.psbase.Invoke("Members"))
foreach ($Member In $Members) {
$Name = $Member.GetType().InvokeMember("Name", 'GetProperty', $Null, $Member, $Null)
$Name
}
}
$strComputer = gc env:computername
"Computer: $strComputer"
$computer = [adsi]"WinNT://$strComputer"
$objCount = ($computer.PSBase.Children | Measure-Object).Count
$i = 0
foreach ($adsiObj in $computer.PSBase.Children) {
switch -regex ($adsiObj.PSBase.SchemaClassName) {
"group" {
$group = $adsiObj.Name
EnumLocalGroup $group
}
}
$i++
}
I already tried this:
function EnumLocalGroup($LocalGroup) | Out-File -FilePath "E:\PS\Malik\group.txt"
But the code won't start if I do that. I also tried to use this whole Out-File line at the end of the code after the } but doesn't work either and this is the only solution I find on Internet.
If you want to incorporate logging into a function you need to put it into the function body, e.g.
function EnumLocalGroup($LocalGroup) {
....
$foo = 'something'
$foo # output returned by function
$foo | Add-Content 'log.txt' # output to log file
...
}
or
function EnumLocalGroup($LocalGroup) {
...
$foo = 'something'
$foo | Tee-Object 'log.txt' -Append # output goes to log file and StdOut
...
}
Otherwise you have to do the logging when you call the function:
EnumLocalGroup $group | Add-Content 'C:\log.txt'

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

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

Pipe output to the clipboard using PowerShell

EDIT: 23 Oct 2020
See postanote's answer.
EDIT: 14 May 2015
After 3 years, I thought I would share my ClipboardModule (I hope I am allowed to):
Add-Type -AssemblyName System.Windows.Forms
Function Get-Clipboard {
param([switch]$SplitLines)
$text = [Windows.Forms.Clipboard]::GetText();
if ($SplitLines) {
$xs = $text -split [Environment]::NewLine
if ($xs.Length -gt 1 -and -not($xs[-1])) {
$xs[0..($xs.Length - 2)]
} else {
$xs
}
} else {
$text
}
}
function Set-Clipboard {
$in = #($input)
$out =
if ($in.Length -eq 1 -and $in[0] -is [string]) { $in[0] }
else { $in | Out-String }
if ($out) {
[Windows.Forms.Clipboard]::SetText($out);
} else {
# input is nothing, therefore clear the clipboard
[Windows.Forms.Clipboard]::Clear();
}
}
function GetSet-Clipboard {
param([switch]$SplitLines, [Parameter(ValueFromPipeLine=$true)]$ObjectSet)
if ($input) {
$ObjectSet = $input;
}
if ($ObjectSet) {
$ObjectSet | Set-Clipboard
} else {
Get-Clipboard -SplitLines:$SplitLines
}
}
Set-Alias cb GetSet-Clipboard
Export-ModuleMember -Function *-* -Alias *
I usually use the cb alias (for GetSet-Clipboard) because it is two way i.e can get or set the clipboard:
cb # gets the contents of the clipboard
"john" | cb # sets the clipboard to "john"
cb -s # gets the clipboard and splits it into lines
If you have WMF 5.0, PowerShell contains two new cmdlets:
get-clipboard and set-clipboard
EDIT: Please look at question instead for solution.
Here is my solution:
Add-Type -AssemblyName 'System.Windows.Forms'
filter Set-Clipboard {
begin {
$cp = #()
}
process {
$_ | Tee-Object -Variable 'cp0'
$cp = $cp + #($cp0);
}
end {
$str = ($cp | Out-String).ToString();
[Windows.Forms.Clipboard]::Clear();
if ( ($str -ne $null) -and ($str -ne '') ) {
[Windows.Forms.Clipboard]::SetText( $str )
}
$cp = #()
}
}
This collects all the objects in an array, $cp. We use Tee-Object to redirect the current element, $_, to both the next process and to store it in the array, $cp. Lastly, once the process is finished we set the clipboard's text.
I have used it in the following way:
dir -Recurse | Set-Clipboard | Select 'Name'
And it seems to work.
To use a function instead:
function Set-Clipboard-Func {
$str = $input | Out-String
[Windows.Forms.Clipboard]::Clear();
if ( ($str -ne $null) -and ($str -ne '') ) {
[Windows.Forms.Clipboard]::SetText( $str )
}
}
Powershell version 6.1 removed this commandlet, so it is no longer built-in.
Instead, you need to install the ClipboardText package. In Powershell's console type:
Install-Module -Name ClipboardText
Then you can use:
Set-ClipboardText "hello clipboard"
Get-ClipboardText
Here is the github issue with the maintainers of Powershell redirecting you to use the ClipboardText package.
Native clip cmdlets in PSv7
$Host
# Results
<#
Name : ConsoleHost
Version : 7.0.3
InstanceId : 54be9bfd-799d-4213-a13a-22403c1d9ed8
UI : System.Management.Automation.Internal.Host.InternalHostUserInterface
CurrentCulture : en-US
CurrentUICulture : en-US
PrivateData : Microsoft.PowerShell.ConsoleHost+ConsoleColorProxy
DebuggerEnabled : True
IsRunspacePushed : False
Runspace : System.Management.Automation.Runspaces.LocalRunspace
#>
Get-Command -Name '*clip*'|Format-Table -a
# Results
<#
CommandType Name Version Source
----------- ---- ------- ------
Function Get-Clipboard 1.3.6 PowerShellCookbook
Function Set-Clipboard 1.3.6 PowerShellCookbook
Function Start-ClipboardHistoryViewer 0.0 ModuleLibrary
Cmdlet Get-Clipboard 7.0.0.0 Microsoft.PowerShell.Management
Cmdlet Set-Clipboard 7.0.0.0 Microsoft.PowerShell.Management
Cmdlet Set-UDClipboard 2.9.0 UniversalDashboard
Application clip.exe 10.0.19041.1 C:\WINDOWS\system32\clip.exe
Application ClipRenew.exe 10.0.19041.1 C:\WINDOWS\system32\ClipRenew.exe
Application ClipUp.exe 10.0.19041.488 C:\WINDOWS\system32\ClipUp.exe
Application rdpclip.exe 10.0.19041.423 C:\WINDOWS\system32\rdpclip.exe
#>
get-clipboard
skips newline characters when text is entered sequentially.
I use
[System.Windows.Forms.Clipboard]::GetText()
as before.
Now that Get-clipboard and Set-Clipboard are built in PSv7
You can have this function in your profile:
"C:\Users<USER_ID>\Documents\WindowsPowerShell\Microsoft.PowerShellISE_profile.ps1"
function To-Notepad {
param(
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]
[object]
$InputObject
)
begin { $objs = #() }
process { $objs += $InputObject }
end {
$old = Get-clipboard # store current value
$objs | out-string -width 1000 | Set-Clipboard
& "notepad2" /c
sleep -mil 500
$old | Set-Clipboard # restore the original value
}
}
And then use in this way:
dir -Path C:\Temp | To-Notepad