Designing Help Documentation for Windows PowerShell Scripts

  • 12/16/2009

Using the Here-String for Multiple-Line Comments

One method that can be used in Windows PowerShell 1.0 to allow for multiline comments is to use a here-string. The here-string allows you to assign text without worrying about line formatting or escaping quotation marks and other special characters. It is helpful when working with multiple lines of text because it allows you to overlook the more tedious aspects of working with formatted text, such as escaping quotation marks. The advantage of using a here-string to store your comments is that it then becomes rather easy to use another script to retrieve all of the comments.

Constructing a Here-String

An example of working with here-strings is the Demo-HereString.ps1 script. The $instructions variable is used to hold the content of the here-string. The actual here-string itself is created by beginning the string with the at and double quotation mark (@”) symbol. The here-string is terminated by reversing the order with the double quotation mark and the at symbol (“@). Everything between the two tags is interpreted as a string, including special characters. The here-string from the Demo-HereString.ps1 script is shown here.

$instructions = @"
This command line demo illustrates working with multiple lines
of text. The cool thing about using a here-string is that it allows
you to "work" with text without the need to "worry" about quoting
or other formating issues.
   It even allows you
     a sort of
       wysiwyg type of experience.
You format the data as you wish it to appear.
"@

The here-string is displayed by calling the variable that contains the here-string. In the Demo-HereString.ps1 script, the response to a prompt posed by the Read-Host cmdlet is stored in the $response variable. The Read-Host command is shown here.

$response = Read-Host -Prompt "Do you need instructions? <y / n>"

The value stored in the $response variable is then evaluated by the If statement. If the value is equal to the letter “y,” the contents of the here-string are displayed. If the value of the $response variable is equal to anything else that the script displays, the string displays “good bye” and exits. This section of the script is shown here.

if ($response -eq "y") { $instructions ; exit }
else { "good bye" ; exit }

The Demo-HereString.ps1 script is seen here.

Example 10-3. Demo-HereString.ps1

$instructions = @"
This command line demo illustrates working with multiple lines
of text. The cool thing about using a here-string is that it allows
you to "work" with text without the need to "worry" about quoting
or other formating issues.
   It even allows you
     a sort of
       wysiwyg type of experience.
You format the data as you wish it to appear.
"@

$response = Read-Host -Prompt "Do you need instructions? <y / n>"
if ($response -eq "y") { $instructions ; exit }
else { "good bye" ; exit }

An Example: Adding Comments to a Registry Script

To better demonstrate the advantages of working with here-strings, consider the following example that employs the registry. A script named GetSetieStartPage.ps1 reads or modifies a few values from the registry to configure the Internet Explorer start pages. The GetSetieStartPage.ps1 script contains pertinent comments and provides a good example for working with documentation.

With Internet Explorer 7.0, there are actually two registry keys that govern the Internet Explorer start page. The registry key that is documented in the Tweakomatic program, available from the Microsoft Script Center, accepts a single string for the start page, which makes sense because traditionally you can have only a single start page. For Internet Explorer 7, an additional registry key was added to accept multiple strings (an array of strings), which in turn gives you the ability to have multiple start pages. You can use the Windows Management Instrumentation (WMI) stdRegProv class to both read and edit the registry keys. The main advantage of this technique is that it gives you the ability to edit the registry remotely.

The registry keys that are involved with the settings are shown in Figure 10-1.

Figure 10-1

Figure 10-1 Internet Explorer registry keys shown in the Registry Editor

First, you must create a few command-line parameters. Two of these are switched parameters, which allow you to control the way the script operates—either by obtaining information from the registry or setting information in the registry. The last parameter is a regular parameter that controls the target computer. You are assigning a default value for the $computer variable of localhost, which means that the script reads the local registry by default as shown here.

Param([switch]$get,[switch]$set,$computer="localhost")

You now need to create the Get-ieStartPage function. The Get-ieStartPage function will be used to retrieve the current start page settings. To create a function, you can use the Function keyword and give it a name as shown here.

Function Get-ieStartPage()

After using the Function keyword to create the new function, you need to add some code to the script block. First, you must create the variables to be used in the script. These are the same types of variables that you use when using WMI to read from the registry in Microsoft VBScript. You must specify the registry hive from which you plan on querying by using one of enumeration values shown Table 10-1.

TABLE 10-1. WMI Registry Tree Values

NAME

VALUE

HKEY_CLASSES_ROOT

2147483648

HKEY_CURRENT_USER

2147483649

HKEY_LOCAL_MACHINE

2147483650

HKEY_USERS

2147483651

HKEY_CURRENT_CONFIG

2147483653

HKEY_DYN_DATA

2147483654

In VBScript, you often create a constant to hold the WMI registry tree values (although a regular variable is fine if you do not change it). If you feel that you must have a constant, the following code is the syntax you need to use.

New-Variable -Name hkcu -Value 2147483649 -Option constant

The $key variable is used to hold the path to the registry key with which you will be working. The $property and $property2 variables are used to hold the actual properties that will control the start pages. This section of the script is shown here.

{
 $hkcu = 2147483649
 $key = "Software\Microsoft\Internet Explorer\Main"
 $property = "Start Page"
 $property2 = "Secondary Start Pages"

You now need to use the [WMICLASS] type accelerator to create an instance of the stdRegProv WMI class. You can hold the management object that is returned by the type accelerator in the $wmi variable. What is a bit interesting is the path to the WMI class. It includes both the computer name, the WMI namespace followed by a colon, and the name of the WMI class. This syntax corresponds exactly to the Path property that is present on all WMI classes (because it is inherited from the System abstract class) as shown in Figure 10-2. (If you are interested in this level of detail about WMI, you can refer to Windows Scripting with WMI: Self-Paced Learning Guide [Microsoft Press, 2006]. All of the examples are written in VBScript, but the book applies nearly 100 percent to Windows PowerShell.)

Figure 10-2

Figure 10-2 The WMI Path property seen in the WbemTest utility

Here is the line of code that creates an instance of the stdRegProv WMI class on the target computer.

$wmi = [wmiclass]"\\$computer\root\default:stdRegProv"

Next, you need to use the GetStringValue method because you want to obtain the value of a string (it really is that simple). This step can be a bit confusing when using the stdRegProv WMI class because of the large number of methods in this class. For each data type you want to access, you must use a different method for both writing and reading from the registry. This also means that you must know what data type is contained in the registry key property value with which you want to work. The stdRegProv methods are documented in Table 10-2.

TABLE 10-2. stdRegProv Methods

NAME

DEFINITION

CheckAccess

System.Management.ManagementBaseObject CheckAccess(System.UInt32 hDefKey, System.String sSubKeyName, System.UInt32 uRequired)

CreateKey

System.Management.ManagementBaseObject CreateKey(System.UInt32 hDefKey, System.String sSubKeyName)

DeleteKey

System.Management.ManagementBaseObject DeleteKey(System.UInt32 hDefKey, System.String sSubKeyName)

DeleteValue

System.Management.ManagementBaseObject DeleteValue(System.UInt32 hDefKey, System.String sSubKeyName, System.String sValueName)

EnumKey

System.Management.ManagementBaseObject EnumKey (System.UInt32 hDefKey, System.String sSubKeyName)

EnumValues

System.Management.ManagementBaseObject EnumValues(System.UInt32 hDefKey, System.String sSubKeyName)

GetBinaryValue

System.Management.ManagementBaseObject GetBinaryValue(System.UInt32 hDefKey, System.String sSubKeyName, System.String sValueName)

GetDWORDValue

System.Management.ManagementBaseObject GetDWORDValue(System.UInt32 hDefKey, System.String sSubKeyName, System.String sValueName)

GetExpandedStringValue

System.Management.ManagementBaseObject GetExpandedStringValue(System.UInt32 hDefKey, System.String sSubKeyName, System.String sValueName)

GetMultiStringValue

System.Management.ManagementBaseObject GetMultiStringValue(System.UInt32 hDefKey, System.StringsSubKeyName, System.String sValueName)

GetStringValue

System.Management.ManagementBaseObject GetStringValue(System.UInt32 hDefKey, System.String sSubKeyName, System.String sValueName)

SetBinaryValue

System.Management.ManagementBaseObject SetBinaryValue(System.UInt32 hDefKey, System.String sSubKeyName, System.String sValueName, System.Byte[] uValue)

SetDWORDValue

System.Management.ManagementBaseObject SetDWORDValue(System.UInt32 hDefKey, System.String sSubKeyName, System.String sValueName, System.UInt32 uValue)

SetExpandedStringValue

System.Management.ManagementBaseObject SetExpandedStringValue(System.UInt32 hDefKey, System.String sSubKeyName, System.String sValueName, System.String sValue)

SetMultiStringValue

System.Management.ManagementBaseObject SetMultiStringValue(System.UInt32 hDefKey, System.StringsSubKeyName, System.String sValueName, System.String[] sValue)

SetStringValue

System.Management.ManagementBaseObject SetStringValue(System.UInt32 hDefKey, System.String sSubKeyName, System.String sValueName, System.String sValue)

The code that obtains the value of the default Internet Explorer home page is shown here.

($wmi.GetStringValue($hkcu,$key,$property)).sValue

After obtaining the value of the string that holds the default home page, you need to obtain the value of a multistring registry key that is used for the additional home pages. To do this, you can use the GetMultiStringValue method. What is convenient about this method is that the values of the array that are returned are automatically expanded, and you can thus avoid the for … next gyrations required when performing this method call when using VBScript. This line of code is shown here.

($wmi.GetMultiStringValue($hkcu,$key, $property2)).sValue

Adding comments to the closing curly brackets is a best practice that enables you to quickly know where the function begins and ends.

Here is the closing curly bracket and associated comment. If you always type comments in the same pattern (for example, #end with no space), they are then easy to spot if you ever decide to write a script to search for them.

} #end Get-ieStartPage

You now need to create a function to assign new values to the Internet Explorer start pages. You can call the Set-ieStartPage function as shown here.

Function Set-ieStartPage()
{

You must assign some values to a large number of variables. The first four variables are the same ones used in the previous function. (You could have made them script-level variables and saved four lines of code in the overall script, but then the functions would not have been stand-alone pieces of code.) The $value variable is used to hold the default home page, and the $aryvalues variable holds an array of secondary home page URLs. This section of the code is shown here.

$hkcu = 2147483649
$key = "Software\Microsoft\Internet Explorer\Main"
$property = "Start Page"
$property2 = "Secondary Start Pages"
$value = "http://www.microsoft.com/technet/scriptcenter/default.mspx"
$aryValues = "http://social.technet.microsoft.com/Forums/en/ITCG/threads/",
"http://www.microsoft.com/technet/scriptcenter/resources/qanda/all.mspx"

After assigning values to variables, you can use the [WMICLASS] type accelerator to create an instance of the stdRegProv WMI class. This same line of code is used in the Get-ieStartPage function and is shown here.

$wmi = [wmiclass]"\\$computer\root\default:stdRegProv"

You can now use the SetStringValue method to set the value of the string. The SetStringValue method takes four values. The first is the numeric value representing the registry hive to which to connect. The next is the string for the registry key. The third position holds the property to modify, and last is a string representing the new value to assign as shown here.

$rtn = $wmi.SetStringValue($hkcu,$key,$property,$value)

Next, you can use the SetMultiStringValue method to set the value of a multistring registry key. This method takes an array in the fourth position. The signature of the SetMultiStringValue method is similar to the SetStringValue signature. The only difference is that the fourth position needs an array of strings and not a single value as shown here.

$rtn2 = $wmi.SetMultiStringValue($hkcu,$key,$property2,$aryValues)

Now, you can print the value of the ReturnValue property. The ReturnValue property contains the error code from the method call. A zero means that the method worked (no runs, no errors), and anything else means that there was a problem as shown here.

  "Setting $property returned $($rtn.returnvalue)"
  "Setting $property2 returned $($rtn2.returnvalue)"
} #end Set-ieStartPage

You are now at the entry point to the script. You must first get the starting values and then set them to the new values that you want to configure. If you want to re-query the registry to ensure that the values took effect, you can simply call the Get-ieStartPage function again as shown here.

if($get) {Get-ieStartpage}
if($set){Set-ieStartPage}

The complete GetSetieStartPage.ps1 script is shown here.

Example 10-4. GetSetieStartPage.ps1

Param([switch]$get,[switch]$set,$computer="localhost")
$Comment = @"
NAME: GetSetieStartPage.ps1
AUTHOR: ed wilson, Microsoft
DATE: 1/5/2009

KEYWORDS: stdregprov, ie, [wmiclass] type accelerator,
Hey Scripting Guy
COMMENTS: This script uses the [wmiclass] type accelerator
and the stdregprov to get the ie start pages and to set the
ie start pages. Using ie 7 or better you can have multiple
start pages.

"@ #end comment

Function Get-ieStartPage()
{
$Comment = @"
FUNCTION: Get-ieStartPage
Is used to retrieve the current settings for Internet Explorer 7 and greater.
The value of $hkcu is set to a constant value from the SDK that points
to the Hkey_Current_User. Two methods are used to read
from the registry because the start page is single valued and
the second start page's key is multi-valued.

"@ #end comment
 $hkcu = 2147483649
 $key = "Software\Microsoft\Internet Explorer\Main"
 $property = "Start Page"
 $property2 = "Secondary Start Pages"
 $wmi = [wmiclass]"\\$computer\root\default:stdRegProv"
 ($wmi.GetStringValue($hkcu,$key,$property)).sValue
 ($wmi.GetMultiStringValue($hkcu,$key, $property2)).sValue
} #end Get-ieStartPage

Function Set-ieStartPage()
{
$Comment = @"
FUNCTION: Set-ieStartPage
Allows you to configure one or more home pages for IE 7 and greater.
The $aryValues and the $Value variables hold the various home pages.
Specify the complete URL ex: "http://www.ScriptingGuys.Com." Make sure
to include the quotation marks around each URL.

"@ #end comment
  $hkcu = 2147483649
  $key = "Software\Microsoft\Internet Explorer\Main"
  $property = "Start Page"
  $property2 = "Secondary Start Pages"
  $value = "http://www.microsoft.com/technet/scriptcenter/default.mspx"
  $aryValues = "http://social.technet.microsoft.com/Forums/en/ITCG/threads/",
  "http://www.microsoft.com/technet/scriptcenter/resources/qanda/all.mspx"
  $wmi = [wmiclass]"\\$computer\root\default:stdRegProv"
  $rtn = $wmi.SetStringValue($hkcu,$key,$property,$value)
  $rtn2 = $wmi.SetMultiStringValue($hkcu,$key,$property2,$aryValues)
  "Setting $property returned $($rtn.returnvalue)"
  "Setting $property2 returned $($rtn2.returnvalue)"
} #end Set-ieStartPage

# *** entry point to script
if($get) {Get-ieStartpage}
if($set){Set-ieStartPage}

Retrieving Comments by Using Here-Strings

Due to the stylized nature of here-strings, you can use a script, such as GetCommentsFromScript.ps1, to retrieve the here-strings from another script, such as GetSetieStartPage.ps1. You are interested in obtaining three comment blocks. The first comment block contains normal script header information: title, author, date, keywords, and comments on the script itself. The second and third comment blocks are specifically related to the two main functions contained in the GetSetieStartPage.ps1 script. When processed by the GetCommentsFromScript.ps1 script, the result is automatically produced script documentation. To write comments from the source file to another document, you need to open the original script, search for your comments, and write the appropriate text to a new file.

The GetCommentsFromScript.ps1 script begins with the Param statement. The Param statement is used to allow you to provide information to the script at run time. The advantage of using a command-line parameter is that you do not need to open the script and edit it to provide the path to the script whose comments you are going to copy. You are making this parameter a mandatory parameter by assigning a default value to the $script variable. The default value you assign uses the Throw statement to raise an error, which means that the script will always raise an error when run unless you supply a value for the -script parameter when you run the script.

Using Throw to Raise an Error

Use of the Throw statement is seen in the DemoThrow.ps1 script. To get past the error that is raised by the Throw statement in the Set-Error function, you first need to set the value of the $errorActionPreference variable to SilentlyContinue, which causes the error to not be displayed and allows the script to continue past the error. (This variable performs the same action as the On Error Resume Next setting from VBScript.) The If statement is used to evaluate the value of the $value variable. If there is a match, the Throw statement is encountered and the exception is thrown.

To evaluate the error, you can use the Get-ErrorDetails function. The error count is displayed first, and it will be incremented by one due to the error that was raised by the Throw statement. You can then take the first error (the error with the index value of 0 is always the most recent error that occurred) and send the error object to the Format-List cmdlet. You choose all of the properties. However, the invocation information is returned as an object. Therefore, you must query that object directly by accessing the invocation object via the Invocationinfo property of the error object. The resulting error information is shown in Figure 10-3.

Figure 10-3

Figure 10-3 The Throw statement is used to raise an error.

The complete DemoThrow.ps1 script is shown here.

Example 10-5. DemoThrow.ps1

Function Set-Error
{
 $errorActionPreference = "SilentlyContinue"
 "Before the throw statement: $($error.count) errors"
 $value = "bad"
 If ($value -eq "bad")
   { throw "The value is bad" }
} #end Set-Error

Function Get-ErrorDetails
{
 "After the throw statement: $($error.count) errors"
 "Error details:"
 $error[0] | Format-List -Property *
 "Invocation information:"
 $error[0].InvocationInfo
} #end Get-ErrorDetails

# *** Entry Point to Script
Set-Error
Get-ErrorDetails

The Param statement is shown here.

Param($Script= $(throw "The path to a script is required."))

You need to create a function that creates a file name for the new text document that will be created as a result of gleaning all of the comments from the script. To create the function, you can use the Function keyword and follow it with the name for the function. In your case, you can call the function Get-FileName in keeping with the spirit of the verb-noun naming convention in Windows PowerShell. The function will take a single input parameter that is held in the $script variable inside the function. The $script variable will hold the path to the script to be analyzed. The entry to the Get-FileName function is shown here.

Function Get-FileName($Script)
{

Working with Temporary Folders

Next, you can obtain the path to the temporary folder on the local computer in many different ways, including using the environmental PS drive. This example uses the static GetTempPath method from the System.Io.Path .NET Framework class. The GetTempPath method returns the path to the temporary folder, which is where you will store the newly created text file. You hold the temporary folder path in the $outputPath variable as shown here.

$outputPath = [io.path]::GetTempPath()

You decide to name your new text file after the name of the script. To do this, you need to separate the script name from the path in which the script is stored. You can use the Split-Path function to perform this surgery. The –leaf parameter instructs the cmdlet to return the script name. If you want the directory path that contains the script, you can use the –parent parameter. You put the Split-Path cmdlet inside a pair of parentheses because you want that operation to occur first. When the dollar sign is placed in front of the parentheses, it creates a subexpression that executes the code and then returns the name of the script. You can use .ps1 as the extension for your text file, but that can become a bit confusing because it is the extension for a script. Therefore, you can simply add a .txt extension to the returned file name and place the entire string within a pair of quotation marks.

You can use the Join-Path cmdlet to create a new path to your output file. The new path is composed of the temporary folder that is stored in the $outputPath variable and the file name you created using Split-Path. You combine these elements by using the Join-Path cmdlet. You can use string manipulation and concatenation to create the new file path, but it is much more reliable to use the Join-Path and Split-Path cmdlets to perform these types of operations. This section of the code is shown here.

 Join-Path -path $outputPath -child "$(Split-Path $script -leaf).txt"
} #end Get-FileName

You need to decide how to handle duplicate files. You can prompt the user by saying that a duplicate file exists, which looks like the code shown here.

      $Response = Read-Host -Prompt "$outputFile already exists. Do you wish to delete
it <y / n>?"
      if($Response -eq "y")
        { Remove-Item $outputFile | Out-Null }
     ELSE { "Exiting now." ; exit }

You can implement some type of naming algorithm that makes a backup of the duplicate file by renaming it with an .old extension, which looks like the code shown here.

  • if(Test-Path -path "$outputFile.old") { Remove-Item -Path "$outputFile.old" }
    Rename-Item -path $outputFile -newname "$(Split-Path $outputFile -leaf).old"

You can also simply delete the previously existing file, which is what I generally choose to do. The action you want to perform goes into the Remove-OutPutFile function. You begin the function by using the Function keyword, specifying the name of the function, and using the $outputFile variable for input to the function as shown here.

Function Remove-outputFile($outputFile)
{

To determine whether the file exists, you can use the Test-Path cmdlet and supply the string contained in the $outputFile variable to the –path parameter. The Test-Path cmdlet only returns a true or false value. When a file is not found, it returns a false value, which means that you can use the If statement to evaluate the existence of the file. If the file is found, you can perform the action in the script block. If the file is not found, the script block is not executed. As shown here, the first command does not find the file, and false is returned. In the second command, the script block is not executed because the file cannot be located.

PS C:\> Test-Path c:\missingfile.txt
False
PS C:\> if(Test-Path c:\missingfile.txt){"found file"}
PS C:\>

Inside the Remove-OutPutFile function, you can use the If statement to determine whether the file referenced by $outputFile already exists. If it does, it is deleted by using the Remove-Item cmdlet. The information that is normally returned when a file is deleted is pipelined to the Out-Null cmdlet providing for a silent operation. This portion of the code is shown here.

  if(Test-Path -path $outputFile) { Remove-Item $outputFile | Out-Null }

} #end Remove-outputFile

After you create the name for the output file and delete any previous output files that might be around, it is time to retrieve the comments from the script. To do this, you can create the Get-Comments function and pass it both the $script variable and $outputFile variable as shown here.

Function Get-Comments($Script,$outputFile)
{

Reading the Comments in the Output File

It is now time to read the text of the script. You can use the Get-Content cmdlet and provide it with the path to the script. When you use Get-Content to read a file, the file is read one line at a time and passed along the pipeline. If you store the result into a variable, you will have an array. You can treat the $a variable as any other array, including obtaining the number of elements in the array via the Length property and indexing directly into the array as shown here.

PS C:\fso> $a = Get-Content -Path C:\fso\GetSetieStartPage.ps1
PS C:\fso> $a.Length
62
PS C:\fso> $a[32]
($wmi.GetMultiStringValue($hkcu,$key, $property2)).sValue

The section of the script that reads the input script and sends it along the pipeline is shown here.

Get-Content -path $Script |

Next, you need to look inside each line to determine whether it belongs to the comment block. To examine each line within a pipeline, you must use the ForEach-Object cmdlet. This cmdlet is similar to a Foreach … next statement in that it lets you work with an individual object from within a collection one at a time. The backtick character (`) is used to continue the command to the next line. The action you want to perform on each object as it comes across the pipeline is contained inside a script block that is delineated with a set of curly brackets (braces). This part of the Get-Content function is shown here.

Foreach-Object `
 {

Once you are inside the ForEach-Object cmdlet process block, it is time to examine the line of text. To do this, you can use the If statement. The $_ automatic variable is used to represent the current line that is on the pipeline. You use the –match operator to perform a regular expression pattern match against the line of text. The –match operator returns a Boolean value—either true or false—in response to the pattern as shown here.

PS C:\fso> '$Comment = @"' -match  '^\$comment\s?=\s?@"'
True

The regular expression pattern you are using is composed of a number of special characters as shown in Table 10-3.

Table 10-3. Regular Expression Match Pattern and Meaning

CHARACTER

DESCRIPTION

^

Match at the beginning

\

Escape character so the $ sign is treated as a literal character and not the special character used in regular expressions

$comment

Literal characters

\s?

Zero or more white space characters

=

Literal character

@”

Literal characters

The section of code that examines the current line of text on the pipeline is shown here.

If($_ -match '^\$comment\s?=\s?@"')

You can create a variable named $beginComment that is used to mark the beginning of the comment block. If you make it past the –match operator, you find the beginning of the comment block. You can set the variable equal to $true as shown here.

{
 $beginComment = $True
} #end if match @"

Next, you can see whether you are at the end of the comment block by once again using the –match operator. You will look for the @” character sequence that is used to close a here-string. If you find this sequence, you can set the $beginComment variable to false as shown here.

If($_ -match '"@')
  {
   $beginComment = $False
  } #end if match "@

After you pass the first two If statements—the first identifying the beginning of the here-string and the second locating the end of the here-string—you now want to grab the text that needs to be written to your comment file by setting the $beginComment variable to true. You also want to ensure that you do not see the @” character on the line because this designates the end of the here-string. To make this determination, you can use a compound If statement as shown here.

If($beginComment -AND $_ -notmatch '@"')
  {

It is now time to write the text to the output file. To do this, you can use the $_ automatic variable, which represents the current line of text, and pipeline it to the Out-File cmdlet. The Out-File cmdlet receives the $outputFile variable that contains the path to the comment file. You can use the –append parameter to specify that you want to gather all of the comments from the script into the comment file. If you do not use the –append parameter, the text file will only contain the last comment because, by default, the Out-File cmdlet will overwrite the contents of any previously existing file. You can then add closing curly brackets for each of the comments that were previously opened. I consider it a best practice to add a comment after each closing curly bracket that indicates the purpose of the brace. This procedure makes the script much easier to read, troubleshoot, and maintain. This section of the code is shown here.

      $_ | Out-File -FilePath $outputFile -append
     } # end if beginComment
  } #end Foreach
} #end Get-Comments

You can now create a function named Get-OutPutFile that opens the output file for you to read. Because the temporary folder is not easy to find and because you have the path to the file in the $outputFile variable, it makes sense to use the script to open the output file. The Get-OutPutFile function receives a single input variable named $outputFile. When you call the Get-OutPutFile function, you pass a variable to the function that contains the path to the comment file that you want to open. That path is contained in the $outputFile variable. You can pass any value to the Get-OutPutFile function. Once inside the function, the value is then referred to by the $outputFile variable. You can even pass a string directly to the function without even using quotation marks around the string as shown here.

Function Get-outputFile($outputFile)
{
 Notepad $outputFile
} #end Get-outputFile

Get-outputFile -outputfile C:\fso\GetSetieStartPage.ps1

By placing the string in a variable, you can easily edit the value of the variable. In fact, you are set up to provide the value of the variable via the command line or to base the value on an action performed in another function. Whenever possible, you should avoid placing string literal values directly in the script. In the code that follows, you can use a variable to hold the path to the file that is passed to the Get-OutPutFile function.

Function Get-outputFile($outputFile)
{
 Notepad $outputFile
} #end Get-outputFile

$outputFile = "C:\fso\GetSetieStartPage.ps1"
Get-outputFile -outputfile $outputFile

The complete Get-OutPutFile function is shown here.

Function Get-outputFile($outputFile)
{
 Notepad $outputFile
} #end Get-outputFile

Instead of typing in a string literal value for the path to the output file, the $outputFile variable receives the path that is created by the Get-FileName function. The Get-FileName function receives the path to the script that contains the comments to be extracted. The path to this script comes in via the command-line parameter. When a function has a single input parameter, you can pass it to the function by using a set of smooth parentheses. On the other hand, if the function uses two or more input parameters, you must use the –parameter name syntax. This line of code is shown here.

$outputFile = Get-FileName($script)

Next, you can call the Remove-OutPutFile function and pass it the path to the output file that is contained in the $outputFile variable. The Remove-OutPutFile function was discussed in the “Working with Temporary Folders” section earlier in this chapter. This line of code is shown here.

Remove-outputFile($outputFile)

Once you are assured of the name of your output file, you can call the Get-Comments function to retrieve comments from the script whose path is indicated by the $script variable. The comments are written to the output file referenced by the $outputFile variable as shown here.

Get-Comments -script $script -outputfile $outputFile

When all of the comments are written to the output file, you can finally call the Get-OutPutFile function and pass it the path contained in the $outputFile variable. If you do not want the comment file to be opened, you can easily comment the line out of your script or you can delete it and the Get-OutPutFile function itself from your script. If you are interested in reviewing each file prior to saving it, leave the line of code in place. This section of the script is shown here.

Get-outputFile($outputFile)

When the GetCommentsFromScript.ps1 script runs, nothing is emitted to the console. The only confirmation message that the script worked is the presence of the newly created text file displayed in Microsoft Notepad as shown in Figure 10-4.

Figure 10-4

Figure 10-4 Comments extracted from a script by the GetCommentsFromScript.ps1 script

The complete GetCommentsFromScript.ps1 script is shown here.

Example 10-6. GetCommentsFromScript.ps1

Param($Script= $(throw "The path to a script is required."))
Function Get-FileName($Script)
{
 $outputPath = [io.path]::GetTempPath()
 Join-Path -path $outputPath -child (Split-Path $script -leaf)
} #end Get-FileName

Function Remove-outputFile($outputFile)
{
  If(Test-Path -path $outputFile) { Remove-Item $outputFile | Out-Null }
} #end Remove-outputFile

Function Get-Comments($Script,$outputFile)
{
 Get-Content -path $Script |
 Foreach-Object `
  {
    If($_ -match '^\$comment\s?=\s?@"')
     {
      $beginComment = $True
     } #end if match @"
   If($_ -match '"@')
     {
      $beginComment = $False
     } #end if match "@
   If($beginComment -AND $_ -notmatch '@"')
     {
      $_ | Out-File -FilePath $outputFile -append
     } # end if beginComment
  } #end Foreach
} #end Get-Comments

Function Get-outputFile($outputFile)
{
 Notepad $outputFile
} #end Get-outputFile

# *** Entry point to script ***
$outputFile = Get-FileName($script)
Remove-outputFile($outputFile)
Get-Comments -script $script -outputfile $outputFile
Get-outputFile($outputFile)