<# .SYNOPSIS An XML report parser and report generator for XML files created by Svendsen Tech's generic Get-WmiObject-Wrapper script. This script lets you parse the resulting XML file from Get-WmiObject-Wrapper and generate flexible reports. See the comprehensive online documentation and examples at: http://www.powershelladmin.com/Get-wmiobject_wrapper Copyright (c) 2011, Svendsen Tech. All rights reserved. Author: Joakim Svendsen .DESCRIPTION See the comprehensive online documentation and examples at: http://www.powershelladmin.com/Get-wmiobject_wrapper Returns custom PowerShell objects representing parser XML data based on the schema/format Get-WmiObject-Wrapper uses. If you specify -AsCsv, output will be text in the format of four fields like this: "","","","" .PARAMETER XmlFile Required. XML file to parse, generated by Get-WmiObject-Wrapper.ps1 .PARAMETER ComputerName Optional. Default "all". Specify computer or computers to filter on. Only computer names matching those you specify here will be displayed. .PARAMETER Class Optional. Default "all". Specify WMI class or classes to filter on. Only classes matching those names you specify here will be displayed. .PARAMETER Property Optional. Default "all". Specify property or properties to filter on. Only properties matching those names you specify here will be displayed. .PARAMETER ComputerNameNotMatch Optional. Default "". Specify computer names you want to exclude. .PARAMETER ClassNotMatch Optional. Default "". Specify classes you want to exclude. .PARAMETER PropertyNotMatch Optional. Default "". Specify properties you want to exclude, for example "Error" and "NoPing". .PARAMETER CombineComputerName Optional. Combine all entries for a computer name on one line. Each class and property will be combined as the new property name, with "Win32_" at the start of the class name omitted to save screen space. .PARAMETER AsCsv Optional. Output CSV strings instead of objects. For piping to Out-File/Set-Content. .PARAMETER CsvDelimiter Optional. Default is a comma. CSV delimiter for the output fields when using -AsCsv. .PARAMETER ValueDelimiter Optional. Default is a semicolon. Delimiter for multi-value fields. .PARAMETER NoHeaders Optional. Do not add default headers to the CSV strings that are produced when using -AsCsv. #> param( [Parameter(Mandatory=$true)][string] $XmlFile, [string[]] $ComputerName = 'all', [string[]] $Class = 'all', [string[]] $Property = 'all', [string[]] $ComputerNameNotMatch = '', [string[]] $ClassNotMatch = '', [string[]] $PropertyNotMatch = '', [switch] $CombineComputerName, [switch] $AsCsv, [string] $CsvDelimiter = ',', [string] $ValueDelimiter = ';', [switch] $NoHeaders ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Format-AsCsv { [CmdletBinding()] param([Parameter(Mandatory=$true)][AllowEmptyString()][string[]] $Strings) process { ($Strings | ForEach-Object { '"' + $_ + '"' }) -join $CsvDelimiter } } function Get-Regex { param([Parameter(Mandatory=$true)][string[]] $Strings) # Prepare regular expressions for later use in the pipeline. # I thought about looping somehow, but this actually seems # more convenient and flexible. if ($Strings[0] -ieq 'all') { # Match everything. $Regex = '.*' } else { $Regex = [regex] ('(?i)\A(?:' + ($Strings -join '|') + ')\z') } $Regex } if (-not (Test-Path -PathType Leaf -Path $XmlFile)) { Write-Error "'$XmlFile' doesn't seem to exist. Exiting with code 1." exit 1 } [regex] $ComputerNameRegex = Get-Regex $ComputerName [regex] $ClassNameRegex = Get-Regex $Class [regex] $PropertyNameRegex = Get-Regex $Property [regex] $ComputerNameNotMatchRegex = Get-Regex $ComputerNameNotMatch [regex] $ClassNotMatchRegex = Get-Regex $ClassNotMatch [regex] $PropertyNotMatchRegex = Get-Regex $PropertyNotMatch $Xml = [xml] (Get-Content $XmlFile) # Print headers when needed if ($AsCsv -and -not $NoHeaders -and -not $CombineComputerName) { Format-AsCsv 'ComputerName', 'Class', 'Property', 'Value' } if ($CombineComputerName -and ($PropertyNotMatch -notcontains 'Error' -or $PropertyNotMatch -notcontains 'NoPing')) { if ($Host.Version.Major -lt 4) { Write-Warning "-CombineComputerName specified, but -PropertyNotMatch is missing either 'Error' or 'NoPing', or both are missing. This is not recommended with PowerShell versions prior to v4." } } # For use with -CombineComputerName $FoundComputers = @{} # Some code duplication to make the script faster by not having to check $AsCsv twice for each property if ($AsCsv) { $Xml.computers.computer | Where-Object { $_.name -imatch $ComputerNameRegex -and $( if ($ComputerNameNotMatch[0] -ine '') { $_.name -inotmatch $ComputerNameNotMatchRegex } else { $true } ) } | Sort-Object Name | ForEach-Object { $ComputerName = $_.name $_.class | Where-Object { $_.name -imatch $ClassNameRegex -and $( if ($ClassNotMatch[0] -ine '') { $_.name -inotmatch $ClassNotMatchRegex } else { $true } )} | Sort-Object Name | ForEach-Object { $ClassName = $_.name $_.classentries | ForEach-Object { $_.classentry | Where-Object { $_.property -imatch $PropertyNameRegex -and $( if ($PropertyNotMatch[0] -ine '') { $_.property -inotmatch $PropertyNotMatchRegex } else { $true } )} | Sort-Object Property | ForEach-Object { if ($CombineComputerName) { # First time seeing a computer. if (-not $FoundComputers.ContainsKey($ComputerName)) { $FoundComputers.$ComputerName = New-Object psobject -Property @{ ComputerName = $ComputerName (($ClassName -replace '^Win32_') + '-' + $_.Property) = (($_.Values | ForEach-Object { $_.Value }) -join $ValueDelimiter) } } else { Add-Member -InputObject $FoundComputers.$ComputerName -MemberType NoteProperty -Name (($ClassName -replace '^Win32_') + '-' + $_.Property) -Value (($_.Values | ForEach-Object { $_.Value }) -join $ValueDelimiter) } } else { Format-AsCsv $ComputerName, $ClassName, $_.Property, (($_.Values | ForEach-Object { $_.Value }) -join $ValueDelimiter) } } } } } # | %{ if ($AsCsv) { $_ } else { $_ | Select-Object Computer, Class, Property, Value } } if ($CombineComputerName) { $Ctr = 0 # Get all headers. Inefficient, but unavoidable? $HeaderHash = @{} foreach ($Obj in $FoundComputers.Values) { $Obj | Get-Member -MemberType NoteProperty | Where-Object { $_.Name -ine 'ComputerName' } | Select-Object -ExpandProperty Name | ForEach-Object { if (-not $HeaderHash.ContainsKey($_)) { $HeaderHash.$_ = 1 } } } foreach ($Obj in $FoundComputers.Values) { $Headers = @($HeaderHash.Keys | Where-Object { $_ -ine 'ComputerName' }) #| Get-Member -MemberType NoteProperty | Where-Object { $_.Name -ine 'ComputerName' } | Select-Object -ExpandProperty Name if (++$Ctr -eq 1) { Format-AsCsv (@('ComputerName') + @($Headers | Sort-Object)) } if ($Ctr -eq 10000000) { $Ctr = 2 } # just in case there are a lot of objects/lines so we don't overflow for no reason at all? Format-AsCsv (@($Obj.ComputerName) + @($Headers | Sort-Object | ForEach-Object { $Value = $_ if ($Obj | Get-Member -MemberType NoteProperty -Name $Value) { $Obj | Select -ExpandProperty $Value } else { '' } })) #| ForEach-Object { '"' + $_ + '"' }) -join $CsvDelimiter } } } else { $Xml.computers.computer | Where-Object { $_.name -imatch $ComputerNameRegex -and $( if ($ComputerNameNotMatch[0] -ine '') { $_.name -inotmatch $ComputerNameNotMatchRegex } else { $true } ) } | Sort-Object Name | ForEach-Object { $ComputerName = $_.name $_.class | Where-Object { $_.name -imatch $ClassNameRegex -and $( if ($ClassNotMatch[0] -ine '') { $_.name -inotmatch $ClassNotMatchRegex } else { $true } )} | Sort-Object Name | ForEach-Object { $ClassName = $_.name $_.classentries | ForEach-Object { $_.classentry | Where-Object { $_.property -imatch $PropertyNameRegex -and $( if ($PropertyNotMatch[0] -ine '') { $_.property -inotmatch $PropertyNotMatchRegex } else { $true } )} | Sort-Object Property | ForEach-Object { if ($CombineComputerName) { # First time seeing a computer. if (-not $FoundComputers.ContainsKey($ComputerName)) { $FoundComputers.$ComputerName = New-Object psobject -Property @{ ComputerName = [string] $ComputerName (($ClassName -ireplace '^Win32_') + '-' + $_.Property) = (($_.Values | ForEach-Object { $_.Value }) -join $ValueDelimiter) } } else { Add-Member -InputObject $FoundComputers.$ComputerName -MemberType NoteProperty -Name (($ClassName -ireplace '^Win32_') + '-' + $_.Property) -Value (($_.Values | ForEach-Object { $_.Value }) -join $ValueDelimiter) } } else { New-Object PSObject -Property @{ 'ComputerName' = [string] $ComputerName #-join '' # Just a hack to lose the array type. 'Class' = $ClassName 'Property' = $_.Property 'Value' = @($_.Values | %{ $_.Value }) } } } } } } | Select-Object -Property ComputerName, Class, Property, Value if ($CombineComputerName) { $HeaderHash = @{} foreach ($Obj in $FoundComputers.Values) { $Obj | Get-Member -MemberType NoteProperty | Where-Object { $_.Name -ine 'ComputerName' } | Select-Object -ExpandProperty Name | ForEach-Object { if (-not $HeaderHash.ContainsKey(([string]$_))) { $HeaderHash.([string]$_) = 1 } } } # This -Property parameter stuff does not work in PowerShell v2. Works in 3 and up. $FoundComputers.Values | Select-Object -Property (@('ComputerName') + @($HeaderHash.Keys | Where-Object { $_ -ine 'ComputerName' } | Sort-Object)) #%{ $t = $_; @{n="$t";e={[scriptblock]::Create("`$_.$t")}} } )) } }