More fun with xml

Started by spl, March 13, 2025, 02:35:20 PM

Previous topic - Next topic

spl

In a previous post, albeit with a typo, I suggested the ease of creating xml with the WB CLR: for example
file = 'C:\temp\xmltest.xml'
if fileexist(file) then filedelete(file)
ObjectClrOption("useany","System.Xml")
xmlDoc = ObjectClrNew( 'System.Xml.XmlDocument' )
; or try "ISO8859-1" for encoding instead of  "UTF-8"
xmlDoc.AppendChild(xmlDoc.CreateXmlDeclaration("1.0", "UTF-8",""))
rootElement = xmlDoc.CreateElement("book")
xmlDoc.AppendChild(rootElement)

titleElement = xmlDoc.CreateElement("title")
titleElement.InnerText = "The Great Gatsby"
rootElement.AppendChild(titleElement) 
 
authorElement = xmlDoc.CreateElement("author")
authorElement.InnerText = "F. Scott Fitzgerald"
rootElement.AppendChild(authorElement) 
 
publicationYearElement = xmlDoc.CreateElement("publication_year")
publicationYearElement.InnerText = "1925"
rootElement.AppendChild(publicationYearElement) 
 
genreElement = xmlDoc.CreateElement("genre")
genreElement.InnerText = "Fiction"
rootElement.AppendChild(genreElement) 
 
xmlDoc.Save(file)
xmlDoc = 0
if fileexist(file)
   Message("xml created",fileget(file)) 
else
   Display(2,'xml not created',file)
endif
exit

run it and it will create a simple xml file (on c:\temp\, or modified to where you want it created. But then I ran an age-old parsing routine to obtain the element names and text, which has worked on other xml files with similar structure. Run it after creating the file above.
file = 'C:\temp\xmltest.xml'
if !fileexist(file) then Terminate(@TRUE,"Exiting","File Not Found:":file)
XDoc = CreateObject("MSXML2.DOMDocument")
XDoc.async = @False 
XDoc.validateOnParse = @False
XDoc.Load(file)
lists = XDoc.DocumentElement
output = "Field,Value":@CRLF
ForEach listNode In lists.ChildNodes
   ForEach fieldNode In listNode.ChildNodes
      output = output:fieldNode.BaseName:",":fieldNode.Text:@CRLF
   Next 
Next 
XDoc = 0
Message("Parsed Nodes",output)
Exit

Did your 'output' seem to indicate a foreach loop gone wild. Oh, and I suggested an alternative encoding for the  first script, but I got the same loose-cannon results. Not sure if creating xml with .Net is compatible with parsing it with COM. Additionally the parsing logic doesn't even work with xml attributes, but that is a different topic.

I'll look into .Net parsing that can be run in WB. Otherwise, this is a mystery unless I made another typo.
Stan - formerly stanl [ex-Pundit]

spl

Well, second script will work if I take out the second foreach loop
file = 'C:\temp\xmltest.xml'
if !fileexist(file) then Terminate(@TRUE,"Exiting","File Not Found:":file)
XDoc = CreateObject("MSXML2.DOMDocument")
XDoc.async = @False 
XDoc.validateOnParse = @False
XDoc.Load(file)
lists = XDoc.DocumentElement
output = "Field,Value":@CRLF
ForEach listNode In lists.ChildNodes
   output = output:listNode.BaseName:",":listNode.Text:@CRLF
Next 
XDoc = 0
Message("Parsed Nodes",output)
Exit

But will mess up if more than one <book> entry. I realize that xPath would be a solution but that is often based on previous knowledge of the node names. The script is meant to determine nodes/values more generic. At least original mystery solved.
Stan - formerly stanl [ex-Pundit]

td

What about something like this?

file = 'C:\temp\xmltest.xml'
if !fileexist(file) then Terminate(@TRUE,"Exiting","File Not Found:":file)
XDoc = CreateObject("MSXML2.DOMDocument")
XDoc.async = @False 
XDoc.validateOnParse = @False
XDoc.Load(file)
lists = XDoc.DocumentElement
while lists
   output = lists.Basename:": Field,Value":@CRLF
   ForEach listNode In lists.ChildNodes
      output = output:listNode.BaseName:",":listNode.Text:@CRLF
   Next 
   lists = lists.nextSibling
endwhile
XDoc = 0
Message("Parsed Nodes",output)
Exit
"No one who sees a peregrine falcon fly can ever forget the beauty and thrill of that flight."
  - Dr. Tom Cade

spl

Nice. Thanks. Moved initialization of output above the while loop - to get a .csv feel. Pretty much the same as my original script. Now change the xml file to the config.xml and you get attached jpeg.
file = 'C:\temp\config.xml'
if !fileexist(file) then Terminate(@TRUE,"Exiting","File Not Found:":file)
XDoc = CreateObject("MSXML2.DOMDocument")
XDoc.async = @False 
XDoc.validateOnParse = @False
XDoc.Load(file)
lists = XDoc.DocumentElement
output = "Field,Value":@CRLF
while lists
   ForEach listNode In lists.ChildNodes
      output = output:listNode.BaseName:",":listNode.Text:@CRLF
   Next 
   lists = lists.nextSibling
endwhile
XDoc = 0
Message("Parsed Nodes",output)
Exit
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Configuration>
  <Version>1.0</Version>
  <AppName>Winbatch Automation</AppName>
  <Database>
    <Server>localhost</Server>
    <Password>password123</Password>
    <Name>wbAuto.db</Name>
    <User>admin</User>
  </Database>
</Configuration>

but what I would want is
Field,Value
Version,1.0
AppName,Winbatch Automation
Server,localhost
Password,password123
Name,wbauto.db
User,Admin
Stan - formerly stanl [ex-Pundit]

JTaylor

You need a little recursive action...

#DefineFunction Rec_XML(lists, output)
  While lists
    ForEach listNode In lists.ChildNodes
      If listNode.CHildNodes.Length > 1 Then
        output = Rec_XML(listNode, output)  ; Process children first
      ElseIf listNode.CHildNodes.Length == 1 Then
        output := listNode.NodeName : "," : listNode.Text : @CRLF
      EndIf
    Next 
    lists = lists.nextSibling
  EndWhile
  Return output
#EndFunction

file = 'config.xml'
if !fileexist(file) then Terminate(@TRUE, "Exiting", "File Not Found:":file)
XDoc = CreateObject("MSXML2.DOMDocument")
XDoc.async = @False 
XDoc.validateOnParse = @False
XDoc.Load(file)
lists = XDoc.DocumentElement
output = "Field,Value" : @CRLF
output = Rec_XML(lists, output)
XDoc = 0
Message("Parsed Nodes", output)
Exit

spl

Thanks Jim. Kinda waiting for you to chip in. My xml skills are wanting. I looked into recursion but tried childnodes.Count instead of childnodes.length, which of course errored. I am also working with a node type map to possibly iterate a given xml file as as a pseudo-schema, although there may be existing WB code to create an .xsd file.
[EDIT] Map elements updated
nodeTypes = $"0=None
1=ELEMENT_NODE
2=ATTRIBUTE_NODE
3=TEXT_NODE
4=CDATA_SECTION_NODE
5=ENTITY_REFERENCE_NODE
6=ENTITY_NODE
7=PROCESSING_INSTRUCTION_NODE
8=COMMENT_NODE
9=DOCUMENT_NODE
10=DOCUMENT_TYPE_NODE
11=DOCUMENT_FRAGMENT_NODE
12=NOTATION_NODE
13=WHITESPACE
14=SIGNIFICANTWHITESPACE
15=ENDELEMENT
16=ENDENTITY
17=XMLDECLARATION$"
nodeTypes= MapCreate(nodeTypes,'=',@lf)
Stan - formerly stanl [ex-Pundit]

spl

Quote from: spl on March 15, 2025, 03:57:51 AMalthough there may be existing WB code to create an .xsd file.

oops... forgot creating an .xsd with the CLR is simple
gosub udfs
IntControl(73,1,0,0,0)

file = 'c:\temp\config.xml'
if !fileexist(file) then Terminate(@TRUE, "Exiting", "File Not Found:":file)
xsd =  strReplace(file,"xml","xsd")
if fileexist(xsd) then Filedelete(xsd)

ObjectClrOption("useany","System.Data")
ds = ObjectClrNew("System.Data.DataSet")
ds.ReadXml(file) 
ds.WriteXmlSchema(xsd)

if fileexist(xsd)
   results = FileGet(xsd)
   Message(file:" schema",results)
else
   Message("Error",xsd:@LF:"File Could Not Be Created")
endif
ds=0
Exit

:WBERRORHANDLER
ds=0
geterror()
Terminate(@TRUE,"Error Encountered",errmsg)

:udfs
#DefineSubRoutine geterror()
   wberroradditionalinfo = wberrorarray[6]
   lasterr = wberrorarray[0]
   handlerline = wberrorarray[1]
   textstring = wberrorarray[5]
   linenumber = wberrorarray[8]
   errmsg = "Error: ":lasterr:@LF:textstring:@LF:"Line (":linenumber:")":@LF:wberroradditionalinfo
   Return(errmsg)
#EndSubRoutine

Return
Stan - formerly stanl [ex-Pundit]

spl

Quote from: JTaylor on March 14, 2025, 01:03:19 PMYou need a little recursive action...

Jim; I just tested your code with
<bookstore>
  <book category="children">
    <title>Harry Potter</title>
    <author>J K. Rowling</author>
    <year>2005</year>
    <price>29.99</price>
  </book>
  <book category="web">
    <title>Learning XML</title>
    <author>Erik T. Ray</author>
    <year>2003</year>
    <price>39.95</price>
  </book>
</bookstore>

the output duplicates the 2nd book. Comment the while loop and lists = lists.nextSibling - no dupes. Can't see much virtue in while loops parsing xml - could be wrong or mistaken. Seems....
  • A foreach loop will process all nodes
  • An i=0 to (XDoc.childNodes.Length) -1 will get all elements, including a declaration and attributes

Probably should close this thread, tangents are inevitable [especially avec moi]. My specific goal would be parsing xml into manageable database or spreadsheet object.
Stan - formerly stanl [ex-Pundit]

JTaylor

I just took what was there and added the recursive action.  Didn't give it much thought other than finding it interesting that it was done that way.  I think you can just remove the while loop and the nextSibling stuff and it will work fine.

Jim

spl

Quote from: JTaylor on March 15, 2025, 02:17:30 PMI just took what was there and added the recursive action.  Didn't give it much thought other than finding it interesting that it was done that way.  I think you can just remove the while loop and the nextSibling stuff and it will work fine.

Jim

I changed my approach, but now in need of recursion help. below is a modified config.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Configuration>
  <Version>1.0</Version>
  <AppName>Winbatch Automation</AppName>
  <Database sqltype="SqLite">
    <Server>localhost</Server>
    <Password>password123</Password>
    <Name>wbAuto.db</Name>
    <User>admin</User>
  </Database>
</Configuration>

and modified code. Goal is to iterate the xml indicating then nodetype, any attributes, and node.Text. The script works but does not fully iterate the attribute for <Database> and the individual sub-nodes. I placed a comment ";need recursion here, i.e Childnodes(node1,nodeTypes)" assuming a recursive call to the function, but that would destroy the retval variable as it is set to "" at the start of the function. A neat recursion would account for multiple levels of a node/attributes... but I am not seeing it, although I'm sure WB can perform function recursion. Appreciate some feedback.
gosub udfs
IntControl(73,1,0,0,0)

nodeTypes = $"0=None
1=ELEMENT_NODE
2=ATTRIBUTE_NODE
3=TEXT_NODE
4=CDATA_SECTION_NODE
5=ENTITY_REFERENCE_NODE
6=ENTITY_NODE
7=PROCESSING_INSTRUCTION_NODE
8=COMMENT_NODE
9=DOCUMENT_NODE
10=DOCUMENT_TYPE_NODE
11=DOCUMENT_FRAGMENT_NODE
12=NOTATION_NODE
13=WHITESPACE
14=SIGNIFICANTWHITESPACE
15=ENDELEMENT
16=ENDENTITY
17=XMLDECLARATION$"
nodeTypes= MapCreate(nodeTypes,'=',@lf)

file = 'c:\temp\config.xml'
if !fileexist(file) then Terminate(@TRUE, "Exiting", "File Not Found:":file)
XDoc = CreateObject("Msxml2.DOMDocument.6.0")
XDoc.async = @False 
XDoc.validateOnParse = @False
XDoc.Load(file)
List = XDoc.childNodes
n=List.Length
output="Item,Value":@CRLF
for i= 0 to n-1
   li=List.item(i)
   b = li.BaseName
   n = li.NodeType
   n1 = nodeTypes[n]
   output = output:b:",":n1:@CRLF
   output = output:Childnodes(li,nodeTypes)
next
XDoc = 0
Message(file:" outout",output)

Exit

:WBERRORHANDLER
XDoc = 0
geterror()
Terminate(@TRUE,"Error Encountered",errmsg)

:udfs
#DefineSubRoutine geterror()
   wberroradditionalinfo = wberrorarray[6]
   lasterr = wberrorarray[0]
   handlerline = wberrorarray[1]
   textstring = wberrorarray[5]
   linenumber = wberrorarray[8]
   errmsg = "Error: ":lasterr:@LF:textstring:@LF:"Line (":linenumber:")":@LF:wberroradditionalinfo
   Return(errmsg)
#EndSubRoutine

#DefineFunction Childnodes(node,nodeTypes)
  retval=""
  atts = node.attributes
  length=atts.Length
  if length>0
     for i=0 to length-1
        att = atts.Item(i)
        name = att.Name
        value = att.Value
        retval = retval:name:",":value:@CRLF
     next
  endif
  nodes = node.childNodes
  length=nodes.Length
  if length>0
     for i=0 to length-1
        node1 = nodes.Item(i)
        name = node1.BaseName
        n = node1.NodeType
        n1 = nodeTypes[n]
        retval = retval:name:",":n1:@CRLF
        ;need recursion here, i.e Childnodes(node1,nodeTypes)
        value = node1.Text
        retval = retval:name:",":value:@CRLF
     next
  endif
  Return retval
#EndFunction

Return
Stan - formerly stanl [ex-Pundit]

JTaylor

Probably want to do some formatting but I think it does what you want, in general.

IntControl(73,1,0,0,0)
 
GoSub LOAD_ROUTINES

file = 'config.xml'
if !fileexist(file) then Terminate(@TRUE, "Exiting", "File Not Found:":file)
XDoc = CreateObject("MSXML2.DOMDocument")
XDoc.async = @False 
XDoc.validateOnParse = @False
XDoc.Load(file)

output = ""
dtype = XDoc.ChildNodes.item(0)
output := dtype.BaseName:",":nodeTypes[dtype.NodeType]: Get_Attributes(dtype) : @CRLF
output := "Field,NodeType/Value" : @CRLF

lists = XDoc.DocumentElement
output = Rec_XML(lists, output, nodeTypes, 0)
XDoc = 0
Message("Parsed Nodes", output)
Exit



:Load_Routines

 #DefineFunction Rec_XML(lists, output, nodeTypes, level)
    attrs =  Get_Attributes(lists)
    output := lists.NodeName : " - " : nodeTypes[lists.NodeType] : attrs : @CRLF
    ForEach listNode In lists.ChildNodes
      If listNode.ChildNodes.Length > 1 Then
        output = Rec_XML(listNode, output, nodeTypes, level+1)  ; Process children first
      ElseIf listNode.ChildNodes.Length == 1 Then
        attrs =  Get_Attributes(listNode)
        output := StrFill(" ",level*4):listNode.NodeName : "," : listNode.Text  : " - "  : nodeTypes[listNode.NodeType] : attrs : @CRLF
      EndIf
    Next 
  Return output
 #EndFunction

 #DefineFunction Get_Attributes(node)
   retval = ""
   atts = node.attributes
   length = atts.Length
   If length > 0
     For i=0 to length-1
       att = atts.Item(i)
       name = att.Name
       value = att.Value
       retval := name:"=":value:","
     Next
     retval = "  (":ItemRemove(-1,retval,","):")"
   EndIf
   Return retval
 #EndFunction

 #DefineSubRoutine geterror()
    wberroradditionalinfo = wberrorarray[6]
    lasterr = wberrorarray[0]
    handlerline = wberrorarray[1]
    textstring = wberrorarray[5]
    linenumber = wberrorarray[8]
    errmsg = "Error: ":lasterr:@LF:textstring:@LF:"Line (":linenumber:")":@LF:wberroradditionalinfo
    Return(errmsg)
  #EndSubRoutine

  nodeTypes = $"0=None
  1=ELEMENT_NODE
  2=ATTRIBUTE_NODE
  3=TEXT_NODE
  4=CDATA_SECTION_NODE
  5=ENTITY_REFERENCE_NODE
  6=ENTITY_NODE
  7=PROCESSING_INSTRUCTION_NODE
  8=COMMENT_NODE
  9=DOCUMENT_NODE
  10=DOCUMENT_TYPE_NODE
  11=DOCUMENT_FRAGMENT_NODE
  12=NOTATION_NODE
  13=WHITESPACE
  14=SIGNIFICANTWHITESPACE
  15=ENDELEMENT
  16=ENDENTITY
  17=XMLDECLARATION$"
  nodeTypes= MapCreate(nodeTypes,'=',@lf)

Return


<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<Configuration>

  <Version>1.0</Version>

  <AppName>Winbatch Automation</AppName>

  <Database>

    <Server>localhost</Server>

    <Password>password123</Password>

    <Name>wbAuto.db</Name>

    <User>admin</User>

  </Database>
  <AppPath>.\XML\</AppPath>

</Configuration>

spl

Quote from: JTaylor on March 16, 2025, 11:48:32 AMProbably want to do some formatting but I think it does what you want, in general.

Thanks. And credit where credit due. Creating separate function for attributes was excellent. Below I combined my original "csv" format with your functions. Thought sqltype="SqLite" would have appeared under Database, not Configuration but that is a minor point. I will be applying this base script against xml with multi-levels of childnodes and CDATA/other nodetypes. But thanks again

[script]
;Winbatch 2025A - iterate/parse xml nodelist
;Credit to Jim Taylor for recursion processing
;3/17/2025
;=====================================================
gosub udfs
IntControl(73,1,0,0,0)

nodeTypes = $"0=None
1=ELEMENT_NODE
2=ATTRIBUTE_NODE
3=TEXT_NODE
4=CDATA_SECTION_NODE
5=ENTITY_REFERENCE_NODE
6=ENTITY_NODE
7=PROCESSING_INSTRUCTION_NODE
8=COMMENT_NODE
9=DOCUMENT_NODE
10=DOCUMENT_TYPE_NODE
11=DOCUMENT_FRAGMENT_NODE
12=NOTATION_NODE
13=WHITESPACE
14=SIGNIFICANTWHITESPACE
15=ENDELEMENT
16=ENDENTITY
17=XMLDECLARATION$"
nodeTypes= MapCreate(nodeTypes,'=',@lf)

file = 'c:\temp\config.xml' ;change as needed
if !fileexist(file) then Terminate(@TRUE, "Exiting", "File Not Found:":file)
XDoc = CreateObject("Msxml2.DOMDocument.6.0")  ;or just Msxml2.DOMDocument
XDoc.async = @False 
XDoc.validateOnParse = @False
XDoc.Load(file)
List = XDoc.childNodes
n=List.Length
;initialize comma-separated output
;this could (1)include other columns (2) write out to file
output="Item,Value":@CRLF
dtype = XDoc.ChildNodes.item(0)
;check if file begins with xml declaration
if  nodeTypes[dtype.NodeType] <> 1
   output := dtype.BaseName:",":nodeTypes[dtype.NodeType]:@CRLF
   output := Get_Attributes(dtype)
endif
;send document elements/childnodes to a function
lists = XDoc.DocumentElement
output = Rec_XML(lists, output, nodeTypes)
XDoc = 0
Message(file:" Parsed",output)

Exit

:WBERRORHANDLER
XDoc = 0
geterror()
Terminate(@TRUE,"Error Encountered",errmsg)
;=====================================================

:udfs
#DefineSubRoutine geterror()
   wberroradditionalinfo = wberrorarray[6]
   lasterr = wberrorarray[0]
   handlerline = wberrorarray[1]
   textstring = wberrorarray[5]
   linenumber = wberrorarray[8]
   errmsg = "Error: ":lasterr:@LF:textstring:@LF:"Line (":linenumber:")":@LF:wberroradditionalinfo
   Return(errmsg)
#EndSubRoutine

#DefineFunction Rec_XML(lists, output, nodeTypes)
    ForEach listNode In lists.ChildNodes
      If listNode.ChildNodes.Length > 1 Then
        output := lists.NodeName : "," : nodeTypes[lists.NodeType]:@CRLF
        output :=  Get_Attributes(listNode)
        output = Rec_XML(listNode, output, nodeTypes)  ; Process children first
      ElseIf listNode.ChildNodes.Length == 1 Then
        output := lists.NodeName : "," : nodeTypes[lists.NodeType]:@CRLF
        output :=  Get_Attributes(listNode)
        output := listNode.NodeName : "," : listNode.Text:@CRLF 
      EndIf
    Next 
  Return output
 #EndFunction
 
 #DefineFunction Get_Attributes(node)
   retval = ""
   atts = node.attributes
   length = atts.Length
   If length > 0
     For i=0 to length-1
       att = atts.Item(i)
       name = att.Name
       value = att.Value
       retval = retval:name:",":value:@CRLF
     Next
   EndIf
   Return retval
 #EndFunction

Return
;=====================================================

[xml]
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Configuration>
  <Version>2.0</Version>
  <AppName>Winbatch Automation</AppName>
  <Database sqltype="SqLite">
    <Server>localhost</Server>
    <Password>password123</Password>
    <Name>wbAuto.db</Name>
    <User>admin</User>
  </Database>
</Configuration>
Stan - formerly stanl [ex-Pundit]

JTaylor

Always happy to help.

Also, assuming I understand, SqLite attribute does appear under Database for me.

Jim

td

Nice script. I get "sqlite" appearing under configuration. If (big "if") I am reading the XML file correctly, that is were is should appear.
"No one who sees a peregrine falcon fly can ever forget the beauty and thrill of that flight."
  - Dr. Tom Cade

spl

Quote from: JTaylor on March 17, 2025, 09:35:51 AMAlways happy to help.

Also, assuming I understand, SqLite attribute does appear under Database for me.

Jim

not for me. Under Configuration
Stan - formerly stanl [ex-Pundit]

spl

Quote from: td on March 17, 2025, 10:13:22 AMNice script. I get "sqlite" appearing under configuration. If (big "if") I am reading the XML file correctly, that is were is should appear.

Like I wrote, just a minor nitpick, but <Database sqltype="SqLite"> one would assume the attribute is under Database, not Configuration as the script reads the node name => then any attributes => then innerText.

[EDIT]
consider this
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Configuration>
  <Version>2.0</Version>
  <AppName>Winbatch Automation</AppName>
  <Database sqltype="SqLite">
    <Server>localhost</Server>
    <Password encrypted="no">password123</Password>
    <Name>wbAuto.db</Name>
    <User>admin</User>
  </Database>
</Configuration>

the encrypted is associated with Password correctly. Probably just depends on reading the output.

Stan - formerly stanl [ex-Pundit]

td

"No one who sees a peregrine falcon fly can ever forget the beauty and thrill of that flight."
  - Dr. Tom Cade

JTaylor

My formatting is different but here is how it reads for me.

Jim

JTaylor

Did you intend to have the duplication? 

Jim

td

 Assuming duplication is intentional. Here's a tweak that fixes the misplaced name.
#DefineFunction Rec_XML(lists, output, nodeTypes)
    ForEach listNode In lists.ChildNodes
      If listNode.ChildNodes.Length > 1 Then
        output := listNode.NodeName : "," : nodeTypes[lists.NodeType]:@CRLF
        output :=  Get_Attributes(listNode)
        output = Rec_XML(listNode, output, nodeTypes)  ; Process children first
      ElseIf listNode.ChildNodes.Length == 1 Then
        output := lists.NodeName : "," : nodeTypes[lists.NodeType]:@CRLF
        output :=  Get_Attributes(listNode)
        output := listNode.NodeName : "," : listNode.Text:@CRLF 
      EndIf
    Next 
  Return output
 #EndFunction
 

Of course, it could break something else as it is only tested with the current example.
"No one who sees a peregrine falcon fly can ever forget the beauty and thrill of that flight."
  - Dr. Tom Cade

JTaylor

The issue is in how you are using lists and listNode.  You print lists.NodeName in the > 1 section but since you haven't called Rec_XML() again, yet, Configuration is still the "lists" Node. 


Jim

td

Not sure who the "you" is but I just change the line

 output := lists.NodeName : "," : nodeTypes[lists.NodeType]:@CRLF

to

 output := listNode.NodeName : "," : nodeTypes[lists.NodeType]:@CRLF

in the "If" part of the if/elseif/endif block.

it appears to have addressed Stan's comment about the "Database" element name not appearing before the "sqltype,SqLite" attribute name and value.

FWIT, Your formatting is easy to read for us non-initiated.
"No one who sees a peregrine falcon fly can ever forget the beauty and thrill of that flight."
  - Dr. Tom Cade

JTaylor

I was talking to Stan.

Thanks.

Jim

spl

Looks like recursion is beginning to be clear as mud. My updated script was meant to parse xml output as comma-delimited item,value. It assumes a nodeType would be duplicated but wanted individual attributes/values on separate lines under the nodetype item. Below is my script, updated to include error handling in each function and in the Rec_XML() function gives both the original and Tony's suggestion for placing the attribute for Database correctly
        ;this will place the Database attribute under Configurstion
        ;assumes you are loading config.xml sample
        ;output := lists.NodeName : "," : nodeTypes[lists.NodeType]:@CRLF
        ;this will place Database attribute correctly
        output := listNode.NodeName : "," : nodeTypes[lists.NodeType]:@CRLF

[full script]
;Winbatch 2025A - iterate/parse xml nodelist
;Credit to Jim Taylor for recursion processing
;3/17/2025
;=====================================================
gosub udfs
IntControl(73,1,0,0,0)

nodeTypes = $"0=None
1=ELEMENT_NODE
2=ATTRIBUTE_NODE
3=TEXT_NODE
4=CDATA_SECTION_NODE
5=ENTITY_REFERENCE_NODE
6=ENTITY_NODE
7=PROCESSING_INSTRUCTION_NODE
8=COMMENT_NODE
9=DOCUMENT_NODE
10=DOCUMENT_TYPE_NODE
11=DOCUMENT_FRAGMENT_NODE
12=NOTATION_NODE
13=WHITESPACE
14=SIGNIFICANTWHITESPACE
15=ENDELEMENT
16=ENDENTITY
17=XMLDECLARATION$"
nodeTypes= MapCreate(nodeTypes,'=',@lf)

file = 'c:\temp\config.xml' ;change as needed
if !fileexist(file) then Terminate(@TRUE, "Exiting", "File Not Found:":file)
XDoc = CreateObject("Msxml2.DOMDocument.6.0")  ;or just Msxml2.DOMDocument
XDoc.async = @False 
XDoc.validateOnParse = @False
XDoc.Load(file)
List = XDoc.childNodes
n=List.Length
;initialize comma-separated output
;this could (1)include other columns (2) write out to file
output="Item,Value":@CRLF
dtype = XDoc.ChildNodes.item(0)
;check if file begins with xml declaration
if  nodeTypes[dtype.NodeType] <> 1
   output := dtype.BaseName:",":nodeTypes[dtype.NodeType]:@CRLF
   output := Get_Attributes(dtype,xDOC)
endif
;send document elements/childnodes to a function
lists = XDoc.DocumentElement
output = Rec_XML(lists, output, nodeTypes, xDOC)
XDoc = 0
Message(file:" Parsed",output)

Exit

:WBERRORHANDLER
XDoc = 0
geterror()
Terminate(@TRUE,"Error Encountered",errmsg)
;=====================================================

:udfs
#DefineSubRoutine geterror()
   wberroradditionalinfo = wberrorarray[6]
   lasterr = wberrorarray[0]
   handlerline = wberrorarray[1]
   textstring = wberrorarray[5]
   linenumber = wberrorarray[8]
   errmsg = "Error: ":lasterr:@LF:textstring:@LF:"Line (":linenumber:")":@LF:wberroradditionalinfo
   Return(errmsg)
#EndSubRoutine

#DefineFunction Rec_XML(lists, output, nodeTypes, xDOC)
    IntControl(73,1,0,0,0)
    ForEach listNode In lists.ChildNodes
      If listNode.ChildNodes.Length > 1 Then
        ;this will place the Database attribute under Configurstion
        ;assumes you are loading config.xml sample
        ;output := lists.NodeName : "," : nodeTypes[lists.NodeType]:@CRLF
        ;this will place Database attribute correctly
        output := listNode.NodeName : "," : nodeTypes[lists.NodeType]:@CRLF

        output :=  Get_Attributes(listNode,xDOC)
        output = Rec_XML(listNode, output, nodeTypes, xDOC)  ; Process children first
      ElseIf listNode.ChildNodes.Length == 1 Then
        output := lists.NodeName : "," : nodeTypes[lists.NodeType]:@CRLF
        output :=  Get_Attributes(listNode,xDOC)
        output := listNode.NodeName : "," : listNode.Text:@CRLF 
      EndIf
    Next 
  Return output
  :WBERRORHANDLER
  XDoc = 0
  geterror()
  Terminate(@TRUE,"Error Encountered",errmsg)
 #EndFunction
 
 #DefineFunction Get_Attributes(node,xDOC)
   IntControl(73,1,0,0,0)
   retval = ""
   atts = node.attributes
   length = atts.Length
   If length > 0
     For i=0 to length-1
       att = atts.Item(i)
       name = att.Name
       value = att.Value
       retval = retval:name:",":value:@CRLF
     Next
   EndIf
   Return retval
   :WBERRORHANDLER
   XDoc = 0
   geterror()
   Terminate(@TRUE,"Error Encountered",errmsg)
 #EndFunction

Return
;=====================================================

Using Tony's change fixes that particular issue for the config.xml. But, now another test. Save the following as  Inventory.xml
<?xml version="1.0" encoding="UTF-8"?>
<Inventory>
  <Roles>
    <Role Name="VirtualMachinePowerUser" Label="Virtual machine power user (sample)" Summary="Provides virtual machine interaction and configuration permissions">
      <Privilege Name="Datastore.Browse" />
      <Privilege Name="Global.CancelTask" />
      <Privilege Name="ScheduledTask.Create" />
    </Role>
    <Role Name="VirtualMachineUser" Label="Virtual machine user (sample)" Summary="Provides virtual machine interaction permissions">
      <Privilege Name="Global.CancelTask" />
      <Privilege Name="ScheduledTask.Create" />
    </Role>
  </Roles>
</Inventory>

Running with my updated script or Jim's original script and the <Privilege> nodes and attributes are ignored. Lots of confusion with Elements / childnodes / attributes / values / Text / innerText ......

Stan - formerly stanl [ex-Pundit]

JTaylor

I had put in a ElseIf instead of just Else.  Also, a minor tweak to get_attribtues()

 #DefineFunction Rec_XML(lists, output, nodeTypes, level)
   attrs =  Get_Attributes(lists)
   output := lists.NodeName : " - " : nodeTypes[lists.NodeType] : attrs : @CRLF: @CRLF
   ForEach listNode In lists.ChildNodes
     If listNode.ChildNodes.Length > 1 Then
       output = Rec_XML(listNode, output, nodeTypes, level+1)  ; Process children first
     Else 
       attrs =  Get_Attributes(listNode)
       output := StrFill(" ",level*4):listNode.NodeName : "," : listNode.Text  : " - "  : nodeTypes[listNode.NodeType] : attrs : @CRLF :@CRLF
     EndIf
   Next 
   Return output
 #EndFunction

 #DefineFunction Get_Attributes(node)
   retval = ""
   atts = node.attributes
   If atts == 0 Then Return ""
   length = atts.Length
   If length > 0
     For i=0 to length-1
       att = atts.Item(i)
       name = att.Name
       value = att.Value
       retval := name:"=":value:","
     Next
     retval = "  (":ItemRemove(-1,retval,","):")"
   EndIf
   Return retval
 #EndFunction

spl

Below is very close to the semi-csv output, with some redundancy - especially with Inventory.xml (but can easily move to Excel or a db and eliminate dupes). Abandons the Rec_XML() function in favor of a foreach loop based on XDoc.SelectNodes("//*"), while still checking for an initial xml declaration node with attributes. Found this works with both the config.xml and Inventory.xml data.
[updated script]
[EDIT] added additional column for type (i.e. Node, Attribute, Textr)

;Winbatch 2025A - iterate/parse xml nodelist
;now uses loop based on XDoc.SelectNodes("//*")
;Stan Littlefield (who to blame)
;3/18/2025
;=====================================================
gosub udfs
IntControl(73,1,0,0,0)

nodeTypes = $"0=None
1=ELEMENT_NODE
2=ATTRIBUTE_NODE
3=TEXT_NODE
4=CDATA_SECTION_NODE
5=ENTITY_REFERENCE_NODE
6=ENTITY_NODE
7=PROCESSING_INSTRUCTION_NODE
8=COMMENT_NODE
9=DOCUMENT_NODE
10=DOCUMENT_TYPE_NODE
11=DOCUMENT_FRAGMENT_NODE
12=NOTATION_NODE
13=WHITESPACE
14=SIGNIFICANTWHITESPACE
15=ENDELEMENT
16=ENDENTITY
17=XMLDECLARATION$"
nodeTypes= MapCreate(nodeTypes,'=',@lf)
;select file to process
file = 'c:\temp\config.xml'
;file = 'c:\temp\Inventory.xml'
if !fileexist(file) then Terminate(@TRUE, "Exiting", "File Not Found:":file)
XDoc = CreateObject("Msxml2.DOMDocument.6.0")  ;or just Msxml2.DOMDocument
XDoc.async = @False 
XDoc.validateOnParse = @False
XDoc.Load(file)
;initialize output variable - comma separated with 3 columns
output="Item,Value,Type":@CRLF
dtype = XDoc.ChildNodes.item(0)
;check if file begins with xml declaration
;use the nodeTypes map to include the description of nodes
if  nodeTypes[dtype.NodeType] <> 1
   output := dtype.BaseName:",":nodeTypes[dtype.NodeType]:",":"Node":@CRLF
   output := Get_Attributes(dtype,xDOC)
endif
nodes = XDoc.SelectNodes("//*")
foreach node in nodes   
   if node.ChildNodes.Length >1
      ;iterate child nodes
      ForEach listNode In node.ChildNodes
         output := listNode.BaseName:",":nodeTypes[listNode.NodeType]:",":"Node":@CRLF
         output := Get_Attributes(listNode,xDOC)
         output := listNode.BaseName : "," : listNode.Text:",":"Text":@CRLF 
      Next
   elseif node.ChildNodes.Length <= 1  ;length could be 0
      output := node.BaseName:",":nodeTypes[node.NodeType]:",":"Node":@CRLF
      output := Get_Attributes(node,xDOC)
      output := node.BaseName : "," : node.Text:",":"Text":@CRLF 
   endif
next
Message(file:" Parsed",output)
Exit

:WBERRORHANDLER
XDoc = 0
geterror()
Terminate(@TRUE,"Error Encountered",errmsg)
;=====================================================

:udfs
#DefineSubRoutine geterror()
   wberroradditionalinfo = wberrorarray[6]
   lasterr = wberrorarray[0]
   handlerline = wberrorarray[1]
   textstring = wberrorarray[5]
   linenumber = wberrorarray[8]
   errmsg = "Error: ":lasterr:@LF:textstring:@LF:"Line (":linenumber:")":@LF:wberroradditionalinfo
   Return(errmsg)
#EndSubRoutine


#DefineFunction Get_Attributes(node,xDOC)
   IntControl(73,1,0,0,0)
   retval = ""
   atts = node.attributes
   length = atts.Length
   If length > 0
     For i=0 to length-1
       att = atts.Item(i)
       name = att.Name
       value = att.Value
       retval = retval:name:",":value:"Attribute":@CRLF
     Next
   EndIf
   Return retval
   :WBERRORHANDLER
   XDoc = 0
   geterror()
   Terminate(@TRUE,"Error Encountered",errmsg)
 #EndFunction

Return
;=====================================================
Stan - formerly stanl [ex-Pundit]

spl

and still more... Going to re-visit the code from previous post. If you look at the inventory.xml you will notice <Privilege Name="ScheduledTask.Create" /> is duplicated under 2 roles each with a different name attribute. Dealing with that and other headaches from duplicates or missing data in other tests, I am going back to a for loop based on item(#)

nodes = XDoc.SelectNodes("//*")
nodelist = ""
for i=0 to nodes.length -1
   nodelist := nodes.item(i).BaseName:"(":i:"),":nodeTypes[nodes.item(i).NodeType]:@LF
   ;then build output from parsing attributes/values from each item
Next

Just displaying nodelist, using the item(#) should hopefully separate duplicate attributes or text (i.e. an xml with multiple <Lastname>Smith</Lastname>. Haven't fully re-coded and want to avoid more version[n] posts. However, if someone reading thinks in advance that the updated loop is futile... let me know.

So
Stan - formerly stanl [ex-Pundit]