Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! When I work on a computer, I often run many commands. I generally use Run from the Start menu to launch these programs. When I am finished, I use a script I copied from the Scripting Guys archive to display all the commands that I ran and then I save them in a Notepad file. However, that script no longer works on Windows 7. Because Windows 7 contains Windows PowerShell 2.0, I am wondering if you could convert that old VBScript script to Windows PowerShell for me. Pretty please?

-- KE

Hey, Scripting Guy! AnswerHello KE,

Microsoft Scripting Guy Ed Wilson here. It is the day after Thanksgiving in the United States as I am answering your e-mail. I feel a little sluggish due to an overdose of carbs. I have had two pots of English Breakfast tea this morning, and I still feel sleepy.

But, KE, your e-mail intrigued me, so it pulled me to action. The script you refer to reads the registry and performs a little cleanup of the data that is contained there. The ReadMostRecentlyRunCommands.vbs script from that post is seen here.

ReadMostRecentlyRunCommands.vbs

Const HKEY_CURRENT_USER = &H80000001
 
strComputer = "."
 
Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")

strKeyPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU"
objRegistry.EnumValues HKEY_CURRENT_USER, strKeyPath, arrValueNames, arrValueTypes
 
For Each strValue in arrValueNames
    If Len(strValue) = 1 Then
        objRegistry.GetStringValue HKEY_CURRENT_USER,strKeyPath,strValue,strRunCommand
        intLength = Len(strRunCommand)
        strRunCommand = Left(strRunCommand, intLength - 2)
        Wscript.Echo strRunCommand
    End If  
Next

There is no magic in Windows PowerShell. If the VBScript script will not work, rewriting it in Windows PowerShell will not work either. To fix something that was broken because of a change in the operating system, you would need to discover a new approach. When I ran the ReadMostRecentlyRunCommands.vbs script, it returned the error seen here:

Image of error returned by script


The “Object not a collection” error occurs in VBScript when the script tries to use For…Each…Next to walk through something that either does not exist or is a single item. The error is displayed because the object is not a collection or an array. When I checked the registry, I saw that the RunMRU registry key was empty. This is shown here:

Image of empty RunMRU registry key


Then it dawned on me: In Windows 7 (as in Windows Vista), the Start/Run menu item is not enabled because you use the Search Programs and Files box to launch programs. To add the Run command, right-click the Start button, click Properties, click Customize, and select the Run command check box (near the bottom of the list). This is shown here:

Image of selecting the Run command check box


After the Run command is added and you run a couple of programs, the ReadMostRecentlyRunCommands.vbs script will work. KE, now we can set about translating your VBScript script into Windows PowerShell.

Rather than use the WMI StdRegProv WMI class that was used in the ReadMostRecentlyRunCommands.vbs script, we will use the Windows PowerShell Registry provider. Instead of using the Len and Left string manipulation functions, we will use regular expressions. The net result is the GetMostRecentlyRunPrograms.ps1 script.

GetMostRecentlyRunPrograms.ps1

$MRUregKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\runmru"
Get-Itemproperty $MRUregKey |
ForEach-Object {
 $_.psbase.members |
 Where-Object { $_.name.length -eq 1 } |
 ForEach-Object { [regex]::match($_.value,"\w*").value }
} #end foreach-object

The GetMostRecentlyRunPrograms.ps1 script begins by assigning the registry key to the $MRUregKey variable. The HKCU:\ Windows PowerShell drive is rooted in the HKEY_CURRENT_USER registry hive. The Get-ItemProperty cmdlet returns all of the values contained in the registry key. In reality, the information about the previously run commands is contained in the results of this one-line command. However, the display is not very clean. This is shown here:

PS C:\> Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\runmru"


PSPath       : Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer
               \runmru
PSParentPath : Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer
PSChildName  : runmru
PSDrive      : HKCU
PSProvider   : Microsoft.PowerShell.Core\Registry
a            : notepad\1
MRUList      : cba
b            : calc\1
c            : regedit\1



PS C:\>

As you can see, the registry values a, b and c contain the most recently run programs. The MRUList value keeps track of each of the entries to avoid duplication. If you run Notepad five times, there will still only be one entry for Notepad: item a.

The results of the Get-ItemProperty cmdlet are piped down the pipeline for further processing. This is shown here:

Get-Itemproperty $MRUregKey |

Because there could be multiple registry values to be processed, the ForEach-Object cmdlet is used. The code block opens with a curly bracket; it is important to remember to close the curly bracket when all the code is written. To keep from forgetting to close the curly bracket, I always type the closing bracket at the same time I type the opening bracket. This is shown here:

ForEach-Object {

}

The action we wish to perform on each item in the collection is placed between the curly brackets. Because the name of the property is not directly accessible from the custom Windows PowerShell object that is created by the Get-ItemProperty cmdlet, it is necessary to reference the underlying Windows PowerShell object. To do this, use the psbase property and choose the members of that base object. We pipe these for further processing. This is seen here:

$_.psbase.members |

Now we want to choose property names that are only one character in length. We can access the name property from the collection of members of the psbase object. This is done by using the $_ automatic variable. The $_ automatic variable refers to the current item on the pipeline. The name property has the length property. If the name is one character in length, it gets piped down the pipeline for further processing. This is shown here:

Where-Object { $_.name.length -eq 1 } |

It is now time to remove the backward slash (\) and the number character (#). To do this, we once again need to use the ForEach-Object cmdlet because we want to walk through all the one-character property names that were found in the previous command. We again use the $_ automatic variable to refer to the current object on the pipeline, and this time we use the [regex] type accelerator to allow us to find objects that match the regular expression pattern “\w*”. The \w pattern will match lowercase letters, uppercase letters, and numbers. But because the numbers, in our example are separated by the backward slash, our pattern will work. To test the pattern, you can work interactively on the Windows PowerShell console. This is seen here:

PS C:\> $a = "notepad\1"
PS C:\> [regex]::match($a,"\w*")


Groups   : {}
Success  : True
Captures : {}
Index    : 0
Length   : 0
Value    :



PS C:\>

The completed ForEach-Object command is shown here:

ForEach-Object { [regex]::match($_.value,"\w*").value }

When the script runs, the following output is displayed on my computer:

PS C:\Users\ed.NWTRADERS> C:\data\ScriptingGuys\GetMostRecentlyRunPrograms.ps1
notepad
calc
regedit

KE, that is all there is to using the registry to retrieve a listing of the most recently run programs. Registry Week will continue tomorrow when we will talk about…wait a minute.

If you want to know exactly what we will be looking at tomorrow, follow us on Twitter or Facebook. If you have any questions, send e-mail to us at scripter@microsoft.com or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys