ThDub
ThDub

Reputation: 1

Find a specific string after a defined string

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:

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
    }
}

Upvotes: 0

Views: 117

Answers (2)

Reino
Reino

Reputation: 3433

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 Xidel:

xidel -s input.xml --xquery "for $x in (//Product,//Flavour) order by $x/SNPID return $x/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 the before mentioned siblings and join them seperated by a "|".

At the moment I can't help you with the 4th element "| Library Path", simply because I don't have those Registry keys.

...and finally align columns.

This is a bit more difficult, but it's possible. It basically comes down to:
- Put the output above in a series of arrays and assign to a variable.
- Calculate the maximum width of each 'column'.
- Add white-space to each value to reach this width.

xidel -s input.xml --xquery ^"^
  let $a:=for $x in (//Product,//Flavour)^
      order by $x/SNPID^
      return $x/[^
        SNPID,^
        Name,^
        (RegKey,../RegKey)^
      ],^
      $b:=(1 to count($a)) ! max(^
        $a(.) ! string-length()^
      )^
  return^
  $a ! join(^
    for $x at $i in .() return^
    substring(^
      $x^|^|string-join((1 to $b[$i]) ! ' '),^
      1,$b[$i]^
    ),^
    ' ^| '^
  )^
"
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

This is a 'prettified' query with the necessary escape-characters (at the end of each line and for every |) which you can copy-paste directly at the command-prompt.

See also this online xidelcgi demo.

Upvotes: 1

Aacini
Aacini

Reputation: 67216

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

Upvotes: 0

Related Questions