
About the Author Ed Wilson is a senior consultant at Microsoft Corporation and a well-known scripting expert. He is a Microsoft-certified trainer who delivers a popular Windows PowerShell workshop to Microsoft Premier Customers worldwide. He has written 14 books including several on Windows scripting, including: Windows PowerShell Scripting Guide, Microsoft Windows PowerShell Step by Step and Microsoft VBScript Step by Step all published by Microsoft Press. Ed holds more than 20 industry certifications, including Microsoft Certified Systems Engineer (MCSE) and Certified Information Systems Security Professional (CISSP). In his spare time he enjoys woodworking, underwater photography, and scuba diving. |
Just when you thought it was safe to return to the PowerShell prompt, along comes event 4. In summary, we were tasked with creating a calendar that looks kind of like this:
February 2008 Sun Mon Tue Wed Thu Fri Sat 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
Dude, I thought it would be rather easy. Take some information, pipe it to format-table and, voila, problem solved. It did not really work out that way.
So how did I approach the problem? The first thing I did was break the problem into two pieces. The first part is to obtain a DateTime object when given a month and a year. Once I have the DateTime object I will need to find out which day of the week the month starts with, and I need to know how many days are in the month. (The DateTime object is smart enough to take into account leap year, so we do not need to worry with that.) To get this information we can use the Get-Date cmdlet. As I am creating a calendar, I know that each month will start with the number 1. The code that obtains the requisite information is seen here.
$global:DayMonthYear = get-date -day 1 -month $month -year $year
$global:MonthStartDay = $dayMonthYear.dayOfWeek
$global:daysInMonth = [dateTime]::DaysInMonth($year,$month)
While I was messing around with creating arbitrary dates, I thought it would be a useful addition to the script to also provide the ability to print out the current month as well, so I added a little bit of extra code to do this. Once again, we call upon the services of the Get-Date cmdlet, and obtain a DateTime object that represents the current month and year. This code is seen here.
$month = (get-date).month
$year = (get-date).year
$global:DayMonthYear = get-date -day 1 -month $month -year $year
$global:MonthStartDay = $dayMonthYear.dayOfWeek
$global:daysInMonth = [dateTime]::DaysInMonth($year,$month)
Since all this code is basically performing a single task, I chucked the entire bunch of code into a single function called funGetCalendar. This function is seen here.
Function funGetCalendar()
{
if($current)
{
$month = (get-date).month
$year = (get-date).year
$global:DayMonthYear = get-date -day 1 -month $month -year $year
$global:MonthStartDay = $dayMonthYear.dayOfWeek
$global:daysInMonth = [dateTime]::DaysInMonth($year,$month)
} #end if current
ELSE
{
$global:DayMonthYear = get-date -day 1 -month $month -year $year
$global:MonthStartDay = $dayMonthYear.dayOfWeek
$global:daysInMonth = [dateTime]::DaysInMonth($year,$month)
} #end else current
FunPrintCalendar
} #end funGetCalendar
With that part of the problem solved, I turned my attention to the more complex portion of the script: given a month, a year, and a starting date, can you print out a calendar? Of course you can. I ended up approaching this problem the same way I would have done if I was using VBScript … I created an array, and some nested loops, created an output string, and printed out the results. Let’s look at this in a bit of detail.
The first thing I did when writing this portion of the script was to write the function in a standalone script. I hard-coded the values month, year and start day in variables at the beginning of the script in order to make testing go smoother. So what did I come up with? Let’s take a look.
To begin with, I initialize a bunch of variables and set them to $null. This is easily done in Windows PowerShell by ganging the variables together. I then create my array of days and store the day names in the variable $dow. The $aryDays array holds the number of days in the month, and will be used to display the actual date values when the calendar is printed out. I use a variable $dayCount as my incremented variable to keep my place inside the loop. The $intPad variable is used to space over the starting day of the month (i.e., when the month does not start on a Sunday). I use the $weekCount variable as the incremented variable to keep track of how many weeks are listed in the printed calendar. This section of the script is seen here.
$aryOut = $aryDays = $dayInMonth = $dayForWeek = $dayCount =$null $padDays = $intPad = $numPad = $pad = $padString = $numDays = $null $dow = ` "Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday" $aryDays = 1..$global:daysInMonth $dayCount = 1 #counter variable. Keeps track of how many days have been added to the output [int]$intpad = $global:monthStartDay #Controls WHICH day of the week we start month on. Sunday is 0. $weekCount = $intPad # we use this for first week, and then reset to 0 for additional loops
The next part of the code is rather straightforward: we need to calculate the amount of padding to use when the month does not begin on Sunday. It is a fairly straightforward for loop. I begin with the day of the week that is represented by the $global:monthStartDay variable and then loop to the end of the week. Add up the length of each of the days in the $dow array, add some extra space for the gap between the days of the week, and we are done. This section of the code is seen here.
for ($numPad = 0 ; $numPad -le $intpad-1 ; $numpad++)
{
$pad += $dow[$numpad].length
} #end for numpad
$padString = " " * ($pad)
$padString += " " * ($numpad)
[int]$numDays = 6
Now we come to the hard part, a couple of nested loops with some if statements thrown in for good measure. We need to create a day for each day of the month. The length of the month is held in the $aryDays variable. We increment $daysInMonth by 7 so we get only 7 days on a line. Next we need to add days for each of the weeks in the month. Once again we use a for statement. If the day number is greater than 9 then it will take up two places, so we add an if..else statement to allocate the appropriate number of spaces for this. For each week created, we throw in a line feed at the end of the line to move to the next week. To do the line feed we use the special character `n. This section of the code is seen here.
for($dayInMonth = 1 ; $dayInMonth -le $aryDays.Length ; $dayInMonth+=7)
{
for($dayForWeek = $weekCount ; $dayForWeek -le $numDays ; $dayForWeek ++ )
{
if($dayCount -le $aryDays.Length)
{
if($dayCount -le 9)
{
$aryOUT += " " * ($dow[$dayForWeek].length) + ($dayCount)
} #end $dayCount le 9
ELSE
{
$aryOUT += " " * ($dow[$dayForWeek].length-1) + ($dayCount)
} #end else daycount
} #end if $dayCount le $aryDays.length
$dayCount += 1
} #end for $dayForWeek
$numDays = 6 #reset for week 2-5. Since days start numbering at 0
$weekCount = 0
$aryOUT = "$aryOut`n"
} #end for $dayInMonth
At this point it’s all over but the shouting, as they say at a basketball game. We now need to control the printed output. First we print out the Header for the calendar. To do this I use the write-host cmdlet and print it in cyan. Next I print out contents of the array that holds the days of the week. To do this, I use the join static method from the system.string .NET Framework class. As this is a static method, I can access it by using the special syntax [string]::join. If the month begins on Sunday, then there is no need for any padding. If, however, the month begins on some other day, then we will need the padding. An if..else logic controls this. This section of code is seen here.
Write-Host -ForegroundColor cyan "Displaying: $month $year"
Funline([string]::Join(" ",$dow)) # print out the days for calendar header
if($intPad -eq 0)
{
"$aryOUT" #display the days in the month
} #end if intpad
Else
{
"$padString$aryOUT"
So that is basically it. However, I decided I would modify the script a little bit more. Rather than prompting for input (you can easily use read-host for that) I decided I would like to use the script by using command-line parameters. Keep in mind that the param statement must be the first executable code in your script. So here is the section that does that:
param(
$month,
$year,
[switch]$current,
[switch]$help,
[switch]$examples,
[switch]$min,
[switch]$full
) #end param
One of my fundamental design parameters is this: if a script uses command-line parameters, it must support a help switch. The fundamental Windows PowerShell design guidelines can be seen in the Windows PowerShell Scripting Guide published by MSPress. To support the help switch, I look for the presence of the help variable, and then call the funhelp function. We break the help function into pieces, so we can support the kind of common parameters used in the PowerShell cmdlets. To do this, our code looks like this:
if($help) { funhelp }
if($examples) { funhelp }
if($full) { funhelp }
Our funHelp function is seen here. It is just a couple of here-strings.
function funHelp()
{
$descriptionText= `
@"
NAME: Event4.ps1
DESCRIPTION:
Displays a calendar of a month supplied from the
command line. The script will also display a
calendar for the current month / year as well. It
takes command line parameters and supports help.
Supports partial parameter completion. Ex: -y for
year. But you need -mo for month and -mi for min
help, due to both params starting with m.
PARAMETERS:
-month the month to display
-year the year to display
-current displays the current month calendar
-help prints help description and parameters file
-examples prints only help examples of syntax
-full prints complete help information
-min prints minimal help. Modifies -help
"@ #end descriptionText
$examplesText= `
@"
SYNTAX:
Event4.ps1
Displays an error missing parameter, and calls help
Event4.ps1 -current
Displays a calendar for the current month
Event4.ps1 -c
Displays a calendar for the current month
Event4.ps1 -month 2 -year 2008
Displays a calendar for feb. 2008
Event4.ps1 -mo 2 -y 2008
Displays a calendar for feb. 2008
Event4.ps1 -help
Prints the help topic for the script
Event4.ps1 -help -full
Prints full help topic for the script
Event4.ps1 -help -examples
Prints only the examples for the script
Event4.ps1 -examples
Prints only the examples for the script
"@ #end examplesText
$remarks = `
"
REMARKS
For more information, type: $($MyInvocation.ScriptName) -help -full
" #end remarks
if($examples) { $examplesText ; $remarks ; exit }
if($full) { $descriptionText; $examplesText ; exit }
if($min) { $descriptionText ; exit }
$descriptionText; $remarks
exit
} #end funHelp function
The underlining of the calendar days is performed by a standard function I use in many of my scripts. I call this function the funline function. The basic logic of it is used in the same way I create the padded spaces for the date offset of the calendar, I multiply the values. Since I use the funline function so much, I have included a help switch for it as well as some parameters. Here is the funline function.
function funline (
$strIN,
$char = "=",
$sColor = "Yellow",
$uColor = "darkYellow",
[switch]$help
)
{
if($help)
{
$local:helpText = `
@"
Funline accepts inputs: -strIN for input string and -char for separator
-sColor for the string color, and -uColor for the underline color. Only
the -strIn is required. The others have the following default values:
-char: =, -sColor: Yellow, -uColor: darkYellow
Example:
funline -strIN "Hello world"
funline -strIn "Morgen welt" -char "-" -sColor "blue" -uColor "yellow"
funline -help
"@
$local:helpText
break
} #end funline help
$strLine= $char * $strIn.length
Write-Host -ForegroundColor $sColor $strIN
Write-Host -ForegroundColor $uColor $strLine
} #end funLine function
Well that is about it for the script. I hope you find the script and the comments useful. These techniques are covered in much more detail in the New MSPress book, Windows PowerShell Scripting Guide, written by me (Ed Wilson), and just released this month.
Here’s the completed script for Event 4:
# =============================================================================
#
# NAME:Event4.ps1
#
# AUTHOR: Ed Wilson , microsoft
# DATE : 2/14/2008
#
# COMMENT:
# Uses Params to allow modification of script at runtime
# Uses funHelp function to display help
# Uses funLine function to underline output
#
#
# =============================================================================
param(
$month,
$year,
[switch]$current,
[switch]$help,
[switch]$examples,
[switch]$min,
[switch]$full
) #end param
# Begin Functions
function funHelp()
{
$descriptionText= `
@"
NAME: Event4.ps1
DESCRIPTION:
Displays a calender of a month supplied from the
command line. The script will also display a
calender for the current month / year as well. It
takes command line parameters and supports help.
Supports partial parameter completion. Ex: -y for
year. But you need -mo for month and -mi for min
help, due to both params starting with m.
PARAMETERS:
-month the month to display
-year the year to display
-current displays the current month calender
-help prints help description and parameters file
-examples prints only help examples of syntax
-full prints complete help information
-min prints minimal help. Modifies -help
"@ #end descriptionText
$examplesText= `
@"
SYNTAX:
Event4.ps1
Displays an error missing parameter, and calls help
Event4.ps1 -current
Displays a calender for the current month
Event4.ps1 -c
Displays a calender for the current month
Event4.ps1 -month 2 -year 2008
Displays a calender for feb. 2008
Event4.ps1 -mo 2 -y 2008
Displays a calender for feb. 2008
Event4.ps1 -help
Prints the help topic for the script
Event4.ps1 -help -full
Prints full help topic for the script
Event4.ps1 -help -examples
Prints only the examples for the script
Event4.ps1 -examples
Prints only the examples for the script
"@ #end examplesText
$remarks = `
"
REMARKS
For more information, type: $($MyInvocation.ScriptName) -help -full
" #end remarks
if($examples) { $examplesText ; $remarks ; exit }
if($full) { $descriptionText; $examplesText ; exit }
if($min) { $descriptionText ; exit }
$descriptionText; $remarks
exit
} #end funHelp function
function funline (
$strIN,
$char = "=",
$sColor = "Yellow",
$uColor = "darkYellow",
[switch]$help
)
{
if($help)
{
$local:helpText = `
@"
Funline accepts inputs: -strIN for input string and -char for seperator
-sColor for the string color, and -uColor for the underline color. Only
the -strIn is required. The others have the following default values:
-char: =, -sColor: Yellow, -uColor: darkYellow
Example:
funline -strIN "Hello world"
funline -strIn "Morgen welt" -char "-" -sColor "blue" -uColor "yellow"
funline -help
"@
$local:helpText
break
} #end funline help
$strLine= $char * $strIn.length
Write-Host -ForegroundColor $sColor $strIN
Write-Host -ForegroundColor $uColor $strLine
} #end funLine function
Function funPrintcalender()
{
$aryOut = $aryDays = $dayInMonth = $dayForWeek = $dayCount =$null
$padDays = $intPad = $numPad = $pad = $padString = $numDays = $null
$dow = "Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"
$aryDays = 1..$global:daysInMonth
$dayCount = 1 #counter variable. keeps track of how many days have been added to the output
[int]$intpad = $global:monthStartDay #Controls WHICH day of the week we start month on. Sunday is 0.
$weekCount = $intPad # we use this for first week, and then reset to 0 for additional loops
# calculate the offset used by days. numpad is counter, intpad is how many days padding
# for the first line
for ($numPad = 0 ; $numPad -le $intpad-1 ; $numpad++)
{
$pad += $dow[$numpad].length
} #end for numpad
$padString = " " * ($pad)
$padString += " " * ($numpad)
[int]$numDays = 6
#calculate the pattern for the days in the month
for($dayInMonth = 1 ; $dayInMonth -le $aryDays.Length ; $dayInMonth+=7)
{
for($dayForWeek = $weekCount ; $dayForWeek -le $numDays ; $dayForWeek ++ )
{
if($dayCount -le $aryDays.Length)
{
if($dayCount -le 9)
{
$aryOUT += " " * ($dow[$dayForWeek].length) + ($dayCount)
} #end $dayCount le 9
ELSE
{
$aryOUT += " " * ($dow[$dayForWeek].length-1) + ($dayCount)
} #end else daycount
} #end if $dayCount le $aryDays.length
$dayCount += 1
} #end for $dayForWeek
$numDays = 6 #reset for week 2-5. Since days start numbering at 0
$weekCount = 0
$aryOUT = "$aryOut`n"
} #end for $dayInMonth
#now we display the output.
Write-Host -ForegroundColor cyan "Displaying: $month $year"
Funline([string]::Join(" ",$dow)) # print out the days for calender header
if($intPad -eq 0)
{
"$aryOUT" #display the days in the month
} #end if intpad
Else
{
"$padString$aryOUT"
} #end else intpad
EXIT
} #end funPrintcalender
Function funGetcalender()
{
if($current)
{
$month = (get-date).month
$year = (get-date).year
$global:DayMonthYear = get-date -day 1 -month $month -year $year
$global:MonthStartDay = $dayMonthYear.dayOfWeek
$global:daysInMonth = [dateTime]::DaysInMonth($year,$month)
} #end if current
ELSE
{
$global:DayMonthYear = get-date -day 1 -month $month -year $year
$global:MonthStartDay = $dayMonthYear.dayOfWeek
$global:daysInMonth = [dateTime]::DaysInMonth($year,$month)
} #end else current
FunPrintcalender
} #end funGetcalender
# Entry Point
if($help) { funhelp }
if($examples) { funhelp }
if($full) { funhelp }
if($current) { funGetcalender }
if($month -and $year) { funGetcalender }
if(!$month -and !$year) { funhelp }