0

I'm trying generating a .txt report based on .xml files for installed libraries. It should report Library SNPID Number| Library Name | Registry Key | Library Path. Sort by ascending number and finally align columns.

Main problem is script works when .xml file has only one SNPID and one Name but not when .xml includes multiple SNPID and Names

Some hints:

  • Searched strings/lines in .xml files ALWAYS have appending spaces
  • SNPID can also have letters.
  • Inside <producthint> </producthint> we can have multiple <product>
  • Inside <product> </product> we can have multiple <flavour> </flavour>
  • The order is always the same, Name is 1st: <Name></Name> <SNPID></SNPID>
  • But Name/SNPID can be either inside <product> </product> or inside <flavour> </flavour>
  • When there is the <flavour> value inside <product>, Product's RegKey is same for all flavours.

Here's my code so far...

@echo off
pushd "%~dp0"
setlocal
set "Backup_Folder=%~dp0NI_Backup"
set "SNPID_List=%Backup_Folder%\SNPID_Report.txt"
set "SNPID_TMP_List_To_Order=%Backup_Folder%\SNPID_TMP_List_To_Order.txt"
set "SNPID_TMP_List_To_Realign=%Backup_Folder%\SNPID_TMP_List_To_Realign.txt"
set "XML_Dir=C:\Program Files\Common Files\Native Instruments\Service Center"
set "Registry_Key=HKLM\SOFTWARE\Native Instruments"
if not exist "%Backup_Folder%" ( mkdir "%Backup_Folder%" >nul 2>&1 )
setlocal EnableDelayedExpansion
REM Loop through .xml
(
    for /f "delims=" %%a in ('dir /s/b/a-d "%XML_Dir%\*.xml"^| find /v "NativeAccess" ^| find /v "ProductHints" ^| find /v "Maschine 2"') do (
        for /f "usebackqtokens=1-3delims=<>" %%E in ("%%a") do (
            if "%%F"=="SNPID" (
                for /f "usebackqtokens=1-3delims=<>" %%I in ("%%a") do (
                    if "%%J"=="Name" (
                        for /f "usebackqtokens=1-3delims=<>" %%M in ("%%a") do (
                            if "%%N"=="RegKey" (
                                for /f  "tokens=2*" %%Q in ('reg query "%Registry_Key%\%%O" /v "ContentDir" 2^>nul ') do (
                                    set "ContentDir=%%R"
                                    if "!ContentDir:~1,2!"==":\" ( echo %%G ^| %%K ^| %Registry_Key%\%%O ^| %%R )
    ))))))))
)>"%SNPID_TMP_List_To_Order%"
REM Rename Paths ending with backslash
call "%~dp0Jrepl.bat" "(.*)\\$" "$1" /xseq /m /f "%SNPID_TMP_List_To_Order%" /o -
REM Remove duplicates
call "%~dp0Jrepl.bat" "\c([\c\r\n]+)\r?\n(?=[\s\S]*\c\1$)" "" /xseq /m /f "%SNPID_TMP_List_To_Order%" /o -
REM echo column title
echo SNPID^| Library Name^| Registry Key^| Library Location>"%SNPID_TMP_List_To_Realign%"
REM Sort by SNPID number
sort <"%SNPID_TMP_List_To_Order%" >>"%SNPID_TMP_List_To_Realign%"
REM Get Columns length
set "SNPID_MaxLength=0"
set "LibName_MaxLength=0"
set "RegKey_MaxLength=0"
for /f "usebackqtokens=1-3 delims=|" %%a in ("%SNPID_TMP_List_To_Realign%") do (
    set "String=%%a" & call :strlen
    for /f "tokens=* delims=0" %%B in ("!result!") do (
        if %%B gtr !SNPID_MaxLength! set "SNPID_MaxLength=%%B"
    )
    set "String=%%b" & call :strlen
    for /f "tokens=* delims=0" %%B in ("!result!") do (
        if %%B gtr !LibName_MaxLength! set "LibName_MaxLength=%%B"
    )
    set "String=%%c" & call :strlen
    for /f "tokens=* delims=0" %%B in ("!result!") do (
        if %%B gtr !RegKey_MaxLength! set "RegKey_MaxLength=%%B"
    )
)
REM Set Columns Spacing
set "Space_Count=%SNPID_MaxLength%"
set "SNPID_Space= "
set "LibName_Space= "
set "RegKey_Space= "

:SNPID_Spacing
if "%Space_Count%"=="0" ( set "Space_Count=%LibName_MaxLength%" & goto :LibName_Spacing )
set "SNPID_Space=%SNPID_Space% "
set /a "Space_Count-=1"
goto :SNPID_Spacing

:LibName_Spacing
if "%Space_Count%"=="0" ( set "Space_Count=%RegKey_MaxLength%" & goto :RegKey_Spacing )
set "LibName_Space=%LibName_Space% "
set /a "Space_Count-=1"
goto :LibName_Spacing

:RegKey_Spacing
if "%Space_Count%"=="0" ( goto :Realign_Columns )
set "RegKey_Space=%RegKey_Space% "
set /a "Space_Count-=1"
goto :RegKey_Spacing

REM Columns Alignment
:Realign_Columns
(
    for /f "usebackqtokens=1-4 delims=|" %%a in ("%SNPID_TMP_List_To_Realign%") do (
        set "SNPID_Aligned=%%a%SNPID_Space%" & set "SNPID_Aligned=!SNPID_Aligned:~0,%SNPID_MaxLength%!"
        set "LibName_Aligned=%%b%LibName_Space%" & set "LibName_Aligned=!LibName_Aligned:~0,%LibName_MaxLength%!"
        set "RegKey_Aligned=%%c%RegKey_Space%" & set "RegKey_Aligned=!RegKey_Aligned:~0,%RegKey_MaxLength%!"
        echo !SNPID_Aligned!^|!LibName_Aligned!^|!RegKey_Aligned!^|%%d
    )
)>"%SNPID_List%"
endlocal
REM del "%SNPID_TMP_List%" "%SNPID_TMP_List_To_Order%" "%SNPID_TMP_List_To_Realign%"
pause & exit /b

:strlen
(
    (set^ tmp=!String!)
    set "len=1"
    for %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
        if "!tmp:~%%P,1!" NEQ "" (
            set /a "len+=%%P"
            set "tmp=!tmp:~%%P!"
    ))
)
(
    set "result=!len!"
    exit /b
)

(I simplified the .xml files here)

<?xml version="1.0" encoding="UTF-8"?>
<ProductHints>
  <Product version="3">
    <Name>Battery 4</Name>
    <Company>Company Name</Company>
    <some value>xxx</some value>
    <RegKey>Battery</RegKey>
    <some value>yyy</some value>
    <BingName>Battery 4</BingName>
    <SNPID>249</SNPID>
  </Product>
</ProductHints>

will give:

249 | Battery 4 | Battery | PATH

while

<?xml version="1.0" encoding="UTF-8"?>
<ProductHints>
  <Product version="3">
    <Name>Maschine 2</Name>
    <Company>Company Name</Company>
    <SNPID>334165166167</SNPID>
    <RegKey>Maschine 2</RegKey>
    <Flavour>
      <Name>Maschine 2</Name>
      <SNPID>334</SNPID>
      <Value>0</Value>
    </Flavour>
    <Flavour>
      <Name>Maschine 2 Essentials</Name>
      <Value>-1</Value>
      <SNPID>165</SNPID>
    </Flavour>
    <Flavour>
      <Name>Another Flavour</Name>
      <SNPID>166</SNPID>
      <some value>yyy</some value>
    </Flavour>
    <Flavour>
      <Name>Another Flavour2</Name>
      <some value>yyy</some value>
      <SNPID>167</SNPID>
    </Flavour>
  </Product>
  <Product version="3">
    <Name>Battery 4</Name>
    <Company>Company Name</Company>
    <some value>xxx</some value>
    <RegKey>Battery</RegKey>
    <some value>yyy</some value>
    <BingName>Battery 4</BingName>
    <SNPID>249</SNPID>
  </Product>
</ProductHints>

will give:

165    | Maschine 2
166    | Maschine 2
167    | Maschine 2
334    | Maschine 2
249    | Maschine 2           
165    | Maschine 2 Essentials
166    | Maschine 2 Essentials
167    | Maschine 2 Essentials
334    | Maschine 2 Essentials
249    | Maschine 2 Essentials
165    | Another Flavour
166    | Another Flavour
167    | Another Flavour
334    | Another Flavour
249    | Another Flavour
etc...

but should be only:

165 | Maschine 2
334 | Maschine 2 Essentials
249 | Battery 4
166 | Another Flavour
167 | Another Flavour2

optionally, also
334165166167| Maschine 2
or without... (would like to see both outputs)

Meaning:

Always associate SNPID with its NAME inside <Product> </Product>, unless there is a <flavour> value inside <Product></Product> then I associate SNPID with the NAME which is inside <flavour> </flavour>.

Omiting first NAME in the case there is flavour, or not...I would need to look the two different parsed output to choose.

NAME is always above its SNPID number but there can be some values in between (and a different number of lines)

When there the flavour value is present, flavour RegKey is Product Regkey

Update: Updated question with PS parsing try, it's working well but I can't solve the "flavour thing", too hard for me...also can't output in UTF8NOBOM

"DummyLine" | Out-File "$PSScriptRoot\Parsed_List.txt" -Encoding UTF8
$items = Get-ChildItem "C:\Program Files\Common Files\Native Instruments\Service Center\*.xml"
foreach ($item in $items) {
    [xml]$XML_File = Get-Content $item
    $XML_File.ProductHints.Product | % {
        $Name = $_.Name
        $RegKey = $_.RegKey
        If (-Not $_.SNPID) {$SNPID = "ThirdParty"} Else {$SNPID = $_.SNPID}
        If (-Not $_.Company) {$Company = "Not specified"} Else {$Company = $_.Company}
        If ($SNPID -eq "334165") {$Name = "Maschine 2 Essential";$SNPID = "165"}
        "$SNPID`|$Name`|$Company`|$RegKey`|Location" | Out-File "$PSScriptRoot\Parsed_List.txt" -append -Encoding UTF8
    }
}
2
  • Well, do not use a batch file for parsing XML data, use a language (like powershell, for instance) that natively supports such data formats... Commented Oct 19, 2019 at 10:39
  • Thanks, I updated the question with my try with powershell...it's parsing correctly, but still struggling with the <flavour></flavour> thing, and output in UTF8BOM instead of UTF8, so I need to output a first dummy line because of that strange first character in batch after, then execution from batch script is slow also (compared to for /f loops ) Commented Oct 23, 2019 at 23:11

2 Answers 2

1

I'm trying generating a .txt report based on .xml files for installed libraries. It should report Library SNPID Number| Library Name | Registry Key | Library Path. Sort by ascending number...

I'm not very familiar with PowerShell and its capabilities, but because of Batch its limitations it's a very bad idea to parse XML with native Batch functions. I guess you've seen for yourself how complex a Batch script can become. And it's even worse to parse XML with regex (no offence to Dave Benham's amazing work with 'jrepl.bat').

Please use a true XML parser like instead:

xidel -s "input.xml" -e "for $node in (//Product,//Flavour) order by $node/SNPID return $node/join((SNPID,Name,(RegKey,../RegKey)),' | ')"
xidel -s "input.xml" -e ^"^
  for $node in (//Product,//Flavour)^
  order by $node/SNPID^
  return^
  $node/join((SNPID,Name,(RegKey,../RegKey)),' ^| ')^
"
165 | Maschine 2 Essentials | Maschine 2
166 | Another Flavour | Maschine 2
167 | Another Flavour2 | Maschine 2
249 | Battery 4 | Battery
334 | Maschine 2 | Maschine 2
334165166167 | Maschine 2 | Maschine 2

If I understand your question correctly, this is what you're looking for:

  • Grab the "Product"- as well as the "Flavour"-element-node and sort by "SNPID".
  • Grab specific children of the before mentioned element-nodes and string-join them together seperated by a |.

I can't help you with the 4th element "| Library Path", simply because I don't have those Registry keys, but perhaps Xidel's own system() function can help you with that.

...and finally align columns.

This is a bit more difficult, but it's possible all with Xidel.

Turn the initial output into a sequence of JSON arrays:

xidel -s "input.xml" -e "for $node in (//Product,//Flavour) order by $node/SNPID return $node/array{(SNPID,Name,(RegKey,../RegKey))}"
["165", "Maschine 2 Essentials", "Maschine 2"]
["166", "Another Flavour", "Maschine 2"]
["167", "Another Flavour2", "Maschine 2"]
["249", "Battery 4", "Battery"]
["334", "Maschine 2", "Maschine 2"]
["334165166167", "Maschine 2", "Maschine 2"]

Calculate the maximum width of 'column' 1 and 2:

xidel -s "input.xml" -e "let $data:=for $node in (//Product,//Flavour) order by $node/SNPID return $node/array{(SNPID,Name,(RegKey,../RegKey))} return (1 to array:size($data[1]) - 1) ! max($data(.) ! string-length())"
12
21

Add white-space to each value to reach this width and string-join everything together seperated by a |:

xidel -s "input.xml" -e "let $data:=for $node in (//Product,//Flavour) order by $node/SNPID return $node/array{(SNPID,Name,(RegKey,../RegKey)) ! text()},$len:=(1 to array:size($data[1]) - 1) ! max($data(.) ! string-length()) for $array in $data return join(for $mem at $pos in $array() return ($mem||string-join((1 to $len[$pos] - string-length($mem)) ! ' ')),' | ')"
xidel -s "input.xml" -e ^"^
  let $data:=for $node in (//Product,//Flavour)^
        order by $node/SNPID^
        return^
        $node/array{(SNPID,Name,(RegKey,../RegKey))},^
      $len:=(1 to array:size($data[1]) - 1) ! max(^
        $data(.) ! string-length()^
      )^
  for $array in $data return^
  join(^
    for $mem at $pos in $array() return (^
      $mem^|^|string-join(^
        (1 to $len[$pos] - string-length($mem)) ! ' '^
      )^
    ),^
    ' ^| '^
  )^
"
165          | Maschine 2 Essentials | Maschine 2
166          | Another Flavour       | Maschine 2
167          | Another Flavour2      | Maschine 2
249          | Battery 4             | Battery
334          | Maschine 2            | Maschine 2
334165166167 | Maschine 2            | Maschine 2
Sign up to request clarification or add additional context in comments.

Comments

0

Your description is confusing and incomplete. If a file includes multiple SNPID and Names, do you want to omit one record? Which one? The first one?

Perhaps this code may help you:

@echo off
setlocal

rem Initialize variables
set "i=0"
for /F "delims==" %%a in ('set A_ 2^>NUL') do set "%%a="

rem Process all files
for /F "delims=" %%a in ('dir /S /B /A:-D *.xml') do (

   rem Accumulate records for this file
   setlocal EnableDelayedExpansion
   for /F "usebackq tokens=1-3 delims=<>" %%E in ("%%a") do (
      if "%%F" == "Name" (
         set /A i+=1
         set "A_Name[!i!]=%%G"
      ) else if "%%F" == "RegKey" (
         set "A_RegKey[!i!]=%%G"
      ) else if "%%F" == "SNPID" (
         set "A_SNPID[!i!]=%%G"
         set "A_Order[%%G]=!i!"
         if !i! equ 1 set "first=%%G"
      )
   )

   rem If there are more than 1 records: omit the first one
   if !i! gtr 1 set "A_Order[!first!]="

   rem Show records
   echo ============ File %%a ================
   for /F "tokens=2 delims==" %%i in ('set A_Order[') do (
      echo !A_SNPID[%%i]!   !A_Name[%%i]!
   )
   echo/

   rem Reset all variables
   endlocal

)
pause

Output example:

============ File C:\Users\Antonio\Documents\ASMB\Modern Batch File Programming\BORRAME\File1.xml ================
249   Battery 4

============ File C:\Users\Antonio\Documents\ASMB\Modern Batch File Programming\BORRAME\File2.xml ================
165   Maschine 2 Essentials
334   Maschine 2

1 Comment

Sorry, that was my first post ever here...I realized that. In fact I don't want to omit any Name except if there is a <flavour> value then I would (maybe) omit the Name and SNPID before <flavour> I updated the .xml example and expected result. Thank you anyway, your posts are always great to learn things

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.