Welcome to TechNet Blogs Sign in | Join | Help

Hey, Scripting Guy! Quick-Hits Friday: The Scripting Guys Respond to a Bunch of Questions (7/10/09)

 

 

Troubleshooting a Windows Powershell Script

Hey, Scripting Guy! QuestionHey Scripting Guy! I tried to use the script from the May 2009 TechNet Magazine article, Hey, Scripting Guy! Working with Access Databases in Windows PowerShell. But when I run it I get an error. This happens both on my Windows Vista  computer (64 bit, Service Pack 2) with Office 2007 SP2, and my Windows 2007 build 7100 (64 bit) with Office 2007 SP2 installed. The database I am using has been created by hand. Here is the error I am receiving.

Exception calling "Open" with "1" argument(s): "Provider cannot be found. It may not be properly installed."

At C:\Users\fvandonk\AppData\Local\Temp\Untitled1.ps1:31 char:19

+   $connection.Open <<<< ("Provider = Microsoft.Jet.OLEDB.4.0;Data Source=$Db" )


I get a similar error when I create the database using the script from the Hey Scripting Guy! Article, How Can I Create a Database with More Than One Table? The error from that script is:

Unexpected token 'Provider=' in expression or statement

Can you tell me what I am doing wrong?

-- FD

 

Hey, Scripting Guy! AnswerHello FD,

The problem is that when I wrote those two scripts, I was using a 32-bit operating system. I have now upgraded to Windows 7 64-bit, and I get the same error messages you are getting. To run the scripts without generating an error, you need to make sure you are using the 32-bit version of Windows PowerShell. On a 64-bit system, you will find both a 32-bit version and a 64-bit version of Windows PowerShell installed. You should see the link for 32-bit Windows PowerShell off the Start menu on Windows 7. Click Accessories and then click Windows PowerShell:

Image of Windows PowerShell location in Windows 7


I used Windows PowerShell ISE (x86) and it works fine. I would consider putting something like this at the beginning of the script:

if($env:PROCESSOR_ARCHITECTURE -ne 'x86')

  { 'This script must be run in x86 powershell' ; exit}

 


Outputting Only the Last Three Lines of a Command

Hey, Scripting Guy! QuestionHey Scripting Guy! I have a request which I believe should be very simple, but all the searching of the Internet has not turned up anything quite right. How can I output only the last three lines of a command into a text document?

For example, I am using the command dir /s to find the total size and number of files and folders in a given directory. These directories can be quite large, so a complete output may cause the text file to exceed 1 GB in size. All I really need is the last three lines that read:

     Total Files Listed:
           299174 File(s)  1,238,631,539 bytes
           27731 Dir(s)  1,667,751,895,040 bytes free

-- KS

Hey, Scripting Guy! AnswerHello KS,

I wrote the GetDirectoryListingStats.vbs script to illustrate what you will need to do. In the GetDirectoryListingStats.vbs script, you first create an instance of the WshShell object as seen here:

Set objShell = CreateObject("WScript.Shell")


You then call the exec method to execute the dir command. To be able to execute the dir command, you also need to call the command interpreter. This is seen here:

Set objExecObject = objShell.Exec(command)


The exec method returns a textstream object. To walk through the textstream object, use the Do Until…Loop as seen here:

Do Until objExecObject.StdOut.AtEndOfStream

    strText = objExecObject.StdOut.ReadAll

Loop


Next, you use the split function to break the output into an array that will allow you to return the last few lines. This is shown here:

aryText = Split(strText, VbNewLine)


The complete GetDirectoryListingStats.vbs script is seen here.

GetDirectoryListingStats.vbs

'==========================================================================

'

' NAME: GetDirectoryListingStats.vbs

'

' AUTHOR: Ed Wilson , Microsoft

' DATE  : 6/18/2009

'

' COMMENT: Uses the exec method from the WshShell object to execute a command

' Uses stdOut to read the output.

'

'==========================================================================

 

'Option Explicit  'is used to force the scripter to declare variables

'On Error Resume Next  ‘ go to the next line if it encounters an Error

Dim objShell  'holds WshShell object   

Dim objExecObject  'holds what comes back from executing the command

Dim strText  'holds the text stream from the exec command.

Dim command  'the command to run

Dim Directory  'directory to query

Directory = "C:\fso"

command = "cmd /c dir " & Directory

WScript.echo "starting program " & Now ' used to mark when program begins

Set objShell = CreateObject("WScript.Shell")

Set objExecObject = objShell.Exec(command)

 

Do Until objExecObject.StdOut.AtEndOfStream

    strText = objExecObject.StdOut.ReadAll

Loop

aryText = Split(strText, VbNewLine)

wscript.Echo "There are " & ubound(aryText) & _

 " items in the " & Directory & " Folder"

WScript.Echo aryText(ubound(aryText) -2 )

WScript.Echo aryText(ubound(aryText) -1)

 
The Measure-Object Cmdlet in Windows PowerShell CTP3

Hey, Scripting Guy! QuestionHey Scripting Guy! The –line switch used with Measure-Object in Windows PowerShell CTP3 seems to only count nonempty lines in .txt files. Is that the supposed behavior?

-- TB

Hey, Scripting Guy! AnswerHello TB,

Let’s take a look. First use the Get-Content cmdlet to read the contents of a text file. This is seen here:

PS C:\Users\edwils> Get-Content 'C:\fso\New Text Document.txt'

line one

 

line two

 

line three

We see there are five lines in the text file, but only three have text. Now use the Get-Content cmdlet and pipeline the results to the Measure-Object cmdlet, and tell it to  count the number of lines. This is seen here:

PS C:\Users\edwils> Get-Content 'C:\fso\New Text Document.txt' | Measure-Object -Line

 

                        Lines Words                         Characters                    Property

                        ----- -----                         ----------                    --------

                            3


As you can see, Measure-Object tells us we have three lines. Lets now try one more thing: Use Get-Content and store the contents into a text file. Now use the count property on the file. This is seen here:

PS C:\Users\edwils> $file = Get-Content 'C:\fso\New Text Document.txt'

PS C:\Users\edwils> $file.Count

5


TB you are correct. The Measure-Object cmdlet only counts lines; it does not count blank lines. If you need to get blank lines in a text file, you can use the count property.



Troubleshooting Output from a VBScript Script

Hey, Scripting Guy! QuestionHey Scripting Guy! I am having an issue with outputting results. My code is reading server names from a .txt file and outputting the logged-on terminal server users and the server name they are logged onto. My code looks like this:

GetTerminalServerUsersAndServers.vbs

Const ForReading = 1

Const wbemImpersonationLevelImpersonate = 3

Const wbemAuthenticationLevelPktPrivacy = 6

 

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objTextFile = objFSO.OpenTextFile("servers.txt", ForReading)

 

Do Until objTextFile.AtEndOfStream

    strComputer = objTextFile.Readline

    Set objLocator = CreateObject("WbemScripting.SWbemLocator")

    Set objWMI = objLocator.ConnectServer (strComputer, "root\cimv2")

    objWMI.Security_.ImpersonationLevel = wbemImpersonationLevelImpersonate

    objWMI.Security_.AuthenticationLevel = wbemAuthenticationLevelPktPrivacy

    Set objWMI = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

    Set colSessions = objWMI.ExecQuery _

        ("Select * from Win32_LogonSession Where LogonType = 10")

          If colSessions.Count = 0 Then

             Wscript.Echo "No interactive user found"

          Else

           For Each objSession in colSessions                 

            Set colList = objWMI.ExecQuery("Associators of " _

              & "{Win32_LogonSession.LogonId=" & objSession.LogonId & "} " _

              & "Where AssocClass=Win32_LoggedOnUser Role=Dependent" )                                                                      

            For Each objItem in colList

             Wscript.Echo "Server Name: " & strComputer

             WScript.Echo objItem.Name                        

            Next                                

           Next

          End If

Loop

The problem is with the output. It is coming out like this (shortened):

Server Name: ctx-07

carrie

Server Name: ctx-07

diane

Server Name: ctx-07

rachel

Server Name: ctx-09

sandra

Server Name: ctx-09

kathy

Server Name: ctx-09

Linda

Instead, I would like to see the users group by server, as seen here:

Server Name: ctx-07

carrie

diane

rachel

Server Name: ctx-09

sandra

kathy

Linda


I think I have an idea how to do thiswith some kind of Do loopbut I am not confident my solution is correct. Could you point me in the right direction?

 

-- WM

Hey, Scripting Guy! AnswerHello WM,

Your problem is that you are emitting the data from inside the For…Each…Next loop. Each time you pass through the loop, the data is being displayed. Instead, what you need to do is capture the information from inside the loop and wait until you are done looping to display the information. Something like this would be relatively easy to do:

GetTerminalServerUsersAndServersRevised.vbs

Const ForReading = 1

Const wbemImpersonationLevelImpersonate = 3

Const wbemAuthenticationLevelPktPrivacy = 6

 

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objTextFile = objFSO.OpenTextFile("servers.txt", ForReading)

 

Do Until objTextFile.AtEndOfStream

    strComputer = objTextFile.Readline

    Set objLocator = CreateObject("WbemScripting.SWbemLocator")

    Set objWMI = objLocator.ConnectServer (strComputer, "root\cimv2")

    objWMI.Security_.ImpersonationLevel = wbemImpersonationLevelImpersonate

    objWMI.Security_.AuthenticationLevel = wbemAuthenticationLevelPktPrivacy

    Set objWMI = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

    Set colSessions = objWMI.ExecQuery _

        ("Select * from Win32_LogonSession Where LogonType = 10")

          If colSessions.Count = 0 Then

             Wscript.Echo "No interactive user found"

          Else

           For Each objSession in colSessions                 

            Set colList = objWMI.ExecQuery("Associators of " _

              & "{Win32_LogonSession.LogonId=" & objSession.LogonId & "} " _

              & "Where AssocClass=Win32_LoggedOnUser Role=Dependent" )                                                                       

            For Each objItem in colList

              colNames = colNames & VBCRLF & objItem.Name    

            Next                                

           Next

          End If

        Wscript.Echo "Server Name: " & strComputer

        Wscript.Echo colNames

Loop

We come to the end of another Quick-Hits Friday, which also brings to an end another week at the Script Center. Join us on Monday for another fun-filled and exciting week in the world of scripting. Until then, have a great weekend.

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

 

Hey, Scripting Guy! How Do I Create a CSV File from Within Windows PowerShell?

  Hey Scripting Guy! I am having a problem trying to create a CSV file from within Windows PowerShell. I thought at first I could use the Write-Host cmdlet to do this. I came up with this command:

Write-Host "col1,col2,col3" >> "C:\fso\test.csv"

But when I go to the test.csv file, it is empty. I am really stuck. I have spent more than 10 hours searching the Internet and have come up with nothing but sore fingers. Help!

-- WS

 Hello WS,

You know a trumpetfish will spend hours hunting for useful items also. A trumpet fish will commonly feed on small fish and even crustaceans. They have a small jaw opening, so they will sneak up on their supper and suck them into their mouth. It is a process that is kind of like eating sushi through a straw.  They are really patient and also good at camouflaging themselves. The following picture is a yellow trumpet fish I saw while I was scuba diving off the coast of Maui.

Image of a trumpet fish


WS, one of the cool things about being a trumpet fish is that you rarely get stuck. This is obviously due to the long cylindrical nature of his design. Scripters, on the other hand, often get stuck. Perhaps we should spend more time eating sushi through straws? It is amazing how persistent the comma-separated value (CSV) file format has been. Personally, I love it because it is simple, easy to understand, and easy to work with. The easy way to write to a Microsoft Excel spreadsheet or a Microsoft Access database, or to create a table in Microsoft Word is to use a CSV file. It is no wonder, then, that one of the consistent themes that comes through the scripter@microsoft.com e-mail box is related to attempts to work with CSV files. A few years ago, we wrote a really good article about creating CSV files from within VBScript. It was called, rather intuitively, How Can I Create a CSV File?

Without cmdlet support for selecting objects and exporting to a CSV file format, one might be tempted to use the filesystemobject from VBScript fame. One could then follow the How Can I Create a CSV File? script from that old Hey, Scripting Guy! article and make a rather easy translation from VBScript to Windows PowerShell. An example of such a script is the FSOBiosToCsv.ps1 script.

FSOBiosToCsv.ps1

$path = "c:\fso\bios1.csv"
$bios = Get-WmiObject -Class win32_bios
$csv = "Name,Version`r`n"
$csv +=$bios.name + "," + $bios.version
$fso = new-object -comobject scripting.filesystemobject
$file = $fso.CreateTextFile($path,$true)
$file.write($csv)
$file.close()

In the FSOBiosToCsv.ps1 script, the first thing that you do is assign a path for the CSV file that will be created. Next, you use the Get-WmiObject cmdlet to retrieve an instance of the Win32_Bios WMI class. This class returns information about the BIOS on the local computer. The resulting BIOS information is stored in a variable named $bios. This is seen here:

$bios = Get-WmiObject -Class win32_bios

The next thing you do is create the header for the CSV file. The bios1.csv file will contain two pieces of information: the name and the version of the BIOS. You next want to have data appear on the second line. To do this, you need to insert a carriage return and a new line (similar to the VBCRLF from VBScript). The `r creates a carriage return, and the `n creates the new line character. This is shown here:

$csv = "Name,Version`r`n"

On the line that follows the header information, you will need to add the data itself to the file. Because the entire Win32_Bios WMI object is stored in the $bios variable, and because there is only one instance of a BIOS on a computer, you can directly access each of the property values through the $bios variable. You want to separate each property value with a comma (that is the point of a CSV file after all). To do this you will use concatenation to glue the comma between the two property values. This is seen here:

$csv +=$bios.name + "," + $bios.version

Now for the really cool part: If you loved working with the FileSystemObject in VBScript, you do not have to give it up. You can create the object by using the New-Object cmdlet. This is seen here:

$fso = new-object -comobject scripting.filesystemobject

To create a text file using the FileSystemObject you use the CreateTextFile method. The returned textfile object is stored in the variable $file as seen here.
$file = $fso.CreateTextFile($path,$true)


The last thing to be done is to write to the text file. Use the Write method for this operation:

$file.write($csv)
$file.close()

When you run the script, nothing is displayed. This is because there is no confirmation message included in the script. Often when I am writing automation scripts that retrieve information from different computers, I have them write information to CSV files because I do not want to clutter the script with confirmation messages. Imagine running a script against 1,000 remote computers. Dealing with 1,000 confirmation messages would be somewhat annoying. However, when you go to the path location, you will find the Bios1.csv file. The contents of this file are seen here:

Image of the Bios1.csv file's contents 

WS, it is no secret that cmdlet support makes working with Windows PowerShell a breeze. If you need to check the status of the BITS service, it is easiest to use the Get-Service cmdlet as shown here:

Get-Service –name bits

To find information about the explorer process, you can use the Get-Process cmdlet. This is seen here:

Get-Process -Name explorer

If you need to stop a process, you can easily use the Stop-Process cmdlet:

Stop-Process -Name notepad

You can even check the status of services on a remote computer, by using the –computername switch from the Get-Service cmdlet as seen here:

Get-Service -Name bits -ComputerName vista

If you are working in a cross-domain scenario where authentication would be required, you will not be able to use Get-Service or Get-Process because those cmdlets do not have a –credential parameter. You would need to use one of the remoting cmdlets in Windows PowerShell 2.0 such as Invoke-Command, which will allow you to supply an authentication context.

Instead of replicating a VBScript into Windows PowerShell syntax, you can check the BIOS information on a local computer, and save the information to a comma-separated value file with just a few lines of code. An example of such a script is the ExportBiosToCsv.ps1 script.

ExportBiosToCsv.ps1

$path = "c:\fso\bios.csv"
Get-WmiObject -Class win32_bios |
Select-Object -property name, version |
Export-CSV -path $path –notypeinformation

The first line of the ExportBiosToCsv.ps1 script is essentially the same as the first line of the previous script (the only difference is the name of the text file). The remainder of the script takes advantage of Windows PowerShell features. After obtaining the BIOS information, the instance of the Win32_Bios WMI class is pipelined to the next cmdlet instead of being stored in a variable. The use of the pipeline is one of Windows PowerShell's strongest features, and it generally results in significant performance improvements over storing results into a variable and then accessing the properties. This is seen here:

Get-WmiObject -Class win32_bios |

Next, you use the Select-Object cmdlet to retrieve the two properties that are of interest: the name and the version of the BIOS. These two property values are then sent along the pipeline as well. This is seen here:

Select-Object -property name, version |

Last, you will use the Export-CSV cmdlet to create the CSV file. The notypeinformation switched parameter is used to keep the CSV file from having a comment written on the first line that describes the source of the file. The use of the notypeinformation switched parameter can save you a lot of frustration when you try to import the CSV file into other programs. This is seen here:

Export-CSV -path $path –notypeinformation

When you run the ExportBiosToCsv.ps1 script, a file named bios.csv is created in the location that you specified for the $path variable. The contents of this file are seen here:

Image of the contents of the bios.csv file's contents


WS, you can see that by using the Windows PowerShell cmdlets, common tasks such as creating CSV files can be made much easier than they were in the past. Speaking of making things easier, if you want to keep up with all the things that are going on the Script Center, you can follow us on Twitter. You can also join the Scripting Guys group on Facebook and find lots of other friends who are interested in scripting. If you ever get stuck again while you are working on a script, you should post your question on the Official Scripting Guys Forum. There are some really smart scripters who hang out there.

This brings our discussion about handling output to an end. Join us tomorrow as we open up the mail bag, and select an assortment of questions that require short answers. Yes, it is time for Quick-Hits Friday! Be there. Aloha!

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Hey, Scripting Guy! How Do I Get Data Out of a Function?

Hey, Scripting Guy! Question

Hey, Scripting Guy! I have been having problems with trying to understand functions in Windows PowerShell. I get that you use the Function keyword to create the function, but I do not know how to get information from the function back to the script. I have been able to make the functions work by using Write-Host from inside the function, but when I want to use the function to change a value (so that it actually works like a function), I run into all kinds of problems. Can you explain this to me?

-- BJ

Hey, Scripting Guy! AnswerHello BJ,

There are lots of things that are hard for me to explain. For example, how does the frogfish I saw while scuba diving in Maui (seen in the following picture) ever make any friends? He sits around all day looking like a really dirty orange sponge, and you never see one swimming, frolicking, and having fun. They just sit there. They have really good camouflage and are really hard to find when they are hiding on the coral. So when the mommy frogfish tells the baby frogfish to go outside and play, how does the baby frogfish ever find its way back home? These are the kinds of things that this scripting guy worries about.

Image of a frog fish 


BJ, luckily, you did not ask about frogfish. Instead you are worrying about getting data out of your function. Functions are not nearly as difficult to spot as are frogfish, so this will not be too difficult. When a function is called, it returns data to the calling code. This behavior is often not understood really well by people who are coming to Windows PowerShell from other scripting languages. When you run the AddOne.ps1 script, the number 6 is displayed on the console. The confusing part is that the data is returned from the line of code that calls the function, not from within the function itself. This is different behavior than might be expected. Most of the time, when two numbers are added together, the data is returned from the line that performs the work. This is seen here:

PS C:\> $int = 5
PS C:\> $int + 1
6
PS C:\>

It is therefore reasonable to expect that the number 6 is coming from inside the AddOne function, and not from outside the function. The AddOne.ps1 script with the AddOne function is seen here.

AddOne.ps1

Function AddOne($int)
{
 $int + 1
}

AddOne(5)

To illustrate from whence the data comes, you can modify the script to store the result of calling the function into a variable. You can then use the Get-Member cmdlet to display the information that is returned. This is seen in AddOne1.ps1.

AddOne1.ps1

Function AddOne($int)
{
 $int + 1
}

$number = AddOne(5)
$number | get-member
'Display the value of $number: ' + $number

When the AddOne1.ps1 script is run, you will see that the information is returned to the code that calls the function. In the first line after the function call, the object that is stored in the $number variable is shown to be a System.Int32 object. Following the Get-Member command, the value that is stored in the $number variable is shown to be equal to 6. The value 5 is not displayed from within the AddOne function. This is seen here:

TypeName: System.Int32

Name        MemberType Definition
----        ---------- ----------
CompareTo   Method     System.Int32 CompareTo(Object value), System.Int32 Co...
Equals      Method     System.Boolean Equals(Object obj), System.Boolean Equ...
GetHashCode Method     System.Int32 GetHashCode()
GetType     Method     System.Type GetType()
GetTypeCode Method     System.TypeCode GetTypeCode()
ToString    Method     System.String ToString(), System.String ToString(Stri...
Display the value of $number: 6

When you use a cmdlet like Write-Host from inside the function, you then circumvent the return process that is inherent in the design of the function. The use of Write-Host from within a function is illustrated in AddOne2.ps1.

AddOne2.ps1

Function AddOne($int)
{
 Write-Host $int + 1
}

$number = AddOne(5)
$number | get-member
'Display the value of $number: ' + $number

When the script is run, you will notice that nothing is returned from inside the function. The $number variable no longer contains an object. This is seen here:

5 + 1
Get-Member : No object has been specified to get-member.
At C:\Documents and Settings\ed\Local Settings\Temp\tmp6.tmp.ps1:9 char:21
+ $number | get-member <<<<
Display the value of $number:

In addition to using cmdlets such as Write-Host from within a function to circumvent the output from a function, it is also possible to store the results of a function to a variable. The problem with storing results from the function to a variable within the function is that when a variable is created within a function, it is not available outside of the function. This is seen here:

AddOne3.ps1

Function AddOne($int)
{
 $number =  $int + 1
}

$number = AddOne(5)
$number | get-member
'Display the value of $number: ' + $number

When the AddOne3.ps1 script is run, there is no object in the $number variable because it is not available outside of the addone function. This is seen here:

Get-Member : No object has been specified to get-member.
At C:\Documents and Settings\ed\Local Settings\Temp\tmp9.tmp.ps1:9 char:21
+ $number | get-member <<<<
Display the value of $number:

One technique that is sometimes used to get the value from within the function to the calling script is to add a scope to the variable. This is shown in AddOne4.ps1.

AddOne4.ps1

Function AddOne($int)
{
 $global:number =  $int + 1
}

AddOne(5)
$global:number | get-member
'Display the value of $global:number: ' + $global:number

There is a potential problem with adding a variable to the global scope—the variable will continue to exist after the script has exited. As long as the Windows PowerShell console is open and until you explicitly remove the global variable, it will continue to be available. This means it will be available in other scripts and will always be available within the console. This may not be a problem, but it could cause scripts that use the same variable names to operate in an erratic fashion. One way to determine if the variable persists is to check the variable drive. This is shown here:

   TypeName: System.Int32

Name        MemberType Definition
----        ---------- ----------
CompareTo   Method     System.Int32 CompareTo(Object value), System.Int32 Co...
Equals      Method     System.Boolean Equals(Object obj), System.Boolean Equ...
GetHashCode Method     System.Int32 GetHashCode()
GetType     Method     System.Type GetType()
GetTypeCode Method     System.TypeCode GetTypeCode()
ToString    Method     System.String ToString(), System.String ToString(Stri...
Display the value of $global:number: 6


PS C:\data\PowerShellBestPractices\Scripts\Chapter12> Get-Item Variable:\number

Name                           Value
----                           -----
number                         6

It is possible to remove the global variable in the last line of the script, by using the Remove-Variable cmdlet, but a better approach is to use the script-level scope instead of the global-level scope. The script-level variable will be available inside and outside the function while the script is running. After the script has run, the variable is removed. The use of the script-level scope is seen in the AddOne5.ps1 script.

AddOne5.ps1

Function AddOne($int)
{
 $script:number =  $int + 1
}

AddOne(5)
$script:number | get-member
'Display the value of $script:number: ' + $script:number

When the AddOne5.ps1 script runs, the value of the $number variable is available outside of the function. When the script has run, an error is returned when the Get-Item cmdlet is used to attempt to retrieve the value of the variable. This is seen here:

   TypeName: System.Int32

Name        MemberType Definition
----        ---------- ----------
CompareTo   Method     System.Int32 CompareTo(Object value), System.Int32 Co...
Equals      Method     System.Boolean Equals(Object obj), System.Boolean Equ...
GetHashCode Method     System.Int32 GetHashCode()
GetType     Method     System.Type GetType()
GetTypeCode Method     System.TypeCode GetTypeCode()
ToString    Method     System.String ToString(), System.String ToString(Stri...
Display the value of $script:number: 6


PS C:\data\PowerShellBestPractices\Scripts\Chapter12> Get-Item variable:number
Get-Item : Cannot find path 'number' because it does not exist.
At line:1 char:9
+ Get-Item  <<<< variable:number

BJ, that is about all there is to getting information out of your function. Next time you are in the water, and you see what looks like a big colorful sponge yawning, it just might be a frogfish that is getting bored not having any friends. Fortunately, you do not have that problem around here. There are lots of scripting friends. You can follow us on Twitter, join our group on Facebook, and hang out in the Scripting Guys Forum. Join us tomorrow as we conclude our discussion of output from scripts. Until then, take care.

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

 

Hey, Scripting Guy! How Can I Both Save Information in a File and Display It on the Screen?

Hey, Scripting Guy! Question

Hey, Scripting Guy! You know the old commercial that goes, "Sometimes you feel like a nut, sometimes you don't"? Well, sometimes I want to save information from a script to a file, and sometimes I want to see it displayed to the screen. And at other times, I want both. In other words, I want my candy bar, and I want to eat it, too.

- NR

SpacerHey, Scripting Guy! Answer

Hi NR,

I never did really get the expression, "I want my cake and I want to eat it too." I mean, what good is cake if you cannot eat it? Or for that matter, who would want a cake that they only used for decoration? To me it doesn't make sense. I think there is art, and there is food. Food is to be consumed, and art is to be appreciated but not eaten. When I go to the Guggenheim in New York City, I go there to nourish my soul, not my belly. When I go to the Sydney Opera House, seen in the following picture I took during my last trip to Australia, I can actually do both because there is an excellent restaurant under the front sail.

Image of the Sydney Opera House

If you want to display information on the screen, you run the command and by default it will send the information to the console. This is seen here:

PS C:\> Get-WmiObject -Class win32_bios


SMBIOSBIOSVersion : A01
Manufacturer      : Dell Computer Corporation
Name              : Default System BIOS
SerialNumber      : 9HQ1S21
Version           : DELL   - 6



PS C:\>

If you want to store the information in a text file, you can use the redirection arrow as seen here:

PS C:\> Get-WmiObject -Class win32_bios >c:\fso\bios.txt

PS C:\>

The problem is there is neither a confirmation message that the command completed successfully, nor an idea of what is in the text file. There is a cmdlet you can use—the Out-File cmdlet. But once again, as seen here, there is no feedback from the command:

PS C:\> Get-WmiObject -Class win32_bios | Out-File -FilePath C:\fso\bios.txt
PS C:\>

One thing you could do is to use the Get-Content cmdlet to inspect the contents of the file to ensure it has the information you require. The thing to keep in mind is that you are not piping the information from the Out-File cmdlet to the Get-Content cmdlet. The semicolon is used to indicate that you are beginning a new command. The semicolon is the equivalent of typing the command on a new line in a script. This is seen here:

PS C:\> Get-WmiObject -Class win32_bios | Out-File -FilePath C:\fso\bios.txt ;
Get-Content -Path C:\fso\bios.txt


SMBIOSBIOSVersion : A01
Manufacturer      : Dell Computer Corporation
Name              : Default System BIOS
SerialNumber      : 9HQ1S21
Version           : DELL   - 6

Because you have already seen that the redirection arrow is the same as using the Out-File cmdlet, for your purposes here you can revise the command to use the redirection arrows. You can also shorten the command a bit by using the alias cat instead of the longer Get-Content cmdlet name. This command is seen here:

PS C:\> Get-WmiObject -Class win32_bios > C:\fso\bios.txt ; cat C:\fso\bios.txt



SMBIOSBIOSVersion : A01
Manufacturer      : Dell Computer Corporation
Name              : Default System BIOS
SerialNumber      : 9HQ1S21
Version           : DELL   - 6

By using an alias for the Get-WmiObject cmdlet and also leaving out the class parameter name, you can create a short command:

PS C:\> gwmi win32_bios > C:\fso\bios.txt ; cat C:\fso\bios.txt


SMBIOSBIOSVersion : A01
Manufacturer      : Dell Computer Corporation
Name              : Default System BIOS
SerialNumber      : 9HQ1S21
Version           : DELL   - 6

Now you have a shorter command that you can use to feed the content from the command to a text file for storage, and then display the information on the console. Though this is a workable solution, it would be easier if there was a cmdlet that would essentially do this for us. As it turns out, there is a cmdlet that will split the output from a cmdlet and direct it to both the screen and to a file. The cmdlet is called the Tee-Object. Most of the time, you will split the output from your command line to a file and to the console. To do this, you will use the filepath parameter and specify the full path to the file. As seen here, the Tee-Object cmdlet supports a number of additional switches and parameters.

Tee-Object [-FilePath] <String> [-InputObject <PSObject>] [-Verbose] [-Debug]
[-ErrorAction <ActionPreference>] [-ErrorVariable <String>] [-OutVariable
<String>] [-OutBuffer <Int32>]

To return to the example, you can replace the redirection arrow (or the Out-File cmdlet) and the Get-Content cmdlet (cat alias) with the Tee-Object cmdlet. The revised code is seen here:

Get-WmiObject -Class win32_bios | Tee-Object -FilePath c:\fso\bios.txt

When you run the command, you receive the output shown here:

Image of the output received


One thing to keep in mind when using the Tee-Object is that it will always overwrite the previous text file if the file already exists. If the file does not exist, the Tee-Object cmdlet will create the file. The Tee-Object cmdlet will not create the folder. If you attempt to use the Tee-Object cmdlet to write to a folder that does not exist, you will receive an error that warns of a missing path. This error message is seen here:

PS C:\> Get-WmiObject -class win32_bios | Tee-Object -FilePath C:\fso5\bios.txt
out-file : Could not find a part of the path 'C:\fso5\bios.txt'.
PS C:\>

You can also use the Tee-Object cmdlet to hold the output of a command in a variable. This offers a convenient way to save the information for use later in the script. Here is how you would save the results of a command in a variable, and then display it later without using the Tee-Object:

PS C:\> $bios = Get-WmiObject -class win32_bios
PS C:\> $bios

SMBIOSBIOSVersion : A01
Manufacturer      : Dell Computer Corporation
Name              : Default System BIOS
SerialNumber      : 9HQ1S21
Version           : DELL   - 6

PS C:\>

The syntax for the Tee-Object when used to store the results of a pipeline in a variable is seen here:

Tee-Object [-InputObject <PSObject>] -Variable <String> [-Verbose] [-Debug]
[-ErrorAction <ActionPreference>] [-ErrorVariable <String>] [-OutVariable
<String>] [-OutBuffer <Int32>]

To store the results of your Get-WmiObject –Class Win32_Bios command in a variable named $bios, you use the following command:

PS C:\> Get-WmiObject -class win32_bios | Tee-Object -Variable bios


SMBIOSBIOSVersion : A01
Manufacturer      : Dell Computer Corporation
Name              : Default System BIOS
SerialNumber      : 9HQ1S21
Version           : DELL   - 6

The one thing you need to keep in mind with the Tee-Object cmdlet when using the variable parameter is that you do not need to use a dollar sign in front of the variable name. This makes the behavior of the cmdlet the same as when using the New-Variable cmdlet.

To see the contents of the $bios variable, you type it on the command line in the Windows PowerShell console as seen here:

PS C:\> $bios

SMBIOSBIOSVersion : A01
Manufacturer      : Dell Computer Corporation
Name              : Default System BIOS
SerialNumber      : 9HQ1S21
Version           : DELL   - 6

PS C:\>

One of the best features of the Tee-Object is that it will also pass the object though the pipeline. This means you are not stuck with the default display of information that is returned by the previous command such as the Get-WmiObject cmdlet. You can store the object in the $bios variable and then choose to display only the name property:

PS C:\> Get-WmiObject -class win32_bios | Tee-Object -Variable bios |

select name

name
----
Default System BIOS

To retrieve the object from the variable, you once again type it on the command line or use it elsewhere in your script. This is seen here:

PS C:\> $bios

SMBIOSBIOSVersion : A01
Manufacturer      : Dell Computer Corporation
Name              : Default System BIOS
SerialNumber      : 9HQ1S21
Version           : DELL   - 6

Well, NR, we have come to the end of our discussion on splitting the output from commands to the command line and storing it in a file. In addition, we decided to show you a cool trick that allows you to store objects in variables and manipulate that output as well. Hopefully you have enjoyed this discussion. Join us tomorrow as we continue looking at handling output from within Windows PowerShell. To keep up to date with all the latest Script Center news, follow us on Twitter. You can also check out the Scripting Guys on Facebook, and join our group to receive notifications of special events. If you find yourself in need of assistance, don't forget the Official Scripting Guys Forum. We have members from all around the world, so there is a good chance someone would be awake and willing to lend a hand. Until tomorrow, take care.

Ed Wilson and Craig Liebendorfer, Scripting Guys

Hey, Scripting Guy! How Can I Simply Have Windows PowerShell Display Information on the Screen?

Hey, Scripting Guy! Question

Hey, Scripting Guy! I am not into all those fancy scripts you seem to write. I am just a basic, everyday network administrator, and I simply need to use scripts to make my job easier. Because of this, I am not interested in displaying progress bars, writing stuff to Excel or Word or even PowerPoint. I am interested simply in having my Windows PowerShell script display information on the screen. What is the best way to do this?

- CO

SpacerHey, Scripting Guy! Answer

Hi CO,

At times it seems like only yesterday, but when I started out in the field of IT, I also was a basic, everyday network administrator. The days were never boring because the problems were never the same. I would show up in my office, and there would be a message from a user stating they could not get logged on to the network; while I was resetting their password, I would check my e-mail and find out that someone else was having problems creating a macro in Office Excel. As I checked my project board, I saw that I needed to get a new network drop installed in a remote office. I also had to teach a class in the afternoon on basic Office Outlook for a dozen new users. Yep, being a basic, everyday network administrator was a lot of fun. I never got bored.

The one thing I never had was free time. Back then I used to drink coffee, but I don't think I ever actually finished a cup of coffee before it was cold. I actually got to where I could drink cold coffee without cringing and shuddering. Because I was so busy running from problem to problem, I seldom wrote scripts. When I did write a script, it would display information on the screen because I always ran the scripts in an interactive fashion. Years later, after I began working at Microsoft, my good friend Jason and I went to Hong Kong where I learned a new appreciation for interactive displays. Here is a good picture of Victoria Harbor I took from a hydrofoil on my way to Macau.

Image of Victoria Harbor

 

CO, when displaying information on the screen, the most important thing to remember is that it is easy. In many cases, you do not have to do anything more complicated than to run the cmdlet. When you do, you are automatically rewarded with nicely formatted output that is displayed on the screen:

Image of formatted output displayed on screen


The reason the output is nicely formatted is the Windows PowerShell team created several format.ps1xml files that are used to control the way different objects are formatted when they are displayed. These XML files are located in the Windows PowerShell installation directory. Luckily, there is an automatic variable, $pshome, that can be used to refer to the Windows PowerShell installation directory. To obtain a listing of all the format.ps1xml files that are installed on your computer, use the Get-ChildItem cmdlet and specify a path that will retrieve any file with the name format in it. Pipeline the resulting fileinfo objects to the Select-Object cmdlet and choose the name property. This is seen here:

PS C:\> Get-ChildItem -Path $PSHOME/*format* | Select-Object -Property name

Name
----
certificate.format.ps1xml
dotnettypes.format.ps1xml
filesystem.format.ps1xml
help.format.ps1xml
powershellcore.format.ps1xml
powershelltrace.format.ps1xml
registry.format.ps1xml

These format.ps1xml files are used by the Windows PowerShell Extended Type System to determine how to display objects. This is required because most objects do not know how to display themselves. Because the format files are XML files, it is possible to edit them to change the default display behavior. This should not be undertaken lightly, though, because the files are rather complicated. If you wish to edit the files, make sure you have a good backup copy of the files before you start messing around with them. Direct manipulation of the format.ps1xml files could result in unexpected behavior. It is also possible to write your own format.ps1xml file (in fact Marc Van Orsouw [MOW] created his own format data file to tune the default output format for the Trace-Route function he wrote as a guest commentator for the 2009 Summer Scripting Games.

The dotnettypes.format.ps1xml file is used to control the output that is displayed by a number of the cmdlets (Get-Process, Get-Service, Get-EventLog, etc.) that return .NET Framework objects. A portion of the dotnettypes.format.ps1xml file is seen in the next image. This is the section of the file that controls the output from the Get-Process cmdlet that is seen in the previous image. Under the <TableHeaders> section, each column heading is specified by the <TableColumnHeader> tag. And under the <TableColumnHeader>, there are three nodes: <Label>, <Width>, and <Alignment>. <Label> controls the column heading, <Width> controls how wide the column is, and <Alignment> is used to specify the alignment (left, center, right) of the data within the column. This is seen here:

Image of code specifying table alignment


To display information in the Command Prompt window, you do not need to worry about format XML files or anything like that. You can rely upon the defaults and allow Windows PowerShell to make the decision for you. To display a string, you place the string in quotation marks and it is displayed in the Command Prompt window. This is seen here:

PS C:\> "this string is displayed to the console"
this string is displayed to the console
PS C:\>

The important thing to keep in mind is that when the string is shown in the Command Prompt window, it retains its type; that is, it is still a string. This is seen here:

PS C:\> "this string is displayed to the console" | Get-Member


   TypeName: System.String

Name             MemberType            Definition
----             ----------            ----------
Clone            Method                System.Object Clone()
CompareTo        Method                int CompareTo(System.Object value), i...
Contains         Method                bool Contains(string value)
CopyTo           Method                System.Void CopyTo(int sourceIndex, c...
EndsWith         Method                bool EndsWith(string value), bool End...
Equals           Method                bool Equals(System.Object obj), bool ...
GetEnumerator    Method                System.CharEnumerator GetEnumerator()
GetHashCode      Method                int GetHashCode()
GetType          Method                type GetType()
GetTypeCode      Method                System.TypeCode GetTypeCode()
IndexOf          Method                int IndexOf(char value), int IndexOf(...
IndexOfAny       Method                int IndexOfAny(char[] anyOf), int Ind...
Insert           Method                string Insert(int startIndex, string ...
IsNormalized     Method                bool IsNormalized(), bool IsNormalize...
LastIndexOf      Method                int LastIndexOf(char value), int Last...
LastIndexOfAny   Method                int LastIndexOfAny(char[] anyOf), int...
Normalize        Method                string Normalize(), string Normalize(...
PadLeft          Method                string PadLeft(int totalWidth), strin...
PadRight         Method                string PadRight(int totalWidth), stri...
Remove           Method                string Remove(int startIndex, int cou...
Replace          Method                string Replace(char oldChar, char new...
Split            Method                string[] Split(Params char[] separato...
StartsWith       Method                bool StartsWith(string value), bool S...
Substring        Method                string Substring(int startIndex), str...
ToCharArray      Method                char[] ToCharArray(), char[] ToCharAr...
ToLower          Method                string ToLower(), string ToLower(Syst...
ToLowerInvariant Method                string ToLowerInvariant()
ToString         Method                string ToString(), string ToString(Sy...
ToUpper          Method                string ToUpper(), string ToUpper(Syst...
ToUpperInvariant Method                string ToUpperInvariant()
Trim             Method                string Trim(Params char[] trimChars),...
TrimEnd          Method                string TrimEnd(Params char[] trimChars)
TrimStart        Method                string TrimStart(Params char[] trimCh...
Chars            ParameterizedProperty char Chars(int index) {get;}
Length           Property              System.Int32 Length {get;}

If you use one of the Out-* cmdlets such as Out-Host or Out-Default, you destroy the object-oriented nature of the string. That is, it is no longer an instance of a system.string .NET Framework class. This is seen here:

PS C:\> "this string is displayed to the console" | Out-Host | Get-Member
this string is displayed to the console
Get-Member : No object has been specified to the get-member cmdlet.
At line:1 char:66
+ "this string is displayed to the console" | Out-Host | Get-Member <<<<
    + CategoryInfo          : CloseError: (:) [Get-Member], InvalidOperationEx
   ception
    + FullyQualifiedErrorId : NoObjectInGetMember,Microsoft.PowerShell.Command
   s.GetMemberCommand

PS C:\>

As a best practice you should avoid using the Out-Host cmdlet or the Out-Default cmdlet unless there is a reason for using it. This is because you lose your object after you send the output to these two cmdlets. The only reason for using Out-Host is to use the –paging parameter. This is seen here:

PS C:\> Get-WmiObject -Class win32_process | Out-Host -Paging

__GENUS                    : 2
__CLASS                    : Win32_Process
__SUPERCLASS               : CIM_Process
__DYNASTY                  : CIM_ManagedSystemElement
__RELPATH                  : Win32_Process.Handle="0"
__PROPERTY_COUNT           : 45
__DERIVATION               : {CIM_Process, CIM_LogicalElement, CIM_ManagedSyste
                             mElement}
__SERVER                   : VISTA
__NAMESPACE                : root\cimv2
__PATH                     : \\VISTA\root\cimv2:Win32_Process.Handle="0"
Caption                    : System Idle Process
CommandLine                :
CreationClassName          : Win32_Process
CreationDate               :
CSCreationClassName        : Win32_ComputerSystem
CSName                     : VISTA
Description                : System Idle Process
ExecutablePath             :
ExecutionState             :
Handle                     : 0
HandleCount                : 0
InstallDate                :
KernelModeTime             : 151488730096
MaximumWorkingSetSize      :
MinimumWorkingSetSize      :
Name                       : System Idle Process
OSCreationClassName        : Win32_OperatingSystem
OSName                     : Microsoftr Windows VistaT Business |C:\Windows|\De
                             vice\Harddisk0\Partition1
OtherOperationCount        : 0
OtherTransferCount         : 0
PageFaults                 : 0
PageFileUsage              : 0
ParentProcessId            : 0
PeakPageFileUsage          : 0
PeakVirtualSize            : 0
PeakWorkingSetSize         : 0
Priority                   : 0
PrivatePageCount           : 0
<SPACE> next page; <CR> next line; Q quit

If you are not using the paging parameter, there is no advantage to using the Out-Host cmdlet. From a display perspective, the following commands are identical:

Get-Process

Get-Process | Out-Host

Get-Process | Out-Default

In fact, on most systems Out-Default and Out-Host do the same thing. This is because by default Out-Host is the default outputter. The only reason to use Out-Default would be if you anticipated changing the default outputter, and you did not want to rewrite the script. By using Out-Default the output from the script will always go to the default outputter, which may or may not be the host.

CO, that is about all there is to say for now about writing to the host. Join us tomorrow as we will continue talking about handling output from scripts. If you want to keep up to date with the Scripting Guys, you can follow us on Twitter. You can also look us up on Facebook. Until tomorrow peace!

Ed Wilson and Craig Liebendorfer, Scripting Guys

Script Center 101: Our first Zunecast!

 

Script Center 101 (link takes you to the Microsoft Download Center)

Scripting Guy Ed Wilson takes you on a guided tour of the new Script Center Web site, which was launched on June 11, 2009. Excuse our dust as we continue to improve the new site. (And please understand that this is our first Zunecast and it does have a couple technical blips that we will correct for the next one.)

Enjoy! And let us know at scripter@microsoft.com if you have any feedback.

 

 

Hey, Scripting Guy! Quick-Hits Friday: The Scripting Guys Respond to a Bunch of Questions (7/3/09)


How Can I List Files and Folders with Special Characters in Their Names?

Hey, Scripting Guy! Question

 Hey, Scripting Guy! First I would like to thank you for all the good humour you present me every time I need you. I’m trying to use one of your scripts to list the files and folders on my file server. My users are very creative and can manage to use all kind of characters in folder and file names. This script crashes on folder names including the ' (straight quote) character. An example of a folder that crashes the execution of this script is c:\scripts\User’s folder\

Could you please point me to a solution for this problem?

- GV


Hey, Scripting Guy! Answer

Hi GV,

The single quote is a reserved string character for WMI. Unfortunately, it is allowed by Windows as a legitimate character for file and path names. The single quote character will need to be escaped with a single backslash character (\). This Hey, Scripting Guy! article talks about this problem.



What's the Difference Between ADODB.Recordset and ADOR.Recordset?

Hey, Scripting Guy! Question

 Hey, Scripting Guy! I was wondering if you could answer a question for me. Is there any difference between the objects ADODB.Recordset and ADOR.Recordset? Both program IDs appear to map to the same object. Am I missing something here? If they are different, is there an advantage one has over the other?

- RD

SpacerHey, Scripting Guy! Answer

 Hi RD,

According to MSDN ADOR.Recordset is outdated. You should use ADODB.Recordset in your scripts.



What Other Options Are Available for objUser and objGroup?

Hey, Scripting Guy! Question

 Hey, Scripting Guy! One thing that I would like to know is how I can find out other things I can do with your scripts. For example, many scripts have a similar format to this snippet here. How can I find out what other options are available for, say, objUser or objGroup:

  For Each objUser In objGroup.Members
        If  objUser.Name = UserAccount Then
          bMemberFound = True
     end if
   Next

- IA

SpacerHey, Scripting Guy! Answer

 Hi IA,

That is the perennial question. When I (Ed) write the Hey, Scripting Guy! articles (since August of last year), I always try to mention the name of the objects that I am working with. It is the object names that will help you out because you can look them up on MSDN. You can use this scoped Bing search that I use on a daily basis to look up object names and their associated members on MSDN.

Inside VBScript, there is a function that will be helpful for you. It is called the typename function and you can use it at anytime in your code to see what type of object you have. You then look up the value that is displayed from typename using the scoped Bing search. This technique is discussed in the Microsoft Press book, Microsoft VBScript Step by Step.

Here is an example of using the typename function.

DemoTypeName.vbs

Dim a
Dim b
Dim c
Dim d
Dim e
Dim f
Dim g
Dim h 'date

b = "this is a string"
c = 55
d = Null
e = Array(1,2,3,4,5)
Set f = CreateObject("wscript.shell")
Set g = CreateObject("scripting.filesystemObject")
h = (#1/2/1957#) 'uses date literal
i = CDate(1/2/1957)
j = DateValue("1/2/1957")

WScript.Echo TypeName (a)
WScript.Echo TypeName (b)
WScript.Echo TypeName(c)
WScript.Echo TypeName(d)
WScript.Echo TypeName (e)
WScript.Echo IsArray(e)
WScript.Echo TypeName (f)
WScript.Echo TypeName (g)
WScript.Echo TypeName (h)
WScript.Echo TypeName (i)
WScript.Echo TypeName (j)



Troubleshooting a Remoting Script

Hey, Scripting Guy! Question

Hey, Scripting Guy! Windows PowerShell really does rock! I’m using it automate just about everything I can! I have a problem that I can’t find an answer tocan you help? I am using WMI to collect various details from workstations, some of which are connected via VPN. I am attempting to export the results to a CSV file, but I have to run the command twice for it to work. Can you explain how to avoid this?

After the CSV is created, the contents are correct. For machines on the LAN, it works first time. 

- SM

SpacerHey, Scripting Guy! Answer

Hi SM,

What you describe sounds more like it is a WMI issue and network connectivity issue and not Windows PowerShell. WMI uses DCOM to connect to remote machines. DCOM was never intended to be used over the Internet; therefore, it does not have the error checking and handling required to work across unreliable networking typologies. WINRM is the solution to these kinds of connectivity issues. You can script WINRM using Windows PowerShell, but it is a bit tricky. It is fully documented in the SDK however.

In Windows PowerShell 2.0 we have better support for this, and even use WINRM for remoting of Windows PowerShell itself. In the meantime, one thing you might try to do is to send a ping to the remote machine to “wake up” the connection. You can stay with WMI and use the Win32_PingStatus class. After you get a good reply, you can proceed with your data gathering.

Here is a script that illustrates using the Win32_PingStatus class to ping a remote computer.

PingAndDoWmi.ps1

# -----------------------------------------------------------------------------
# # Ed Wilson, MSFT, 6/23/2009
# illustrates using the Win32_PingStatus WMI class to ping a remote range of
# addresses, and then when obtaining a reply, perform WMI query
#
# -----------------------------------------------------------------------------
[int]$intPing = 5
[string]$intNetwork = "192.168.1."

for ($i=1;$i -le $intPing; $i++)
{
$strQuery = "select * from win32_pingstatus where address = '" + $intNetwork + $i + "'"
   $wmi = get-wmiobject -query $strQuery
   "Pinging $intNetwork$i ... "
   if ($wmi.statuscode -eq 0)
      { gwmi -class win32_bios -computername $intNetwork$i }
      else
         {"error: " + $wmi.statuscode + " occurred"}
} # end for



How Can I Replace a Special Character in a Text File?

Hey, Scripting Guy! Question

Hey, Scripting Guy! In your article, How Can I Find and Replace Text in a Text File, you have a routine that works really great. However, it does not work when I try to replace the quotation mark in my text file. I need to replace " chr(34) with tab chr(9).  Can you help?

- LS

SpacerHey, Scripting Guy! Answer

Hi LS,

You need to use the VBScript method of specifying ASCII characters. Here is an example of doing this.

ReplaceQuoteWithTab.vbs

'==========================================================================
'
' NAME: ReplaceQuoteWithTab.vbs
'
' AUTHOR: Ed Wilson , Microsoft
' DATE  : 6/19/2009
'
' COMMENT: This script opens a text file, and uses the replace
' function to replace a " chr(34) with a <tab> chr(9)
' Modification to: http://tinyurl.com/ywtprz
'==========================================================================

Const ForReading = 1
Const ForWriting = 2
Dim FilePath
FilePath = "C:\fso\test.txt"
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile(FilePath, ForReading)

strText = objFile.ReadAll
objFile.Close
strNewText = Replace(strText,Chr(34) ,Chr(9) )

Set objFile = objFSO.OpenTextFile(FilePath, ForWriting)
objFile.WriteLine strNewText
objFile.Close



Troubleshooting a Windows PowerShell Script with the PagedPoolBytes Object

Hey, Scripting Guy! Question

Hey, Scripting Guy! I need to gather data on a particular object property in Windows PowerShell. The object, PagedPoolBytes, does not include the server property as far as I can tell. Here is what I need the results to look like:

Server           PoolPaged Bytes

ServerName1      Value
ServerName2      Value
ServerName3      Value

My script is seen here. 

$strComputers = get-content C:\TEMP\Servers.txt

$PoolPagedBytes = {[math]::truncate($_.poolpagedbytes / 1000000)}

Foreach ($computer in $strComputers)
{
get-wmiobject Win32_PerfFormattedData_PerfOS_Memory -computername $strComputers |  Select-Object -Property $PoolPagedBytes | export-csv c:\SCRIPTS\DATA\PPB-x.csv
}

Any help would be appreciated. Thank you. 

- MS

SpacerHey, Scripting Guy! Answer

Hi MS,

All WMI classes have a system property named _Server that is always available. Here is an example of obtaining the server name, and the performance information you seek:

get-wmiobject Win32_PerfFormattedData_PerfOS_Memory  | select p*, __Server

When you run the code above, here is the output you will receive:

PageFaultsPersec           : 586
PageReadsPersec            : 0
PagesInputPersec           : 0
PagesOutputPersec          : 0
PagesPersec                : 0
PageWritesPersec           : 0
PercentCommittedBytesInUse : 28
PoolNonpagedAllocs         : 301815
PoolNonpagedBytes          : 117280768
PoolPagedAllocs            : 221638
PoolPagedBytes             : 233611264
PoolPagedResidentBytes     : 223879168
Path                       : \\MrEdComputer\root\cimv2:Win32_PerfFormattedData_PerfOS_Memory=@
Properties                 : {AvailableBytes, AvailableKBytes, AvailableMBytes, CacheBytes...}
__SERVER                   : MrEdComputer



Troubleshooting a VBScript Script That Gives Error 80041001

Hey, Scripting Guy! Question

Hey, Scripting Guy! I am pretty new to using VBScript and seem to have hit a wall. I put together the script below to output a file containing all the software installed on a machine. Every time I run the script, I get an 80041001 error, which points to the line with the “Next” statement.  I have been scouring the Web for information about this particular issue, but cannot seem to find anything that might be related to the issue I am facing. I am on a Windows XP computer with Service Pack 3. This is part of a bigger project that I am working on that will query network machines for various properties such as CPU, OS, RAM, RAID, NIC, etc. I have managed to get all the other parts working except for this one. Any assistance would be greatly appreciated.

Here is the script that is giving me problems.

QueryProductsWriteToFile.vbs

Set objFSO = CreateObject("Scripting.FileSystemObject")
 
Set objTextFile = objFSO.CreateTextFile("C:\software.txt", True)
 
StrComputer = “.”
 
Set objWMIService = GetObject("winmgmts:" & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
 
Set colSoftware = objWMIService.ExecQuery ("Select * from Win32_Product")
               
For Each objSoftware in colSoftware
                                strDesc = objSoftware.Description
                                strIdent = objSoftware.IdentifyingNumber
                                strInstDate = objSoftware.InstallDate2
                                strName = objSoftware.Name
                                strVend = objSoftware.Vendor
                                strVer = objSoftware.Version
                                objTextFile.WriteLine strDesc & vbtab & strIdent & vbtab & strInstDate & vbtab & strName & vbtab & strVend & vbtab & strVer
Next  
 
objTextFile.Close
 
WScript.Quit()

- DM

SpacerHey, Scripting Guy! Answer

Hi DM,

The error you are receiving, 80041001, is WbemFailed. This means that the error is coming from WMI. I ran the QueryProductsWriteToFile.vbs script on my Windows 7 computer, and it runs just fine. By the way, that is a nice script, especially because you say you are new to VBScript. I believe you probably need to rebuild the WMI repository on your computer, because it has more than likely become corrupted.

 

This brings us to the end of another Quick-Hits Friday! It also brings to a close the first week after the 2009 Summer Scripting Games. The cheers, flags, pomp, and circumstance are still fresh in our minds as is the virtual popcorn. We sincerely hope you have a great weekend, and join us next week. If you want to know what we will be doing next week follow us on Twitter. Have you seen Script Center 101? Dude, the critics love it. Two thumbs up! Until Monday, peace!

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

 

Hey, Scripting Guy! How Can I Prompt Users for Information When a Script Is Run?

Hey, Scripting Guy! Question

Hey, Scripting Guy! I would like to have my script prompt the user for information when it is run. But I would like the person running the script to be able to supply more than one piece of information when the script is run. For example, I would like to provide information about hard drives on the computer, but I do not want to list all of the drives. I would like the user to be able to select one specific drive on the computer. Does this make sense?

- MS

SpacerHey, Scripting Guy! Answer

Hi MS,

There are lots of things that do not make sense to me: Burning Man is a good example; however, your request makes perfectly good sense. It does indeed seem like a good idea to be able to provide a user of your script the ability to make choices, or to supply a variety of information to your script when it is launched. This goliath grouper I saw while scuba diving off the coast of Little Cayman also seems to have some good ideas. He likes to hang out under coral ledges and wait for crustaceans, small fish, or even an octopus when he can find one.

Image of a goliath grouper


As you know, MS, scripts often require input. One reason a script needs input is to customize the information that is returned to the user. In VBScript you could use the WshArguments object to obtain information from the command line. You could also use an input box. In Windows PowerShell, you have the Read-Host cmdlet that will allow you to prompt for information from the command line. This is seen here:

PS C:\> Read-Host "Type your name"

type your name: ed

ed

PS C:\>

 

If you prompt for information, but do not save the information that is returned, all you have done is halt the execution of the script. However, this can be effectively used. For example, if you save a script that will copy files to a USB drive, you may wish to prompt the user to ensure the USB drive is connected before continuing. It might look something like the DemoPromptToCopy.ps1 script. In the DemoPromptToCopy.ps1 script, we first display a string that states we are getting ready to copy some files to a USB drive. To do this, we place the string in quotation marks—it will automatically be shown on the screen. This is seen here:

"Preparing to copy files to USB drive"

Next we use the Read-Host cmdlet to display a message prompt on the screen. The first parameter of the Read-Host cmdlet is the prompt parameter. We leave it off here, because the command is easy to read and to understand without it. This is seen here:

Read-Host "Ensure usb drive is in the computer. Press <enter> when ready to copy"

When the script gets to the Read-Host cmdlet, it will halt execution of the script until ENTER is pressed. This is seen here:

Image of user prompted to ensure USB drive is in computer


The last thing the DemoPromptToCopy.ps1 script does is copy a directory from the C: drive to the D: drive. The complete DempPromptToCopy.ps1 script is seen here.

DemoPromptToCopy.ps1

"Preparing to copy files to USB drive"

Read-Host "Ensure USB drive is in the computer. Press <enter> when ready to copy."

Copy-Item -path "C:\fso" -destination "D:\fso" -recurse

It is possible to expand on the capabilities of the Read-Host cmdlet by saving the results from the cmdlet in a variable. After you have done this, you have a wide range of things you can do. One of my favorite Windows PowerShell scripting techniques is to use the Switch statement to evaluate the data that is returned from the command line. By using the Switch statement, you have the ability to use a regular expression pattern match to verify what is typed at the command line.

In the ReadHostQueryDrive.ps1 script, the Read-Host cmdlet is used to prompt the user to enter the drive letter that will be used to request volume information from WMI. The Switch statement is used to evaluate the value that is typed in response to the prompt. This time we must store the results of the Read-Host cmdlet into a variable. We use the $response variable for that job. The Switch statement uses the –regex parameter to use regular expressions to evaluate the value that is stored in the $response variable. If the data stored in the $response variable contains the letter "c" it will use the Get-WmiObject cmdlet to query the Win32_Volume WMI class for information on the "c" drive. If the string contained in the $response variable contains the letter "d" the script will query for information related to the "d" drive. The big advantage of the ReadHostQueryDrive.ps1 script is it allows the user to type in a wide variety of responses such as the following:

·         C

·         C:

·         C:\

This solves a problem that that often crops up when trying to use command-line utilities from the command line—how does the utility want the drive letter specified? The ReadHostQueryDrive.ps1 script is seen here.

ReadHostQueryDrive.ps1

$response = Read-Host "Type drive letter to query <c: / d:>"
Switch -regex($response) {
  "C" { Get-WmiObject -class Win32_Volume -filter "driveletter = 'c:'" }
  "D" { Get-WmiObject -class Win32_Volume -filter "driveletter = 'd:'" }
} #end switch

A more elegant approach to requesting information from the user is to use the $host.ui.PromptForChoice class to handle the prompting. The PromptForChoice class uses the choices that are created by the System.Management.Auomation.Host.ChoiceDescription class. Because the choice descriptions are an array, we use array notation to specify the different choices. When creating the choice descriptions, each choice is preceded by the ampersand and stored in an array. This is seen here:

$choices = [System.Management.Automation.Host.ChoiceDescription[]] `

@("&C:", "&D:", "&All")


Next we create a variable that is used to specify the default choice. Because it is an array, each value has numeric value that begins counting a 0. Now that we have both the choices created and the default choice identified, it is time to create the PromptForChoice class. This class needs the caption, the message, the choices and the default choice, as seen here:

$choiceRTN = $host.ui.PromptForChoice($caption,$message, $choices,$defaultChoice)


We use the Switch statement to evaluate the choice that is selected. This is seen here:

switch($choiceRTN)

{

 0    { Get-WmiObject -class Win32_Volume -filter "driveletter = 'c:'"  }

 1    { Get-WmiObject -class Win32_Volume -filter "driveletter = 'd:'"  }

 2    { Get-WmiObject -class Win32_Volume  }}

The complete PromptForChoice.ps1 script is seen here. 

PromptForChoiceQueryDrive.ps1

$caption = "Please select the drive to query"

$message = "Select drive to query"

$choices = [System.Management.Automation.Host.ChoiceDescription[]] `

@("&C:", "&D:", "&All")

[int]$defaultChoice = 0

$choiceRTN = $host.ui.PromptForChoice($caption,$message, $choices,$defaultChoice)

 

switch($choiceRTN)

{

 0    { Get-WmiObject -class Win32_Volume -filter "driveletter = 'c:'"  }

 1    { Get-WmiObject -class Win32_Volume -filter "driveletter = 'd:'"  }

 2    { Get-WmiObject -class Win32_Volume  }

}

 

When the PromptForChoiceQueryDrive.ps1 script is run, these choices are displayed: 

Image of choices displayed when script is run


Well, MS, we hope we have given you some ideas you can use to elicit information from the people who use your scripts. This also concludes Input Week on the Script Center. Join us tomorrow as we open the mail bag and look at questions that do not require such a long explanation. That’s right, it is time for Quick-Hits Friday! Until then, peace.

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Hey, Scripting Guy! How Can I Use a Named Parameter When Running Scripts?

Hey, Scripting Guy! Question

Hey, Scripting Guy! I would like to be able to use a named parameter when running my scripts. There are a couple of reasons for this. The first is that I think that it looks cool to use a named parameter. The second reason is probably just as important as the first: I think it makes the script easier to use if I use a named parameter rather than needing to rely upon remembering the order of the parameters. How can I do that? In VBScript I created a special object, the WshNamed object, but I tried and that does not work on Windows PowerShell. What am I missing?

- SC

SpacerHey, Scripting Guy! Answer

Hi SC,

What I am missing is my afternoon teaEarl Grey leaves, fresh drawn spring water, a cinnamon stick (Ceylon not cassia), a dribble of milk, and an ANZAC biscuit. Why, you may ask, am I missing my afternoon tea? Well I will tell you: Never make a Scripting Wife mad! Yesterday, she caught me in the pantry getting into her Tim Tams, which are chocolate-covered cookies (“biscuits”) made in Australia. They have chocolate cream in the middle, and the cookie itself is chocolate. If you are keeping count, that is three different types of chocolate! Every once in a while, I will relent and make a pot of fresh Kona coffee.

What do a Tim Tam and cup of coffee have to do with not getting afternoon tea? Well, let me finish. When I was in Brisbane, Australia, one of the students who was attending my VBScript class showed me a trick he called the "exploding Tim Tam." What you do is bite the opposite corners off the Tim Tam, stick it in your coffee, and suck the hot coffee through the Tim Tam as if it were a straw. You have to be really quick because the hot coffee will melt the soft chocolate cream that is in the middle of the cookie (biscuit). If you are not careful or are out of practice, you can end up with a terrible mess in the middle of the kitchen. If you have stolen one of the Scripting Wife's carefully rationed Tim Tams to make this mess, you probably will be missing your afternoon tea. (It is only fair. The ANZAC biscuits are mine, and the Tim Tams are hers. She is of course right, and I should have stayed out of the Tim Tamsor at least asked.)

This banded coral shrimp (Stenopus hispidus) I saw while scuba diving off the coast of Little Cayman did not get any afternoon tea either. Perhaps it too should have stayed out of the Tim Tams.

Image of a banded coral shrimp


Well, CJ, as you have no doubt seen using the $args automatic variable is a quick and easy method to receive input to your script from the command line. As we saw yesterday, this simplicity is not without cost.

The cost is flexibility. While it works great for retrieving single values, it does not work as well when multiple parameters have to be supplied. In addition there is no way to make switched parameters when using the $args automatic variable.

The param statement lets you create named arguments and switched arguments. To use the param statement to create a named argument, you use the param keyword, open a set of parentheses, and specify your parameter name. This is illustrated here.

Param($computer)


To specify a default value for the parameter you use the param keyword, specify the parameter name inside a set of parentheses, and use the equality operator to assign a value. This is shown here:

Param($computer = "localhost")


The param statement must be the first noncommented line in the script. If you try to use the param statement in another position, you will receive two errors. The first is an error from WMI that mentions that the value supplied to the computername parameter of the Get-WmiObject cmdlet is null. The second error that you will receive states that param is not recognized as a cmdlet, function, script file, or operable program. This error is seen here:

Image of error returned when param statement is in wrong position


The Get-BiosParam.ps1 script illustrates using the param keyword to create a named argument, and to assign a default value for the $computer variable. The Get-WmiObject cmdlet uses the WIN32_Bios WMI class to return BIOS information to the display from the computer that was specified in the $Computer variable. This will be either a computer name that was typed when the Get_BiosParam script was run, or the localhost computer.

There are three different ways the computer parameter may be supplied from the command line:

·         Type the entire parameter name.

·         Type a partial parameter name. You must type enough of the parameter name to uniquely identify the parameter.

·         Omit the parameter name and rely upon position.

These three different ways of using the command-line parameters are illustrated here with the Get-BiosParam.ps1 script.

PS C:\> Get-BiosParam.ps1 –computer loopback

SMBIOSBIOSVersion : 7LETB7WW (2.17 )
Manufacturer      : LENOVO
Name              : Ver 1.00PARTTBLx
SerialNumber      : L3L4518
Version           : LENOVO – 2170

PS C:\> Get-BiosParam.ps1 –c loopback

SMBIOSBIOSVersion : 7LETB7WW (2.17 )
Manufacturer      : LENOVO
Name              : Ver 1.00PARTTBLx
SerialNumber      : L3L4518
Version           : LENOVO – 2170

PS C:\> Get-BiosParam.ps1 loopback

SMBIOSBIOSVersion : 7LETB7WW (2.17 )
Manufacturer      : LENOVO
Name              : Ver 1.00PARTTBLx
SerialNumber      : L3L4518
Version           : LENOVO – 2170


The complete Get-BiosParam.ps1 script is shown here.

Get-BiosParam.ps1

Param($computer = "localhost")
Get-WmiObject -Class win32_bios -computername $computer

If you would like to make the parameter mandatory, you need to check for the existence of the variable you used to hold the command-line input. To check for the existence of the variable you can use the –not operator and an if statement:

if(-not($computer) )


If the variable does not exist, it means that a value was never supplied for the parameter. The cool thing we do in the script is use the Read-Host cmdlet to prompt for the missing value. This is a much better experience than raising an error. Here is how we prompt for the missing value:

{ $computer = Read-Host "Please supply computer name" }


After we have the requested value, we proceed to the remainder of the script. The complete PromptForMissingParameter.ps1 script is seen here.

PromptForMissingParameter.ps1


Param($computer )
if(-not($computer) ) { $computer = Read-Host "Please supply computer name" }
Get-WmiObject -Class win32_bios -computername $computer


CJ, you can see that it is easy to use the param statement to request values for parameters from the command line. By using the param statement, you gain a tremendous amount of flexibility when it comes to working with command-line arguments. Join us tomorrow as we continue with Input Week. If you want to keep up with the latest information for the Script Center, follow us on Twitter. You can also join our group on Facebook. If you get stuck with a scripting problem, you can always post a question on the Official Scripting Guys Forum.


Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Hey, Scripting Guy! How Can I Supply More Than One Value from the Command Line?

Hey, Scripting Guy! Question

Hey, Scripting Guy! I do not think I write very complicated scripts, but quite often I find that it would be nice to be able to supply more than one value from the command line. It seems that everything I see written for Windows PowerShell uses the $args variable to get command line input, and I cannot figure out how to make $args accept more than one value. Can you help me? I looked over all the submissions I saw for the 2009 Summer Scripting Games, and I did not see anything there either.

- KA

SpacerHey, Scripting Guy! Answer

Hi KA,

Your question reminds me of a time when I was scuba diving off the coast of the Big Island, in the state of Hawaii in the United States. It was a beautiful spring day, with high-level puffy white clouds and a sea state that is best described as glass. From the deck of side of the dive boat, I could look down and see the rock formations 100 feet below the surface of the boat. Schools of humuhumunukunukuapuaa were swimming around looking for food or scuba divers to harass. (The humuhumunukunukuapuaa is a brightly colored reef trigger fish. By the way, if you ever get a scuba certification in Hawaii, you have to be able to pronounce the name of that fish.) The water was a warm 82 degrees. The highlight of the dive was a 40-foot-long lava tube, which was about as wide as an elevator. After doing a rolling entry into the water and joining my dive buddy, we headed over to the entrance to the lava tube and began our descent. The further down we went, the darker the inside of the lava tube became. After one rather narrow passage, we came upon a shelf, and lying there was this shark:

Image of a shark 

When your flashlight uncovers a smiling nine-foot shark inside a darkened space about the size of an elevatorlet's just say that you begin to have multiple sources of input.

KA, handling multiple input parameters from the command line of your script will not require you to wrestle elevator sharks. But if you need to supply multiple values via the command line, and you attempt to do so via the $args automatic variable, you will be greeted with the following error message that warns of a type mismatch. The error does not use the term “type mismatch,” but this is what is meant by the error. It states you are attempting to supply an object array to a string. The computername parameter, it states, requires a string for its input:

Get-WmiObject : Cannot convert 'System.Object[]' to the type 'System.String' required by parameter 'ComputerName'. Specified method is not supported.
At C:\Users\edwils.NORTHAMERICA\AppData\Local\Temp\tmp774.tmp.ps1:18 char:47
+  Get-WmiObject -Class win32_bios -computername <<<<  $args
    + CategoryInfo          : InvalidArgument: (:) [Get-WmiObject], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgument,Microsoft.PowerShell.Commands.GetWmiObjectCommand

The error is not caused by the array. The error is caused because the $args automatic variable arrives as a System.Object array. The Get-WmiObject cmdlet will accept an array of computer names to the computername parameter. This is seen here where an array of computer names is supplied directly to the computername parameter and BIOS information is retrieved via the WIN32_Bios WMI class.

PS C:\> Get-WmiObject -Class Win32_Bios -ComputerName localhost,loopback


SMBIOSBIOSVersion : 7LETB7WW (2.17 )
Manufacturer      : LENOVO
Name              : Ver 1.00PARTTBLx
SerialNumber      : L3L4518
Version           : LENOVO - 2170

SMBIOSBIOSVersion : 7LETB7WW (2.17 )
Manufacturer      : LENOVO
Name              : Ver 1.00PARTTBLx
SerialNumber      : L3L4518
Version           : LENOVO - 2170


There are a couple of ways to solve this issue. The first way is to index into the array and force the retrieval of the computer names. This is shown in the Get-BiosArray1.ps1 script:

Get-BiosArray1.ps1

Get-WmiObject -Class win32_bios -computername $args[0]


The technique of indexing directly into the $args automatic variable works well. The problem is it looks like it would only retrieve the first item in the array, but it in fact retrieves both items. Because Windows PowerShell automatically handles the transition between a single item and multiple items in an array, the technique of indexing into element 0 of the array works whether one or more items are supplied. This issue of the way the Windows PowerShell $args automatic variable handles an array of information can be seen in the StringArgs.ps1 script:

StringArgs.ps1

'The value of arg0 ' + $args[0] + ' the value of arg1 ' + $args[1]


When the StringArgs1.ps1 script is run with the array "string1","String2" supplied from the command line, the entire array is displayed in $args[0] and nothing is displayed for $args[1]. This is shown here:

PS C:\> StingArgs.ps1 "string1","String2"
The value of arg0 string1 String2 the value of arg1
PS C:\>


A better way to handle an array that is supplied to the $args automatic variable is to use the ForEach-Object cmdlet and pipeline the array to the Get-WmiObject cmdlet. This is seen in Get-BiosArray2.ps1:

Get-BiosArray2.ps1

$args | Foreach-Object {
Get-WmiObject -Class win32_bios -computername $_
} 

When the Get-BiosArray2.ps1 script is started with an array of computer names from the Windows PowerShell prompt, the following output is displayed:

PS C:\> Get-BiosArray2.ps1 localhost,loopback
SMBIOSBIOSVersion : 7LETB7WW (2.17 )
Manufacturer      : LENOVO
Name              : Ver 1.00PARTTBLx
SerialNumber      : L3L4518
Version           : LENOVO - 2170

SMBIOSBIOSVersion : 7LETB7WW (2.17 )
Manufacturer      : LENOVO
Name              : Ver 1.00PARTTBLx
SerialNumber      : L3L4518
Version           : LENOVO – 2170


There are two advantages to using the ForEach-Object cmdlet. The first is readability of the code. It meets the principle of “least shock.” When someone reads the code and they see that the script accepts an array for input via the $args variable, they are not surprised when they see the script using the ForEach-Object cmdlet to walk through the array. The other advantage is that the script will work when a single value is supplied for the input.

Unfortunately, if the same approach is tried with the StringArgsArray.ps1 script, the value of the $args array is repeated twice. The StringArgsArray1.ps1 script is seen here:

StringArgsArray1.ps1

$args | ForEach-Object {
'The value of arg0 ' + $_ + ' the value of arg1 ' + $_
}


When the StringArgsArray1.ps1 script is started, the results seen here are displayed:

PS C:\> StingArgsArray1.ps1 "string1","String2"
The value of arg0 string1 String2 the value of arg1 string1 String2
PS C:\>

If you examine the output from the StringArgsArray1.ps1 script, you will see both elements of the $args array displayed. If you modify the StringArgsArray1.ps1 script so that you index into the array that is contained in the $_ automatic variable (which represents the current item on the pipeline), you will be able to retrieve both items from the array. The revised script is called StringArgsArray2.ps1, and it is shown here:

StringArgsArray2.ps1

$args | Foreach-Object {
'The value of arg0 ' + $_[0] + ' the value of arg1 ' + $_[1]
}

When the script is run, the correct information is displayed. This is seen here:

PS C:\> StingArgsArray1.ps1 "string1","String2
The value of arg0 string1 the value of arg1 String2
PS C:\>


Now that is pretty cool huh? Well KA, we have reached the end of another exciting Hey, Scripting Guy! article. Join us tomorrow as we continue looking at handling input to a Windows PowerShell script. If you want a sneak peek of what is coming up, you can always follow us on Twitter or join our Facebook group. If you get stuck while you are working on a script, don't forget the Official Scripting Guys Forum, which we link to from a tab on our home page. If you are having problems finding things on the new Script Center, don’t feel like the Lone Ranger. Keep your eyes out for Script Center 101 in which we will unravel the mysteries of the new site.

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Hey, Scripting Guy! How Can I Run a Script Without Involving Notepad?

Hey, Scripting Guy! Question

Hey, Scripting Guy! I love looking at the scripts on the Script Center. I think it is amazing when a person writes you an e-mail stating they have this problem that will take like a zillion years to do manually, and you whip up a script that they can run in just a few seconds and it solves their problem. I am not sure how much you guys make, but it is not enough as far as I am concerned. There is just one thing (of course, or I would not be writing), and that nearly all of the scripts require you to open them up in Notepad or some other script editor, and make changes to the script before you run. To be perfectly honest, I do not want my help desk people opening up scripts and messing around with them.

Isn't there a way to let a script run like it was an application? I am okay with the help desk people typing the path to the script and giving it the name of a target computer, but I really don't want them fooling around with script editors. Can you help me?


- SC

SpacerHey, Scripting Guy! Answer

Hi SC,

We are completely worn out after the 2009 Summer Scripting Games. (And the new site launch. And TechEd. And...) This year was the best Scripting Games so far by nearly every metric imaginable. But you did not write and ask how the 2009 Summer Scripting Games went (don’t worry, we will post a 2009 Summer Scripting Games recap later). You are concerned about handling input for your script.

A couple of years ago while scuba diving off the coast of the island of Maui in the state of Hawaii in the United States, I (Ed) saw a beautiful raccoon butterfly fish floating along the side of a coral ledge. These fish are not exactly omnivores (I have never seen one scarfing down burgers), but they do eat a variety of food such as sponges, algae, and coral polyps. Because of the variety of food the raccoon butterfly fish eats, it must be able to sense both danger and food sources from a variety of inputs. Here's the raccoon butterfly fish I snapped that day:

Image of raccoon butterfly fish

SC, just like a raccoon butterfly fish, the possibilities are varied, and the potential combinations for input and output are many. Choosing the best input method and output destination is not always an exact science, and often the best solution might be dependent upon external factors such as limitations of network infrastructure, ease of use, or speed of development.

Reading from the command line is a traditional way to provide input to a script. It has the advantage of simplicity, which means it is easy to implement and reduces development time.

If you want the ability to alter the behavior of a script at run time and you plan to run the script in an interactive fashion, accepting input from the command line may be the best solution for you. And accepting input from the command line can be simple to implement. The biggest limitation of command-line input is the requirement for user intervention. You can circumvent the requirement of user interaction by assigning default values to the command-line parameters and by selecting default actions for script behavior.

There are several methods for receiving command-line input in a script. The simplest method is to use command-line arguments. When a Windows PowerShell script is run, an automatic variable, $args, is created. The $args variable will hold values supplied to the script when it is started.

Get-Bios.ps1

Get-WmiObject -Class win32_bios -computername $args

The Get-Bios.ps1 script starts when you call the script and supply the name of the target computer. Because $args automatically accepts a string for the input, you do not have to place the name of the target computer in quotation marks. The period in this example is used to refer to the current directory. It is the same as typing the full path: C:\scriptingguy\get-bios.ps1 for the script. This is shown here:

PS ScriptingGuy:\> .\Get-Bios.ps1 localhost

This result of running the Get-Bios.ps1 script is seen here:

Image of result of running Get-Bios.ps1 script

Because the script accepts command-line input, you can also use the Windows PowerShell pipeline to modify the way the script runs. If you have a text file that has a listing of computer names in it, you can use the Get-Content cmdlet to read the contents of the file, pipeline it to a ForEach-Object cmdlet, and query each computer in the text file. The syntax for doing this is seen here (note that I used the foreach alias for the ForEach-Object cmdlet; this makes the command a bit easier to type and does not reduce the readability of the command):

Get-Content C:\fso\Computers.txt | foreach { C:\fso\Get-Bios.ps1 $_ }

When the command runs, this result is displayed:

Image of result displayed when command is run


If you think the previous command is too much typing, you can shorten it by the use of additional aliases. The problem with such a shortened command is it is rather difficult to read. The alias cat is used for the Get-Content cmdlet and the alias % is used for the ForEach-Object cmdlet. The shortened command would look similar to the one seen here:

cat C:\fso\Computers.txt | % { C:\fso\Get-Bios.ps1 $_ }

When you run the command, this output is displayed:

Image of output displayed when command is run


While the script is running, the value you supplied from the command is present on the Windows PowerShell variable drive. You can determine the value that was supplied to the script by querying the Windows PowerShell variable drive for the $args variable. This is seen here:

Get-Item -path variable:args

The result of running the above query is seen here:

PSPath        : Microsoft.PowerShell.Core\Variable::args
PSDrive       : Variable
PSProvider    : Microsoft.PowerShell.Core\Variable
PSIsContainer : False
Name          : args
Description   :
Value         : {localhost}
Visibility    : Public
Module        :
ModuleName    :
Options       : None
Attributes    : {}

Even though accessing the value of $args via the Windows PowerShell variable drive provides a significant amount of information, it is easier to use the Get-Variable cmdlet:

Get-Variable args

SC, we hope we have given you some useful information about gathering input for your script. Join us tomorrow as we continue with Input Week. If you want to keep up with the latest information about the Script Center, follow us on Twitter. You can also join our group on Facebook. And if you get stuck with a scripting problem, you can always post a question to the Official Scripting Guys Forum.
 

Ed Wilson and Craig Liebendorfer, Scripting Guys

Hey, Scripting Guy! Event 10 *Solutions* from Expert Commentators (Beginner and Advanced; the 1,500-meter race)

  

(Note: These solutions were written for Event 10.) 

Beginner Event 10:  The 1,500-meter race

In the 1,500-meter race, you will go for the gold by writing a script that counts down from three minutes.

Guest commentator: Andrew Willett

Image of guest commentator Andrew Willett 

Andrew Willett is the projects manager for the IT department of a Steinhoff group company. Based in London, he spends most of his time deploying, designing, and developing Microsoft-based architectures.

VBScript solution

I wrote the solution to the Beginner 1,500-meter race event using probably the most ubiquitous developer environment in the world: Notepad!

To perform the actual counting down by a second, I used a While loop containing a call to WScript.Sleep().  To make the script sleep for 1 second, we pass it 1,000 milliseconds as a parameter, and decrementing the seconds remaining by 1 each time.

When run from the console using cscript.exe, the script prints each decrement to the console when counting down to zero. When run interactively using Windows Scripting Host, this is suppressed (it would normally manifest itself as a blocking, modal dialog box) and only notifies the user on start and finish.

The script allows the user to change the default duration of 180 seconds to any other integer value by the use of a simple command-line argument, in the form BeginnerEvent10Solution.vbs <int>. The script performs basic error-checking to validate the value before using it in place of the default. If no command-line argument is passed and the default is used, command-line users see a reminder of the syntax required for passing the argument.

When the script is run, this output is displayed:

Image of the output when script is run


The complete BeginnerEvent10Solution.vbs script is seen here.

BeginnerEvent10Solution.vbs

'The time remaining/duration of the script.
Dim remaining

'Whether the script is running interactively.
Dim nonInteractive

'Initial duration of 180 seconds.
remaining = 180

'Initially presume the script is running interactively.
nonInteractive = false

'If the script is running non-interactively through the console.
If "cscript.exe" = LCase(Right(WScript.FullName, 11)) Then

    'The script is running non-interactively.
    nonInteractive = true

End If

'Check if a command-line argument was passed.
If (WScript.Arguments.Length <> 0) Then

    'See if the first command-line argument is an *integer*.
    On Error Resume Next
    i = CInt(WScript.Arguments.Item(0))
    If (Err.Number = 0) And (WScript.Arguments.Item(0) = CStr(i)) Then

        'Set the duration to that of the command-line argument.
        remaining = WScript.Arguments.Item(0)

    Else

        'Inform the user that the argument was not valid, and quit.
        WScript.Echo("Error: The command-line argument passed was not understood.")
          WScript.Quit

    End If
    On Error GoTo 0

Else

    'If the script is running from the console
    If (nonInteractive) Then

        'Suggest that command-line arguments can be used.
        WScript.Echo("To change the duration, use script.vbs <duration-in-seconds>")
        WScript.Echo("")

    End If

End If

'Read-out the summary.
WScript.Echo("The script will now count-down from " & remaining & " seconds.")

'If the script is running from the console
If (nonInteractive) Then

    'Blank line to keep the console looking tidy.
    WScript.Echo("")

End If

'While there is still time remaining.
While remaining > 0

    'If script is running interactively, supress the 'ticking' as it blocks, otherwise:
    If (nonInteractive) Then

        'Echo the number of seconds remaining.
        WScript.Echo(CStr(remaining) & " seconds remain")

    End If

    'Decrement the time remaining by one.
    remaining = remaining - 1

    'Pause for 1000 milliseconds (1 second).
    WScript.Sleep(1000)

WEnd

'If the script is running from the console
If (nonInteractive) Then

          'Blank line to keep the console looking tidy.
          WScript.Echo("")

End If

'Echo that the time has elapsed.
WScript.Echo("> time elapsed <")
 


Guest commentator: Thomas Lee

Image of guest commentator Thomas Lee

Thomas Lee has been scripting pretty much forever. He was pretty hot at JCL on the IBM 360 in the late 1960s, and did a ton of shell scripting in the 70s on ICL VME. He learned batch scripting with DOS 2.0 but managed to avoid VBScript. After he saw the beta of Windows PowerShell in September 2003, he never looked back. Thomas is proficient in both Windows PowerShell 2.0 and 1.0, and he specializes in both the .NET and WMI aspects of the language. Thomas has the distinction of being the first person to blog about Windows PowerShell. He is also a moderator on the Hey, Scripting Guy! Forum and maintains the Under the Stairs blog.

Windows PowerShell solution

This year for the Scripting Games I was asked to write the solution for Beginner Event 10, the 1,500-meter race. This script is a countdown timer that goes from three minutes to zero seconds. When the time is up, it displays a message indicating that the given amount of time has elapsed. That sounded fairly simple so I set to work.

I saw this as really being two separate things to do. These two tasks are listed here:

· Construct some sort of loop that displays the current time remaining, wait a second, and then do it again.

· Display the number of seconds in a nice way.

The basic outline of the script is pretty easy. I use the Start-Sleep cmdlet and wait for a second. The basic script is seen here:

$time = 180
do {display $time; start-sleep 1; $time--} Until $time=0
"All Done"

That is all fine and well, but there is only one small problem. If the script sleeps for exactly 1 second, the total time for each iteration is 1 second plus however long it takes to do the time display, etc. In other words, the whole script will run longer than 3 minutes. The time between each call to the display function would be a little more than 1 second.  To get around this, I added in a fudge factorsome number of milliseconds I would deduct to account for the additional activities. Thus, I'd go to sleep for 1 second less the fudge factor. In the script that I use, I've set the fudge factor to 5 milliseconds. You can see at the end of the script the actual time used. When you run this script on your own machine, you can adjust the script to suit you.

The second problem is how to display the number of seconds nicely. To separate the display aspects from the rest of the script, I created a function that takes the time left (in seconds) and displays it nicely.  That meant I could get the basic script working, and then work out how to make the output look better.

To get the script working, I initially just cleared the screen, and displayed the number of seconds available.  Pretty boring but it was progress! Next I got the idea of leveraging the System.TimeSpan object. I created a new object from the total number of seconds remaining. The timespan object converts the total number of seconds into the minutes and seconds that are left. That made the display almost complete. I then decided to display "minute" when the number of minutes left was one instead of "1 minutes". And I did the same thing for number of seconds.

When the script runs it clears the screen and displays the time remaining. This is seen here:

Image of results of running script


When the BeginnerEvent10Solution.ps1 script has completed the counting, it displays the final time the script really took. This is seen here:

Image of final time the script took

As you can see, the total time was a tad more than 3 minutes, but that is close enough for our purposes.

So there's a basic timer script. If I had more skills with System.Forms, I might have been able to create a nicer bit of output. But I'll leave that as an exercise for the more skilled!

There are a number of solutions for easily creating Windows Forms from within Windows PowerShell. One such solution is Primal Forms from SAPIEN. Another is PowerBoots, which is a CodePlex project. Another one is the presentation framework being developed by James Brundage.

The completed BeginnerEvent10Solution.ps1 script is seen here.  This script requires Windows PowerShell 2.0.

BegginerEvent10Solution.ps1

<#
.SYNOPSIS
    This script counts down from 3 minutes and displays the time remaining.
.DESCRIPTION
    This script is an entry in the 2009 Summer Scripting Games.
.NOTES
    File Name  : Display-Counter.ps1
          Author     : Thomas Lee - tfl@psp.co.uk
          Requires   : PowerShell V2 CTP3
.LINK
    This script posted to:
              http://www.pshscripts.blogspot.com
.EXAMPLE
    Left as an exercise for the reader.
#Requires –version 2.0
#>

# First helper function to display the time remaining
function display-time {
param ($timetodisplay)
# clear the screen
cls

# now create a timespan object from number of seconds
$display = New-Object System.TimeSpan 0,0,$timetodisplay

# Get minutes and seconds
$min=$display.minutes
$sec=$display.seconds

# Now work out "second" vs "seconds" and minutes
if ($min -gt 1 -or $min -eq 0){$mintag="Minutes"}
          else {$mintag="Minute"}
if ($sec -gt 1 -or $sec -eq 0) {$sectag="seconds"}
                          else {$sectag="second"}

# now print out minute(s) and second(s) remaining
"{0} {1}, {2} {3}" -f $min,$mintag,$sec,$sectag
}

# start of script

#define time in seconds (3 minutes or 180 seconds)
$time = 180 

# define fudgefactor - number of milliseconds to wait to avoid
# timing errors in start-sleep etc.
$fudgefactor = 5
$interval    = 1000 - $fudgefactor

# start time
$starttime=Get-Date
do {
display-time $time

Start-Sleep -Milliseconds $interval
$time--
} while ($time -gt 0)
cls
"Done - counted down to $time seconds"

# Now calculate how long it really took
$finishtime=Get-Date
"Script took this long:"
$finishtime-$starttime
# end of script

Advanced Event 10: The 1,500-meter race

In the 1,500-meter race event, your script will need to be able to go the distance as you dynamically change the priority of a particular process every time the process is launched.

Guest commentator: Eric Lippert

Image of guest commentator Eric Lippert

Eric Lippert is in the Developer Division at Microsoft. He was on the team that designed and implemented new features for VBScript, Jscript, and Windows Script Host from 1996 through 2001. After a few years working on Tools for Office, he now works on the C# compiler. He maintains the Fabulous Adventures In Coding blog on MSDN that is mostly about C# these days, but there is a large archive of articles about the design of VBScript and Jscript in there. This makes fascinating reading if you’re interested in that sort of thing.

VBScript solution

There’s an odd thing that you learn when working on developer tools: The people who design and build the tools are often not the experts on the actual real-world use of those tools. I could tell you anything you want to know about the VBScript parser or the code generator or the runtime library, but I’m no expert on writing actual scripts that solve real problems. This is why I was both intrigued and a bit worried when the Scripting Guys approached me and asked if I’d like to be a guest commentator for the 2009 Summer Scripting Games.

I wrote this script the same way most scripters approach a technical problem that they don’t immediately know how to solve; I searched the Internet for keywords from the problem domain to see what I could come up with. Of course, I already knew about our MSDN documentation, I had a (very) basic understanding of WMI, and I knew that the Scripting Guys had a massive repository of handy scripts.

My initial naïve thought was that I would have to go with a polling solution; sit there in a loop, querying the process table every couple of seconds, waiting for new processes to pop up. Fortunately, my Internet searches quickly led me to discover that process startup events can be treated as an endless collection of objects returned by a WMI query.

That got me thinking about the powerful isomorphism between events and collections.

A collection typically uses a “pull” modelthe consumer asks for each item in the collection one at a time as needed, and the call returns when the item is available. Events typically work on a “push” modelthe consumer registers a method that gets called every time the event fires. But not necessarily; the WMI provider implements events on a “pull” model. The event is realized as a collection of “event objects.” It can be queried like any other collection. Asking for the next event object that matches the query simply blocks until it is available.

Similarly, collections could be implemented on a “push” model. They could call a method whenever the next item in the collection becomes available. The next version of the CLR framework is likely to have standard interfaces that represent “observable collections”, that is, collections that “push” data to you, like events do. The ability to treat events as collections and collections as events can lead to some interesting and powerful coding patterns.

I seem to have digressed somewhat from the topic at hand.

The code is straightforward. We begin by checking the validity of the command-line arguments, and then wait for new processes that match by name to be created. When one is created, we look it up in the process table to get its process object, and then set the priority of the process to the appropriate level.

One interesting point about the AdvancedEvent10Solution.vbs script is that it avoids an unlikely but nevertheless possible bug. In the microseconds after the creation of the new process but before we set its priority, it is possible that the original process ends and a new process with a different name begins. If the operating system is running low on unique process IDs, it is possible that the one that just freed up could be re-used. Therefore I ensure that the process that gets its priority set matches in both process ID and name. That way, we ensure that we never set the priority of the wrong process.

I deliberately omitted error handling code, except for checking whether the query result was null. There are a number of situations where the script could fail. For example, setting a process priority to RealTime is a dangerous operation that is typically restricted to administrators; a badly behaved process with such high priority can “starve” important processes, preventing them from ever getting any processor time. If an attempt to set priority fails, the AdvancedEvent10Solution.vbs script will simply crash. Arguably, that is the safer thing to do rather than to attempt to recover from the situation and continue. An alternative approach would be to detect the failure and attempt to “do the best we can” by setting the priority to High should an attempt to set to RealTime fail. Because error handling was not in the specification of the problem, that’s not what I implemented.

The complete AdvancedEvent10Solution.vbs script is seen here.

AdvancedEvent10Solution.vbs

Option Explicit

Const IdlePriority         = &h0040&
Const BelowNormalPriority  = &h4000&
Const NormalPriority       = &h0020&
Const AboveNormalPriority  = &h8000&
Const HighPriority         = &h0080&
Const RealTimePriority     = &h0100&

Dim ProcessName
Dim PriorityName
Dim Priority

Main

Sub Main()
    CheckArguments
    SetPriority
End Sub

Sub CheckArguments()
    Dim Dictionary
   
    If WScript.Arguments.Count <> 2 Then
        ShowUsage
        WScript.Quit
    End If
   
    Set Dictionary = CreateObject("Scripting.Dictionary")
    Dictionary.CompareMode = vbTextCompare
    Dictionary.Add "Idle", IdlePriority
    Dictionary.Add "BelowNormal", BelowNormalPriority
    Dictionary.Add "Normal", NormalPriority
    Dictionary.Add "AboveNormal", AboveNormalPriority
    Dictionary.Add "High", HighPriority
    Dictionary.Add "RealTime", RealTimePriority
   
    ProcessName = WScript.Arguments(0)
    PriorityName = WScript.Arguments(1)
    If Not Dictionary.Exists(PriorityName) Then
        ShowUsage
        WScript.Quit
    End If
   
    Priority = Dictionary(PriorityName)
   
End Sub

Sub ShowUsage()
    WScript.Echo _
        "Usage: " & WScript.ScriptName & " process.exe priority " & vbCrLf & _
        "priority: one of Idle, BelowNormal, Normal, AboveNormal, High, RealTime" & vbCrLf
End Sub

Sub SetPriority()

    Dim WMI, Events, Process, Processes, ProcessStartTrace
   
    Set WMI = GetObject("winmgmts:")
  
    Set Events = WMI.ExecNotificationQuery _
        ("Select * From Win32_ProcessStartTrace Where ProcessName = '" & ProcessName & "'")
 
    Do While(True)
        WScript.Echo "Waiting for " & ProcessName & " to start"
        Set ProcessStartTrace = Events.NextEvent

        ' This avoids the race condition where the process shuts down
        ' and a new process with the same process ID but different name
        ' starts up
        
        Set Processes = WMI.ExecQuery _
            ("Select * From Win32_Process Where ProcessId = " & ProcessStartTrace.ProcessId & _
             " And Name = '" & ProcessStartTrace.ProcessName & "'")
        If Not (Processes Is Nothing) Then
            For Each Process in Processes
                WScript.Echo "Setting priority of " & ProcessName & " to " & PriorityName
                Process.SetPriority Priority
            Next
        End If
    Loop

End Sub
 

Guest commentator: Don Jones

Image of guest commentator Don Jones

Don Jones has more than a decade of professional experience in the IT industry. He’s the author of more than 30 IT books, including Windows PowerShell: TFM; VBScript, WMI, and ADSI Unleashed; Managing Windows with VBScript and WMI; and many more. He’s a top-rated and in-demand speaker at conferences such as Microsoft TechEd and TechMentor, and writes the monthly Windows PowerShell column for Microsoft TechNet Magazine. Don is a multiple-year recipient of Microsoft’s Most Valuable Professional (MVP) Award with a specialization in Windows PowerShell. Don’s broad IT experience includes work in the financial, telecommunications, software, manufacturing, consulting, training, and retail industries and he’s one of the rare IT pros who can not only “cross the line” between administration and software development, but also between IT workers and IT management. He is a co-founder of Concentrated Technology where he blogs nearly every day about Microsoft-related technologies, including Windows PowerShell.

Windows PowerShell solution

PowerShell architect Jeffrey Snover set me on the right path to this one with his blog post about trapping WMI events. I decided to split up the tasks: One function watches for WMI events and outputs them to the pipeline, and a second function receives information about events that have happened. This split-brain approach makes it easier to repurpose the event-watching function later. After an event occurs, I check to see if it’s the desired process name (passed via an input parameter to the function), and if it is, I set its priority. As shown in the image below, the script tells you what it’s doing and you can see that the priority of the process (in Task Manager) has indeed been changed. Notice that I used Write-Host to write output messages; you could substitute Write-Verbose, and let the shell-wide $VerbosePreference variable dictate whether or not any output was shown. I worry about forgetting which key to press to cancel it, though!

You could take this script a bit further to add flexibility: The process name input parameter, for example, could be treated as an array, allowing a comma-delimited list of process names to be passed to the function. A downside of the script is that, in Windows PowerShell 1.0, the only way to catch and respond to WMI events is to run in a continuous loop. In Windows PowerShell 2.0, this script could be rewritten to leverage new event-response features, allowing you to continue working in the shell while waiting for events to occur.

The completed AdvancedEvent10Solution.ps1 script is seen here.

AdvancedEvent10Solution.ps1

function New-ProcessStartWatcher {

          # constants for key codes
          $ESCkey = 27
         
          # warning message
          Write-Host "Started watching for new processes; press ESC to quit" -foreground Green -background Black

          # start WMI event sink
          $query = New-Object system.Management.WqlEventQuery 'Select * from Win32_ProcessStartTrace'
          $scope = New-Object system.Management.ManagementScope 'root\cimv2'
          $watcher = New-Object system.Management.ManagementEventWatcher $scope,$query
          $options = New-Object system.Management.EventWatcherOptions
          $options.TimeOut = [timespan]'0.0:0:1'
          $watcher.Options = $options
          $watcher.Start()

          # wait for event
          while ($true) {
                   trap [System.Management.ManagementException] {continue}
                   $watcher.WaitForNextEvent()
                  
                   # watch for ESC or Q keypress to abort
                   if ($Host.UI.RawUi.KeyAvailable) {
                             $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyUp')
                             if ($key.VirtualKeyCode -eq $ESCkey) {
                                      $watcher.stop()
                                      break
                             }
                   }
          }

}

function Set-ProcessPriority {
          param ( [string]$processname = 'notepad.exe',
          [string]$priority = 'normal'
          )
          BEGIN {
                   # translate priority string to the numeric value needed
                   # by WMI
                   $prioritynumber = 0
                   switch ($priority) {
                             'idle' { $prioritynumber = 64; break }
                             'belownormal' { $prioritynumber = 16384; break }
                             'normal' { $prioritynumber = 32; break }
                             'abovenormal' { $prioritynumber = 32768; break }
                             'highpriority' { $prioritynumber = 128; break }
                             'realtime' { $prioritynumber = 256; break }
                   }
          }
          PROCESS {
                   if ($_.processname -eq $processname) {
                             $process = gwmi Win32_Process -filter "Name='$processname'"
                             Write-Host "Setting $processname to $priority" -foreground green -background black
                             $process.SetPriority($prioritynumber) | Out-Null
                            
                   }
          }
}

new-processstartwatcher | set-processpriority 'notepad.exe' 'idle'

When the AdvancedEvent10Solution.ps1 script is run, this output is displayed:

Image of output displayed when script is run

 

So…this brings us to the end of another round of great commentaries for the 2009 Summer Scripting Games. This also brings us to the end of this year’s Scripting Games. Stay tuned next week to see what we come up with as Craig and I try to catch our breath! It will be cool; we can at least say that. If you would like to catch all the latest news, follow us on Twitter. If you want to be really cool, you can join the The Scripting Guys group on Facebook.

Ed Wilson and Craig Liebendorfer, Scripting Guys

Hey, Scripting Guy! Event 9 *Solutions* from Expert Commentators (Beginner and Advanced; the javelin throw)

  

(Note: These solutions were written for Event 9.) 

Beginner Event 9: The javelin throw

For the javelin throw event, you will soar as you write a time logger.

Guest commentator: Salvador Manaois III

 Image of guest commentator Salvador Manaois III

Salvador Manaois III is a senior systems engineer at Infineon Technologies Asia Pacific Pte Ltd. He currently holds the MCITP certification for both Enterprise Administrator and Server Administrator. He also holds the MCTS (x5), MCSE, and MCSA certifications. He actively evangelizes the use of automation (through scripts and other technologies) both at work and in the various IT user groups in which he is involved.  He is also a moderator for The Official Scripting Guys Forum and maintains the Bytes & Badz blog.

 

VBScript solution

The Beginner Event 9 javelin throw was made easier by the fact that I am an operations guy. Attending to users, troubleshooting problems, and project-related work are part and parcel of what I do.

The first thing to do is to define the way with which the user inputs the data. I was thinking of popping up input boxes for the user but this approach is not too efficient. I ended up using an argument passed via the command line. Using the command line offers both simplicity and speed.

The next thing to do is to validate the data that is entered via the command line and to store the input into an array. Some datafor example, the Category fieldare mapped to a predefined category; the date and time will default to the current date and time.  Additionally, if the Category field is greater than 4 (cannot be mapped to the predefined categories), it is reassigned the value of 4 (Others). By defining default values for values, it makes it easier for the busy IT pro to make an entry.

For the BeginnerEvent9Solution.vbs script, I used two subroutines. The first subroutine, ValidateParameter, accepts the command-line argument and is used to validate the command-line values as mentioned earlier. The second subroutine, ShowUsage, is used to provide command-line help.

The complete BeginnerEvent9Solution.vbs script is seen here.

BeginnerEvent9Solution.vbs

Const ForWriting = 2
Const ForAppending = 8

Set objFSO = CreateObject("Scripting.FileSystemObject")

If Wscript.Arguments.Count = 0 Then
    ShowUsage
Else
    sParams = Wscript.Arguments(0)
    ValidateParameter(sParams)
End If

strNewText = Date
strNewText = Replace(strNewText,"/", "-")
OutputFile = strNewText & "_MyWork.csv"

If objFSO.FileExists(OutputFile) then
   Set oOutputFile = objFSO.OpenTextFile(OutputFile, ForAppending)
   oOutputFile.Writeline sParams
   oOutputFile.Close
Else
   Set oOutputFile = objFSO.CreateTextFile(OutputFile, ForWriting)
   oOutputFile.Writeline "Category,Time Completed,Time Used (in hours),Description,Status,Remarks"
   oOutputFile.Writeline sParams
   oOutputFile.Close
End if

Sub ValidateParameter(Params)
  arrParams = Split(Params,",")
  If Ubound(arrParams)+1 <> 6 then
    Wscript.Echo "You have entered an invalid number of parameters. Please try again."
    Exit Sub
  End if
  Select Case arrParams(0)
   Case 1 arrParams(0) = "Incident"
   Case 2 arrParams(0) = "Change Task"
   Case 3 arrParams(0) = "Project"
   Case 4 arrParams(0) = "Others"
   Case Else
        Wscript.Echo "You have entered an invalid category. Selecting ""Others""."
        arrParams(0) = "Others"
  End Select
  if arrParams(1) = nul then
    arrParams(1) = Now
  end if
  sParams = Join(arrParams, ",")
End Sub

Sub ShowUsage
  Wscript.Echo "LogMyWork.vbs Category, TimeCompleted, TimeSpent, Description, Status, Remarks"
  Wscript.Echo vbReturn
  Wscript.Echo "Description : This script is used to log daily activities to an Office Excel file."
  Wscript.Echo vbReturn
  Wscript.Echo "Parameter List:"
  Wscript.Echo vbTab & "Category" & vbTab & "Specifies the category of the task: 1 for Incidents,"
  Wscript.Echo vbTab & vbTab & vbTab & "2 for Change Tasks/Requests, 3 for Projects, and"
  Wscript.Echo vbTab & vbTab & vbTab & "4 for Other"
  Wscript.Echo vbReturn
  Wscript.Echo vbTab & "TimeCompleted" & vbTab & "Specifies the date and time the task was completed,"
  Wscript.Echo vbTab & vbTab & vbTab & "stopped, or put to pending. Leave this field blank if current"
  Wscript.Echo vbTab & vbTab & vbTab & "date and time is to be used."
  Wscript.Echo vbReturn
  Wscript.Echo vbTab & "TimeSpent" & vbTab & "Specifies the amount of time spent to complete"
  Wscript.Echo vbTab & vbTab & vbTab & "the task (in hours)."
  Wscript.Echo vbReturn
  Wscript.Echo vbTab & "Description" & vbTab & "A brief description of the task done."
  Wscript.Echo vbReturn
  Wscript.Echo vbTab & "Status" & vbTab & vbTab & "Status of the task (Completed, Pending, Closed)."
  Wscript.Echo vbReturn
  Wscript.Echo vbTab & "Remarks" & vbTab & vbTab  & "Remarks or comments."
  Wscript.Echo vbReturn
  Wscript.Echo "Example:"
  Wscript.Echo vbReturn
  Wscript.Echo vbTab & "LogMyWork.vbs " & """1,,1,Install Application A,Completed,None"""
End Sub

When the script is run, the information from the script is stored in a comma-separated values (.CSV) file. Each new entry is appended to previous entries in the file. The generated file name uses the current date as part of the file name (for example, 18-5-2009_MyWork.csv).

I would love to port this script to an ASP application, expand the CTI field to cover almost all IT-related stuff, and store the data in a back-end database. With this, I should be able to generate more comprehensive, management-friendly reports to better understand the strengths, weaknesses, and needs of the IT services.

When you open the CSV file in Microsoft Excel, a spreadsheet appears that is similar to this one:

Image of the spreadsheet that appears

 

Guest commentator: Daniele Muscetta

Daniele Muscetta’s journey with computers and software started when he began programming on a Commodore 64 at the age of ten. Daniele preferred to write programs on his Commodore 64 rather than playing games with it like the other kids did. Today, Daniele works at Microsoft Italy as a premier field engineer who specializes in both System Center Operations Manager and scripting technologies such as VBScript and Windows PowerShell. Daniele maintains a personal blog on his Web site.


Windows PowerShell solution

The most basic time logger would be a single command that writes the current date and time, and a description or name of the activity being logged to a text file.  There are multiple ways to log a datetime, and I tend to like a method I saw mentioned on the Windows PowerShell team blog.  One advantage of this method is it can be changed to some other format.

But the biggest challenge with the Beginner Event 9 scenario is the usability of such an application. I came up with two questions:

·         How do I tell the script I am starting to do something and supply the name of the new task?  I could use command-line parameters or the Read-Host cmdlet.  Both options seemed ugly to me. The script is not one that processes data in log filesit is a user application.

·         How do I tell the script when I am actually done doing something? I could end script execution with CTRL+C. I could require the user to enter another more data into a Read-Host cmdlet. But what if someone makes a mistake and presses the wrong key?

In the end I decided against all these. Thought it is true this is a script, this script challenge looked to me more like an application!

The basic task of writing time-stamped entries is the easy part of the problem. It is the usability of the tool that is the most important part of the task. At the same time I need to keep it simple.

I then thought of breaking loose from the command-line interface (CLI), and decided to use the power of the .NET Framework classes from Windows PowerShell. This is an approach I also used in other situations. It is not too difficult to build a simple Windows Form.

When BasicEvent9Solution.ps1 script is run, a text box appears:

Images of the text box that appears

It has a simple text box where you type the task name and then click Start. At this point, the script will write an entry in the log file saying that we started the task and initialize a System.Diagnostics.StopWatch .NET Framework object (I got the basic idea from a posting made by Thomas Lee who is a moderator for the Official Scripting Guys forum and a fellow 2009 Summer Scripting Games commentator).

At this point, according to my use case scenario, you minimize the application and keep doing your work.

When you are done with your work, bring the form back and click Stop. At that point the timer will stop, and another entry will be written in the log file saying that you have done that task and how long it took you to complete it. A sample of the output from the logfile.txt file is seen here:

Image of a sample of the ouput from logfile.txt

Of course the BeginnerEvent9Solution.ps1 script can be improved in a number of ways. Some of the things I have thought about doing to the script include the following:

·         Display the actual timer progress in the GUI while it runs. In this way, the script could double as a stop watch.

·         Use a better storage format. Instead of using a simple text file I could use a CSV file, an Office Excel spreadsheet, or even a database.

·         Use multiple timers and allowing for “multitasking.” In this approach, which might be useful in a consulting type of environment, you declare to the tool you are actually performing multiple tasks at once.

The BeginnerEvent9Solution.ps1 script is commented, so it should be easy to read. As with most scripts that create a graphical interface, a large part of the code generates the Form and the entire GUI, while very little of it is the actual “logging engine.” The compete BeginnerEvent9Solution.ps1 script is seen here.

BeginnerEvent9Solution.ps1

#this is our logfile
$logfile = "c:\scripts\logfile.txt"

#StopWatch Object we use to track elapsed time
$sw = new-object System.Diagnostics.StopWatch
$sw.Start()

Function CreateMainGUI
{
  # Loads useful .net Assemblies, required to create the GUI....
  [void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms")
  [void][reflection.assembly]::LoadWithPartialName("System.Drawing")

  #creates form and controls objects
  $form = New-Object System.Windows.Forms.Form
  $buttonStart = New-Object System.Windows.Forms.Button
  $buttonStop = New-Object System.Windows.Forms.Button
  $textBoxTask = new-object System.Windows.Forms.TextBox

  #pauses rendering until later
  $form.SuspendLayout();

  # textBoxTask
  $textBoxTask.Location = new-object System.Drawing.Point(10, 10);
  $textBoxTask.Name = "textBoxTask";
  $textBoxTask.Size = new-object System.Drawing.Size(350, 25);
  $textBoxTask.TabIndex = 7;
  $textBoxTask.Text = "Enter Task Name";

  # buttonStart
  $buttonStart.Location = new-object System.Drawing.Point(370, 10);
  $buttonStart.Name = "buttonStart";
  $buttonStart.Size = new-object System.Drawing.Size(50, 20);
  $buttonStart.TabIndex = 4;
  $buttonStart.Text = "Start";
  $buttonStart.UseVisualStyleBackColor = $true;
  $buttonStart.add_click({
           $taskname = $textBoxTask.Text
           StartLogTask $taskname
      })


  # buttonStop
  $buttonStop.Location = new-object System.Drawing.Point(430, 10);
  $buttonStop.Name = "buttonStop";
  $buttonStop.Size = new-object System.Drawing.Size(50, 20);
  $buttonStop.TabIndex = 4;
  $buttonStop.Text = "Stop";
  $buttonStop.UseVisualStyleBackColor = $true;
  $buttonStop.Enabled = $false #this will start disabled - we want to START a task first!
  $buttonStop.add_click({
           StopLogTask $taskname
      })

  # Form1
  $form.ClientSize = new-object System.Drawing.Size(500, 50);
  $form.FormBorderStyle=[System.Windows.Forms.FormBorderStyle]::FixedSingle
  $form.Controls.Add($textBoxTask);
  $form.Controls.Add($buttonStart);
  $form.Controls.Add($buttonStop);
  $form.Name = "Form1";
  $form.Text = "Time Logging Script";
  $form.ResumeLayout($false);
  $form.PerformLayout();
  $form.Load
  $form.ShowDialog()
  $form.Add_Shown({$form.Activate()})
}


Function StartLogTask([string]$taskname)
{
         #reset the stopwatch in case it was previously running
         $sw.Reset()
         #start the stopwatch again
         $sw.Start()
         #logs the entry
         "$(get-date -f o) Started Working on $taskname">>$logfile
         #switches the buttons so that START is now disabled and STOP gets enabled
         $buttonStart.Enabled = $false
         $buttonStop.Enabled = $true
}


Function StopLogTask([string]$taskname)
{
         #gets the stopwatch's elapsed time in milliseconds
         $ts = $sw.Elapsed
         #converts those milliseconds to a nicely (?) formatted string
         $ElapsedTime = [system.String]::Format("{0:00}:{1:00}:{2:00}.{3:00}", $ts.Hours, $ts.Minutes, $ts.Seconds, $ts.Milliseconds / 10)
         #logs the entry
         "$(get-date -f o) Stopped Working on $taskname - Time worked on this was: $ElapsedTime">>$logfile

         #switches the buttons so that STOP is now disabled and START is enabled
         $buttonStop.Enabled = $false
         $buttonStart.Enabled = $true
}


# *** Entry Point to the Script ***
CreateMainGUI


 

Advanced Event 9:  The javelin throw

In the javelin throw event, you will throw your heart into your work as you attempt to sort a tab-delimited text file based upon a particular column.

Guest commentator: Alex Angelopoulos

Image of guest commentator Alex Angelopoulos

Alex K. Angelopoulos is an IT consultant, an MCSE, and a contributing editor for Windows IT Pro magazine. An avid scripter, Alex writes about Windows task automation using Windows PowerShell and the Windows Script Host.

VBScript solution

You can minimize the effort for the javelin throw event if you realize that the data set is already designed for consumption from Microsoft Office Excel. If you choose to use Excel to solve the problem, you avoid low-level coding in VBScript to handle the rankings.

Even using Excel, the problem isn't trivial, but you can apply most of the coding techniques to any problem where you may want to leverage Excel as a tool.

Although I didn't start by declaring constants (I added them to the script after I had the prototype working), the script starts there because it's a convenient location to show all the values that I'll hang onto (and it makes it simpler to find them if you ever need to modify the constants). So the first few lines, which won't mean a lot until you work through the script, look like this:

Option Explicit
Const xlMinimized = 2, xlYes = 1, xlDescending = 2
Const ShortestAutoQualifyThrow = 82, MinimumQualifierCount = 12
Const TabDelimiter = 1
const sourcefile = "Javelin Throw Data.txt"
const targetfile = "Javelin Throw Results.txt"

Because I use Excel as the engine for the javelin throw calculations, the next step is to set up a reference to Excel. This is seen here:

Dim xl
Set xl = CreateObject("Excel.Application")

The next step is some defensive scripting to deal with the fact that Excel is first and foremost an interactive tool, not a script component.

One of the side effects of its design is that Excel will often prompt you about particular actions, causing the script to pause until you respond. We can suppress this behavior by telling Excel that we don't want special alerts shown:

xl.DisplayAlerts = False

Furthermore, as an actual application, Excel doesn't necessarily quit when the script exits. If the script crashes or if a buggy add-in keeps a reference to Excel, Excel will continue to run. Although we can't prevent all possible problems, we can choose to make Excel visible while the script runs; then if it doesn't shut down, the user can see it in the Taskbar and shut it down manually. If you set Excel's window state to minimized, this automatically makes it visible as well as running it in the Taskbar where it won't interfere with other tasks:

xl.WindowState = xlMinimized

At this point, we're ready to open the data file. There are two new problems to handle here, however. We need to find the file and we need to tell Excel how to interpret the contents.

The script assumes that Javelin Throw Data.txt is in the same folder as the script. We can't just specify the bare name of the data file, however. Excel starts out with a different home folder, depending on the version of Excel and Windows, so we need to get the complete path to the data file. I use the Scripting.FileSystemObject object to expand the bare file name to the complete path like this:

Dim fso
Set fso = CreateObject("Scripting.FileSystemObject")
Dim sourcepath
sourcepath = fso.GetFile(sourcefile).Path

Next, we need to tell Excel that the file is tab-delimited data. Excel's Workbooks collection has an Open method that allows you to specify a large number of parametersso many that it's difficult to work with at times. Fortunately, you can simply omit parameters that you want to assume default values. All we're concerned with is telling Excel the path to the file to open (the first parameter) and what kind of delimiter to use for columns (the fourth parameter for the Open method). Excel doesn't use a built-in constant for the delimiter type, but the Excel VBA documentation does discuss the numbers used to represent delimiter types. A value of 1 tells Excel that columns are delimited with tabs; for reference, 2 means the delimiters are commas; 3 means spaces; and 4 means semicolons. So I wouldn't have a meaningless number floating around in the Open method, I defined a constant called TabDelimiter at the top of the script and use it with Open:

Dim workbook
Set workbook = xl.Workbooks.Open(sourcepath, , , TabDelimiter)

We now have the data file open and can begin to work with the contents. Technically we'll be operating on a worksheet, not a workbook. With a tab-delimited text file, they amount to the same thing, because there's only one table of data in the file, but we need to explicitly specify the worksheet to Excel, like this:

Dim sheet
Set sheet = workbook.ActiveSheet

Our first task is to find the best throw for each competitor. This takes some thinking about how the data is structured. For simplicity, I assumed that the columns are always guaranteed to be in the order shown in the sample data. The Best Throw is then always Column H in the Excel worksheet. What reasonably might vary is the number of competitors. Fortunately, worksheets have a UsedRange object, which contains a Rows object, and Rows has a Count property. Because the top row of the worksheet contains the headers, we only want to look at rows 2 through sheet.UsedRange.Rows.Count. Excel uses the column letter and the row number for specifying ends of a range, so we can set our range this way:

Dim range
Set range = sheet.Range("H2","H" & sheet.UsedRange.Rows.Count)

After all this effort, calculating the best throw is a one-liner. Excel has a built-in Max function that will select the highest value in a range of cells, and it automatically treats an "x" entry as 0:

range.FormulaR1C1 = "=MAX(RC[-3]:RC[-1]"
Sorting the data is another single line:
sheet.usedRange.Sort range, xlDescending, , , , , , xlYes

The Sort method is of course yet another Excel function with limitless parameters. The 8th parameter with the value xlYes simply tells Excel that the first row is a set of headers and should not be included in the sorting.

The next step is to make the output data look more like input data. First, if a contestant had no qualifying throws, we change the Best Throw value from 0 to "x" so it looks like the input data:

Dim cell
for each cell in range.Cells
          if cell.Value = 0 Then cell.value = "x"
next

Finally, we perform the rankings, a task simplified because competitors are now sorted in order of their best throw:

Dim i
for i = 2 to sheet.UsedRange.Rows.Count
          set range = sheet.Range("I" & i)
          dim bestThrow: bestThrow = sheet.Range("H" & i).Value
          dim ranking: ranking = i - 1
          if bestThrow = "x" Then
                   range.Value = "x"
          elseif bestThrow => ShortestAutoQualifyThrow Then
                   range.Value = ranking
          elseif ranking <= MinimumQualifierCount Then
                   range.Value = ranking
          else
                   range.Value = "x"
          end if
next

And finally, we save the target file, close the workbook, and quit Excel:

xl.ActiveWorkbook.SaveAs targetfile
xl.ActiveWorkbook.Close
xl.Quit

The completed AdvancedEvent9.vbs script is shown here.

AdvancedEvent9.vbs

Option Explicit

Const xlMinimized = 2, xlYes = 1, xlDescending = 2
Const ShortestAutoQualifyThrow = 82, MinimumQualifierCount = 12
Const TabDelimiter = 1
const sourcefile = "Javelin Throw Data.txt"
const targetfile = "Javelin Throw Results.txt"

dim xl
Set xl = CreateObject("Excel.Application")

' Office Excel has a variety of dialogs that appear to stop you from
' doing things that it thinks may be harmful: for example, saving
' a workbook as a text file, particularly when it contains formulas.
' We can suppress this using DisplayAlerts.
xl.DisplayAlerts = False


' It's possible to run Excel hidden, but if there are issues
' due to either script errors or buggy add-ins that don't let
' Excel unload cleanly, it will continue to run invisibly.
' To make this solvable without taking over the desktop, we
' set Excel's windowstate to minimized. This also automatically
' makes Excel visible.
xl.WindowState = xlMinimized

' We need to get the COMPLETE PATH to the source data file.
' Although the script knows where it is running from,
' Excel is a separate process and has its own default startup folder.
' If we pass it a bare file name, Excel will probably be looking for
' the file in the wrong folder; so expand sourcefile to a sourcepath.
' When we save the modified file, we don't have anything to worry about;
' Excel will assume a relative path is relative to the already-opened
' source file.
Dim fso
Set fso = CreateObject("Scripting.FileSystemObject")
Dim sourcepath
sourcepath = fso.GetFile(sourcefile).Path

Dim workbook
Set workbook = xl.Workbooks.Open(sourcepath, , , TabDelimiter)
Dim sheet
Set sheet = workbook.ActiveSheet

' Now select the range for the best throw column and calculate the values.
' Excel handily treats X as a non-value.
Dim range
Set range = sheet.Range("H2","H" & sheet.UsedRange.Rows.Count)
range.FormulaR1C1 = "=MAX(RC[-3]:RC[-1]"

' Now we sort the sheet based on these values and then change 0 to "x"
' This marks contestants who had no qualifying throws
sheet.usedRange.Sort range, xlDescending, , , , , , xlYes

Dim cell
for each cell in range.Cells
          if cell.Value = 0 Then cell.value = "x"
next

Dim i
for i = 2 to sheet.UsedRange.Rows.Count
          set range = sheet.Range("I" & i)
          dim bestThrow: bestThrow = sheet.Range("H" & i).Value
          dim ranking: ranking = i - 1
          if bestThrow = "x" Then
                   range.Value = "x"
          elseif bestThrow => ShortestAutoQualifyThrow Then
                   range.Value = ranking
          elseif ranking <= MinimumQualifierCount Then
                   range.Value = ranking
          else
                   range.Value = "x"
          end if
next

xl.ActiveWorkbook.SaveAs targetfile
xl.ActiveWorkbook.Close
xl.Quit

 

Guest commentator: Bruce Payette

Image of guest commentator Bruce Payette

Bruce Payette is a principal developer on the Windows PowerShell team. He is one of the founding members of the team, co-designer of the Windows PowerShell language and the principal author of the Windows PowerShell language implementation. Bruce is the author of one of the top-selling Windows PowerShell books on Amazon.com, Windows PowerShell in Action, from Manning Publications.

Windows PowerShell solution

The solution to this problem is quite straightforward. Step one is to load the data into memory for processing. Because the data file is a simple CSV file with fields separated by tabs, we can load it in Windows PowerShell 2.0 by doing the following:

$data = Import-CSV -Delimiter "`t"  '.\Javelin Throw Data.txt'

The next step is to calculate the result field for each athlete. This is somewhat complicated by two things:  the data in the table is stored as strings and some of these strings aren't numbers because of the “x” fields for disqualified throws. There are two approaches we can use to calculate this. The first is to look through each throw and pick the largest one. We can deal with both of the type conversion problems by using the -as operator to convert the results to a floating point value. If the argument to -as is a string that looks like a number, it will convert the string to a number. If the argument is a string that doesn't look like a number, it will return $null. This is fine because when $null is used in numeric conversions it will be treated as 0. To loop through the throws, we could simply list each one.  However we can also use string expansion and the fact that the right side of the '.' operator in PowerShell need not be a constant value allowing us to do the following:

foreach ($a in $data)   # for each athlete
{
    $a.Result = 1..3 | # find the longest throw...
        foreach `
            {$max=0} `
            {
                $current = $a."throw $_" -as [double]
                if ($current -gt $max) {$max = $current}
            } `
            {$max}
}

 

The result of the selection is assigned to the Result field in the data row for that athlete. We then sort the data into descending order and add descending ranking number for each athlete:

$data = $data | sort -desc Result

$rank = 1
$data | foreach { $_.Rank = $rank++ }

 

Finally, we need to select the top twelve qualifying athletes, or the top twelve if there aren't enough that meet the minimum score. We can do this with the where cmdlet:

$count = 0
$nextRound = $data | where { $count++ -lt $minimumNumber -or
          $_.result -ge $minimumAcceptableScore }

 

The final step is to write the new data out to a file using Export-CSV. Again we'll use tabs to separate the fields:

$nextRound  | Export-Csv -Path $outputFile -Delimiter "`t" -NoTypeInformation

This complete solution, AdvancedEvent9Solution.ps1, is seen here.

AdvancedEvent9Solution.ps1

#
# Solution 1 - straightforward incremental steps
#
$inputFile = '.\Javelin Throw Data.txt'
$outputFile = "Next Round.txt"
$minimumAcceptableScore = 82.5
$minimumNumber = 12

#
# Load the csv file into a variable. The columns are separated by a tab
#
$data = Import-Csv -Delimiter "`t" $inputFile

#
# Loop through the data, calculating and updating the
# result property for each athlete. We'll treat “x”
# as zero when picking the best result
#
foreach ($a in $data)                                      <# for each athlete #>
{
    $a.Result = 1..3 | # find the longest throw...
        foreach `
            {$max=0} `
            {
                $current = $a."throw $_" -as [double]
                if ($current -gt $max) {$max = $current}
            } `
            {$max}
}

# Now sort in descending order best to worst, using the result field
$data = $data | sort -desc Result

# Fill in the rank field
$rank = 1
$data | foreach { $_.Rank = $rank++ }

#
# Select the set that will go on to the next round. We want everyone
# who meets the minimum score or the top 12 if there aren't enough
# people meeting the minimum.
#
$count = 0
$nextRound = $data | where { $count++ -lt $minimumNumber -or $_.result -ge $minimumAcceptableScore }


#
# Write out the data for the next round
#
$nextRound  | Export-Csv -Path $outputFile -Delimiter "`t" -NoTypeInformation

Earlier we mentioned that there were two approaches to calculating the best result for each athlete. The second approach doesn't use a loop. Instead we can use the Windows PowerShell operators to perform each step in the conversion:

    $a.Result =
        @(1 .. 3 | foreach { $a."throw $_" })     <# get the three throws #> `
            -replace "x","0"                      <# replace the 'x's with 0 #> `
                -as [double[]] |                  <# convert the results to numbers #>
                    sort -desc |                  <# sort in descending order #>
                        select -first 1           <# take the first (best) value #>

With this approach, we extract each result as in the previous solution, use the -replace operator to remove the x’s, convert the collection to an array of double, sort that array in descending order, and then select the first (largest) result. The result of the solution remains the same. The complete solution using this technique is shown in AdvancedEvent9Solution2.ps1.

AdvancedEvent9Solution2.ps1

#
# Solution 1 - straightforward incremental steps
#
$inputFile = '.\Javelin Throw Data.txt'
$outputFile = "Next Round.txt"
$minimumAcceptableScore = 82.5
$minimumNumber = 12

#
# Load the csv file into a variable. The columns are separated by a tab
#
$data = Import-Csv -Delimiter "`t" $inputFile

#
# Loop through the data, calculating and updating the
# result property for each athlete. We'll treat “x”
# as zero when picking the best result
#
foreach ($a in $data)                                      <# for each athlete #>
{
    $a.Result = 1..3 | # find the longest throw...
        foreach `
            {$max=0} `
            {
                $current = $a."throw $_" -as [double]
                if ($current -gt $max) {$max = $current}
            } `
            {$max}
}

# Now sort in descending order best to worst, using the result field
$data = $data | sort -desc Result

# Fill in the rank field
$rank = 1
$data | foreach { $_.Rank = $rank++ }

#
# Select the set that will go on to the next round. We want everyone
# who meets the minimum score or the top 12 if there aren't enough
# people who meet the minimum.
#
$count = 0
$nextRound = $data | where { $count++ -lt $minimumNumber -or $_.result -ge $minimumAcceptableScore }


#
# Write out the data for the next round
#
$nextRound  | Export-Csv -Path $outputFile -Delimiter "`t" -NoTypeInformation

The final solution, AdvancedEvent9Solution3.ps1, is the same as the second solution, removing all of the intermediate variables and composing the solution as a single, top-level pipeline. This is included for academic purposes only and is not recommended if you want other people to be able to maintain your code. The complete AdvancedEvent9Solution3.ps1 script is seen here.

AdvancedEvent9Solution3.ps1


#
# Solution 3 - solution 2, but done as a single pipeline
#
$inputFile = '.\Javelin Throw Data.txt'
$outputFile = "Next Round.txt"
$minimumAcceptableScore = 82.5
$minimumNumber = 12

#
# Load the csv file into a variable. The columns are separated by a tab
#
import-csv -delimiter "`t" $inputFile |
    foreach {
        $a = $_
        $a.Result =
            @(1 .. 3 | foreach { $a."throw $_" })     <# get the three throws #> `
                -replace "x","0"                      <# replace the x’s with 0 #> `
                    -as [double[]] |                  <# convert the results to numbers #>
                        sort -desc |                  <# sort in descending order #>
                            select -first 1
        $a <# emit the updated object #> } |
    sort -desc Result |                               <# sort in descending order by result #>
    foreach {$rank=0; $count=0} { $_.Rank = $rank++; $_ } | <#Calculate the rank field #>
    where { $count++ -lt $minimumNumber -or $_.result -ge $minimumAcceptableScore } |
    Export-Csv -Path $outputFile -Delimiter "`t" -NoTypeInformation

 


Our most hardy thanks go out to Salvador, Daniele, Alex, and Bruce for their excellent commentaries today. Once again we have gained some tremendous insights into the mysteries of the script-writing process. Join us tomorrow as we reveal the last of our guest commentators and the last set of expert solutions. Though we are not saying we saved the best for last, join us tomorrow to find out. If you run into any snags while working on today's events, remember that you can post questions to the 2009 Summer Scripting Games forum. To keep up with all the latest information, follow us on Twitter. Until tomorrow, peace!

 

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

 

Prize Winners List for the 2009 Summer Scripting Games (So Far)

These are the winners so far. Events 7, 8, and 9 remain to be drawn. We will determine the prizes each person won next week (June 29).  Bobbleheads, books, software licenses, $25 Amazon gift certificates--just some of the possibilities.

How to be eligible for prizes

Winners 

W.S., Ohio, United States

D.R., Ohio, United States

A.C., Norfolk, England

K.M., Ontario, Canada

D.B., Oklahoma, United States

B.G., Brazil

C.J., Switzerland

R.D.J., Denmark

VP, Latvia

R.M., California, United States

H.K., Russia

D.C., Illinois, United States

J.B., New York, United States

R.S., Germany

A.K. Ontario, Canada

D.W., Kentucky, United States

S.L., Israel

J.R., Germany

D.K., Australia

P.W., United Kingdom

G.H.Y., Beijing, PRC

T.J., California, United States

D.T., Russian Federation

M.S., Vermont, United States

S.Z., Missouri, United States

A.J., Pennsylvania, United States

R.R., Florida, United States

J.M.S., Belgium

O.E., Israel

H.P., The Netherlands

S.B., Missouri, United States

P.S., Wisconsin, United States

S.H., Australia

M.A., Colorado, United States

D.R., Ohio, United States

R.R., Vermont, United States

N.H., Oregon, United States

C.C., North Carolina, United States

C.J., Illinois, United States

D.C., Missouri, United States

A.C., Missouri, United States

D.M., Czech Republic

 

Hey, Scripting Guy! Event 8 *Solutions* from Expert Commentators (Beginner and Advanced; the pole vault)

  

(Note: These solutions were written for Event 8.) 

Beginner Event 8: The pole vault

In the pole vault event, you will raise the bar as you run down the folder that is consuming the most disk space on your computer.

Guest commentator: Michael Frommhold

Image of guest commentator Michael Frommhold 

Michael is a premier field engineer at Microsoft Germany.


VBScript solution

Archimedes gave the hint; Dr. Watson was supposed to do the job. Finding the folder consuming the most disk space should be easy said Dr. Watson. You connect to the drive that contains the folder you wish to scan by using the GetFolder method from the FileSystemObject. Next you use a query to obtain the size of the subfolders by using the subfolders property. At this point you can also obtain the size of the folder from the size property of the folder object.

Store the path to the largest folder in a variable named sWinner and the amount of disk space in the dbFileSize variable. Now you compare each new folder size with the one who claimed to be the largest one up to now. When enumerated all folders, convert the size from bytes to a readable number. This is done via Sub Handlesize. Our first attempt at solving the Beginner Event 8 is BeginnerEvent8Solution_1.vbs script, which is seen here.

BeginnerEvent8Solution_1.vbs

On Error Resume Next

Dim oFSO 
Dim oFolder
Dim oSubFolder
Dim sPath
Dim dbFileSize
Dim dbCtrl
Dim sWinner
Dim sUnit

dbFileSize = CDbl(0)

Set oFSO = CreateObject("Scripting.FileSystemObject")
Set oFolder = oFSO.GetFolder("\")

For Each oSubFolder In oFolder.SubFolders
          sPath = vbNullString
          sPath =  oSubFolder.Path
         
          If Not Len(sPath) = 0 Then
                   dbCtrl = CDbl(0)
                   dbCtrl = CDbl(oSubFolder.Size)
                  
                   If dbCtrl > dbFileSize Then
                             dbFileSize = dbCtrl
                             sWinner = sPath
                   End If
          End If
Next 'oSubFolder
         
Set oFolder = Nothing
         
HandleSize dbFileSize, sUnit

WScript.Echo "Archimedes says: Lever thrown!"
WScript.Echo "The folder consuming the largest amount of disk space is:"
WScript.Echo vbTab & sWinner & " : " & dbFileSize & " " & sUnit

Sub HandleSize(ByRef dbRet, ByRef sRet)
         
          On Error Resume Next
         
          If CDbl(dbRet / 1024) > 1 Then
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "KB"
          End If
                  
                   If CDbl(dbRet / 1024) > 1 Then
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "MB"
          End If
         
          If CDbl(dbRet / 1024) > 1 Then
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "GB"
          End If

          'just for completeness
          If CDbl(dbRet / 1024) > 1 Then
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "TB"
          End If
         
          dbRet = Round(dbRet, 1)
         
End Sub

When the BeginnerEvent8Solution_1.vbs script is run, the output displayed in the following image is shown (keep in mind the red background seen here is a product of my own personal laptop configuration, and not a result of anything in the script):

Image of the output of the script 

There are a few things that could be improved in the BeginnerEvent8Solution_1.vbs script.

If you do not have access to any subfolder of the first-level subfolders, you will not get any value for the folder size, even if you would have access after agreeing to a UAC dialogue. This is because VBScript is not UAC aware.

If there are any junction points or symbolic links on your drive, you will scan these as well. This could result in a larger size that is actually larger than the total disk space of your drive.

So, what's the proper solution? Consulting Archimedes, we find that he tells us to find a sufficiently large lever. When working with VBScript perhaps the biggest lever we have is WMI. We query the Win32_Directory class from the root\cimV2 namespace, and handle the returned directories. The Win32_Directory class has a property called FileSize but unfortunately the FileSize property is always null. The reason is that a directory is an entrypoint for a collection of files and directories and this entrypoint has no size.

We will need to use another WMI class, the CIM_DataFile, which can be associated with a Win32_Directory object to obtain the FileSize for files. Now all we have to do is add all FileSize values in a directory and we have the size of the directory.  We also need to add the queried FileSize of any folder to his parent and to the parent of his parent and…you get the point.


To do this we can use a dictionary object with pairs of folder paths and folder sizes. For every new folder we process, the dictionary will be checked for parents and grandparents and so on.  The size will be added to the right side of the pair of the elders. As an add-on to the script, we save the pairs in another dictionary as well, but this time we only save the amount of used space in the directories root.

When we are finished with enumerating the drive, the pair with the highest number will be the folder consuming the most disk space.

Be sure to start the script in an administrative console. The drive to be scanned has to be passed as argument /disk to the script. You can optionally scan a remote machine (but due to performance considerations I wouldn't do it). Verbose output shows you that each folder scanned each and every updated dictionary pair. It is therefore very verbose.

You will find further information about the details of the script inline as comments. The complete BeginnerEvent8Solution_2.vbs script is seen here.

BeginnerEvent8Solution_2.vbs

'=====================================================================
' Usage: cscript lever.vbs /disk:<disk to scan>
'                                                                  {Optional /comp:<targetmachine> /dbg:True}
'=====================================================================

'handle errors when you need to
On Error Resume Next

'_____________________
' #region declarations

'constants
Const SUCCESS = 0
Const ERROR_WMI = 1
Const ERROR_WQL_DIR = 2
Const ERROR_DISK_ACCESS = 3
Const ERROR_NO_DRIVE = 4

Const WbemAuthenticationLevelPktPrivacy = &h6

'WMI stuff
Dim oWMILoc                  'As SwbemLocator
Dim oWMI           'As SwbemService

'store arguments
Dim sMachine       'As String
Dim sDrive                   'As String
Dim blOutput       'As Boolean

'misc
Dim aDrives                  'As String()
Dim iCount                   'As Integer

Dim dcOverAll      'As Scripting.Dictionary
Dim dcList                   'As Scripting.Dictionary

Dim sMSG           'As String

Dim sOverall       'As String
Dim sList          'As String

' #endregion

'_____________
' #region Main

'we're starting
WScript.Echo Now
WScript.Echo "Throwing the lever" & VbCrLf

'init return code
sMSG = vbNullString

'init dictionaries
Set dcOverAll = CreateObject("Scripting.Dictionary")
Set dcList = CreateObject("Scripting.Dictionary")

'_________________________
'anything to take care of?
ReadArguments sMachine, sDrive, blOutput

'____________________
'check WMI connection
If Not ConnectWMI(sMachine) Then _
          HastaLaVistaDotCom ERROR_WMI, _
                                                          "Failed to connect WMI!"

'____________________________
'told me which drive to scan?
If Len(sDrive) = 0 Then _
          HastaLaVistaDotCom ERROR_NO_DRIVE, _
                                                                   "No drive to scan given!"
         
'drive accessable?
If Not CheckAccess(sDrive) Then _
          HastaLaVistaDotCom ERROR_DISK_ACCESS, _
                                                                   "Access to " & sDrive & " is not granted!"
         
'__________
'scan drive
If Not WalkDirectories(sDrive, dcOverAll, dcList, sMSG) Then _
          HastaLaVistaDotCom ERROR_WQL_DIR, _
                                                          "Failed to query for directories: " & sMSG

'_______________
'process results
TheWinnerIS dcOverAll, dcList
         
'__________________________
'we're done -> big clean up
HastaLaVistaDotCom SUCCESS, vbNullString

' #endregion

'__________________________________
' #region functions and subsequents

'get all directories in given partition
Function WalkDirectories(ByVal sDisk, _
                                                          ByRef dcWinners, _
                                                          ByRef dcDirs, _
                                                         ByRef sRet) 'As Boolean
         
          On Error Resume Next
         
          Dim sWQL           'As String
          Dim colDirs                  'As Collection
          Dim oDir           'As Object
         
          Dim dbFileSize     'As Double
         
          Dim sFolder        'As String
         
          WalkDirectories = True
         
          sWQL = "SELECT Name FROM Win32_Directory WHERE Drive = '" & sDisk & "'"
         
          If blOutput Then WScript.Echo sWQL
         
          Set colDirs = oWMI.ExecQuery(sWQL)
                  
                   If colDirs Is Nothing Then
                  
                             sRet = sWQL & " :: " & Err.Description : WalkDirectories = False
                             sWQL = vbNullString : Exit Function
                            
                   End If
                  
                   'walk directories
                   For Each oDir In colDirs
                            
                             'get added up file size in dir
                             '(dirs are just collections of files and subfolders
                             'they do not have sizes)!
                   HandleDirectory oDir.Name, dbFileSize, sMSG
                  
                   'add dir to collection cverall
                             dcWinners.Add oDir.Name, dbFileSize
                             'add dir to collection for single info
                             dcDirs.Add oDir.Name, dbFileSize
                            
                             If blOutput Then WScript.Echo oDir.Name & " " & dbFileSize
                            
                             'is scanned dir a subfolder or subsub...folder of any
                             'of the already scanned dirs(?) -> add size
                             For Each sFolder In dcWinners.Keys
                                     
                                      If InStr(oDir.Name & "\", sFolder) > 0 Then
                                               
                                                dcWinners.Item(sFolder) = dcWinners.Item(sFolder) + dbFileSize
                                               
                                                If blOutput Then _
                                                          WScript.Echo sFolder & " " & dcWinners.Item(sFolder)
                                               
                                      End If
                                     
                             Next 'sFolder
                  
                   Next 'oDir
                  
          Set colDirs = Nothing
         
          'lil clean up
          sWQL = vbNullString
         
End Function

'walk files in directory and add up file sizes
Function HandleDirectory(ByVal sPath, _
                                                          ByRef dbRet, _
                                                          ByRef sRet) 'As Boolean
         
          On Error Resume Next
         
          Dim colFiles       'As Collection
          Dim oFile          'As Object
         
          dbRet = CDbl(0)
         
          'get all files in given dirtectory
          Set colFiles = oWMI.ExecQuery("ASSOCIATORS OF " & _
                                                                             "{Win32_Directory.Name='" & sPath & "'} " & _
                                                                             "WHERE resultClass = CIM_DataFile")
                  
                   For Each oFile In colFiles
                            
                             'add up file size
                             dbRet = dbRet + CDbl(oFile.FileSize)
                            
                   Next 'oFile
                  
          Set colFiles = Nothing
         
End Function

'partition is accessable (ex: not BitLocked?)
Function CheckAccess(ByVal sInput) 'As Boolean
         
          On Error resume Next
         
          Dim colRet                   'As Collection
          Dim oRet           'As Object
          Dim sRet           'As String
         
          sRet = vbNullString
         
          CheckAccess = True
         
          'can I read the given drive?
          Set colRet = oWMI.ExecQuery("SELECT FileSystem " & _
                                                                   "FROM Win32_LogicalDisk " & _
                                                                   "WHERE Name = '" & sInput & "'")
                  
                   For Each oRet In colRet
                            
                             sRet = oRet.FileSystem      
                            
                   Next 'oRet
                  
          Set colRet = Nothing 
         
          If Len(sRet) = 0 Then CheckAccess = False
         
          'lil clean up
          sRet = vbNullString
         
End Function
         
'connect WMI using SwbemLocator + Security_
Function ConnectWMI(ByVal sInput) 'As Boolean
         
          On Error Resume  Next
         
          ConnectWMI = True
         
          Set oWMILoc = CreateObject("WbemScripting.SwbemLocator")
                  
                   ' AuthLevel 0 - 5 + encrypts the argument value of each remote procedure call
                   '(see http://msdn.microsoft.com/en-us/library/aa393972(VS.85).aspx)
                   oWMILoc.Security_.AuthenticationLevel = WbemAuthenticationLevelPktPrivacy
                  
                   'I really want to know it!           
                   oWMILoc.Security_.Privileges.AddAsString "SeBackupPrivilege", True
         
          'connect to SwbemService on given machine
          Set oWMI = oWMILoc.ConnectServer(sInput, "root\cimv2")
         
          'failed?
          If oWMI Is Nothing Then ConnectWMI = False
         
End Function

'the Oscar goes to...
Sub TheWinnerIS(ByVal dcWinners, _
                                      ByVal dcDirs)
         
          On Error Resume Next
         
          Dim dbWinner       'As Double
          Dim dbDir                    'As Double
          Dim sWinner                  'As String
          Dim sDir           'As String
          Dim sKey           'As String
          Dim sUnit          'As String
         
          dbWinner = CDbl(0) : dbDir = CDbl(0)
         
          'walk overall collection
          For Each sKey In dcWinners.Keys
                  
                   'ok - I won't tell you, that the drive root is the largest 'folder'...
                   If Len(sKey) > 3 Then
                            
                             If dbWinner < dcWinners.Item(sKey) Then
                            
                                      dbWinner = CDbl(dcWinners.Item(sKey))
                                      sWinner = sKey
                                     
                             End If 'dbWinner < dcWinners.Item(sKey
                            
                   End If 'Len(sKey) > 3
                  
          Next 'sKey
         
          'you want to know KB, or MB or GB... and not Bytes, I guess
          HandleSize dbWinner, sUnit
         
          'return overall winner
          sWinner = sWinner & " : " & dbWinner & " " & sUnit
         
          'walk single info collection
          For Each sKey In dcDirs.Keys
                  
                   '...see last for each
                   If Len(sKey) > 3 Then
                            
                             If dbDir < dcDirs.Item(sKey) Then
                            
                                      dbDir = CDbl(dcDirs.Item(sKey))
                                      sDir = sKey
                                     
                             End If 'dbWinner < dcWinners.Item(sKey
                            
                   End If 'Len(sKey) > 3
                  
          Next 'sKey
         
          'you want to know KB, or MB or GB... and not Bytes, I guess
          HandleSize dbDir, sUnit
         
          'return overall winner
          sDir = sDir & " : " & dbDir & " " & sUnit
         
          'display results
          WScript.Echo vbNullString
          Wscript.Echo "Archimedes says: Lever thrown!"
          Wscript.Echo "The folder consuming the largest amount of diskspace is:"
          WScript.Echo vbTab & sWinner
          WScript.Echo "The folder with the most used file space in his root is:"
          WScript.Echo vbTab & sDir
         
          'lil clean up
          sKey = vbNullString : sUnit = vbNullString
          sWinner = vbNullString : sDir = vbNullString
          dbWinner = 0 : dbDir = 0
         
End Sub  

'return readable file size
Sub HandleSize(ByRef dbRet, _
                                      ByRef sRet)
         
          On Error Resume Next
         
          If CDbl(dbRet / 1024) > 1 Then
                  
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "KB"
                  
          End If
                  
                   If CDbl(dbRet / 1024) > 1 Then
                  
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "MB"
                  
          End If
         
          If CDbl(dbRet / 1024) > 1 Then
                  
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "GB"
                  
          End If
          CDbl
          'just for completeness
          If CDbl(dbRet / 1024) > 1 Then
                  
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "TB"
                  
          End If
         
          dbRet = Round(dbRet, 1)
         
End Sub

'what did you tell me to do?
Sub ReadArguments(ByRef sBox, _
                                                ByRef sDisk, _
                                                ByRef blDbg)
         
          Dim oNamedArgs     'As WshNamed
         
          'egt given arguments
          Set oNamedArgs = WScript.Arguments.Named       
                  
                   'did you name a remote machine?
                   sBox = "."
                   If oNamedArgs.Exists("comp") Then sBox = oNamedArgs.Item("comp")
                  
                   'told me about a disk to scan?
                   sDisk = vbNullString
                   If oNamedArgs.Exists("disk") Then
                            
                             sDisk = oNamedArgs.Item("disk")
                             'forgot the : ?
                             If Len(sDisk) = 1 Then sDisk = sDisk & ":"
                            
                   End If
                  
                   'should I show you verbose output?
                   blDbg = False
                   If oNamedArgs.Exists("dbg") Then
                  
                             If UCase(oNamedArgs.Item("dbg")) = "TRUE" Then
                                     
                                      blDbg = True
                                     
                             Else
                                     
                                       blDbg = False
                                     
                             End If
                            
                   End If
         
          Set oNamedArgs = Nothing
         
End Sub

'clean up 'n' leave
Sub HastaLaVistaDotCom(ByVal iQuit, _
                                                          ByVal sQuit)
         
          WScript.Echo sQuit
          WScript.Echo Now
         
          Set oWMI = Nothing : Set oWMILoc = Nothing
          Set dcOverAll = Nothing : Set dcList = Nothing
          sMachine = vbNullString : sDrive = vbNullString
          sMSG = vbNullString
          blOutput = 0 : iCount = 0
          ReDim aDrives(0)
         
          WScript.Quit(iQuit)
         
End Sub

' #endregion

When you run the BeginnerEvent8Solution_2.vbs script, this output is shown:

Image of the output of the script


Guest commentator: Clint Huffman

Image of guest commentator Clint Huffman

Clint is best known for the Performance Analysis of Logs (PAL) tool (a VBScript), which simplifies the analysis of performance monitor logs. He is an author of many of the recent BizTalk performance guides on MSDN and recently spoke at TechEd 2008 about BizTalk performance analysis. Clint has appeared on RunAs Radio, and he maintains the Counter of the Week blog on TechNet.


VBScript solution

While I was working on the Summer Scripting Games Event 8 (the advanced event), I saw the details for the beginner division. Because I love to write scripts, I decided to go ahead and take a swat at it. Unlike my colleague Michael, I decided to stick with the FileSystemObject object.

The first thing I do is create an instance of the FileSystemObject and initialize a few variables. I then call the EnumFolders subroutine and pass in the path to the starting folder. I use the Wscript.Echo to display the path to the folder, as well as the size of the largest folder in megabytes. The size is determined by using the ConvertBytesToMegabytes function. The complete BeginnerEvent8Solution.vbs script is seen here.

BeginnerEvent8Solution.vbs

Option Explicit
Dim oFSO, oFolder, oSubFolder, sStartingFolder
Dim oLargestFolder

sStartingFolder = "C:\Program Files"
Set oFSO = CreateObject("Scripting.FileSystemObject")
oLargestFolder = Null

EnumFolders sStartingFolder
WScript.Echo oLargestFolder.Path
WScript.Echo ConvertBytesToMegaBytes(TotalSizeOfFiles(oLargestFolder)) & "MB"

Sub EnumFolders(sFolderPath)
    Set oFolder = oFSO.GetFolder(sFolderPath)
    If sFolderPath <> sStartingFolder Then
        Set oLargestFolder = ReturnTheLargestFolder(oLargestFolder, oFolder)
    End If
    For Each oSubFolder in oFolder.SubFolders
        EnumFolders oSubFolder.Path
    Next
End Sub

Function ConvertBytesToMegaBytes(iNumber)
    Dim iNumInMBs   
    iNumInMBs = CInt((iNumber / 1024) / 1024)
    iNumInMBs = FormatNumber(iNumInMBs, 0)
    ConvertBytesToMegaBytes = iNumInMBs
End Function

Function TotalSizeOfFiles(oFolder)
    Dim oFile, iFolderSize
    iFolderSize = 0
    For Each oFile in oFolder.Files
        iFolderSize = iFolderSize + oFile.Size
    Next
    TotalSizeOfFiles = iFolderSize
End Function

Function ReturnTheLargestFolder(oFolderA, oFolderB)
    If IsNull(oFolderA) = True AND IsNull(oFolderB) = False Then
        Set ReturnTheLargestFolder = oFolderB
        Exit Function
    End If
    If TotalSizeOfFiles(oFolderA) > TotalSizeOfFiles(oFolderB) Then
        Set ReturnTheLargestFolder = oFolderA
    Else
        Set ReturnTheLargestFolder = oFolderB
    End If
End Function


Guest commentator: Brandon Shell

Image of guest commentator Brandon Shell

Brandon is a Microsoft MVP, moderator for the Official Scripting Guys forum. He maintains a personal blog named BSonPoSH.

Windows PowerShell solution

This specific script was born out of necessity. I had a Terminal Server that was running out of space and I needed to determine where all the space was being used. I started doing this manually and found that it was the “Documents and Settings” folder.  As this server had 100s of users and the idea of right-clicking my way through each one to find out which user was taking the most space was a little daunting.

My initial approach for that project was only to address the problem at hand and that led to a simple script that basically got all the folders in the “Documents and Settings” folder and calculate the size to see what users I should yell at about cleaning up their directories. I started by collecting all the folders into a variable. I then processed each folder individually. The processing included getting all the files recursively and calculating the total size by using the Measure-Object cmdlet and creating a custom object ($myobj) for the folder. The final step was to collect the custom objects into an array ($mycol).

After I put out my immediate fire, I decided to add some of the cool features of the script. First I wanted to add the ability to override the default behavior and include hidden folders. To accomplish this I needed to be able to take a –Force para