Expert Commentary: 2012 Scripting Games Beginner Event 3

Expert Commentary: 2012 Scripting Games Beginner Event 3

  • Comments 1
  • Likes

Summary: Windows PowerShell MVP, Kirk Munro, provides expert commentary for 2012 Scripting Games Beginner Event 3.

Microsoft Scripting Guy, Ed Wilson, is here. Kirk Munro is the expert commentator for 2012 Scripting Games Beginner Event 3.

Photo of Kirk Munro

Kirk is a five-time recipient of the Microsoft Most Valuable Professional (MVP) award for his deep involvement with the Windows PowerShell community. He has worked in the IT industry for more than 15 years, with the last five years focused 100% on Windows PowerShell and software solutions that use Windows PowerShell. He currently works at Devfarm Software as Product Manager for the PowerWF and PowerSE products.

When he's not working on PowerWF and PowerSE, Kirk spends his time working with Windows PowerShell in the community. He is the president of the PowerShellCommunity.org site, a central hub for all things PowerShell. He is also a frequent blogger, and he presents at conferences, launch events, and user group events whenever he gets the opportunity.

Blog: Poshoholic: Totally addicted to PowerShell 
Twitter: http://twitter.com/Poshoholic 
Linked-in profile: http://www.linkedin.com/in/kirkmunro 
Facebook profile: http://www.facebook.com/kirk.munro
Personal profile: http://poshoholic.com/about

Now that spring is here, it is time to get back into shape, and what better way to start than some mental exercises with the 2012 Scripting Games! OK, so maybe the mental exercises will not get you back into top physical shape, but the Scripting Games are always a lot of fun and definitely worth participating in. Every year I like to offer up some of my time with an expert commentary for one of the events so that others can learn from my Windows PowerShell scripting experience, and this year is no exception.

This year, Ed asked me if I would be willing to provide a solution to Event 3 in the Beginner category. This blog will show you how I solved the problem. It is important to note that this is only one of many possible ways to complete this event. Scripting comes with a certain amount of style preference, and this solution reflects how I like to style my scripts, but you may like to style yours differently. With that said, let us get to the solution!

Beginner Event 3 is pretty straightforward. All you need to do is write a Windows PowerShell script that writes local process information (name and ID) to a file in a folder that may or may not exist. It is always important when creating a script that is based on a set of predetermined rules to clearly identify what those rules are. Based on the description and the list of design points, here are the rules that I identified as a required part of this event:

  1. The file that is created must be called process3.txt.
  2. The file must contain a list of processes on the local system, showing the name and ID of each process.
  3. The process3.txt file must be in a nested folder hierarchy, ending with 2012sg\event3.
  4. By default, the script must create the file on the system (root) drive.
  5. If the 2012sg\event3 folder does not exist on the system drive, and if the user does not have permission to create that folder hierarchy there, the folder hierarchy must be created in a location where the user does have permission.
  6. If the script generates errors, they must be terminating errors (that is to say, make sure that no errors appear when the script runs if they are handled by the script itself).
  7. Additional points are given if the script accepts command-line parameters to provide additional functionality (for example, being able to specify a different location where you want the file to be created). Note that if you include command-line parameters, you must also include comment-based Help describing those parameters, and include examples showing how they are used.

Now that we have identified the rules for the event, we can create the script while adding our own personal style to the script. In terms of style, following are two best practices that I always apply to my scripts:

  1. Include a header comment at the top, which identifies I that created the script. That way, if someone finds it later and has questions, they know where the script came from.
  2. Use #region statements to identify business logic in the script. This best practice has two benefits. First, it provides comments so that someone looking at the script later can see what is going on. Second, in modern editors such as PowerSE (my editor of choice) and in the Windows PowerShell ISE in version 3 (currently in Windows 8 Consumer Preview and Window Server “8” Beta), you can use code folding to collapse regions so that all you see is the business logic. The code folding is really useful because it makes it much easier to focus on the most important portion of a script at the time that you are working on it.

Before showing you the script that I created, I should break down each step in the script by using the rules identified above.

Rule 1: The file that is created must be called process3.txt.

This rule tells us two things: that we are creating a file and what the name of that file will be. Windows PowerShell has a cmdlet called Out-File that allows you to write a collection of objects to a file. The -FilePath parameter is used to specify the path to the file that is being created. If you want to overwrite a file if it exists, even if it is read-only, you can use the -Force parameter. With that knowledge, somewhere in our script we’re going to pipe our collection of processes to the Out-File cmdlet, something like this:

… | Out-File -FilePath $Path\process3.txt -Force

You may prefer to prevent overwriting read-only files; however, because this script is something that I expect to run regularly to update the contents of a file, I think the contents should be overwritten, so I’m going to use -Force. Also the $Path variable that is identified here isn’t defined yet. But because we have different possible paths to write to, we can get ready by using a $Path variable now.

Rule 2: The file must contain a list of processes on the local system, showing the name and ID of each process.

It’s great that we’re creating a file, but what good is a file without contents? This rule identifies that the file must contain a list of processes on the local system, and that it must show the name and ID of each process. Get-Process is a Windows PowerShell cmdlet that allows you to retrieve all processes from a computer. You don’t even have to use any parameters; just call Get-Process and you’ll have the data you need. Get-Process returns a lot of information about processes though, and we only want to see the name and ID in a table. Format-Table is a cmdlet that is designed to allow you to define what properties of an object you want to see when it is shown in tabular format. It takes a -Property parameter, where you can name the properties you care about. It also accepts pipeline input, and it will show a table that contains all the objects that it receives from the pipeline. In addition, it has a convenient -AutoSize parameter that allows you to create a table with the least width possible. This is important because there are a lot of programs where you can run Windows PowerShell scripts, and depending on which program you run your script in, your table may have a different default width. Using -AutoSize will ensure that the table is always narrow so that you see the data you care about easily when you open the file and view the contents. With that information, we can put the two cmdlets together in a pipeline and get our formatted table like this:

Get-Process | Format-Table -Property Name,Id -AutoSize

Rule 3: The process3.txt file must be in a nested folder hierarchy, ending with 2012sg\event3.

This rule is interesting. There is a firm requirement that the process3.txt file must be in a nested folder hierarchy that ends with 2012sg\event3. To do that, we need to be able to inspect the path and see if it ends with 2012sg\event3 or only event3. Windows PowerShell has regular expression support that makes this really easy. The logic is simple. If the path doesn’t end with 2012sg\event3 and if the path doesn’t end with event3, then add 2012sg\event3 to the path. Otherwise, if the path ends with 2012sg, then add event3 to the path. If the path already has 2012sg\event3 at the end, we want to leave it alone. For regular expression comparisons, Windows PowerShell has -Match and -NotMatch operators. These operators allow you to perform the comparisons needed here when inspecting the path you’re working with.

When it comes to extending paths, there’s nothing like the Join-Path cmdlet. This cmdlet allows you to append additional portions to a path that you already have in a variable. The -Path parameter identifies the path you’re starting with, and the -ChildPath parameter identifies the extension you want to add to the path. It doesn’t matter if the path you’re starting with ends with a backslash or not; this cmdlet will do the right thing when extending the path.

With all of that knowledge, here is a piece of script that can make sure our $Path variable ends with 2012sg\event3:

if ($Path -notmatch '\\2012SG\\event3$') {

            if ($Path -notmatch '\\2012SG$') {

                        $Path = Join-Path -Path $Path -ChildPath '2012SG\event3'

            } else {

                        $Path = Join-Path -Path $Path -ChildPath 'event3'

            }

}

Rule 4: By default, the script must create the file on the system (root) drive.

Most people set up Windows such that the system on drive C. But not all people choose to configure their systems this way. We have a firm requirement that the script must create the file on the system root drive. We also know from our previous rule, that by default, the path must be in a 2012sg\event3 folder on that drive. Now all we need to do is assign the default value for our path.

To determine which drive is the system drive, we can use environment variables. Environment variables are very easy to access in Windows PowerShell. You simply use the syntax ${env:EnvironmentVariableName} to get the value of any environment variable. In our case, we want the value of the SystemDrive environment variable, so we’ll get that using this syntax: ${env:SystemDrive}. The beauty of this syntax is that you can use it directly inside of a double-quoted string, and Windows PowerShell is smart enough to insert the value you’re referring to directly into that string. Putting that together with the 2012sg\event3 folder requirement, we can set the default value of our $Path variable like this:

$Path = "${env:SystemDrive}\2012SG\event3"

Rule 5: If the 2012sg\event3 folder does not exist on the system drive, and if the user does not have permission to create that folder hierarchy there, then the folder hierarchy must be created in a location where the user does have permission.

With this rule, we’re starting to get a little more complex. First, this rule and the previous rule identify that we must create the folder hierarchy if it does not exist. Second, if we cannot create that folder in the location that is provided (the default location or the location the user provides), we must default to a location where the user does have permission to create folders. It makes sense to use the current user’s My Documents folder as the fallback location when they cannot create the folder hierarchy elsewhere, because it’s going to be possible there…unless something is really, really wrong with the system.

Breaking this down, there are a few things we need to do:

  • Check if the path provided (default or user specified) exists, and try to create it if it does not exist.
  • If the creation of the path did not work (that is, if the path still does not exist), default to using the My Documents folder, and create the hierarchy there.
  • If the path still doesn’t exist (that is, if it couldn’t be created in the My Documents folder), notify the user of the error and stop.

To check for the existence of a file or folder, use Test-Path. Test-Path allows you to test paths by using wildcards (with the -Path parameter). You can also test literal paths and skip wildcard matching (with the -LiteralPath parameter). In our case, we’re working with a literal path so we’ll use the appropriate parameter for that. If you test for the existence of the path and you determine it does not exist, you need to create it.

New-Item allows you to create items on Windows PowerShell drives. The FileSystem provider includes default drives for each drive letter, and we can use New-Item to create the folders required on that drive. When you use New-Item, -Path identifies the path that you want to create, -ItemType identifies the type of item you want to create (in our case, we want to create folders, so we’ll specify an item type of Directory), and the ­-Force parameter tells New-Item to create the entire hierarchy if it doesn’t exist. -Force is really important because you don’t really care what portion of the path doesn’t exist, and you simply want to make sure that the entire path does exist when you create your output file.

Another parameter that is important to mention when working out the details of this rule is a common parameter called -ErrorAction. The -ErrorAction parameter allows you to instruct Windows PowerShell when an error should be ignored (optionally silently), or when an error should cause a script to stop executing. By default if New-Item fails to create something, the error will be reported and the script will continue to run. This may not be desirable, such as in our case. We want to make sure our script doesn’t report an error if it can’t create a path on the system drive, and we want the script to fail if it can’t create the fallback My Documents folder path, so we can use ­-ErrorAction to control the handling in this case.

Now with all of that knowledge, we can complete this portion of the script. First, test to see if the path exists, and if not, try to create it while silently ignoring any errors that come up (due to permissions, for example).

if (-not (Test-Path -LiteralPath $Path)) {

            New-Item -Path $Path -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null

}

Note the important things at play here: -Force to make sure that the entire hierarchy is created if it doesn’t exist, -ErrorAction SilentlyContinue to ensure that any errors that occur don’t prevent the script from continuing and are hidden from the user, and the pipe at the end to Out-Null. Out-Null allows us to mask any output from a command. We don’t want to see any output here, so we pass it to Out-Null to hide it.

Next, check again to see if the path exists. If not, set the path to the fallback location. Remember, the fallback location is the My Documents folder. To get the location of the My Documents folder on any system, you need to use a little bit of the .NET Framework. The System.Environment class has a method called GetFolderPath that allows you to look up common folder paths. For My Documents, the path is identified by the [System.Environment+SpecialFolder]::MyDocuments static member, or ‘MyDocuments’ for short. You can get the location by using this syntax:

$myDocumentsFolder = [System.Environment]::GetFolderPath('MyDocuments')

Then you can use Join-Path again to join that root folder to the required 2012sg\event3 subfolder hierarchy. Put those pieces together, and you add this to your script logic:

if (-not (Test-Path -LiteralPath $Path)) {

            $myDocumentsFolder = [System.Environment]::GetFolderPath('MyDocuments')

            $Path = Join-Path -Path $myDocumentsFolder -ChildPath 2012SG\event3

}

Lastly, we need to check one more time if the path exists because, at this point, if the script failed to create a path, we just assigned the path variable to the backup location in My Documents, we haven’t tested for the existence of that path yet, and we tried to create it. We do that like this:

if (-not (Test-Path -LiteralPath $Path)) {

            New-Item -Path $Path -ItemType Directory -Force -ErrorAction Stop | Out-Null

}

However, if the path was properly created, this logic will simply pass through because the path will exist at that point. Note the important use of -ErrorAction Stop in the call to New-Item here. This tells Windows PowerShell that it should stop executing the script if an error occurs at this point. It is important because you don’t want a series of errors showing up if the folder couldn’t be created, which would make it more difficult to troubleshoot. You only want one error, and the error would come from this command.

Rule 6: If the script generates errors, they must be terminating errors (that is to say, make sure no errors appear when the script runs if they are handled by the script itself).

Meeting the requirements for this rule are pretty easy. We already used -ErrorAction in our logic to create the path. Now we need to add -ErrorAction in one other location: our call to Out-File that we wrote earlier. This is only necessary if you want to do anything extra when you create the file. I want to show the contents of the file on the screen when I create it, so in my script I use the extra handling to make sure I only show the file if everything worked well. With that, we can modify the Out-File call we made earlier to this:

... | Out-File -FilePath $Path\process3.txt -Force -ErrorAction Stop

When that’s in place, we can take the extra step and show the file at the end. To do this we can use the Invoke-Item cmdlet to invoke the text file we create. This will show the text file by using the default editor according to the .txt file association on the system. This will likely be Notepad, but it could be something else. We’re going to use Invoke-Item instead of calling Notepad directly, so that we respect that not all systems are configured alike.

Invoke-Item -LiteralPath $Path\process3.txt

Rule 7: Additional points are given if the script accepts command-line parameters to provide additional functionality (like being able to specify a different location where you want the file to be created). Note that if you include command-line parameters, you must also include comment-based Help that describes those parameters, and include examples that show how they are used.

Last, but definitely not least, is to configure the script so that it can work with any path. For this to work, we’re going to put a $Path parameter at the top of the script. We already have the default $Path value assignment, so we just need to expand that by converting it into a parameter. We can also be smart and add a few safeguards, tell Windows PowerShell that the parameter can be invoked using the -Path name or the first position, and make sure that Windows PowerShell checks the value and raises an error if a null or empty path is used. Here is what the $Path parameter definition looks like in our script:

[CmdletBinding()]

param(

            [Parameter(Position=0)]

            [ValidateNotNullOrEmpty()]

            [System.String]

            $Path = "${env:SystemDrive}\2012SG\event3"

)

The CmdletBinding attribute indicates that we’re using the Advanced Function syntax for our script, which enables support for parameter attributes and validators, and also enables support for comment-based Help. We’re adding a parameter, so we need to add comment-based Help too. That’s a large block comment that is part of the script, and due to the size, I’ll let you look at the syntax in the following script that is listed in its entirety.

Putting it all together

Now we have gone through all of the required steps to solve this event, including any options for bonus points. Things to watch for are anything that may vary from system to system, such as the System drive, the location of MyDocuments, and the default text editor if you choose to show the file when it’s created. Also be careful about and overwriting read-only files intentionally if you want to, and leverage the -ErrorAction common parameter and the Out-Null cmdlet to ensure that you don’t output anything at the wrong time. Add to that your style preferences like the ones mentioned previously, and you may end up with a solution that looks something like this:

<#

            File   = Get-ProcessList.ps1

            Author  = Kirk "Poshoholic" Munro

            Blog   = http://www.poshoholic.com

#>

 

<#

            .SYNOPSIS

                        Gets the processes that are running on the local computer and writes the name and id for each process to a process3.txt file.

 

            .DESCRIPTION

                        Gets the processes that are running on the local computer and writes the name and id for each process to a process3.txt file. Once the file is created, it will be opened in the default text editor.

 

            .PARAMETER Path

                        The path where the file will be created. By default this will be on the system drive in a 2012SG\event3 folder. If the path does not exist and cannot be created, the script will create the process3.txt file in a 2012SG\event3 folder in the current user's My Documents folder instead. If a path is provided that does not include a 2012SG\event3 subdirectory, the missing portions of that subdirectory will be added to the path automatically.

 

            .EXAMPLE

                        PS C:\> Get-ProcessList

                       

                        Get the process list from the local computer and write it to a process3.txt file in the default path. Once the file is created, show it in the default text editor.

 

            .EXAMPLE

                        PS C:\> Get-ProcessList -Path C:\TestPath

                       

                        Get the process list from the local computer and write it to C:\TestPath\2012SG\event3\process3.txt. Once the file is created, show it in the default text editor.

 

            .EXAMPLE

                        PS C:\> Get-ProcessList -Path C:\TestPath\2012SG

                       

                        Get the process list from the local computer and write it to C:\TestPath\2012SG\event3\process3.txt. Once the file is created, show it in the default text editor.

 

            .EXAMPLE

                        PS C:\> Get-ProcessList -Path C:\TestPath\2012SG\event3

                       

                        Get the process list from the local computer and write it to C:\TestPath\2012SG\event3\process3.txt. Once the file is created, show it in the default text editor.

 

            .INPUTS

                        System.String

 

            .OUTPUTS

                        None

 

            .LINK

                        about_functions_advanced

 

            .LINK

                        about_comment_based_help

 

            .LINK

                        Get-Process

 

            .LINK

                        Test-Path

 

            .LINK

                        Join-Path

 

            .LINK

                        New-Item

 

            .LINK

                        Invoke-Item

 

            .LINK

                        Format-Table

 

            .LINK

                        Out-File

 

            .LINK

                        Out-Null

#>

[CmdletBinding()]

param(

            [Parameter(Position=0)]

            [ValidateNotNullOrEmpty()]

            [System.String]

            $Path = "${env:SystemDrive}\2012SG\event3"

)

 

#region Append the portions of the 2012SG\event3 subdirectory to the path if they are not present.

 

if ($Path -notmatch '\\2012SG\\event3$') {

            if ($Path -notmatch '\\2012SG$') {

                        $Path = Join-Path -Path $Path -ChildPath '2012SG\event3'

            } else {

                        $Path = Join-Path -Path $Path -ChildPath 'event3'

            }

}

 

#endregion

 

#region If the path does not exist, try to create it, but ignore any errors.

 

if (-not (Test-Path -LiteralPath $Path)) {

            New-Item -Path $Path -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null

}

 

#endregion

 

#region If the path still does not exist, default to using the My Documents folder instead.

 

if (-not (Test-Path -LiteralPath $Path)) {

            $myDocumentsFolder = [System.Environment]::GetFolderPath('MyDocuments')

            $Path = Join-Path -Path $myDocumentsFolder -ChildPath 2012SG\event3

}

 

#endregion

 

#region Check one more time to see if the path exists. If it does not, try to create it, and stop processing the script if it cannot be created.

 

if (-not (Test-Path -LiteralPath $Path)) {

            New-Item -Path $Path -ItemType Directory -Force -ErrorAction Stop | Out-Null

}

 

#endregion

 

#region Now that we know the path exists, get the processes, show the name and id properties in a fixed width table, and write them to the process3.txt file in the path.

 

Get-Process | Format-Table -Property Name,Id -AutoSize | Out-File -FilePath $Path\process3.txt -Force -ErrorAction Stop

 

#endregion

 

#region Lastly, once we have a process3.txt file that we created, show it in the default text editor.

 

Invoke-Item -LiteralPath $Path\process3.txt

 

#endregion

If you open a file with these contents in PowerSE, and then collapse the comment-based Help, you end up with a view with the regions collapsed that is very easy to read and understand. This collapsed view also allows you to identify the portion of the script that may be causing you problems when you are troubleshooting it later. This is why I think the #region statement is very important when writing Windows PowerShell scripts.

That pretty much wraps up this event. As usual, I have been pretty detailed in my solution, but I provide this detail in hopes that you will understand all of the intricacies that are involved in a Windows PowerShell script so that you will be better armed when creating your own scripts in the future. Good luck with the rest of the competition, and happy scripting!

~Kirk out.

The 2012 Scripting Games Guest Commentator Week will continue tomorrow when we will present the scenario for Event 4.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • <p>Your commentary is brilliant. I like your scripting style and the use or &#39;regions&#39; of code. Brilliant, again!</p> <p>Carlo</p> <p>happysysadm.com</p>