A PowerShell script, in its most basic form, can be a simple sequential list of unstructured commands.  Scripts of this nature will typically exist for adhoc, non-critical tasks, where minimal repition is required.  It is not uncommon for a systems administrator to amass a collection of such scripts to perform daily tasks.  For example, the script below will return the subdirectories of a given directory and their respective sizes:

# # List sizes of sub-directories of specified directory. # param ( [Parameter(Mandatory=$true)] [Alias('folder')] [String] $directory ) Get-ChildItem -LiteralPath $directory -Directory -ErrorAction SilentlyContinue | Foreach-Object { $objDir = $_; Get-ChildItem -LiteralPath $objDir.FullName -File -Recurse -ErrorAction SilentlyContinue | Measure-Object -Sum -Property Length | Select-Object -Property @{ Name = 'Directory'; Expression = {$objDir.FullName} }, @{ Name = 'Size (MB)'; Expression = { [Math]::Round($_.Sum / 1MB, 2) } } } | Sort-Object -Property 'Size (MB)' -Descending;

The functions discussed in this section are referred to as basic functions.  Basic functions operate in the same way as those found in traditional 3GLs, that is, they accept parameterised input and return scalar or composite data types.  It's important to learn how to construct such functions, but the true potential of PowerShell comes from its ability to return objects, which can be manipulated using operators.   Functions of this nature are referred to as advanced functions or , and will be discussed later on.

It won't be long before your PowerShell scripts become unwieldy in an unstructured form.  It's at this point that procedures and functions become necessary.    Procedures and functions make code re-use possible.  More importantly, they hide complexity, make code more readable and therefore easier to maintain (read for further information).

PowerShell procedures and functions require "declaration before use", that is, the interpreter must have ingested them before they are called.   The most elegant way of achieving this is to layout your scripts as follows:

function main() { # # Main body of script. The name of this function is arbitrary. # return $intMainRc; } function helperFunction() { # # Some function. # return $intFunctionRc; } function helperProcedure() { # # Some procedure. # return; } # # Entry point of script (after functions and procedures ingested). Here we can: # # i) Define any ; # ii) Enable ; # iii) Import required and load required snap-ins; # iv) Deal with command line arguments (refer to section for further information); # v) Call the main() function; and # vi) Return a return code to the caller. # #Requires -RunAsAdministrator #Requires -Modules ActiveDirectory, PKI Set-StrictMode -Version 2; [Int32] $Local:intScriptRc = 0; Import-Module -Name @( 'ActiveDirectory', 'PKI' ) -ErrorAction SilentlyContinue; if ( $? ) { # # Call main() function here. # } else { Write-Host -Object 'Failed to load module(s).'; $intScriptRc = -1; } #else-if exit $intScriptRc;

In order for a function or procedure to be of any use, it must be able to accept data from the caller.  The data is passed by means of parameters defined in the param() section of the function or procedure, for example:

function addTwoNumbers() { param( $firstNumber, $secondNumber ) return $firstNumber + $secondNumber; } addTwoNumbers -firstNumber 2 -secondNumber 3;

In the above example, the function doesn't specify the data types of its parameter input, meaning that PowerShell will use its dynamic typing to try to select the most appropriate data type.  However, as discussed in the section, this behaviour is not ideal.   It is therefore strongly recommended that you specify the expected data types of your parameters, that is, you specify static data types, for example:

function addTwoNumbers() { param( [Int32] $firstNumber, [Int32] $secondNumber ) return $firstNumber + $secondNumber; } addTwoNumbers -firstNumber 2 -secondNumber 3;

By default, when calling a PowerShell function or procedure, providing values for the given parameter(s) is optional (we'll cover shortly).   If the caller chooses not to supply a value for a given parameter, it will assume the value $null.  However, you can easily tell PowerShell to assume a default value for a parameter, should one not be provided, for example:

function addTwoNumbers() { param( [Int32] $firstNumber = 0, [Int32] $secondNumber = 0 ) return $firstNumber + $secondNumber; } addTwoNumbers;

A parameter can be defined as being mandatory by using the mandatory parameter attribute, for example:

function addTwoNumbers() { param( [Parameter(Mandatory=$true)] [Int32] $firstNumber, [Parameter(Mandatory=$true)] [Int32] $secondNumber ) return $firstNumber + $secondNumber; } addTwoNumbers -firstNumber 2 -secondNumber 3;

Notice that we no longer need to specify default values.  The reason for this is that the mandatory attribute will cause PowerShell to prompt for the required values, if they are not provided.  For example:

cmdlet addTwoNumbers at command pipeline position 1 Supply values for the following parameters: firstNumber: 101 secondNumber: 19 120

Mandatory parameters are particularly useful for scripts that rely heavily on human-computer interaction (HCI), such as command-line tools.  However, they aren't suitable for scripts that will be to run automatically, where a parameter prompt will cause an automated task to stall.  In situations such as this, it is better to specify .

PowerShell supports the concept of switch parameters, where a parameter provided in the form -<PARAMETER_NAME> causes the given parameter value to be set to $true.  The absence of the parameter causes the given parameter value to be set to $false.  This feature emulates the behaviour of command-line switches.

function removeTempFiles() { param ( [Switch] $subdirs ) if ( $subdirs ) { Get-ChildItem -LiteralPath . -Filter *.tmp -Recurse | Remove-Item; } else { Get-ChildItem -LiteralPath . -Filter *.tmp | Remove-Item; } #else-if return; } removeTempFiles -subdirs;

By default, PowerShell's function and procedure parameters are named parameters.   In order to use them, the caller must specify their name in the function or procedure call, for example:

function displayTwoNumbers() { param( [Int32] $firstNumber = 0, [Int32] $secondNumber = 0 ) Write-Host -Object ( 'First number : {0}' -f $firstNumber ); Write-Host -Object ( 'Second number : {0}' -f $secondNumber ); return; } displayTwoNumbers -firstNumber 2 -secondNumber 3;

This behaviour isn't a bad thing; named parameters make function and procedure calls easier to read and understand.  This, in turn, makes code more easy to maintain and share (read for further information).

PowerShell, however, also allows us to define a named parameter as being a positional parameter.  This means that parameters that are passed without a name prefix will be processed in sequence and assigned to the relevant position (PowerShell processes all of the named parameters first before attempting to map the remaining un-named parameters).

This mixed approach allows a function, procedure or to be written in such a way that it can be called using meaningful named parameters in scripts, Etc., but will also accept traditional positional parameters when being called in a "short hand" form from the command line.

The example below shows positions being assigned to two parameters (with zero being the first position):

function displayTwoNumbers() { param( [Parameter(Position=0)] [Int32] $firstNumber = 0, [Parameter(Position=1)] [Int32] $secondNumber = 0 ) Write-Host -Object ( 'First number : {0}' -f $firstNumber ); Write-Host -Object ( 'Second number : {0}' -f $secondNumber ); return; }

This makes it possible for the procedure to be called in two ways:

# # Using Named parameters. # displayTwoNumbers -firstNumber 2 -secondNumber 3; # # Using Positional parameters. # displayTwoNumbers 2 3;

Note that PowerShell function, procedure and cmdlet positional parameters are specified without using the traditional parenthesis.  This format is reserved for .NET function calls, for example, $foo.Substring( 4, 3 );.

The types of Validation Declaration that PowerShell supports are are covered in section .   In this section, we'll discuss how they can be used to harden functions and procedures.   Validation declarations have one limitation when used in the param() section of a function or procedure (i.e.: during "parameter binding"), this being that any exceptions raised cannot be caught in the function itself.  Consider the following example:

function addMeetingToCalendar() { param ( [ValidateCount(5,12)] [String[]] $meetingAttendees, # Exceptions raised here can only be [ValidateScript({$_ -ge (Get-Date)})] [DateTime] $meetingDateTime # caught outside of the function. ) # # Do something... # return; } addMeetingToCalendar -meetingAttendees @('Rod', 'Jane', 'Freddie') -meetingDateTime ([Convert]::ToDateTime('13/04/2020 11:21'));

In the above example, having insufficient meeting attendees will raise a ParameterBindingValidationException exception.   However, this cannot be caught within the function, as exception handlers cannot be defined during parameter binding.  A workaround involves moving the validation declaration to within the function body, where validation failure will cause a ValidationMetadataException exception to be raised, for example:

function addMeetingToCalendar() { param ( [String[]] $meetingAttendees = @(), [DateTime] $meetingDateTime = 0 ) # # Attempt to initialise local versions of the supplied parameters. # try { [ValidateCount(5,12)] [String[]] $Local:arrMeetingAttendees = $meetingAttendees; [ValidateScript({$_ -ge (Get-Date)})] [DateTime] $Local:objMeetingDateTime = $meetingDateTime; # # Do something... # } #try catch [System.Management.Automation.ValidationMetadataException] { # # Handle parameter validation errors. # Write-Host -Object 'Invalid parameter(s).'; } #catch return; } addMeetingToCalendar -meetingAttendees @('Rod', 'Jane', 'Freddy') -meetingDateTime ([Convert]::ToDateTime('13/04/2020 11:21'));

Implementing this workaround means that the ValidationMetadataException exception can be safely caught and handled.   Combining this approach with static data type assignments and providing default values for parameters makes it possible to create very robust functions and procedures.

PowerShell provides five prepreprocessor directives that can be used to define certain requisite conditions that must be satisfied in order for a script to execute.   The directives take the form of #Requires statements, and can be placed anywhere within a script.  The directives are as follows:

Directive Purpose Example(s)
#Requires -Version <major>[.<minor>] Defines the minimum version of PowerShell that can be used to execute the script. #Requires -Version 5.0
#Requires -PSSnapin <PSSnapin Name> [-Version <major>[.<minor>]] Specifies the name, and optional version, of a PSSnapin that must be installed.  Multiple directives of this type can be placed in a given script. #Requires Microsoft.Exchange.Management.PowerShell.SnapIn

#Requires -PSSnapin VMware.VimAutomation.Core
#Requires -Modules { <Module Name> | <Hash Table> } Specifies the module(s) that must be present in the module search path ($Env:PSModulePath).  Multiple module names can be specified, delimeted using commas.  If a specific version of a module is required, its version and, optionally, it's GUID can be provided in the form of a . #Requires -Modules ActiveDirectory, AdcsAdministration #Requires -Modules ModuleFoo, @{ ModuleName='MyModule'; ModuleVersion='1.1'; } #Requires -Modules @{ ModuleName='ModuleBar'; ModuleVersion='1.2'; GUID='13b25430-959d-49c4-a907-758c5ac93418'; }
#Requires -ShellId <Shell ID> Specifies the required Shell.  This directive isn't of any practical use.  PowerShell "shells" are only relevant to developers who embed PowerShell instances into their applications.
#Requires -RunAsAdministrator Specifies that the script must be running with elevated rights (i.e.: the User Account Control administrative token must be set; typically using Run as administrator).

PowerShell scripts are essentially ANSI or Unicode encoded text files, saved with a .PS1 file extension.  The structure of a script file is discussed in sections and , above.

There are multiple ways of invoking PowerShell scripts from within PowerShell itself.  The available methods are described below.


Syntax Notes Example
Invoke-Expression -Command <file_path>; Using Invoke-Expression is the preferred method of script invocation from within scripts, as its more verbose syntax makes scripts easier to read when compared with the alternative methods. try { Invoke-Expression -Command 'c:\scripts\foo.ps1'; } #try catch [System.Management.Automation.CommandNotFoundException] { Write-Warning -Message 'Failed to invoke script.'; } #catch
& <file_path> The ampersand (&) is PowerShell's call operator and can be used to invoke scripts or execute . try { & 'c:\scripts\foo bar.ps1'; } #try catch [System.Management.Automation.CommandNotFoundException] { Write-Warning -Message 'Failed to invoke script.'; } #catch
.\<file_path> Use the .\ prefix to execute a script from current directory. .\foo.ps1 .\'foo bar.ps1'

PowerShell scripts and can be invoked from a legacy command shell (CMD.EXE) by calling POWERSHELL.EXE.  The syntax for POWERSHELL.EXE can be viewed by typing powerhell.exe [-help|-?|/?], for example:

C:\Users\JohnDoe> powershell.exe -help PowerShell[.exe] [-PSConsoleFile <file> | -Version <version>] [-NoLogo] [-NoExit] [-Sta] [-Mta] [-NoProfile] [-NonInteractive] [-InputFormat {Text | XML}] [-OutputFormat {Text | XML}] [-WindowStyle <style>] [-EncodedCommand <Base64EncodedCommand>] [-File <filePath> <args>] [-ExecutionPolicy <ExecutionPolicy>] [-Command { - | <script-block> [-args <arg-array>] | <string> [<CommandParameters>] } ]

To invoke a PowerShell script, use the following syntax (note that we use fully qualified paths throughout):

%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -file <FULL PATH TO POWERSHELL SCRIPT> <ARGUMENT WITH NO SPACES> "<ARGUMENT WITH SPACES>"

Upon completion, PowerShell's return code can be queried using the %ERRORLEVEL% environment variable.

You can use the Windows Task Scheduler or any 3rd party scheduling tool to schedule PowerShell scripts.  The syntax for the scheduled job is the same as that described in section .   For example, you can use SCHTASKS.EXE to schedule a PowerShell script to execute every day at 00:00hrs:

C:\Users\JohnDoe> schtasks.exe /Create /sc DAILY /st 00:00 /tn MyPowerShellJob /tr "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -file d:\MyScript.ps1 \"hello world\" foo" /ru JohnDoe /rp Please enter the run as password for JohnDoe: *************

Not that the double quotes around the argument hello world are escaped using back-slashes.  The resultant task can be viewed from the Windows Task Scheduler (enter TASKSCHD.MSC to launch it).

When a file is downloaded using Internet Explorer, an NTFS Alternate Data Stream (ADS) is created in the target file.  The ADS is used to store a zone identifier, indicating from which Internet Explorer security zone the file was sourced.  You can view the data streams within a given file using Get-Item, and you can view the data in a given data stream using Get-Content, for example:

PS C:\Users\JohnDoe> Get-Item -LiteralPath .\DownloadedFile.ps1 -Stream *; FileName: C:\Users\JohnDoe\DownloadedFile.ps1 Stream Length ------ ------ :$DATA 35 Zone.Identifier 26 PS C:\Users\JohnDoe> Get-Content -LiteralPath .\DownloadedFile.ps1 -Stream Zone.Identifier; [ZoneTransfer] ZoneId=3 PS C:\Users\JohnDoe>

This information is used by the operating system and PowerShell to prevent accidental execution of Internet-sourced files.   PowerShell will not allow such files to be executed, unless the is set to Unrestricted or Bypass.  If the execution policy is set to Unrestricted, the following warning will still be displayed:

Security warning Run only scripts that you trust. While scripts from the internet can be useful, this script can potentially harm your computer. If you trust this script, use the Unblock-File cmdlet to allow the script to run without this warning message. Do you want to run C:\Users\JohnDoe\DownloadedFile.ps1? [D] Do not run [R] Run once [S] Suspend [?] Help (default is "D"):

If the file is known to be safe, it can be "unblocked" using Windows Explorer, or PowerShell.   To unblock a file using Windows Explorer, view its properties and select Unblock from the General tab, for example:

To unblock a file using PowerShell, use the Unblock-File cmdlet, for example:

PS C:\Users\JohnDoe> Unblock-File -LiteralPath .\DownloadedFile.ps1; PS C:\Users\JohnDoe>

As with most scripting or programming languages, PowerShell supports both single-line and multi-line comments.  However, it also supports embedded comments.  PowerShell's comment syntax is as follows:

# This is a single-line comment. $foo = 1; <# Embedded comment. #> $bar = 2; <# This is a multi-line comment. #>

The use of comments within scripts will be discussed further in section .  PowerShell also introduces the concept of ; this will be discussed when we cover .

By default, PowerShell's variables don't have to be declared before use.  However, as discussed in section , this causes PowerShell to use its dynamic typing behaviour, which should be avoided, especially in production server-side scripts.  It is better to enable PowerShell's mode and pre-declare your variables.   To declare a variable, use the following syntax:

<DATA_TYPE> $[<SCOPE>:]<NAME> = <INITAL_VALUE>;

...where:

Some examples:

# A string value. [String] $Local:myString = 'foo'; # A 32-bit signed integer. [Int32] $Local:myInt = -900; # A DateTime value. [DateTime] $Local:myDate = '01/01/2000 00:00:00'; # An empty array of strings. [String[]] $Local:myEmptyStringArray = @(); # An array of pre-defined strings. [String[]] $Local:myPopulatedStringArray = @( 'apple', 'banana', 'cherry', 'damson' ); # A hash table. [Hashtable] $Local:myHash = @{ 'Apple' = 'Red'; 'Banana' = 'Yellow'; 'Cherry' = 'Red'; 'Damson' = 'Purple'; }

The overall objective when dealing with server-side scripts is consistent and predictable execution, where malfunctions are detected, logged and reported to the caller.  In the context of any given script, this objective begins with the interpretation of command line arguments.  A well written "hardened" script will be able to detect erroneous input, for example, invalid options, incorrect data types, out-of-range values, Etc.

PowerShell has two methods of accepting and processing Command Line Arguments, both of which have their own merits and drawbacks.  The first method is the use of the already familiar param() construct; the second is the use of the built-in $args array (see ).

In this section, we'll discuss various implementations of both of these methods.  With each, we'll begin with simple, less robust, implementations and will progress through increasingly more complex and more robust implementations.

A PowerShell script can utilise the param() construct, just like functions and procedures (see section , above).  The param() construct must appear before any executable code, and allows the caller to provide command line arguments in the familiar PowerShell manner.

# # Example script. Only comments and preprocessor directives can be placed before the param() construct. # #Requires -Version 2.0; param ( $ipaddress, $protocol, $port ) function main() { # # Main body of script. The name of this function is arbitrary. # return $intMainRc; } function helperFunction() { # # Some function. # return $intFunctionRc; } # # Entry point of script. # [Int32] $Local:intScriptRc = 0; $intScriptRc = main $ipaddress $protocol $port; exit $intScriptRc;

The simple example above uses data types, meaning that any parameter data type can be supplied.  It therefore becomes incumbent on the script writer to perform parameter validation.  However, we have already seen that we can harden the handling of parameters using data type assignments, parameter values, parameters and .  So let's start by assigning data types and making all three parameters mandatory:

param ( [Parameter(Mandatory=$true)] [String] $ipaddress, [Parameter(Mandatory=$true)] [String] $protocol, [Parameter(Mandatory=$true)] [Int32] $port )

The script can be further hardened using , for example:

param ( [Parameter(Mandatory=$true)] [ValidatePattern('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')] [String] $ipaddress, [Parameter(Mandatory=$true)] [ValidateSet('tcp', 'udp')] [String] $protocol, [Parameter(Mandatory=$true)] [ValidateRange(0, 65535)] [Int32] $port )

We now have a script that prompts for mandatory parameters that haven't been supplied, imposes data types and performs fairly rigorous data validation.  Should this validation fail, we know that a ParameterBindingValidationException will be raised.  However, as we've already seen in section , such exceptions cannot be caught during parameter binding.  This, unfortunately, means that an exception raised as the result of bad parameter input will cause the script to fail spectacularly, for example:

C:\Users\JohnDoe> .\test.ps1 -ipaddress 192.168.0.1 -protocol icmp -port 49152; C:\Users\JohnDoe\test.ps1 : Cannot validate argument on parameter 'protocol'. The argument "icmp" does not belong to the set "tcp,udp" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again. At line:1 char:46 + .\test.ps1 -ipaddress 192.168.0.1 -protocol icmp -port 49152; + ~~~~ + CategoryInfo : InvalidData: (:) [test.ps1], ParameterBindingValidationException + FullyQualifiedErrorId : ParameterArgumentValidationError,test.ps1 C:\Users\JohnDoe>

The solution to this problem involves returning the param() construct in the script body back to its most simple form (with the exception of the parameter declaration).   The data type assignments, Etc., are then moved inboard, allowing us to place a try...catch around our own internal variable assignments.  Our objectives here are as follows:

The param() construct in the script body will now take the following form:

param ( [Parameter(Mandatory=$true)] $ipaddress, [Parameter(Mandatory=$true)] $protocol, [Parameter(Mandatory=$true)] $port, [Parameter(ValueFromRemainingArguments=$true)] [System.Object] $objExtraneousParams = $null )

Note the addition of the [Parameter(ValueFromRemainingArguments=$true)] declaration.  This causes any extraneous parameters to be "swallowed", rather than raising a NamedParameterNotFound exception.   We now need to find a way of passing the script parameters to our main() function (once again, the name of this function is arbitrary).  Thankfully, PowerShell makes this easy, by means of the $PSBoundParameters .

# # Define all script parameters here, but don't specify data types. Also, the only # parameter attribute that should be used here is the mandatory attribute. If # used, this will cause PowerShell to prompt the user for missing value(s). # # Any extraneous parameters are caught using the ValueFromRemainingArguments declaration. # param ( [Parameter(Mandatory=$true)] $ipaddress, [Parameter(Mandatory=$true)] $protocol, [Parameter(Mandatory=$true)] $port, [Parameter(ValueFromRemainingArguments=$true)] [System.Object] $objExtraneousParams = $null ) function main() { # # Receive the parameter hash. # param ( [System.Object] $objParameterHash ) [Int32] $Local:intRc = 0; # # Map and validate parameters. # try { [ValidatePattern('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')] [String] $Local:strIPAddress = $objParameterHash['ipaddress']; [ValidateSet('tcp', 'udp')] [String] $Local:strProtocol = $objParameterHash['protocol']; [ValidateRange(0, 65535)] [Int32] $Local:intPort = $objParameterHash['port']; # # Do something useful... # Write-Host -Object ( 'IP Address : {0}' -f $strIPAddress ); Write-Host -Object ( 'Protocol : {0}' -f $strProtocol ); Write-Host -Object ( 'Port : {0:N0}' -f $intPort ); } #try catch [System.Management.Automation.ValidationMetadataException] { Write-Host -Object ( 'Bad args; exception was [{0}]' -f $_.Exception.Message ); $intRc = -1; } #catch catch [System.Management.Automation.PSInvalidCastException] { Write-Host -Object ( 'Bad data types.' ); $intRc = -2; } #catch catch [System.Exception] { Write-Host -Object ( 'Unexpected exception; its type is [{0}].' -f $_.Exception.GetType().FullName ); $intRc = -3; } #catch return $intRc; } Set-StrictMode -Version 2; [Int32] $Local:intScriptRc = 0; # # Pass all script parameters to main() in the form of a hash. # $intScriptRc = main $PSBoundParameters; Write-Host -Object ( 'Exiting with rc {0}.' -f $intScriptRc ); exit $intScriptRc;

We now have a robust mechanism for dealing with command line arguments.  Erroneous input will be caught using and the resultant will be handled in a controlled fashion.  Finally, a meaningful error will be returned to the caller.

The built-in $args array is an that contains all command line arguments passed to the script.  The array behaves like many other languages' argument handling variables, for example C's argv[n], Perl's $ARGV[n], PHP's $argv[n], Pyhton's sys.argv[n] and VBScript's WScript.Arguments.Item(n), that is, arguments are presented in a positional manner, and must be retrieved and processed by the script developer.

The $args array is an instance of a System.Array object, which is capable of holding varying data types (see section ).  We can interact with the array just like any other array, for example:

# # Retrieve number of arguments passed. # Write-Host -Object ( 'No. of arguments: {0}' -f $args.count ); # # Retrieve a specific argument. # Write-Host -Object ( 'First argument is "{0}".' -f $args[0] ); # # Step through all arguments. # [Int32] $Local:intIndex = 0; for ( $intIndex = 0; $intIndex -lt $args.count; $intIndex++ ) { Write-Host ( 'Argument #{0} is "{1}".' -f $intIndex, $args[$intIndex] ); } #for exit 0;

Clearly, the use of the $args array isn't as slick as PowerShell's param() construct.  However it does allow us to process command line arguments that have been provided in a non-PowerShell form, for example, the script below will accept command line arguments in the more traditional form of /<name>:<value>.  This is particularly useful for PowerShell scripts that will be called from outside of PowerShell.

[String] $Local:strIPAddress = ''; [String] $Local:strProtocol = ''; [Int32] $Local:intPort = 0; try { switch -regex ( $args ) { '^/ipaddress:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$' { $strIPAddress = $matches[1]; continue; } '^/protocol:(tcp|udp)$' { $strProtocol = $matches[1]; continue; } '^/port:(\d+)$' { [ValidateRange(0, 65535)] $intPort = [Convert]::ToInt32( $matches[1] ); continue; } default { throw( 'Unrecognised or invalid parameter [{0}].' -f $_ ); } } #switch # # Do something useful... # Write-Host -Object ( 'IP Address : {0}' -f $strIPAddress ); Write-Host -Object ( 'Protocol : {0}' -f $strProtocol ); Write-Host -Object ( 'Port : {0:N0}' -f $intPort ); } #try catch [System.Management.Automation.ValidationMetadataException] { Write-Host -Object ( 'ERROR : Validation error.' ); } #catch catch [System.Management.Automation.RuntimeException] { Write-Host -Object ( 'ERROR : {0}' -f $_.Exception.Message ); } #catch

The script uses the RegEx conditional statement and to validate the supplied command line arguments.   The $args array belongs to the Script and can therefore be referenced from anywhere within the script, as long as the reference includes the $Script: prefix.  This means command line arguments can be easily processed by functions and procedures, without having to pass them as parameters.  We can now bring this together, as we did for the param() construct:

function main() { [String] $Local:strIPAddress = ''; [String] $Local:strProtocol = ''; [Int32] $Local:intPort = 0; [Int32] $Local:intRc = 0; try { switch -regex ( $Script:args ) { '^/ipaddress:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$' { [$strIPAddress = $matches[1]; continue; } '^/protocol:(tcp|udp)$' { $strProtocol = $matches[1]; continue; } '^/port:(\d+)$' { [ValidateRange(0, 65535)] $intPort = [Convert]::ToInt32( $matches[1] ); continue; } default { throw( 'Unrecognised or invalid parameter [{0}].' -f $_ ); } } #switch # # Do something useful... # Write-Host -Object ( 'IP Address : {0}' -f $strIPAddress ); Write-Host -Object ( 'Protocol : {0}' -f $strProtocol ); Write-Host -Object ( 'Port : {0:N0}' -f $intPort ); } #try catch [System.Management.Automation.ValidationMetadataException] { Write-Host -Object ( 'ERROR : Validation error.' ); $intRc = -1; } #catch catch [System.Management.Automation.RuntimeException] { Write-Host -Object ( 'ERROR : {0}' -f $_.Exception.Message ); $intRc = -2; } #catch return $intRc; } Set-StrictMode -Version 2; [Int32] $Local:intScriptRc = 0; $intScriptRc = main; Write-Host -Object ( 'Exiting with rc {0}.' -f $intScriptRc ); exit $intScriptRc;

In section , we discussed how functions and procedures soon become necessary in order to hide complexity, improve readability and make code more easy to maintain.  The next logical step from here is bundling related functions and procedures into self-contained libraries, or modules.   Creating a PowerShell module is a trivial task and involves the creation of two files:

The files should be placed in a directory with the same name as the module itself and the directory needs to be a sub-directory of one of the module search path directories (see the $Env:PSModulePath ).  For example:

PS C:\Users\JohnDoe> $Env:PSModulePath; C:\Users\JohnDoe\Documents\WindowsPowerShell\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules PS C:\Users\JohnDoe> ls .\Documents\WindowsPowerShell\Modules\my_module\*.ps?1 Directory: C:\Users\JohnDoe\Documents\WindowsPowerShell\Modules\my_module Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 24/10/2015 20:31 290 my_module.psd1 -a---- 24/10/2015 20:31 87 my_module.psm1 PS C:\Users\JohnDoe>

The manifest (.PSD1) file can be created manually or by using the New-ModuleManifest cmdlet.  Either way, the resultant file simply contains a of manifest attribute names and values, for example:

@{ Author = 'John Doe'; Description = 'Meaningful description.'; CompanyName = 'Acme Corporation'; Copyright = '(c)2016 Acme Corporation. All rights reserved.'; ModuleVersion = '1.0.0.0'; PowerShellVersion = '2.0'; }

The actual module code resides in the .PSM1 file and can consist of variable definitions, aliases, functions, procedure and cmdlets.   By default, all functions, procedures and cmdlets are exposed by the module.  However, the Export-ModuleMember cmdlet can be used to limit visibilty and can also be used to expose selected aliases and variables.

The simplified example below demonstrates how a module can be used to control the visibility of code.  Once again, this makes it possible to hide complexity and promote consistent interaction with libraries of code (that is, if people cannot call your private methods, you know that you can safely modify their input, behaviour and outputs).

# # A private variable. # [String] $Local:strPrivateVar = 'Everything is quiet!'; # # A private method. # function privateMethod() { param ( [String] $text ); $text; return; } # # A public method (see Export-ModuleMember, below). # function publicMethod() { # # Call a private method and reference a private variable. # privateMethod -text ('Hello world. {0}' -f $strPrivateVar); return; } Export-ModuleMember -Function publicMethod;

If we import the module, we can see that the privateMethod function is not visible:

PS C:\Users\JohnDoe> Import-Module -Name my_module.psm1; PS C:\Users\JohnDoe> privateMethod; privateMethod : The term 'privateMethod' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. At line:1 char:1 + privateMethod; + ~~~~~~~~~~~~~ + CategoryInfo : ObjectNotFound: (privateMethod:String) [], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundException PS C:\Users\JohnDoe> publicMethod; Hello world. Everything is quiet! PS C:\Users\JohnDoe>

As we saw above, the Import-Module cmdlet allows us to load modules into the current PowerShell process.  By default, a failure to import a module generates a (see section ).  However, we can modify this behaviour and trap failures using an .  For example:

[String[]] $Local:arrRequiredModules = @( 'ActiveDirectory', 'NetNat' ); # Define the modules we require. [String] $Local:strModuleName = ''; # # Attempt to load modules. # foreach ( $strModuleName in $arrRequiredModules ) { try { Write-Host -Object ( 'Loading module "{0}"...' -f $strModuleName ); Import-Module -Name $strModuleName -ErrorAction Stop; Write-Host -Object 'Ok.'; } #try catch [System.Exception] { Write-Host -Object ( 'ERROR : Failed to load module; the error was "{0}".' -f $_.Exception.Message ); break; } #catch } #foreach