2013 Scripting Games – Learning points from Event 4

The 2013 Scripting Games started April 25th, and event 4 is now open for voting. As I did in the previous events, I will pick out two entries from each event and provide some learning points to highlight both good and bad practices.

Before reading my comments, you might want to download the event instructions:

The first entry I will comment on is from Beginner 4:

  • Comment based help is leveraged to provide a synopsis, description and basic usage. Comment-based help and the Active Directory module which is leverage was introduced in PowerShell 2.0, thus #requires –version 2.0 could have been used to prevent users trying to run this in PowerShell 1.0 getting error messages.
  • HTML-formatting (head, pre- and post-content) as well as a couple of other variables is defined (note that I have updated my previous article with more info on working with HTML in PowerShell). I would also suggest the search base to be specified as a variable rather than a hard coded path in the script. Although, this is specifically stated in the synopsis.
  • The time stamp for when the report was created is included in the bottom of the report as required by the event instructions. In addition the username of the user generating the report is added, which is nice.
  • The actual cmdlets generating the report is a one-liner, nicely formatted on multiple lines for readability.
  • The lastlogon date property is used to determine when the users last logged on to the domain. There are ways to retrieve this information, and not all of them is accurate. For example the lastlogontimestamp property will be 9-14 days behind the current date. For more information, see this article on the “Ask the Directory Services Team” blog. To get a more accurate time stamp, you might want to query all domain controller, see this TechNet article for one example on how to accomplish this. Although I would not withdraw any points for not catching this “gotcha”, I would give a bonus point for those who did.

 

The second entry I have decided to comment on is from Advanced 4:

  • Leveraging the #requires statement is a good practice, however, this should be separated into two lines. When the script is run “as is” in PowerShell 2.0 the following error will be returned: “Cannot process the “#requires” statement at line 1 because it is not in the correct format.”. If separating into two lines, it will work in both 2.0 and 3.0:

#Requires -Version 3.0
#Requires -Module ActiveDirectory

  • Comment based help is leveraged in an excellent way, providing both help for all parameters as well as several examples.
  • [CmdletBinding()] is defined in order to use [Parameter()] decorators to validate input. Although there is nothing wrong with parameterizing this as a script, I would consider to turn it into a function (a personal preference).
  • The FilePath parameter is marked as mandatory, as well as positional. This is perfectly fine, but another option would be to default to a path using an environment variable (for example $env:userprofile\Documents\Report.html). The requirement of validating the filename extension (htm or html) is accomplished using a regular expression inside a ValidateScript validation.
  • The Count parameter is configured with a default value of 20 as required. In addition ValidateRange is leveraged to make sure the number specified is valid. This is not necessary, as defining the parameter as an integer would perform the necessary validation:

Cannot convert value “2222222222222222″ to type “System.Int32″. Error: “Value was
either too large or too small for an Int32.

  • Two additional parameters is supplied: One for overwriting the report-file if it exists (-Force) and one returning the newly created HTML file (-PassThru). These two parameters provide additional functionality not required by the instructions.
  • The parameter values for ConvertTo-Html is quite large, and providing these as a hashtable and leveraging splatting makes it more readable. This also makes it possible to collapse/expand the HTML-code (the hashtable) in PowerShell ISE.
  • The PSCustomObject type accelerator is used to create new objects. There are many ways to create new objects in PowerShell 3.0, but this is the easiest/preferred way in my opinion. Also, the new [ordered] type adapter in PowerShell 3.0 is leveraged to decide the order of the properties, rather than using Select-Object which will actually create new objects (more overhead).

Like the previous events, there have been many great submissions in event 4. Keep up the good work, and good luck with event 5 – “The Logfile Labyrinth”.

2013 Scripting Games – Learning points from Event 3

The 2013 Scripting Games started April 25th, and event 3 is now open for voting. As I did in the previous events, I will pick out two entries from each event and provide some learning points to highlight both good and bad practices.

Before reading my comments, you might want to download the event instructions:

The first entry I will comment on is from Beginner 3:

  • Two pieces of information is required for this event, which we typically find in WMI. Since the instructions clearly stated that both CIM and WMI is available, and we do not have to think about authentication or firewalls, I find leveraging CIM a good practice. CIM is the newest technology, based on standards based management. It also requires just one firewall port to be open, compared to WMI which requires a lot more. Although, I would not give less points if WMI was leveraged in this event, since this is also a valid option in the instructions. In the real world however, we would typically need to think about authentication and firewalls in addition to other considerations, such as legacy systems. If Windows 2000 was still present, CIM would not have worked against WMI on that system without specifying the DCOM protocol for the CIM session.
  • Filtering is performed on the server side, which speeds up the query. A bad practice would have been skipping the –Filter parameter, and performed the filtering on the client side using Where-Object.
  • Extracting the data required by the instructions is done using Select-Object. What I also like is that formatting the data is done in the same operation. The –f operator is used to format the data, and PowerShell`s built-in constants for converting values to MB and GB is leveraged.
  • PowerShell has built-in capabilities to convert objects to HTML, in the form of the ConvertTo-Html cmdlet. This is leveraged in a way that meets the goals for the event instructions: The 3 properties needed is specified, and a header and a footer is added with the required data. However, a bonus point would have been added if the computer name was added to the header as specified in the event instructions. This is an example of how important it is to read the instructions carefully. I would recommend you to read through the instructions one more time before submitting your entry, as you might miss things like this.
  • The FilePath parameter is omitted on Out-File. This works fine since the parameter is positional. However, I prefer to use full parameter names when sharing scripts or even one-liners with others.
  • The instructions required a one-liner. Many people takes this literally, and place everything on one line. That is very bad for readability. This submission leverages the pipeline symbol, the comma and the back tick to continue on the next line. This is fully counted as a one-liner. However, the semi-colon as some people used does not qualify for a one-liner. This is simply a statement separator, and will break the pipeline.

The second entry I have decided to comment on is from Advanced 3:

This entry does produce a nice looking HTML-report with the required data. However, there are some bad practices I would like to highlight as learning points:

  • A ping function is created. This is not required, as PowerShell has a Test-Connection cmdlet giving the same functionality.
  • The naming of the function is not very descriptive for the end user sitting at the helpdesk.
  • The HTML code is created in a here-string rather than leveraging PowerShell`s ConvertTo-Html cmdlet. This makes it harder than necessary.
  • Aliases is used. This is ok when working interactively when you want to type fast, but not in a script or function you will share with others (think readability).
  • Write-Host is used (don`t kill puppies), use Write-Verbose to output additional information. Alternatively, output an object even if the computer could not be contacted. You could add a “Connectivity” property to inform the user whether the computer was reachable or not, and add this to the report.

There have been many great suggestions in this event. Keep up the good work, and good luck with event 4 – “An Auditing Adventure”.

Update 2013-05-21: The good thing about the Scripting Games is that everyone learns something, including the judges. I haven`t been using here-strings for creating HTML, but as Rob Campbell (@mjolinor) stated on Twitter: “Here strings are an AWESOME tool for creating HTML or XML documents. ” -Jeffrey Snover. (link). Of course, this implies that you know HTML. On the topic of HTML, I would also like to recommend Don Jones` free ebook “Creating HTML Reports in PowerShell“. Available with the book is also a PowerShell module for working with HTML. In addition, there is also a very good session recording from the 2013 PowerShell Summit by Jeffrey Hicks called “Creating HTML Reports With style“.

2013 Scripting Games – Learning points from Event 1

The 2013 Scripting Games started April 25th, and event 1 is now open for voting. As I am participating as a judge this year, I will pick out two entries from each event and provide some learning points to highlight both good and bad practices.

The first entry I have decided to comment on is from Beginner 1: An Archival Atrocity.

$RootPath = "C:\ScriptingGames\Application\Log"
$Paths = Get-ChildItem $RootPath
$ArchivePath = "C:\ScriptingGames\Archive"
$varTime = (Get-Date).Adddays(-90)

ForEach ($Path in $Paths)
{
    If ((Test-Path "$ArchivePath\$Path") -eq "True")
        {
            Echo "Path Exists"
        }
    Else
        {
            Echo "$Path Created"
            Mkdir "$ArchivePath\$Path"
        }
    
        Get-ChildItem -literalpath "$RootPath\$path" -Include "*.*" -Recurse | Where-Object {$_.CreationTime -lt "$varTime"} | Move-Item -Destination "$ArchivePath\$path\" 
}

Learning Points

  • Add comment based help. This makes it more easy for others to leverage the script, you also get the benefit of using Get-Help on your function or script.
  • The instructions did not require any output, consider using Write-Verbose instead of Write-Output. This makes it up to the user leveraging the script to decide if any output is wanted, by editing the $VerbosePreference automatic variable.
  • Avoid using aliases in scripts (Echo and Mkdir in this case), although it is great for interactive use when you want to type quickly.
  • When sharing a script, I also prefer to use full parameter names. For example Get-ChildItem -Path $RootPath instead of Get-ChildItem $RootPath.

The second entry is from Advanced 1: An Archival Atrocity.

function Move-Log{
<#
.SYNOPSIS
Moves old log files in the specified repository to another location.

.DESCRIPTION
Move-Log is used to move old log files from specified application folders in a root log repository to a new location. 
By default only files older than 90 days are moved, but this can be configured with the -Period parameter.
Subfolder structure is mantained in the destination path.

.PARAMETER LogPath
Specifies the root path of the log repository.
Default = C:\Application\Log

.PARAMETER ApplicationLogFolder
Specifies application folders in the log repository that may contain log files.

.PARAMETER DestinationPath
Specifies the new location for the log files.

.PARAMETER Period
Specifies how old, in days, must be the log files to be moved.
Default = 90

.EXAMPLE
Move-Log -ApplicationLogFolder App1, OtherApp, ThisAppAlso -DestinationPath \\NASServer\Archives

This will move files older than 90 day located in "C:\Application\Log\App1", "C:\Application\Log\OtherApp" and "C:\Application\Log\ThisAppAlso" 
to their respective destination folders "\\NASServer\Archives\App1", "\\NASServer\Archives\OtherApp" and "\\NASServer\Archives\ThisAppAlso".

.EXAMPLE
Move-Log -LogPath C:\MyLogRepository\ -ApplicationLogFolder App1,OtherApp,ThisAppAlso -DestinationPath \\NASServer\Archives -Period 365

This will move files older than 1 year from "C:\MyLogRepository\App1", "C:\MyLogRepository\OtherApp" and "C:\MyLogRepository\ThisAppAlso" 
to their respective destination folders "\\NASServer\Archives\App1", "\\NASServer\Archives\OtherApp" and "\\NASServer\Archives\ThisAppAlso".

.INPUTS
None.
.OUTPUTS
None.
#>
    [CmdletBinding()]
    param(
    [ValidateScript({ Test-Path $_ -PathType Container })]
    [string]$LogPath="C:\Application\Log",
    [Parameter(Mandatory=$true)]
    [ValidateScript({ Test-Path "$LogPath\*" -Include $_ -PathType Container })]
    [string[]]$ApplicationLogFolder,   
    [Parameter(Mandatory=$true)] 
    [ValidateScript({ Test-Path $_ -PathType Container })]
    [string]$DestinationPath,
    [int]$Period=90
    ) 
    
    # Get the date limit. Only files older than that date will be moved    
    $DateLimit = (Get-Date).AddDays(-$Period)

    foreach ($AppFolder in $ApplicationLogFolder)
    {
        $AppLogPath      = "$LogPath\$AppFolder"
        $DestAppLogPath  = "$DestinationPath\$AppFolder"

        # Create destination folder if not exists
        if (!(Test-Path $DestAppLogPath -PathType Container))
        {
            try
            {
                New-Item -Path $DestAppLogPath -ItemType Directory -ErrorAction Stop -ErrorVariable $DirectoryError
            }
            catch
            {
                Write-Error "Error creating destination directory $DestAppLogPath. Error: $DirectoryError"  
                # Jump to the next $AppFolder              
                continue
            }
        }

        # Get the name of the files to move
        $FilesToMove = Get-ChildItem $AppLogPath -File | Foreach { if ($_.LastWriteTime -lt $DateLimit) { $_.Name } }

        foreach ($File in $FilesToMove)
        {
            $FilePath = "$AppLogPath\$File"
            $DestPath  = "$DestAppLogPath\$File"

            # Move the file
            try
            {
                Move-Item -Path $FilePath -Destination $DestPath -ErrorAction Stop -ErrorVariable $MoveError
            }
            catch
            {
                Write-Error "Error moving file $FilePath to destination $DestPath. Error: $MoveError"
            }
        }
    }     
}

Learning Points

  • The use of a function makes this tool easier to reuse without the need to call a script each time.
  • The use of an advanced function makes it work more like an actual cmdlet, since you can do more advanced things such as marking a parameter as mandatory and validating the arguments. For more information, see Get-Help about_Functions_Advanced.
  • This is also a good example on leveraging comment based help. All parameters is properly described, and a few examples is provided. Often the first thing people look at before reading the actual help is the examples, so it is important to provide at least a couple of them.
  • The use of parameter validation is excellent, as this simplifies the function and is leveraging functionality PowerShell is providing “for free”.
  • Default values is configured for the LogPath and Period parameters. This means that these parameter values will be used if the user using the function does not specify these parameters. It is also easy to change the values if needed, which was a requirement for this task.

There have been many great submissions for the first event. Keep up the good work, and good luck with event 2 which starts May 2.

2012 Scripting Games

The Microsoft Scripting Guys is hosting the 2012 Scripting Games April 2 – April 13 2012. The Scripting Games started out several years ago as a scripting competition based on VBScript. After the introduction of Windows PowerShell the Scripting Games did both a VBScript and a PowerShell track for a few years, until last year when VBScript was phased out in advantage of Windows PowerShell.

We can see the same thing happening in terms of automation capabilities in both Microsoft and non-Microsoft products. When we also look at the massive PowerShell capabilities coming in Windows 8 and Windows Server 8, we can see that PowerShell has turned into an essential skill for IT Professionals.

The Scripting Games is an excellent opportunity to get started with the scripting capabilities in Windows PowerShell. If you are new to PowerShell, the Beginner track is the recommended track for your first year participating in the Scripting Games. For more experienced scripters there is an Advanced track with more challenging tasks. Note that using features in the current beta of Windows PowerShell 3.0 is allowed in the Scripting Games submissions.

For those who want an introduction to PowerShell before the Scripting Games begin, Scripting Guy Ed Wilson will host a 5-series Live Meeting called “Windows PowerShell for the Busy Admin” beginning March 12, 2012.

Like last year, I will participate in the Scripting Games as a judge, which will be great fun! For those interested in learning PowerShell I highly recommend to take the time to participate in the Scripting Games, as it is a great learning experience. If your on Twitter, remember to use the 2012 Scripting Games Twitter tag #2012SG.

Good luck to all participants!

 

2012 Scripting Games Resources

2012 Windows PowerShell Scripting Games: All Links on One Page

2012 Scripting Games: Frequently Asked Questions

2012 Scripting Games Judging Criteria Revealed

Announcing the PowerShell Judges for the 2012 Scripting Games

TechNet Radio podcast: Announcing the 2012 Scripting Games

PowerScripting Podcast: Episode 177 – Scripting Guy Ed Wilson on the 2012 PowerShell Scripting Games

Official Scripting Guys Forum

The Scripting Guys on Facebook

The Scripting Guys on Twitter