Bookmark and Share

 

Q: Hey Scripting Guy! I need to convert a VBScript into Windows PowerShell 2.0. The script reads the first line of all text files that are in a particular folder, and if a specific word is in that file, it moves the text file to an archive folder.

-- AO

A: Hello AO, Microsoft Scripting Guy Ed Wilson here, I have been helping the Scripting Wife get Windows 7 configured on her new laptop. She did a great job doing a fresh install of Windows 7 Ultimate, installing Office 2010, and the antivirus. She ran into problems with getting the drivers updated and flashing the BIOS – go figure. To make matters worse, some the drivers from the laptop maker did not work, and we had to chase around the internet to the component makers web sites to download drivers from them. That is part of the problem with recycling old hardware, but Windows 7 has lower hardware requirements than Windows Vista did. She is happy with the results, and even posted an update to her status on Facebook to that effect. I think I heard her rattling around working on a new Scripting Wife article detailing her experience.

AO, there was a Hey Scripting Guy! article from January of 2007 that was named How Can I Take Action Based on the First Line of a Text file that contains a script similar to the one you describe. You may wish to refer to that article for a discussion of the ReadFirstLineOfTextTakeAction.vbs script that I have copied here.

NOTE: This is the third article in a series of four articles about migrating VBScript code to Windows PowerShell. Other articles in this series appeared earlier in the week.

ReadFirstLineOfTextTakeAction.vbs

Const ForReading = 1
strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery _
("ASSOCIATORS OF {Win32_Directory.Name='C:\fso2'} Where " _
& "ResultClass = CIM_DataFile")
Set objFSO = CreateObject("Scripting.FileSystemObject")
For Each objItem In colItems
Set objFile = objFSO.OpenTextFile(objItem.Name, ForReading)
strLine = objFile.ReadLine
strLine = LCase(strLine)
objFile.Close
If InStr(strLine, "fabrikam.com") or InStr(strLine, "contoso.com") Then
objFSO.MoveFile objItem.Name, "C:\fso3\"
End If
Next

AO, you can certainly convert the ReadFirstLineOfTextTakeAction.vbs script to Windows PowerShell. A translated version of your script is the ReadFirstLineOfTextTakeActionTransFmVBS.ps1 script. It follows the syntax of the original VBScript, uses the same variable names, and is basically the same length. With a few exceptions, the script will make perfect sense to an experienced VBScripter. The complete translated VBScript is seen here.

ReadFirstLineOfTextTakeActionTransFmVBS.ps1

add-type -AssemblyName microsoft.visualbasic
$strings = "microsoft.visualbasic.strings" -as [type]
$forReading = 1
$strComputer = "."
$colItems = Get-WmiObject -Query "ASSOCIATORS OF {Win32_Directory.Name='C:\fso2'} `
Where ResultClass = CIM_DataFile" -computerName $strComputer
$objFSO = New-Object -ComObject scripting.filesystemobject
Foreach($objItem in $colItems)
{
$objFile = $objFSO.OpenTextFile($objItem.Name, $forReading)
$strLine = $objfile.ReadLine()
$strLine = $strings::LCase($strLine)
$objFile.close()
if($strings::instr($strLine, "fabrikam.com") -or $strings::instr($strLine, "contoso.com"))
{$objFSO.MoveFile($objItem.Name,"C:\fso3\")}
}

The one deviation I made from the VBScript was to use the Get-WmiObject Windows PowerShell cmdlet instead of using the GetObject method to access the Winmgmts WMI provider. You can access the GetObject static method from the Microsoft.visualbasic.interaction class. To illustrate this process, I wrote the GetObjectWinmgmts.ps1 script. It begins by adding the Microsoft.visualbasic assembly, and creating an alias for the interaction .NET Framework class. The GetObject method from the interaction class is nearly the same as the VBScript GetObject command. It returns a connection to the WMI namespace in the form of an SwbemServices object. The code is a bit simpler than the corresponding VBScript code because you can avoid concatenation issues due to the way that Windows PowerShell handles expanding strings. This section of the script is seen here.

$objWMIService = $Interaction::GetObject("Winmgmts:\\$strComputer\root\cimv2")

The SwbemServices object contains the execquery method just like it does if it is created via VBScript. Therefore this line of code is nearly the same as the VBScript code.

$colItems = $objWMIService.ExecQuery("ASSOCIATORS OF {Win32_Directory.Name='C:\fso2'}`
Where ResultClass = CIM_DataFile")

The execquery method returns a collection, and therefore the foreach statement can be used to walk through it. One difference between Windows PowerShell and VBScript, is that the properties are accessed via the properties_ collection. But this leads to a cool technique that allows you to display all of the properties and their associated values (which is something that could be done with VBScript). This section of the script is seen here.

Foreach($objItem in $colItems)
{
Foreach($property in $objItem.Properties_)
{ write-host $property.name, $property.value }
}

The complete GetObjectWinmgmts.ps1 script is seen here.

GetObjectWinmgmts.ps1

add-type -AssemblyName microsoft.visualbasic
$Interaction = "microsoft.visualbasic.Interaction" -as [type]
$strComputer = "."
$objWMIService = $Interaction::GetObject("Winmgmts:\\$strComputer\root\cimv2")
$colItems = $objWMIService.ExecQuery("ASSOCIATORS OF {Win32_Directory.Name='C:\fso2'}`
Where ResultClass = CIM_DataFile")
Foreach($objItem in $colItems)
{
Foreach($property in $objItem.Properties_)
{ write-host $property.name, $property.value }
}

By using the Get-WmiObject cmdlet, you simplify the script considerably. It is easy to migrate a complex WMI query into Windows PowerShell by using the –query parameter. See the Hey Scripting Guy! How do I migrate my VBScript WMI Queries to Windows PowerShell blog article for more details on this technique. This section of the script is shown here.

$colItems = Get-WmiObject -Query "ASSOCIATORS OF {Win32_Directory.Name='C:\fso2'} `
Where ResultClass = CIM_DataFile" -computerName $strComputer

Because Windows PowerShell handles WMI so much better, I never use GetObject to connect to WMI in the old scripting style. It is way too much work, and most people did not understand WMI all that well in the first place. Most people relied on the Scriptomatic to generate their WMI code, and I have created a Windows PowerShell version of the Scriptomatic that is very similar to the VBScript one – except that it is written in C# instead of an HTA and therefore it works without modification on Windows Vista and Windows 7.

To gain access to the VBScript functions add a reference to the Microsoft.VisualBasic assembly. To add the assembly in Windows PowerShell 2.0 use the Add-Type cmdlet. The use of the Microsoft.VisualBasic assembly is discussed in Mondays Hey Scripting Guy! article. After the Microsoft.VisualBasic assembly is added, an alias is created for the Microsoft.VisualBasic.Strings .NET Framework class. This is done to make it easier to access the members of the class. This technique is discussed in Tuesdays Hey Scripting Guy! article. Following the two .NET Framework commands, a couple of variables are created. The $forReading variable is used with the the openTextFile method of the FileSystemObject; a value of 1 means open the file for reading, and a value of 2 means to open the file for writing. The $strComputer variable is used with the WMI command. A value of “.” means use the local computer. The first four lines of the script are seen here.

add-type -AssemblyName microsoft.visualbasic
$strings = "microsoft.visualbasic.strings" -as [type]
$forReading = 1
$strComputer = "."

To access the FileSystemObject use the new-object cmdlet to create an instance of it. This is shown here.

$objFSO = New-Object -ComObject scripting.filesystemobject

The remainder of the Windows PowerShell script works pretty much the same way as the VBScript does. This is seen here.

Foreach($objItem in $colItems)
{
$objFile = $objFSO.OpenTextFile($objItem.Name, $forReading)
$strLine = $objfile.ReadLine()
$strLine = $strings::LCase($strLine)
$objFile.close()
if($strings::instr($strLine, "fabrikam.com") -or $strings::instr($strLine, "contoso.com"))
{$objFSO.MoveFile($objItem.Name,"C:\fso3\")}
}

Using Windows PowerShell native commands, the process can be made cleaner. This is seen in the ReadTextTakeAction.ps1 script.

ReadTextTakeAction.ps1

$filenames = @()
$files = Get-ChildItem -Path C:\fso2 -Recurse
foreach($file in $files)
{
if(Get-Content -Path $file.fullname -TotalCount 1 |
Select-String -Pattern "contoso.com", "fabrikam.com")
{ $filenames += $file.fullname }
}
Move-Item -Path $filenames -Destination C:\fso3

The ReadTextTakeAction.ps1 script begins by creating an empty array. It then uses the Get-ChildItem cmdlet to return a list of files from the c:\fso2 folder. These fileinfo objects are stored in the $files variable. The foreach statement is used to walk through the collection of fileinfo objects. The Get-Content cmdlet is used with the –totalcount parameter to retrieve only the first line of the text file. This line is then pipelined to the Select-String cmdlet where the two strings “contoso.com” and “fabrikam.com” are searched for; if found the path to the file is stored in the $filenames array. The move-item cmdlet is used to move the located files to the new location.

If it is ok to search for the strings anywhere inside the text files, the script can be reduced even further. The ReadallTextTakeAction.ps1 script seen here illustrates this.

ReadallTextTakeAction.ps1

$filenames = @()
Select-String -Path C:\fso2\* -Pattern "contoso.com", "fabrikam.com" |
ForEach-Object {
$filenames += $_.path
}
Move-Item -Path $filenames -Destination C:\fso3

The key to this optimization is the fact that the Select-String cmdlet will examine an entire folder for a pattern. The path property of the matchinfo object that is returned contains the path to the file that contains the match. Unfortunately, you cannot use the syntax seen in ReadTextTakeActionDoesNotWork.ps1 because the file is still in use when Move-Item attempts to move the file.

ReadTextTakeActionDoesNotWork.ps1

Select-String -Path C:\fso2\* -Pattern "contoso.com", "fabrikam.com" |
ForEach-Object {
Move-Item -Path $_.path -Destination C:\fso3
}

If on the other hand, a copy would suffice instead of a move, you could use the ReadTextCopyFile.ps1 script seen here.

ReadTextCopyFile.ps1

Select-String -Path C:\fso2\* -Pattern "contoso.com", "fabrikam.com" |
ForEach-Object {
copy-Item -Path $_.path -Destination C:\fso3
}

AO that is all there is to migrating a VBScript that searches files for a specific string and takes an action to Windows PowerShell 2.0. VBScript migration 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 them on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson and Craig Liebendorfer, Scripting Guys