Though this really seems like a job for validating against a schema file (in which case see How do I use PowerShell to Validate XML files against an XSD?), if you want to perform hard-coded validation you can use the XDocument
class to get line information.
However you obtain an XDocument
instance ([System.Xml.Linq.XDocument]::Load()
or [System.Xml.Linq.XDocument]::Parse()
), you'll need to pass a [System.Xml.Linq.LoadOptions]
value specifying that you want line information to be tracked. Line information is made available via the IXmlLineInfo
interface which is explicitly implemented by the XObject
class and its descendants, though in PowerShell you can access its members automagically.
Given SO59240313.xml
...
<?xml version="1.0"?>
<Products>
<Product_Group id="Robot">
<Product id="RSAPRO2017">
<DefaultShortcut>Autodesk Robot Structural Analysis Professional 2017.lnk</DefaultShortcut>
<ProgramFolder>C:\Program Files\Autodesk\Autodesk Robot Structural Analysis Professional 2017</ProgramFolder>
<UserAppDataRoaming>C:\Users\$(userName)\AppData\Roaming\Autodesk\Autodesk Robot Structural Analysis Professional 2017</UserAppDataRoaming>
</Product>
<Productt id="SomeId">
<DefaultShortcut>Some shortcut</DefaultShortcut>
<ProgramFolder>Some program path</ProgramFolder>
<UserAppDataRoaming>Some roaming path</UserAppDataRoaming>
</Productt>
</Product_Group>
</Products>
...this code...
function ReportUnexpectedElement([System.Xml.Linq.XElement] $element)
{
Write-Warning "Unexpected element ""$($element.Name)"" found at position $($element.LinePosition) of line $($element.LineNumber)."
}
$document = [System.Xml.Linq.XDocument]::Load(
"$PWD\SO59240313.xml",
[System.Xml.Linq.LoadOptions]::SetLineInfo
)
if ($document.Root.Name -ne 'Products')
{
ReportUnexpectedElement $document.Root
}
else
{
$productGroupElement = $document.Root.Element('Product_Group')
if ($null -eq $productGroupElement)
{
Write-Warning 'Product_Group element not found.'
}
else
{
foreach ($productGroupChildElement in $productGroupElement.Elements())
{
if ($productGroupChildElement.Name -ne 'Product')
{
ReportUnexpectedElement $productGroupChildElement
}
}
}
}
...will print this output...
WARNING: Unexpected element "Productt" found at position 4 of line 9.
I'm not seeing a readily-accessible way to retrieve an element's complete opening tag, but you could reconstruct it yourself by modifying the ReportUnexpectedElement
function like this...
function ReportUnexpectedElement([System.Xml.Linq.XElement] $element)
{
$elementBuilder = New-Object -TypeName 'System.Text.StringBuilder'
# Most StringBuilder methods return a reference to that same instance,
# so remember to suppress them from the function output
$elementBuilder.Append('<').Append($element.Name) `
| Out-Null
foreach ($attribute in $element.Attributes())
{
$elementBuilder.AppendFormat(' {0}="{1}"', $attribute.Name, $attribute.Value) `
| Out-Null
}
$elementBuilder.Append('>') `
| Out-Null
Write-Warning "Unexpected element ""$($elementBuilder.ToString())"" found at position $($element.LinePosition) of line $($element.LineNumber)."
}
...which, given the same XML file, will print this output...
WARNING: Unexpected element "<Productt id="SomeId">" found at position 4 of line 9.