none
Powershell XML handling anomolies? RRS feed

  • Question

  • I have some XML which I can read and write quite happily but during my learning curve I have come across some interesting behaviour and wanted to know if this is intentional or a bug as it will affect what happens in the script I have when/if it changes.

    When you gather information from an XML file you can get a collection of nodes (these are in the variable $Global:nodes). When that collection is two or more then this statement:

    $Global:Nodes.Count

    Returns a count of 2 and each node/node attribute can be accessed by:

    $Global:DriveUID=$Global:Nodes.uid[$x-1]

    Where $x is the value of $Global:Nodes.Count The array starts at zero so you always have to subtract one to get the item you are interested in.

    However when the XML one has one item that is returned the $Global:Nodes.Count does not contain 1 it is a $Null value and when you try to address the array using $Global:DriveUID=$Global:Nodes.uid[0] you actually get the first character of the UID rather than the whole UID when there is more than one entry in the array of returned nodes.

    The XML looks like this (edited a little with the vertical ellipses) The collection of nodes is returned using this NodePath:

    "extentions.drive"

      <extentions>
        <lastdrive>4f004956-23dc-2c43-aedd-301bf783b090</lastdrive>
        <drive uid="4f004956-23dc-2c43-aedd-301bf783b090" letter="V" label="Video" textfile="Y">
          <directory>V:\MyScript\MyScriptTest\</directory>
    .
    .
    .
          <lastrun PreBackupDate="2016-03-24" PreBackupTime="17:48" Type="normal" Date="2016-03-24" Time="17:48">
          </lastrun>
          <defaultcomment>My Default comment</defaultcomment>
          <free_space_threshold active="1">2</free_space_threshold>
        </drive>
        <drive uid="7fa49166-29e2-11e4-82bd-00019510a995" letter="E" label="FC 500GB(3)" textfile="Y">
          <directory>E:\MyScript\MyScriptTest\</directory>
    .
    .
    .
          <lastrun PreBackupDate="2016-03-24" PreBackupTime="15:12" Type="normal" Date="2016-03-24" Time="15:12">
          </lastrun>
          <defaultcomment>
          </defaultcomment>
          <free_space_threshold active="1">2</free_space_threshold>
        </drive>
        <drive uid="f2c3cb0e-6111-11e4-832f-00019510a995" letter="E" label="FC 500GB(1)" textfile="Y">
          <directory>E:\MyScript backup\MyScripttest\</directory>
    .
    .
    .
          <lastrun PreBackupDate="2016-03-24" PreBackupTime="16:47" Type="normal" Date="2016-03-24" Time="15:19">
          </lastrun>
          <defaultcomment>
          </defaultcomment>
          <free_space_threshold active="1">2</free_space_threshold>
        </drive>
        <drive uid="a3aa9702-c12d-11e5-861f-00019510a995" letter="R" label="MyScript F&amp;F" textfile="Y">
          <directory>R:\MyScript\MyScriptTest\</directory>
    .
    .
    .
          <lastrun PreBackupDate="2016-03-24" PreBackupTime="15:19" Type="normal" Date="2016-03-24" Time="15:19">
          </lastrun>
          <defaultcomment>
          </defaultcomment>
          <free_space_threshold active="1">2</free_space_threshold>
        </drive>
      </extentions>

    Thanks for any helpful comments.


    Regards Ray

    Friday, March 25, 2016 2:42 PM

Answers

  • What you are seeing with thingle singleton is that PowerShell unwraps collections in a pipeline.  This will cause the singleton to be returned raw in this case.

    $drivesy=@(one)


    \_(ツ)_/

    • Marked as answer by ray.g Wednesday, May 18, 2016 10:13 AM
    Wednesday, March 30, 2016 4:33 PM

All replies

  • This is normal. If you used XML XPath this would not happen.  Direct access will produce this result.

    Avoid direct addressing by subscript.  Also avoid using "global".

    $DriveUID=$Nodes | select uid -first 1

    Also your question does not match the XML so it is hard to know exactly what you are asking.


    \_(ツ)_/

    Friday, March 25, 2016 2:59 PM
  • I will have to do some re-writing to use that construct, I will see what happens.

    Another issue is that if I call function1 which calls function2 to obtain the nodes and each does a

    return $Nodes

    When I get back to the initating line in the script the $Nodes in that function has an added extra item and the count is 5 The first item has all null values.

    If I call function2 directly the count returned is 4 which is correct.

    I have removed the "Global"


    Regards Ray

    Friday, March 25, 2016 5:34 PM
  • That is a new question that is unrelated.  It is also to vague to answer.

    Start a new question and provide a good example of what you are asking about including test data.


    \_(ツ)_/

    Friday, March 25, 2016 6:20 PM
  • $DriveUID=$Nodes | select uid -first 1

    Having slept on it I am still confused, how when always using -first and a numerical value do you step through a list of returned items? OK there is -skip but that then requires two values to be kept and two select statements. The other method is to use a foreach ($nodes in $Nodes) construct.

    Why it it incorrect to to use direct addressing by subscript, when the correct item in the list is identified it is simple to address the array directly and surely quicker than having to use a number of piped select statements.

    The script outputs a numbered list to the screen and asks the user to select the one they want. I can then use the number they enter to directly address the item/values in the $Nodes Array.

    I am still trying to get to grips with powershell. The XML in the first post is an example any items where the ellipses are is irelevant

    e.g.

    ====================


    1). Drive: [V] Label: [Video] Directory: [V:\MyScript\MyScriptTest\]
        UniqueID: [4f004956-23dc-2c43-aedd-301bf783b090]
        Last run date and time: 2016-03-25 16:29


    2). Drive: [E] Label: [FC 500GB(3)] Directory: [E:\MyScript\MyScriptTest\]
        UniqueID: [7fa49166-29e2-11e4-82bd-00019510a995]
        Last run date and time: 2016-03-24 15:12


    3). Drive: [E] Label: [FC 500GB(1)] Directory: [E:\MyScript backup\MyScripttest\]
        UniqueID: [f2c3cb0e-6111-11e4-832f-00019510a995]
        Last run date and time: 2016-03-24 15:19


    4). Drive: [R] Label: [MyScript F&F] Directory: [R:\MyScript\MyScriptTest\]
        UniqueID: [a3aa9702-c12d-11e5-861f-00019510a995]
        Last run date and time: 2016-03-24 15:19

    ====================

    In this pseudo code the $nodes.count will return 5 and there will be a null item as the first item in the $nodes array returned to the calling function

    function one
    {get required node(s);return $nodes}

    function two
    {open XML document;$nodes=(one);close XML Document;return $nodes}

    function calling
    {$nodes=(two);$nodes.count}

    If the calling function is changed to

    function calling
    {open XML document;$nodes=(one);close XML Document;$nodes.count}

    then $nodes.count returns 4 and the first item in the array is correct.

    If functions calling and two use $Global:Nodes (which is why I had it that way in the first place) then because the second return is not required the correct data is returned.

    I therefore think that all this is related and I still can't quite see why direct addressing an array of one does not work as it should $nodes.count never returns a value of 1 - why? It is always $Null or >= 2

    Sorry if I am being thick I just don't understand.


    Regards Ray


    • Edited by ray.g Saturday, March 26, 2016 10:25 AM
    Saturday, March 26, 2016 10:24 AM
  • Without an example of XML that actually matches you question it is not possible to explain what needs to be done.

    As you have alaredy posted, direct addressing won't work in all situations.  Actual XML querying will work:

    $xml.SelectNodes('//drive') | %{$_..uid}

    Will work for all amounts from 0 and beyond. That is the design of the XML query language.  Direct addressing is not as clear or clean so you need to account for more issues.

    Of course if you XML is not designed correctly than things may not work as expected.

    Also you are displaying an attribute of a target node where all other related information is embedded in child nodes.  This is another issue with the structure.  It will need to have a custom sscript to "flatten" it depending on what you need.

    Example of custom "flattening":

    $xml.SelectNodes('//drive')|%{$uid=$_.Uid;$_|select @{n='uid';e={$uid}},directory}

    All of these things are made much more difficult by trying to "subscript" your way into an arbitrary collection.

    You explanation is also convoluted and unnecessary.  If you understand XML and PowerShell you will be able to design a simple solution.  All of the commentary is  nice but it does not help use to answer your initial question.

    Again - actual XML and a simple question would be best.


    \_(ツ)_/

    Saturday, March 26, 2016 10:46 AM
  • Also note that we have no idea what you mean by outputting a numbered list.  A numbered list of what?

    Do you want to list drives in the file and return a selected item?

    $i=0;
    if($drives=$xml.SelectNodes('//drive')|%{$uid=$_.uid;$_|select @{n='Number';e={$i;$script:i++}},letter,directory,@{n='uid';e={$uid}}}){
         $drives
         $x=Read-Host 'Select drive'
         $uid=$drives[$x].uid
         $xml.SelectNodes("//drive[@uid='$uid']")
    }


    \_(ツ)_/


    • Edited by jrv Saturday, March 26, 2016 11:10 AM
    Saturday, March 26, 2016 11:08 AM
  • $i=0;
    if($drives=$xml.SelectNodes('//drive')|%{$uid=$_.uid;$_|select @{n='Number';e={$i;$script:i++}},letter,directory,@{n='uid';e={$uid}}}){
         $drives
         $x=Read-Host 'Select drive'
         $uid=$drives[$x].uid
         $xml.SelectNodes("//drive[@uid='$uid']")
    }


    So you are doing exactly what I am doing!

    $uid=$drives[$x].uid

    directly subscripting the array of returned nodes to extract the detail that gives you the single node required.

    I have done some more work and it seems that when you call functions to do this it works correctly when you do:

    return $xml.SelectNodes('//drive')...

    if you return the $drives varaible (as per my example above)

    $drives=$xml.SelectNodes('//drive')...
    return $drives

    through more than one function/return statement it goes wrong and we are back to my original problem. I have altered my code to use the first example and it now works with multiple nodes and no extra item in the array. I now need to see what happens when there is only one item. That will take me some more time.

    As I said I am no expert in powershell/xml so your comment:

    "If you understand XML and PowerShell you will be able to design a simple solution."

    is uneccessary.


    Regards Ray

    Saturday, March 26, 2016 11:45 AM
  • You can always use SelectNodes.  It does not have anything to do with functions.

    Your original request said nothing about SelectNodes.  SelectNodes will always return a collection even it there ism only one node.

    I am not going to debate this.  You have the necessary information.  If you cannot understand how to use it then you will need to contact someone as a paid consultant.  It is beyond the scope of this forum to provide design consulting or incremental training.

    Also no amount of technical discussion about functions will help you to understand how too use XML.  Just realize that the method I posted works as required as stated.  You only need to understand it to use it.

    Your discussion of all of the return statements is just nonsense.  Without an exact example of what you are doing it means nothing and we cannot comment on why it doesn't work for you.

    Sorry. We can but will not design a custom solution for you.

    This may help you: https://social.technet.microsoft.com/Forums/scriptcenter/en-US/c47b1bc2-f7fd-4d2e-8ff2-e8a81ce090d4/this-forum-is-for-scripting-questions-rather-than-script-requests?forum=ITCG


    \_(ツ)_/

    Saturday, March 26, 2016 12:11 PM
  • If you do nothing else please explain why:

    $XmlDocument.SelectNodes($fullyQualifiedNodePath, $xmlNsManager)

    when you select nodes and there is more than one node returned  $nodes.count contains the correct value

    when you select in the same way and there is only one node return $nodes.count does not return 1

    ($XmlDocument.SelectSingleNode($fullyQualifiedNodePath, $xmlNsManager)) has the same issue

    I am not and have not asked you to design a custom solution. I have one that works all I wanted was an explanation of two things.


    Regards Ray

    Saturday, March 26, 2016 12:22 PM
  • I have no idea what you are saying:

    PS >$xml=[xml]@'
      <extentions>
         <lastdrive>4f004956-23dc-2c43-aedd-301bf783b090</lastdrive>
         <drive uid="4f004956-23dc-2c43-aedd-301bf783b090" letter="V" label="Video" textfile="Y">
           <directory>V:\MyScript\MyScriptTest\</directory>
           <lastrun PreBackupDate="2016-03-24" PreBackupTime="17:48" Type="normal" Date="2016-03-24" Time="17:48">
           </lastrun>
           <defaultcomment>My Default comment</defaultcomment>
           <free_space_threshold active="1">2</free_space_threshold>
         </drive>
       </extentions>
    '@
    PS > $drives=$xml.SelectNodes('//drive')
    PS > $drives.count
    1

    "SelectNodes" always returns a nodelist (collection).  It will be 0 or greater.  This is the design of XML.

    SelectSingleNode always returns only one node or null if no nodes are found.  This is also by design.

    By design I mean the design of the XML committee.  It has nothing to do with PowerShell.

    I suggest that you go back and redo your tests until you can see what is happening.


    \_(ツ)_/


    • Edited by jrv Saturday, March 26, 2016 12:34 PM
    Saturday, March 26, 2016 12:32 PM
  • PS >$xml=[xml]@' <extentions> <lastdrive>4f004956-23dc-2c43-aedd-301bf783b090</lastdrive> <drive uid="4f004956-23dc-2c43-aedd-301bf783b090" letter="V" label="Video" textfile="Y"> <directory>V:\MyScript\MyScriptTest\</directory> <lastrun PreBackupDate="2016-03-24" PreBackupTime="17:48" Type="normal" Date="2016-03-24" Time="17:48"> </lastrun> <defaultcomment>My Default comment</defaultcomment> <free_space_threshold active="1">2</free_space_threshold> </drive> </extentions> '@ PS > $drives=$xml.SelectNodes('//drive')| Select-Object -ExpandProperty Node PS > $drives.count

    If you make this change (| Select-Object -ExpandProperty Node) to your script as above $drives.count will not be "1"

    If you add a second drive node to the XML, $drives.count will return "2".

    At least that is what it looks like to me, do you see the same?


    Regards Ray

    Monday, March 28, 2016 4:47 PM
  • y would you use select object?

    $drives=$xml.SelectNodes('//drive')
    $drives.Count

    There is no need to add that.


    \_(ツ)_/

    Monday, March 28, 2016 4:52 PM
  • Please explain this, run both examples. In example 1 the count is not 1 but the data is returned.  In example 2 the count is two as it the data returned.

    example 1

    $xml=[xml]@'
      <extentions>
         <lastdrive>4f004956-23dc-2c43-aedd-301bf783b090</lastdrive>
         <drive uid="4f004956-23dc-2c43-aedd-301bf783b090" letter="V" label="Video" textfile="Y">
           <directory>V:\MyScript\MyScriptTest\</directory>
           <lastrun PreBackupDate="2016-03-24" PreBackupTime="17:48" Type="normal" Date="2016-03-24" Time="17:48">
           </lastrun>
           <defaultcomment>My Default comment</defaultcomment>
           <free_space_threshold active="1">2</free_space_threshold>
         </drive>
       </extentions>
    '@
    Function one
    {
    Return (two)
    }
    
    Function two
    {
    Return ($xml.SelectNodes('//drive'))
    }
    $drivesy=(one)
    $drivesy.count
    $drivesy

    Example 2

    $xml=[xml]@'
      <extentions>
         <lastdrive>4f004956-23dc-2c43-aedd-301bf783b090</lastdrive>
         <drive uid="4f004956-23dc-2c43-aedd-301bf783b090" letter="V" label="Video" textfile="Y">
           <directory>V:\MyScript\MyScriptTest\</directory>
           <lastrun PreBackupDate="2016-03-24" PreBackupTime="17:48" Type="normal" Date="2016-03-24" Time="17:48">
           </lastrun>
           <defaultcomment>My Default comment</defaultcomment>
           <free_space_threshold active="1">2</free_space_threshold>
         </drive>
         <drive uid="4f004956-23dc-2c43-aedd-301bf783b090" letter="V" label="Video" textfile="Y">
           <directory>V:\MyScript\MyScriptTest\</directory>
           <lastrun PreBackupDate="2016-03-24" PreBackupTime="17:48" Type="normal" Date="2016-03-24" Time="17:48">
           </lastrun>
           <defaultcomment>My Default comment</defaultcomment>
           <free_space_threshold active="1">2</free_space_threshold>
         </drive>
       </extentions>
    '@
    Function one
    {
    Return (two)
    }
    
    Function two
    {
    Return ($xml.SelectNodes('//drive'))
    }
    $drivesy=(one)
    $drivesy.count
    $drivesy


    Regards Ray

    Wednesday, March 30, 2016 10:09 AM
  • First PowerShell does not use parens around arguments.  Also avoid using unnecessary "return" statements.

    Function one{
         two
    }

    Function two{
         $xml.SelectNodes('//drive')
    }

    PS C:\scripts> $drivesy=one
    PS C:\scripts> $drivesy.count
    2
    PS C:\scripts> $drivesy


    uid                  : 4f004956-23dc-2c43-aedd-301bf783b090
    letter               : V
    label                : Video
    textfile             : Y
    directory            : V:\MyScript\MyScriptTest\
    lastrun              : lastrun
    defaultcomment       : My Default comment
    free_space_threshold : free_space_threshold

    uid                  : 4f004956-23dc-2c43-aedd-301bf783b090
    letter               : V
    label                : Video
    textfile             : Y
    directory            : V:\MyScript\MyScriptTest\
    lastrun              : lastrun
    defaultcomment       : My Default comment
    free_space_threshold : free_space_threshold


    \_(ツ)_/

    Wednesday, March 30, 2016 4:27 PM
  • What you are seeing with thingle singleton is that PowerShell unwraps collections in a pipeline.  This will cause the singleton to be returned raw in this case.

    $drivesy=@(one)


    \_(ツ)_/

    • Marked as answer by ray.g Wednesday, May 18, 2016 10:13 AM
    Wednesday, March 30, 2016 4:33 PM