Alternative to Throwing Param Exceptions in PowerShell? - powershell

Bottom Line Up Front
I'm looking for a method to validate powershell (v1) command line parameters without propagating exceptions back to the command line.
Details
I have a powershell script that currently uses param in conjunction with [ValidateNotNullOrEmpty] to validate command line paramaters:
param(
[string]
[ValidateNotNullOrEmpty()]$domain = $(throw "Domain (-d) param required.")
)
We're changing the paradigm of error handling where we no longer want to pass exceptions back to the command line, but rather provide custom error messages. Since the param block can not be wrapped in a try catch block, i've resorted to something like the following:
param(
[string]$domain = $("")
)
Try{
if($domain -like $("")){
throw "Domain (-d) param required."
}
...
}Catch{
#output error message
}
My concern is that we're bypassing all of the built-in validation that is available with using param. Is my new technique a reasonable solution? Is there a better way to validate command line params while encapsulating exceptions within the script? I'm very much interested in see how PowerShell professionals would handle this situation.
Any advice would be appreciated.

You can write a custom validation script. Give this parameter a try.
Param(
[ValidateScript({
If ($_ -eq $Null -or $_ -eq "") {
Throw "Domain (-d) param required."
}
Else {
$True
}
})][string]$Domain
)

As I mentioned in a comment: more I read your description, more I come to the conclusion that you should not worry about "bypassing all built-in validation". Why? Because that's exactly your target. You want to bypass it's default behavior, so if that's what you need and have to do - than just do it. ;)

One way is to use default parameters like this [from msdn] -
Function CheckIfKeyExists
{
Param(
[Parameter(Mandatory=$false,ValueFromPipeline=$true)]
[String]
$Key = 'HKLM:\Software\DoesNotExist'
)
Process
{
Try
{
Get-ItemProperty -Path $Key -EA 'Stop'
}
Catch
{
write-warning "Error accessing $Key $($_.Exception.Message)"
}
}
}
So, here, if you try calling the function without passing any parameters, you will get warning what you have defined in your try/catch block. And, you are not using any default validation attributes for that. You should always assume that you will encounter an error, and write code that can survive the error. But the lesson here is if you implement a default value, remember that it is not being validated.
Read more here

Related

Writing tests for PowerShell functions

I have the following function (the function is an auxiliary function of an another function and it works correctly):
function Get-UserManager {
[CmdletBinding()]
param (
[pscredential] $credential,
[ValidateSet('ID')][string]$searchType,
[string]$searchString,
)
try {
$reply = Invoke-RestMethod -Method Get -Uri $full_uri -Credentia $cred
} catch {
Write-Verbose "Couldn't connect to the End Point"
Write-Debug "$($_.Exception)"
return $userListObject
}
$reply.elements | ForEach-Object {
return $_
}
}
I am required to write a PowerShell test for the following function (The test must include all possible outputs, because I need the code coverage to be 100%).
Can someone please help me how do I write a PowerShell test that can check all the possible outputs of this function?
The test should be like this:
$moduleRoot = Resolve-Path "$PSScriptRoot\.."
$moduleName = Split-Path $moduleRoot -Leaf
$cred = Get-Credential
Describe "Demonstarting Code Coverage of: $moduleName" {
It "Calls Function: get-UserManager" {
{Get-UserManager -credential $cred -searchType ID -searchString
'12345' -delimiter} | Should Be $userListObject
}
}
I assume you're using Pester, which is a Behaviour-Driven Development (BDD) framework. That is, it is designed to help you verify the behaviour of your code.
Ideally, you'd design the tests first according to the specification, then write the code, but as you already have the code, you'll need to think about possible ways it could be used and how you expect it to behave in each case. For example, looking at your code, what do you expect to happen if $searchString is empty or invalid credentials are passed? How can you test this actually happens?
Incidentally, code coverage is related to the execution paths in your code and just because you have 100% coverage, doesn't mean you have completely tested the code. For example, consider this basic function:
function Get-Product {
Param (
$Param1,
$Param2
)
return $Param1 * $Param2
}
A single test that calls, say, Get-Product -Param1 12 -Param2 3 will have 100% code coverage as it tests all possible paths in the code, but it doesn't tell me how my code handles, for example, $Param1 being a string (e.g. "12") or one parameter is negative, etc, so I haven't really tested it thoroughly.
Your code is currently non-functional, I assume because you reduced it to share on StackOverflow but have left some key elements out. For example $full_uri and $userListObject aren't populated but are used in the function and you have an extra comma in your param block.
That being said, you probably want to take the approach of using Mocking to simulate parts of your script so you can force different behaviour to occur and visit every path in order to get 100% code coverage. For example you need a test where the API returns an exception and enters your Catch block. That might look like this:
Describe "Demonstarting Code Coverage of: $moduleName" {
Context 'Unable to connect to the endpoint' {
Mock Invoke-RestMethod { Throw 'Endpoint unavailable' }
Mock Write-Verbose
Mock Write-Debug
It 'Should enter the catch block when the endpoint returns an error' {
Get-UserManager -Verbose -Debug
Assert-MockCalled Write-Verbose -Times 1 -Exactly
Assert-MockCalled Write-Debug -Times 1 -Exactly
}
}
}
If you're completely new to Pester, Mocking can be a tricky topic to get your head around at first. I recommend doing some learning on Pester first. I did a talk on Getting Started with Pester at PSDay last year that you might find informative.

Non-terminating Error Handling - Difference between if $Error.count and $ErrorActionPreference = 'stop'

I need to handle a non-terminating error in a powershell script. What is the most efficient way to this?
To set $ErrorActionPreference variable to stop and use try/catch
$ErrorActionPreference = 'stop'
try{
functionThatCanFail
}catch{
#Do Stuff
}
Or to clear $Error variable and then evaluate if it is populated
$Error.Clear()
functionThatCanFail
if( $Error.Count -ne 0){
#Do Stuff
}
I would add [CmdletBinding()]to your function and put it inside a try/catch.
Because of the CmdletBinding you are now able to call your function with the parameter -ErrorAction Stop.
The other suggestion with the $ErrorActionor $Error.Clear()would also work but is not a 'clean' way.
function functionThatCanFail
{
[CmdletBinding()]
param
(
$Param1
)
#Do stuff
}
try
{
functionThatCanFail -ErrorAction Stop
}
catch
{
#Error case
}
The simplest approach is to use the -ErrorVariable / -ev common parameter, which records all non-terminating errors reported by a given cmdlet in a designated variable.
Note, however, that this only works with cmdlets and advanced functions / scripts, because only they support common parameters - see Patrick's helpful answer for how to define your own function as an advanced function.
# Provoke a non-terminating error and silence it,
# but store it in custom variable $err via common parameter -ErrorVariable
Get-Item /NoSuchFile -ErrorAction SilentlyContinue -ErrorVariable err
if ($err) { # if a / a least one non-terminating error was reported, handle it.
"The following non-terminating error(s) occurred:`n$err"
}
That way, the error analysis is command-scoped, without having to record the state of or alter the state of the session-level $Error collection.
Note, however, that you cannot handle terminating errors this way.
For a comprehensive overview of PowerShell error handling, see here.

How to ignore warning errors?

I have the following PowerShell script. It picks up the NetBIOS name of computers within a given IP address. I'm using a pipe so as to dump the results into a text file. The problem is that if an IP address is not available, a warning is printed.
This is the PowerShell script:
function Get-ComputerNameByIP {
param( $IPAddress = $null )
BEGIN {
$prefBackup = $WarningPreference
$WarningPreference = 'SilentlyContinue'
}
PROCESS {
if ($IPAddress -and $_) {
throw ‘Please use either pipeline or input parameter’
break
} elseif ($IPAddress) {
([System.Net.Dns]::GetHostbyAddress($IPAddress))
}
} else {
$IPAddress = Read-Host “Please supply the IP Address”
[System.Net.Dns]::GetHostbyAddress($IPAddress)
}
}
END {
$WarningPreference = $prefBackup
}
This is the error message I wish to ignore:
WARNING: The requested name is valid, but no data of the requested type was found
You can use common parameter -WarningAction:SilentlyContinue with the command that generates warning. It's better than separately overriding $WarningPreference before executing the command and reverting it back afterwards as was suggested above - this parameter basically does that for you.
The WarningAction parameter overrides the value of the $WarningPreference variable for the current command. Because the default value of the $WarningPreference variable is Continue, warnings are displayed and execution continues unless you use the WarningAction parameter.
See more here.
You want to suppress warnings, not errors. Warnings can be silenced completely by setting the $WarningPreference variable to SilentlyContinue:
PS C:\> Write-Warning 'foo'
WARNING: foo
PS C:\> $prefBackup = $WarningPreference
PS C:\> $WarningPreference = 'SilentlyContinue'
PS C:\> Write-Warning 'foo'
PS C:\> $WarningPreference = $prefBackup
PS C:\> Write-Warning 'foo'
WARNING: foo
The setting pertains to the current scope, so if you want to suppress all warnings for your function you'd simply set the preference at the beginning of your function:
function Get-ComputerNameByIP {
param( $IPAddress = $null )
BEGIN {
$WarningPreference = 'SilentlyContinue'
}
PROCESS {
if ($IPAddress -and $_) {
throw ‘Please use either pipeline or input parameter’
break
} elseif ($IPAddress) {
[System.Net.Dns]::GetHostbyAddress($IPAddress)
}
[System.Net.Dns]::GetHostbyAddress($_)
} else {
$IPAddress = Read-Host "Please supply the IP Address"
[System.Net.Dns]::GetHostbyAddress($IPAddress)
}
}
END {}
}
If you want warnings suppressed for specific statements only, a simpler way is to redirect the warning output stream to $null:
[System.Net.Dns]::GetHostbyAddress($IPAddress) 3>$null
Warning stream redirection is only available in PowerShell v3 and newer, though.
$ErrorActionPreference = 'SilentlyContinue'
This global var controls error output of those commands that provide intermittent (non-terminating) errors and warnings. Your error is of this kind, so set preference to silently continue to suppress these warnings.
You could use a try/catch block for something like this. Consider the following example using a proper formed IP address but had no associated record.
try{
[System.Net.Dns]::GetHostbyAddress("127.0.0.56")
} Catch [System.Management.Automation.MethodInvocationException]{
Write-Host "Nothing Record Found"
}
When I tested this the error you were seeing was being caught as [System.Management.Automation.MethodInvocationException] so I checked for that specific error type. Based on it's name I'm sure there are other reasons for it to be called. It is possible to just omit that part altogether and it will catch all errors. Since you account for some other possibilities maybe you don't need it.
If that was a concern you could check the text of the $_.Exception.InnerException to see if it matches the error as well. In the above case it contains the text "The requested name is valid, but no data of the requested type was found".
This might be wrong because I am curious as to why your error is prefixed with "WARNING" where mine is not. A little more research on both our parts might be needed.
You can trap the error and force PowerShell to do nothing with it, kind of like a Try/Catch but global for the whole script:
TRAP {"" ;continue}
[System.Net.Dns]::GetHostbyAddress($IPAddress)

What is the best practice for returning error from a PowerShell cmdlet?

I am writing a PowerShell-based XML module for our application configuration needs. Following is the one of the functions.
<#
.Synopsis
To update an XML attribute value
.DESCRIPTION
In the XML file for a particular attribute, if it contains valueToFind then replace it with valueToReplace
.EXAMPLE
-------------------------------Example 1 -------------------------------------------------------------------
Update-XMLAttribute -Path "C:\web.Config" -xPath "/configuration/system.serviceModel/behaviors/serviceBehaviors/behavior/serviceMetadata" -attribute "externalMetadataLocation" -valueToFind "http:" -ValueToReplace "https:"
Look for the XPath expression with the attribute mentioned and search whether the value contains "http:". If so, change that to "https":
.EXAMPLE
-------------------------------Example 2 -------------------------------------------------------------------
Update-XMLAttribute -Path "C:\web.Config" -xPath "/configuration/system.serviceModel/behaviors/serviceBehaviors/behavior/serviceMetadata" -attribute "externalMetadataLocation" -valueToFind "http:" -ValueToReplace "https:"
Same as Example 1 except that the attribute name is passed as part of the XPath expression
#>
function Update-XMLAttribute
{
[CmdletBinding()]
[OutputType([int])]
Param
(
# Web configuration file full path
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
[string]$Path,
# XPath expression up to the parent node
[string] $xPath,
# This parameter is optional if you mentioned it in xPath itself
[string] $attribute,
[string] $valueToFind,
[string] $ValueToReplace
)
Try
{
If (Test-path -Path $Path)
{
$xml = New-Object XML
$xml.Load($Path)
# If the xPath expression itself contains an attribute name then the value of attribute will be processed and taken
If ($xPath.Contains("#")) {
$xPath, $attribute = $xPath -split '/#', 2
}
# Getting the node value using xPath
$Items = Select-Xml -XML $xml -XPath $xPath
ForEach ($Item in $Items)
{
$attributeValue = $Item.node.$attribute
Write-Verbose "Attribute value is $attributeValue "
if ($attributeValue.contains($valueToFind)) {
Write-Verbose "In the attribute $attributeValue - $valueToFind is to be repalced with $ValueToReplace"
$Item.node.$attribute = $attributeValue.replace($valueToFind, $ValueToReplace)
}
}
$xml.Save($Path)
Write-Verbose " Update-XMLAttribute is completed successfully"
}
Else {
Write-Error " The $path is not present"
}
}
Catch {
Write-Error "$_.Exception.Message"
Write-Error "$_.Exception.ItemName"
Write-Verbose " Update-XMLAttribute is failed"
}
} # End Function Update-XMLAttribute
As this cmdlet will be consumed by many I don't think simply writing into console will be the right approach.
As of now in my script if no errors, I can assume that mine is successfully completed.
What is the standard practice to get the results from a PowerShell cmdlet so that the consumer knows whether it is successfully completed or not?
The standard practice is to throw exceptions. Each different type of error has a separate exception type which can be used to diagnose further.
Say, file is not represented, you do this:
if (-not (Test-Path $file))
{
throw [System.IO.FileNotFoundException] "$file not found."
}
Your cmdlet should document all the possible exceptions it will throw, and when.
Your function should throw if it runs into an error. Leave it to the caller to decide how the error should be treated (ignore, log a message, terminate, whatever).
While you can throw an exception, which PowerShell will catch and wrap in an ErrorRecord, you have more flexibility using the ThrowTerminatingError method. This is the typical approach for a C# based cmdlet.
ThrowTerminatingError(new ErrorRecord(_exception, _exception.GetType().Name, ErrorCategory.NotSpecified, null));
This allows you to pick an error category and provide the target object. BTW what you have above isn't what I'd call a cmdlet. Cmdlets are compiled C# (typically). What you have is an advanced function. :-)
From an advanced function you can access this method like so:
$pscmdlet.ThrowTerminatingError(...)

Debugging PowerShell

I'm not certain what is wrong with this scriptlet.
I'm trying to break out functionality into several other functions (I have a programming background not a scripting one per se) and to me LOGICALLY the following should execute starting at the "main" function Test-SgnedMpsPackage, accepting the various optional parameters (the script is not yet complete) then when the function Check-Path is called, that is run, then work would resume in the original calling function.
Am I missing something here?
On a side note, how does one return a value to the calling function? a simple return?
function CheckPath($path)
{
if ( test-path -Path $path )
{ Write-Host "{0} confirmed to exist." -f $path }
else
{ Write-Host "{0} DOES NOT exis.\nPlease check and run the script again" -f $path }
exit { exit }
}
function Test-SignedMpsPackage
{
Param(
[string] $PkgSource,
[string] $SigSource,
[string] $Destination
)
Process
{
#Check that both files exist
Write-Host "Check for file existence..."
CheckPath($PkgSource)
CheckPath($SigSource)
#retrieve signatures from file
}
}
Unlike C, C++ or C# there is no "main" entry point function. Any script at the top level - outside of a function - executes. You have defined two functions above but you haven't called either one. You need to do something like this:
function Test-SignedMpsPackage
{
...
}
Test-SignedMpsPackage params
Also as mentioned by #Bill_Stewart, you call your defined functions just like you call PowerShell commands - arguments are space separated and you don't use parens except to evaluate an expression inside the parens.
As for returning a value from a function, any output (Output stream) not captured by assigning to a variable or being redirected to a file is automatically part of the function's output. So I would modify your CheckPath function to this:
function CheckPath($path)
{
if (Test-Path -Path $path) {
Write-Verbose "{0} confirmed to exist." -f $path
$true
}
else {
Write-Verbose "{0} DOES NOT exist.\nPlease check and run the script again" -f $path
$false
}
}
You can use Write-Host as you had before but sometimes, perhaps in a script, you don't want to see the extra output. That is where Write-Verbose comes in handy. Set $VerbosePreference = 'Continue' to see the verbose output.