rund
rund

Reputation: 21

Windows/Batch: Extracting specific registry key from REG QUERY based on search string

my goal is to read a key value from the registry, which shows me the patch of the used SSH/Telnet client (most likely Putty) of the user. On Windows XP I could use:

@echo off
FOR /F "tokens=1 delims=    " %%A IN ('REG QUERY "HKEY_CURRENT_USER\Software\Microsoft\Windows\ShellNoRoam\MUICache" ^| findstr "SSH"') DO SET putty=%%A
FOR /F "tokens=1 delims=" %%A IN ('echo %putty%') DO SET putty=%%A
start "Putty" "%putty%" "my.domain.com" "myport" -telnet

On Windows 10 the MUICache path changed to HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache, which is not the problem.

The problem is, that on Windows XP the result rows of REG QUERY where separated by TAB, in Windows 10 the separation is done by using 4 space characters.

Lets say the Result line looks like...:

     C:\FirstName LastName\Putty\putty_0.77.exe.FriendlyAppName    REG_SZ    SSH, Telnet, Rlogin, and SUPDUP client

Note: It might contains spaces in the key(path) ... how can I extract the path and assign it to a variable called "putty"?

With perl regex this would be simple to match... /^\s*(.*?)\.FriendlyAppName/ ... but I can only rely on what is already installed on the user system.

Upvotes: 2

Views: 261

Answers (3)

rund
rund

Reputation: 21

I took the approach from Magoo to first save the tail to a new variable and then replace it in the export line.

The final script I build looks now as follows:

@ECHO OFF
setlocal enableExtensions disableDelayedExpansion
for /f "tokens=4-7 delims=[.] " %%A in ('ver') do (if %%A==Version (set winver=%%B.%%C) else (set winver=%%A.%%B))

if "%winver%" == "5.1" (
   for /f "tokens=1 delims= " %%A in ('REG QUERY "HKEY_CURRENT_USER\Software\Microsoft\Windows\ShellNoRoam\MUICache" ^| findstr "SSH"') do (
      set putty=%%A
   )
) else (
   for /f "tokens=* delims=" %%A in ('REG QUERY "HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache" ^| findstr "SSH"') do (
      set mui_cache_export=%%A
   )
)
if defined mui_cache_export (
   set "mui_cache_tail=%mui_cache_export:*.FriendlyAppName=%"
)
if defined mui_cache_tail (
   call set "putty=%%mui_cache_export:.FriendlyAppName%mui_cache_tail%=%%"
)

if defined putty (
   for /f "tokens=1 delims=" %%A in ('echo %putty%') do set putty=%%A
) else (
   @echo Could not find MUI cache entry - trying default...
   if exist "%ProgramFiles%\PuTTY\putty.exe" (
      set "putty=%ProgramFiles%\PuTTY\putty.exe"
   )
)

if defined putty (
   @echo Starting putty...
   start "Putty" "%putty%" "my.domain.com" "my.port" -telnet
) else (
   @echo Could not find putty.exe - please start Putty one time manually to create MUI cache entry or place putty.exe in '%ProgramFiles%\PuTTY\' ^(default^).
)
endlocal

I kept compatibility with 32bit-XP even, if it is not really needed. I also added a default path that is checked if no MUI cache entry exist (in my tests with Win11 I experienced that it is not set from all paths in which putty.exe can reside, maybe it's a policy thing)

What I currently still don't understand is, that I have to close all brackets until a 'set' statement takes effect, so I had to build new global if checks for set commands that rely on earlier set command results. But it's OK for now, it works on WinXP (32 bit), Win10 and Win11.

Upvotes: 0

Magoo
Magoo

Reputation: 80073

Given your response report, noting that I placed it in a file named u:\your files\q78837934.txt which suits my system and if the filename does not contain separators like spaces, then both usebackq and the quotes around %filename1% can be omitted.

@ECHO OFF
SETLOCAL

SET "sourcedir=u:\your files"
SET "filename1=%sourcedir%\q78837934.txt"

FOR /f "usebackqdelims=" %%e IN ("%filename1%") DO SET "putty=%%e"
SET "tail=%putty:*.FriendlyAppName=%"
CALL SET "putty=%%putty:%tail%=%%"
FOR /f "tokens=*" %%e IN ("%putty%") DO SET "putty=%%~dpne"
ECHO putty="%putty%"
GOTO :EOF

Set the required response in putty, remove the string up to .FriendlyAppName and place in tail, then remove tail from putty and finally treat putty as a string, use tokens=* to defeat tokenising, interpret as a full filename, removing the extension .FriendlyAppName and display result.

I prefer to avoid ADFNPSTXZ (in either case) as metavariables (loop-control variables) 
ADFNPSTXZ are also metavariable-modifiers which can lead to difficult-to-find bugs 
(See `for/f` from the prompt for documentation)

Upvotes: 2

mklement0
mklement0

Reputation: 438813

The most robust solution indeed requires PowerShell; you can place the following in a PowerShell script (.ps1) and execute it from a PowerShell session:

# ->, e.g.: 'C:\FirstName LastName\Putty\putty_0.77.exe'
((Get-Item 'HKCU:\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache').GetValueNames().
  Where({ $_ -like '*ssh*' }, 'First') -replace '\.[^.]+$')[0]
  • Get-Item is used to retrieve the target registry key as a [Microsoft.Win32.RegistryKey] instance.

  • .GetValueNames() returns an array of the names of the key's values.

  • The intrinsic .Where() method is used to extract the first name that contains substring ssh, using a wildcard expression with the -like operator.

  • The last .-prefixed component (.FriendlyName) is then stripped from the resulting name, using the regex-based -replace operator.

  • [0] is used to return the result as a single string.

    • This is necessary, because .Where() always returns an array-like list, even with just one result (or none); similarly, applying -replace to an array-like LHS makes it return a regular PowerShell array (of type System.Object[]), so [0] is needed to extract the string stored in the - first and only - result array.

If you want to stick with a batch file, you can call the same command via powershell.exe, the Windows PowerShell CLI, and parse the output via for /f, as in your attempt; note, however, that the startup cost of the PowerShell CLI is noticeable:

@echo off

for /f "usebackq delims=" %%p in (`
  powershell.exe -NoProfile -Command "(Get-Item 'HKCU:\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache').GetValueNames().Where({ $_ -like '*ssh*' }, 'First') -replace '\.[^.]+$'"
`) do set "putty=%%p"

:: Sample command to verify the path returned.
echo Putty's executable path: [%putty%]

Note:

  • Since no script files are called from the PowerShell code passed to -Command, there is no need to use an -ExecutionPolicy Bypass argument in this case (though specifying it will do no harm):

    • You only need -ExecutionPolicy Bypass:
      • If a script file (.ps1) involved, either via the -File CLI parameter, or via a call from code passed to -Command.
      • If code passed to -Command (possibly implicitly) loads a script module (.psm1).
      • If code passed to -Command loads a module that has formatting and type-extension files (*.Format.ps1xml and *.Types.ps1xml).
      • However, note that -ExecutionPolicy Bypass will not work if a GPO (Group Policy Object) prevents script execution; see this answer for more information.
  • The [0] part from the original command is omitted above, because it isn't necessary, given that outputting a single-element array as itself vs. its only element directly produce the same stdout output as seen by the batch file.

Upvotes: 1

Related Questions