Search code examples
powershellxamlxpathpowershell-cmdletselect-xml

How to query XAML file with Powershell's `select-xml` commandlet?


I'm trying to use the select-xml cmdlet in Powershell to query some XAML files in my .NET project. Here's what I've tried:

select-xml -content (cat $xaml_file_path) -xpath "//GroupBox"

Where $xaml_file_path is simply a string containing the file path to the XAML file of interest.

This throws an error:

Select-Xml: Cannot validate argument on parameter 'Content'. The argument is null, empty, or an element of the argument collection contains a null value. Supply a collection that does not contain any null values and then try the command again.

I know the XAML is valid since it compiles fine on Visual Studio. So I'm thinking there might be something else going on here.

Is there a way to query XAML files using Powershell? If so how?


Solution

  • To query an XML (XAML) document that uses namespaces, Select-Xml requires you to:[1]

    • declare all the namespaces that any nodes involved in your XPath query are in, via a hashtable that maps self-chosen prefixes to namespace URIs (parameter -Namespace).

      • Those self-chosen prefixes may, but needn't, be the same as in the input document, with the exception of the default namespace (xmlns), for which a name must be chosen too, but which can not be xmlns (the example below uses _).
    • use those self-chosen prefixes for all the nodes referenced in the XPath query (parameter -XPath), including those in the default (implied) namespace.

    A simple example:

    # Create a sample XAML file.
    @"
    <Window
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:Test"
            Title="MainWindow" Height="450" Width="500">
        <Grid>
            <TextBox x:Name="UserInnput" Height="140" TextWrapping="Wrap" VerticalAlignment="Top" AcceptsReturn="True" AcceptsTab="True" Padding="4" VerticalScrollBarVisibility="Auto" />
            <Button x:Name="Save" Content="Save" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" IsDefault="True" Height="22" Margin="170,150,0,0" />        
        </Grid>
    </Window>
    "@ | Set-Content sample.xml
    
    # Create the hashtable that uses self-chosen names to the URIs
    # of those namespaces involved in the XPath query below.
    $namespaces = @{ 
      # This is the default namespace, which the elements in the input
      # document that have *no* namespace prefix are *implicitly* in.
      # You must NOT use 'xmlns' as the prefix.
      _ = 'http://schemas.microsoft.com/winfx/2006/xaml/presentation'
      # For non-default namespaces, you may choose the same prefixes 
      # as in the original document.
      # Note:
      #  * This namespace isn't actually used in the query below.
      #  * However, if the nodes involved in the query do fall into
      #    this or other namespaces, they must be declared here too,
      #    and their prefixes must then be used in the query.
      x = 'http://schemas.microsoft.com/winfx/2006/xaml' 
    }
    
    # Query the XAML file, passing the hashtable to -Namespace, and
    # using its keys as namespace prefixes; here, '_:' must be used
    # to refer to nodes in the default namespace.
    Select-Xml -LiteralPath sample.xml -Namespace $namespaces -XPath '//_:TextBox'
    

    If you want to avoid having to deal with namespaces:

    • In the context of Select-Xml (as well as the underlying System.Xml.XmlDocument.SelectNodes() .NET method), the only way to avoid having to deal with namespaces is to use the *[local-name()='...'] workaround shown in your own answer.

    • In the context of PowerShell's adaption of the [xml] DOM, which adds "virtual" properties to instances of [xml] (System.Xml.XmlDocument):

      • These properties are always named for the non-prefixed node names; that is, namespaces are effectively ignored.

      • This is convenient and often sufficient, but it limits you to "drilling down" with dot notation into the document, as opposed to having being able to run XPath queries against the document; for the latter, you can use the .SelectNodes() and .SelectSingleNode() methods, which, however, again necessitate namespace management.[1]

      • The equivalent example with dot notation, building on the same sample file:

        # Parse the XML file into an [xml] instance.
        ($doc = [xml]::new()).Load((Convert-Path sample.xml))
        
        # Drill down to the <TextBox> element.
        # Note: If the <Grid> element had *multiple* <TextBox> children,
        #       they would *all* be returned, as a System.Xml.XmlElement array.
        $doc.Window.Grid.TextBox
        

    [1] The need for namespace management applies analogously to direct use of the underlying System.Xml.XmlDocument.SelectNodes() and System.Xml.XmlDocument.SelectSingleNodes() .NET API, although constructing the prefix-to-URI mapping table is a little more cumbersome there - see this answer.