Well it is time for another pure powershell post. This time I have been working with the registry. Feels like I have been living in different hives no matter what area I have been working in in the last 15 years. It has become rather pleasant if I could say so compared to the old VBSCRIPT days and before that reg.exe and ini files.
Fist a little history and a discussion. You have a couple of options in regards to reading and/or editing settings in the registry
- Use the built-in PS-drives (gives you access to HKLM and HKCU)
- Codeplex module (search on codeplex or in your favorite searchengine – most used and referenced: http://psrr.codeplex.com/)
- Use the .Net class (normally [microsoft.win32.registrykey]). This gives you access to all the hives in the registry
- Powershell Desired State Configuration (There is a built-in resource to access the registry)
- WMI/CIM
Overall goal:
- Query one or several registry values
- Would be nice to be able to query remote computers? Yes it would.
Function Get-RegistryHive
{
[cmdletbinding()]
Param(
[ValidateSet("LocalMachine","Users","CurrentUser", "CurrentConfig", "ClassesRoot")]
[String] $Hive = "LocalMachine"
)
$f = $MyInvocation.MyCommand.name
$HiveObject = $null
$ErrorActionPreference = "stop"
try
{
Write-verbose "Creating .net object and connecting to hive '$hive'"
$HiveObject = [Microsoft.Win32.RegistryKey]::OpenBaseKey($Hive, "default")
}
catch
{
Write-Verbose "$($_.Message)"
$_.message
}
finally
{
$ErrorActionPreference = "Continue"
}
$HiveObject
}
We are using the advanced function template and I have thrown in a ValidateSet on the only parameter Hive. This gives us intellisence when we type the command, example:
Pretty sweet eh? As you may have noticed, I declare a variable $f in the function. I usually add that to all my function. This way if I need logging, I can write the function name to the debug/verbose stream or to a log file. As a bonus I do not need to worry about updating my script if I chose to change the function name and it gives me the option to create a function template which already has the $f variable declared.
Next we create the $HiveObject and use a static method of the .Net class. If you need to specify a view other than “default” which I have in the script you could set it to “Registry64” or “Registry32”.
Now we have the registry-hive object from the .Net class. Next we need a function to specify which path or location in the registry we want to pickup our value from:
function Get-RegistryKey
{
[cmdletbinding()]
Param(
[Parameter(
Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)
]
[String[]] $RegPath = ""
,
[ValidateSet("LocalMachine","Users","CurrentUser", "CurrentConfig", "ClassesRoot")]
[String] $Hive = "LocalMachine"
)
BEGIN{
$f = $MyInvocation.MyCommand.name
[string] $LoggMsg = ""
Write-Verbose "Begin $f"
}
PROCESS{
foreach ($path in $RegPath)
{
$HiveObject = Get-RegistryHive -Hive $hive
$key = $HiveObject.OpenSubkey($path)
if($key -ne $null)
{
$key
}
else
{
$LoggMsg = "Error - Could not find path '$($HiveObject.Name)\$RegPath'"
Write-Verbose $LoggMsg
throw($LoggMsg)
}
}
}
END{
Write-Verbose "END $f"
}
}
A little more going on here. First the parameters:
- RegPath – Notice this is an array parameter and has the ValueFromPipeline attribute set to True. This way we have our options open if we want to use the pipeline to submit values to our function.
- Hive – Again we specify the Hive parameter with the ValidateSet statement. Notice it is used when we call the other function Get-RegistryHive
Notice I have added no error handling here. It is not very likely that the Get-RegistryHive –Hive $Hive should fail since we have validated the input parameter, however when we use the object returned from Get-RegistryHive and execute the method OpenSubkey we could specify a path that is not valid. For my purpose, it is enough to check if the return value from OpenSubkey is null. You could wrap it in a Try-Catch statement if you like.
Now we were suppose to support registry queries for remote computers and I have not forgotten. Next up is the Get-RegistryValue function and here we need the computername parameter.
Function Get-RegistryValue
{
[cmdletbinding()]
Param(
[Parameter(
Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)
]
[String] $RegPath = ""
,
[ValidateSet("LocalMachine","Users","CurrentUser", "CurrentConfig", "ClassesRoot")]
[String] $Hive
,
[Parameter(Mandatory=$true)]
[String[]] $RegValueName = ""
,
[string] $computername = ""
)
BEGIN{
$f = $MyInvocation.MyCommand.name
[string] $LoggMsg = ""
Write-Verbose "Begin $f"
}
PROCESS{
$ErrorActionPreference = "stop"
Try
{
if($computername -eq "")
{
Write-Verbose "Assuming local registry query"
$key = Get-RegistryKey -Hive $hive -RegPath $RegPath
foreach($keyValue in $RegValueName)
{
$output = "" | select Name, Value
$output.value = $key.GetValue($keyValue)
$output.name = $keyValue
$output
}
}
else
{
Write-Verbose "Assuming remote registry query"
Write-Verbose "Getting helper-functions and creating command"
[string] $functions = Get-Fullcommand -Name "Get-RegistryHive"
$Functions += Get-Fullcommand -Name "Get-RegistryKey"
$Functions += Get-Fullcommand -Name "Get-RegistryValue"
[string] $Command = "Get-RegistryValue -Hive $hive -RegPath '$RegPath' -RegValueName $RegValueName"
if($VerbosePreference -ne "SilentlyContinue")
{
Write-Verbose "VerbosePreferance is turned on, adding verbose option"
$command += " -verbose"
}
else
{
Write-Verbose "VerbosePreferance is turned OFF"
}
Write-Verbose "Command is: $command"
$sb = [scriptblock]::Create("$functions $command")
Write-Verbose "Invoking command on computer '$computername'"
Invoke-Command -ComputerName $computername -ScriptBlock $sb
}
}
catch
{
Write-Verbose "$($_.Message)"
$_.message
}
finally
{
$ErrorActionPreference = "Continue"
}
}
END{
}
}
Parameters:
- RegPath – Used when we request the registry key by running the Get-RegistryKey function
- RegValueName – The name of the Value-element we want to get.
- Computername – The name of the computerobject we want to query
Let’s do the easy scenario first.
We are querying the “HKLM:\Software\Microsoft\DirectX” key and looking for the value ‘Version’ on the local computer. Here is the result:
According to the code in the Get-RegistryValue function, if computername equals “”, it runs the function Get-RegistryKey –Hive $hive –RegPath $RegPath. The return object $key is used in a foreach loop for all valuenames specified in the RegValueName parameter. Nothing fancy and it works. We could throw in some additional error handling if the valuename is not found etc, however in is okay for now.
Now the trick for the remote connection. If you look at the code, I call a function named Get-FullCommand. This function returns the definition of the command/function, here is the listing:
function Get-Fullcommand { [cmdletbinding()] Param( [Parameter(Mandatory=$true)] [string] $Name ) $ErrorActionPreference = "stop" [String] $FullCommand = "" try { [string]$FunctionBody = (Get-Command -Name $Name).ScriptBlock.ToString() if($FunctionBody -ne $null -and $FunctionBody -ne "") { $FullCommand = "Function $name { $FunctionBody }" } else { [string] $msg = "Could not find command '$name'" Write-Verbose $msg throw($msg) } } Catch { Write-Verbose "$($_.Message)" $_.message } Finally { $ErrorActionPreference = "Continue" } $FullCommand }
Now the idea is to use the functions we have that works locally on the “test” computer, collect them in an string variable, create a string variable for the command we are going to run on the remote computer and create a ScriptBlock that is going to be executed on by the Invoke-Command cmdlet. Sounds like a lot of work, but really, it is not.
[string] $Functions = Get-Fullcommand -Name "Get-RegistryHive" $Functions += Get-Fullcommand -Name "Get-RegistryKey" $Functions += Get-Fullcommand -Name "Get-RegistryValue"
First we create a [string] variable to hold the content of the first function ‘Get-RegistryHive’. Then we run the Get-FullCommand for the other 2 functions and adding the content of those functions to the $functions variable.
[string] $Command = "Get-RegistryValue -Hive $hive -RegPath '$RegPath' -RegValueName $RegValueName"
if($VerbosePreference -ne "SilentlyContinue")
{
Write-Verbose "VerbosePreference is turned on, adding verbose option"
$command += " -verbose"
}
else
{
Write-Verbose "VerbosePreferance is turned OFF"
}
We then create a [string] variable for the command we want to execute and use the parameters for the function. We check the $VerbosePreference variable and echo the verbose level for the current scope and add the –verbose switch parameter if VerbosePreference is turned on.
$sb = [scriptblock]::Create("$functions $command") Write-Verbose "Invoking command on computer '$computername'" Invoke-Command -ComputerName $computername -ScriptBlock $sb
Next we create a [scriptblock] variable and use the static method Create and add the content of the $functions variable and the command we want to run. Finally we run the invoke-command cmdlet and specify the computername variable and the scriptblock variable. The final result:
Now, for those of you that is into .Net, you are probably screaming by now, since the class [Microsoft.Win32.RegistryKey] class has a method called OpenRemoteBaseKey that enables you to remotely query registry of another computer. So if you cannot see the nice feature of doing it my way, why don’t you jump right in and use that method in your scripts. I will not blame you, I will wish you the best of luck, however you do not need it because it works :-)
Cheers
Tore
Comments
Post a Comment