Change value of a param in the midst of a function - function

Trying to create a PowerShell function that will output a single-line of text using multiple sets of fore and back colors. I have a switch that defines the color sets.
The function has one param that defines the switch value and another param that, if I can get this working, defines the next color set using the same switch:
function Write-Custom
{
param($Say,$ThenSay,$Level,$ExtraLevel)
switch([array]$level)
{
none {$c = 'Black','White'}
name {$c = 'Cyan','DarkBlue'}
good {$c = 'White','DarkGreen'}
note {$c = 'Gray','White'}
info {$c = 'White','DarkGray'}
warn {$c = 'Yellow','Black'}
fail {$c = 'Black','Red'}
}
$s = " $Say"
$ts = " $ThenSay "
Write-Host $s -ForegroundColor $c[0] -BackgroundColor $c[1] -NoNewLine
Clear-Variable Level
$Level = $ExtraLevel
Write-Host $ts -ForegroundColor $c[0] -BackgroundColor $c[1]
}
Write-Custom -Say 'hi there' -Level 'name' -ThenSay 'stranger ' -ExtraLevel 'warn'
Can't seem to clear and re-define the $level variable. Seems the output ' hi there ' should have a foreground/background of cyan/darkblue, with the ' stranger ' part being yellow/black....but the whole string comes out cyan/darkblue.
Do I need to create a more elaborate switch?

You need to invoke the switch each time to get a different color set. One way to do this is to put a function inside your function e.g.:
function Write-Custom
{
param($Say,$ThenSay,$Level,$ExtraLevel)
function GetColors([string]$level)
{
switch([array]$level)
{
none {'Black','White'}
name {'Cyan','DarkBlue'}
good {'White','DarkGreen'}
note {'Gray','White'}
info {'White','DarkGray'}
warn {'Yellow','Black'}
fail {'Black','Red'}
default { throw "Unrecognized level $level" }
}
}
$c = GetColors($Level)
Write-Host " $Say" -ForegroundColor $c[0] -BackgroundColor $c[1]
$c = GetColors($ExtraLevel)
Write-Host " $ThenSay " -ForegroundColor $c[0] -BackgroundColor $c[1]
}

Related

With PowerShell and ReportingService2010, how to change empty shared datasource or custom datasource to shared datasource?

I'm quite in an impasse here. I started writing a script with PowerShell to handle setting Shared Datasources to all the reports of a folder.
My logic :
For each report in the folder and For each datasource of the report :
If the datasource name matches with the one to be replace
1 : If its reference is already set up : replace it with the shared one
2 : If its a Custom Datasource : replace it with the shared one
3 : If its reference is empty : replace it with the shared one
Case 1 works perfectly, while Case 2 and Case 3 fail. I don't understand why for the first case it accept a single element while for the other, it fails telling me that it needs an array :
Impossible de convertir l'argument «DataSources» (valeur «SSRS.ReportingService2010.DataSource») de «SetItemDataSources
» en type «SSRS.ReportingService2010.DataSource[]»: «Impossible de convertir la valeur «
SSRS.ReportingService2010.DataSource» du type «SSRS.ReportingService2010.DataSource» en type «
SSRS.ReportingService2010.DataSource[]».»
Au caractère Ligne:87 : 1
+ $proxy.SetItemDataSources($ssrsItem.Path, $newDataSource);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument
Here's the entire code I came up with (its verbose but I think it helps readability) :
# Usage : SetConnection "msbireports" "/2134_DELIVRI/Test/PS1" "EDUCFI_SSAS" "/2134_DELIVRI/Test/PS1/Datasources/EDUCFI_SSAS.rds" true
function SetConnection(
$reportServerName = $(throw "reportServerName is required."),
$serverPath = $(throw "serverPath is required."),
$connectionName = $(throw "connectionName is required."),
$toConnection = $(throw "toConnection is required."),
$replaceCustom = $(throw "replaceCustom is required.")
)
{
Write-Host "> Connecting to $reportServerName" -ForegroundColor Yellow
$reportServerUri = "https://{0}/ReportServer/ReportService2010.asmx" -f $reportServerName
$proxy = New-WebServiceProxy -Uri $reportServerUri -Namespace SSRS.ReportingService2010 -UseDefaultCredential
Write-Host " Done !" -ForegroundColor Yellow
Write-Host ""
# List everything on the Report Server, not recursively, but filter to keep Reports
Write-Host "> Getting Items from $serverPath"
$items = $proxy.ListChildren($serverPath, $false) | Where-Object {$_.TypeName -eq "Report"}
Write-Host "Found $($items.Length) reports..."
Write-Host ""
# Loop through reports and data sources
Foreach($ssrsItem in $items)
{
# Handling report.rdl...
Write-Host "> Handling $($ssrsItem.Path).rdl"
# Get Datasources having the wanted name
$dsItems = $proxy.GetItemDataSources($ssrsItem.Path)
$dsTarget = $proxy.GetItemDataSources($ssrsItem.Path) | Where-Object {$_.Name -eq $connectionName}
if ($dsTarget.Length -eq 0)
{
Write-Host ">> Skipped : no DataSource named $connectionName" -ForegroundColor Gray
Write-Host ""
}
else
{
Foreach($dsItem in $dsItems)
{
if ($dsItem.Name -eq $connectionName)
{
# Check if the object has a reference or not (i.e. : its filled shared datasource or not : custom dataset OR empty filled shared datasource)
if($dsItem.Item.PSObject.Properties.Match('Reference').Count)
{
# Does the shared Datasource already points to the good one ?
if ($dsItem.Item.Reference -eq $toConnection)
{
Write-Host ">> Nothing done : $connectionName already set up as $toConnection" -ForegroundColor Gray
Write-Host ""
}
else
{
# Shared Datasource with an already filled Shared Datasource which points diffrently : change it.
Write-Host ">> Changing $connectionName from $($dsItem.Item.Reference) to $toConnection..." -ForegroundColor Gray
$dsItem.Item.Reference = $toConnection;
$proxy.SetItemDataSources($ssrsItem.Path, $dsItem);
Write-Host " Done!" -ForegroundColor Green
Write-Host ""
}
}
else
{
# Two cases here : 1. Custom Datasource or 2. SharedDatasource but empty or
# Check for Custom Datasource (has a property called Enabled which ShareDatasource has not)
if($dsItem.Item.PSObject.Properties.Match('Enabled').Count)
{
if ($replaceCustom -eq "replace")
{
Write-Host ">> Changing Custom Connection $connectionName to a Shared one pointing to $toConnection..." -ForegroundColor Gray
$newDataSource = New-Object("SSRS.ReportingService2010.DataSource")
$newDataSource.Name = $connectionName
$newDataSource.Item = New-Object ("SSRS.ReportingService2010.DataSourceReference")
$newDataSource.Item.Reference = $toConnection
$proxy.SetItemDataSources($ssrsItem.Path, $newDataSource);
Write-Host " DOES NOT WORK !" -ForegroundColor RED
Write-Host ""
}
else
{
Write-Host ">> Nothing done : $connectionName set up as a Custom Connection, but was told not to change it" -ForegroundColor Gray
Write-Host ""
}
}
else
{
Write-Host ">> OF TYPE SHARED... But not really ???"
$newDataSource = New-Object("SSRS.ReportingService2010.DataSource")
$newDataSource.Name = $connectionName
$newDataSource.Item = New-Object ("SSRS.ReportingService2010.DataSourceReference")
$newDataSource.Item.Reference = $toConnection
$proxy.SetItemDataSources($ssrsItem.Path, $newDataSource);
Write-Host " DOES NOT WORK !" -ForegroundColor RED
Write-Host ""
}
}
}
}
}
}
}
SetConnection "msbireports" "/2134_DELIVRI/Test/PS1" "EDUCFI_SSAS" "/1446_EDUCFI/Datasources/EDUCFI_SSAS" "replace"
This is driving me nuts. I also tried to do this differently, that is building an array of DataSources and at the end of the report's handling pushing it to with SetItemDataSources but everytime, those two cases failed, with a weird error, telling me it can't convert a DataSource to.. a DataSource. (?!)
My sanity begs for your help !
Well, after playing a bit with the code, I changed my method and built an array containing already valid datasources and new ones and changing them at the very end. This should be better but still, I still have this strange casting error...
Here's the new code :
function SetConnection(
$reportServerName = $(throw "reportServerName is required."),
$serverPath = $(throw "serverPath is required."),
$connectionName = $(throw "connectionName is required."),
$toConnection = $(throw "toConnection is required."),
$replaceCustom = $(throw "replaceCustom is required.")
)
{
Write-Host "> Connecting to $reportServerName" -ForegroundColor Yellow
$reportServerUri = "https://{0}/ReportServer/ReportService2010.asmx" -f $reportServerName
$proxy = New-WebServiceProxy -Uri $reportServerUri -Namespace SSRS.ReportingService2010 -UseDefaultCredential
Write-Host " Done !" -ForegroundColor Yellow
Write-Host ""
# List everything on the Report Server, not recursively, but filter to keep Reports
Write-Host "> Getting Items from $serverPath"
$items = $proxy.ListChildren($serverPath, $false) | Where-Object {$_.TypeName -eq "Report"}
Write-Host "Found $($items.Length) reports..."
Write-Host ""
# Loop through reports and data sources
Foreach($ssrsItem in $items)
{
# Handling report.rdl...
Write-Host "> Handling $($ssrsItem.Path).rdl"
# Get Datasources having the wanted name
$dsItems = $proxy.GetItemDataSources($ssrsItem.Path)
$dsUpdated = #()
$dsTarget = $proxy.GetItemDataSources($ssrsItem.Path) | Where-Object {$_.Name -eq $connectionName}
if ($dsTarget.Length -eq 0)
{
Write-Host ">> Skipped : no DataSource named $connectionName" -ForegroundColor Gray
Write-Host ""
}
else
{
Foreach($dsItem in $dsItems)
{
if ($dsItem.Name -ne $connectionName)
{
$dsUpdated += $dsItem;
}
else
{
# Check if the object has a reference or not (i.e. : its filled shared datasource or not : custom dataset OR empty filled shared datasource)
if($dsItem.Item.PSObject.Properties.Match('Reference').Count)
{
# Does the shared Datasource already points to the good one ?
if ($dsItem.Item.Reference -eq $toConnection)
{
Write-Host ">> Nothing done : $connectionName already set up as $toConnection" -ForegroundColor Gray
Write-Host ""
$dsUpdated += $dsItem;
}
else
{
# Shared Datasource with an already filled Shared Datasource which points diffrently : change it.
Write-Host ">> Changing $connectionName from $($dsItem.Item.Reference) to $toConnection..." -ForegroundColor Gray
Write-Host ""
$dsItem.Item.Reference = $toConnection;
$dsUpdated += $dsItem;
}
}
else
{
# Two cases here : 1. Custom Datasource or 2. SharedDatasource but empty or
# Check for Custom Datasource (has a property called Enabled which ShareDatasource has not)
if($dsItem.Item.PSObject.Properties.Match('Enabled').Count)
{
if ($replaceCustom -eq "replace")
{
Write-Host ">> Changing Custom Connection $connectionName to a Shared one pointing to $toConnection..." -ForegroundColor Gray
Write-Host ""
$newDataSource = New-Object("SSRS.ReportingService2010.DataSource")
$newDataSource.Name = $connectionName
$newDataSource.Item = New-Object ("SSRS.ReportingService2010.DataSourceReference")
$newDataSource.Item.Reference = $toConnection
$dsUpdated += $newDataSource;
}
else
{
Write-Host ">> Nothing done : $connectionName set up as a Custom Connection, but was told not to change it" -ForegroundColor Gray
Write-Host ""
$dsUpdated += $dsItem;
}
}
else
{
Write-Host ">> OF TYPE SHARED... But not really ???"
Write-Host ""
$newDataSource = New-Object("SSRS.ReportingService2010.DataSource")
$newDataSource.Name = $connectionName
$newDataSource.Item = New-Object ("SSRS.ReportingService2010.DataSourceReference")
$newDataSource.Item.Reference = $toConnection
$dsUpdated += $newDataSource;
}
}
}
}
$proxy.SetItemDataSources($ssrsItem.Path, $dsUpdated);
}
}
}

Cant use variable from a function in another function

function User-Search($input)
{
Write-Host "Searching for user: $input"
pause
}
function Show-Menu
{
param (
[string]$Title = 'MainMenu'
)
cls
Write-Host "================ $Title ================"
Write-Host " "
Write-Host "Specify computer / username"
Write-Host " "
Write-Host "Q: Press 'Q' to quit."
Write-Host " "
}
do
{
Show-Menu
$input = Read-Host "Search"
User-Search -input $input
}
until ($input -eq 'q')
Outputs: "Searching for user:", it's empty.
There must be some small mistake i am doing, probably easy for you guys :)
$INPUT is an automatic variable:
Contains an enumerator that enumerates all input that is passed to a
function. The $input variable is available only to functions and
script blocks (which are unnamed functions).
So just use another variable, e. g. $user instead of $input

What am I doing wrong on PowerShell function parameters?

I would really appreciate it if somebody could point out what I am doing wrong in passing parameters from a function back to the mainline code. I have a variable which has been successfully extracted in a function, but I cannot seem to pass that back to the mainline code
This is the code I am using:
function get-field ($field, $heading) {
$fieldPos = $script:source.AllElements.InnerText.IndexOf($heading) +1
$field = $script:source.AllElements.InnerText[$fieldPos]
# If states "Not Available", or contains a heading, process as if not found.
if ($field -eq "Not Available ") {$fieldPos = 0}
if ($field -eq $heading) {$fieldPos = 0}
# Check that a valid entry was received
if ($fieldPos -eq 0) {
Write-Host "Warning:" $heading "was not found"
} else {
$field = $field.Trim()
}
return $field
}
get-field $email "Name"
get-field $address "Address"
I have verified that within the function, the $field and $heading parameters contain the correct information, so why aren't the $email and $address fields being populated?
You're not doing it totally wrong.
Have a look at this example:
function get-field ($field, $heading) {
return "$field - $heading"
}
$address = get-field "AddressFiled" "AddressHeading"
$address
to catch the returned value in a variable for further use, you should call the function like in the above example.
Parameters in PowerShell are normally used for passing values into a function. The output of a function must be assigned to a variable in the statement that invokes the function. Also, it's bad design to use global variables inside a function, because that makes debugging significantly more difficult.
Your code should look somewhat like this:
function Get-Field ($data, $heading) {
$fieldPos = $data.IndexOf($heading) + 1
$field = $data[$fieldPos].Trim()
# If states "Not Available", or contains a heading, process as if not found.
if ($field -eq 'Not Available' -or $field -eq $heading) {
Write-Host "Warning: ${heading} was not found"
}
$field
}
$email = Get-Field $script:source.AllElements.InnerText 'Name'
$address = Get-Field $script:source.AllElements.InnerText 'Address'
You can have out parameters if you want to, but they're rather uncommon in PowerShell, probably because they're not as straight-forward to use as one would like.
function Get-Field ([ref]$field, $data, $heading) {
$fieldPos = $data.IndexOf($heading) + 1
$field.Value = $data[$fieldPos].Trim()
# If states "Not Available", or contains a heading, process as if not found.
if ($field -eq 'Not Available' -or $field -eq $heading) {
Write-Host "Warning: ${heading} was not found"
}
}
$email = $null
Get-Field ([ref]$email) $script:source.AllElements.InnerText 'Name'
$address = $null
Get-Field ([ref]$address) $script:source.AllElements.InnerText 'Address'

PowerShell adds other values to return value of function

It seems that PowerShell adds an additional variable to the return value of a function.
The function subfoo2 itself delivers the correct values, but as soon as PowerShell jumps back to the postion where I called the function (in foo1), value contains the value of an other variable ($msg)
(Have a look at the comments in the code)
writeMessageLog($msg){
...
Add-Content $msg
...
}
subfoo2{
writeMessageLog($msg)
return $UserArrayWithValues #During Debug, $Array is fine (1)
}
foo1{
$var = subfoo2 $UserArray # $var has now the value of $msg and $UserArrayWithValues (2)
#do something with var
}
Realcode:
function WriteLog
{
param ( [string] $severity , $msgNumber, [string] $msg )
...
$msgOut = $date + ... + $msg
Add-Content $msgout ( $msgOut )
...
}
function getFeatures
{
writelog 'I' 1002 $true $true "Load Features"
$Features = importCsv -pPath $FeatureDefintionFilePath
Writelog 'I' 1000 $true $true "Features Loaded"
return $Features # $Features has value as expected (1)
}
function GetUserFeatures ($pUserObject)
{
$SfBFeatures = ""
$SfBFeatures = getFeatures #SfBFeaures has Value of $msg and $Features (2)
...
}
Do I use the functions/return values wrong? What could lead to such behavior? Is it an issue if i call a function within a function?
If I remove $msgOut = $date + ... + $msg in writeMessageLog, the values are fine.
I'm pretty lost right now, and have no ideas where this comes from. Any ideas welcome.
This is how powershell works, basically everything that you print out will be returned as the function output. So don't output extra stuff. To force something to not output stuff you can do:
$null = some-command_that_outputs_unwanted_things
since everybody is obsessed with Out-Null I'll add this link showing several other ways to do that.
Within a function, everything you don't assign or pipe to a consuming cmdlet will get put to the pipeline and returned from the function - even if you don't explicit return it. In fact the return keyword doesn't do anything in PowerShell so the following is equivalent:
function Test-Func
{
"Hello World"
}
function Test-Func
{
return "Hello World"
}
So it looks like your writeMessageLog puts anything on the pipeline thus you have to either assign the value to anything:
$notUsed = writeMessageLog($msg)
or (prefered) pipe it to the Out-Null cmdlet:
writeMessageLog($msg) | Out-Null

Can I get detailed exception stacktrace in PowerShell?

Runing such script:
1: function foo()
2: {
3: bar
4: }
5:
6: function bar()
7: {
8: throw "test"
9: }
10:
11: foo
I see
test
At C:\test.ps1:8 char:10
Can I get a detailed stack trace instead?
At bar() in C:\test.ps1:8
At foo() in C:\test.ps1:3
At C:\test.ps1:11
There is a function up on the PowerShell Team blog called Resolve-Error which will get you all kinds of details
Note that $error is an array of all the errors you have encountered in your PSSession. This function will give you details on the last error you encountered.
function Resolve-Error ($ErrorRecord=$Error[0])
{
$ErrorRecord | Format-List * -Force
$ErrorRecord.InvocationInfo |Format-List *
$Exception = $ErrorRecord.Exception
for ($i = 0; $Exception; $i++, ($Exception = $Exception.InnerException))
{ "$i" * 80
$Exception |Format-List * -Force
}
}
There is the automatic variable $StackTrace but it seems to be a little more specific to internal PS details than actually caring about your script, so that won't be of much help.
There is also Get-PSCallStack but that's gone as soon as you hit the exception, unfortunately. You could, however, put a Get-PSCallStack before every throw in your script. That way you get a stack trace immediately before hitting an exception.
I think one could script such functionality by using the debugging and tracing features of Powershell but I doubt it'd be easy.
Powershell 3.0 adds a ScriptStackTrace property to the ErrorRecord object. I use this function for error reporting:
function Write-Callstack([System.Management.Automation.ErrorRecord]$ErrorRecord=$null, [int]$Skip=1)
{
Write-Host # blank line
if ($ErrorRecord)
{
Write-Host -ForegroundColor Red "$ErrorRecord $($ErrorRecord.InvocationInfo.PositionMessage)"
if ($ErrorRecord.Exception)
{
Write-Host -ForegroundColor Red $ErrorRecord.Exception
}
if ((Get-Member -InputObject $ErrorRecord -Name ScriptStackTrace) -ne $null)
{
#PS 3.0 has a stack trace on the ErrorRecord; if we have it, use it & skip the manual stack trace below
Write-Host -ForegroundColor Red $ErrorRecord.ScriptStackTrace
return
}
}
Get-PSCallStack | Select -Skip $Skip | % {
Write-Host -ForegroundColor Yellow -NoNewLine "! "
Write-Host -ForegroundColor Red $_.Command $_.Location $(if ($_.Arguments.Length -le 80) { $_.Arguments })
}
}
The Skip parameter lets me leave Write-Callstack or any number of error-handling stack frames out of the Get-PSCallstack listing.
Note that if called from a catch block, Get-PSCallstack will miss any frames between the throw site and the catch block. Hence I prefer the PS 3.0 method even though we have fewer details per frame.
You can not get a stack trace from exceptions of the PowerShell code of scripts, only from .NET objects. To do that, you will need to get the Exception object like one of these:
$Error[0].Exception.StackTrace
$Error[0].Exception.InnerException.StackTrace
$Error[0].StackTrace
I took what I found here as inspiration and created a nice function anyone can drop into their code.
This is how I call it:
Write-Host "Failed to write to the log file `n$(Resolve-Error)" -ForegroundColor Red
Function Resolve-Error
{
<#
.SYNOPSIS
Enumerate error record details.
.DESCRIPTION
Enumerate an error record, or a collection of error record, properties. By default, the details
for the last error will be enumerated.
.PARAMETER ErrorRecord
The error record to resolve. The default error record is the lastest one: $global:Error[0].
This parameter will also accept an array of error records.
.PARAMETER Property
The list of properties to display from the error record. Use "*" to display all properties.
Default list of error properties is: Message, FullyQualifiedErrorId, ScriptStackTrace, PositionMessage, InnerException
Below is a list of all of the possible available properties on the error record:
Error Record: Error Invocation: Error Exception: Error Inner Exception(s):
$_ $_.InvocationInfo $_.Exception $_.Exception.InnerException
------------- ----------------- ---------------- ---------------------------
writeErrorStream MyCommand ErrorRecord Data
PSMessageDetails BoundParameters ItemName HelpLink
Exception UnboundArguments SessionStateCategory HResult
TargetObject ScriptLineNumber StackTrace InnerException
CategoryInfo OffsetInLine WasThrownFromThrowStatement Message
FullyQualifiedErrorId HistoryId Message Source
ErrorDetails ScriptName Data StackTrace
InvocationInfo Line InnerException TargetSite
ScriptStackTrace PositionMessage TargetSite
PipelineIterationInfo PSScriptRoot HelpLink
PSCommandPath Source
InvocationName HResult
PipelineLength
PipelinePosition
ExpectingInput
CommandOrigin
DisplayScriptPosition
.PARAMETER GetErrorRecord
Get error record details as represented by $_
Default is to display details. To skip details, specify -GetErrorRecord:$false
.PARAMETER GetErrorInvocation
Get error record invocation information as represented by $_.InvocationInfo
Default is to display details. To skip details, specify -GetErrorInvocation:$false
.PARAMETER GetErrorException
Get error record exception details as represented by $_.Exception
Default is to display details. To skip details, specify -GetErrorException:$false
.PARAMETER GetErrorInnerException
Get error record inner exception details as represented by $_.Exception.InnerException.
Will retrieve all inner exceptions if there is more then one.
Default is to display details. To skip details, specify -GetErrorInnerException:$false
.EXAMPLE
Resolve-Error
Get the default error details for the last error
.EXAMPLE
Resolve-Error -ErrorRecord $global:Error[0,1]
Get the default error details for the last two errors
.EXAMPLE
Resolve-Error -Property *
Get all of the error details for the last error
.EXAMPLE
Resolve-Error -Property InnerException
Get the "InnerException" for the last error
.EXAMPLE
Resolve-Error -GetErrorInvocation:$false
Get the default error details for the last error but exclude the error invocation information
.NOTES
.LINK
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$false, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[ValidateNotNullorEmpty()]
[array]$ErrorRecord,
[Parameter(Mandatory=$false, Position=1)]
[ValidateNotNullorEmpty()]
[string[]]$Property = ('Message','InnerException','FullyQualifiedErrorId','ScriptStackTrace','PositionMessage'),
[Parameter(Mandatory=$false, Position=2)]
[switch]$GetErrorRecord = $true,
[Parameter(Mandatory=$false, Position=3)]
[switch]$GetErrorInvocation = $true,
[Parameter(Mandatory=$false, Position=4)]
[switch]$GetErrorException = $true,
[Parameter(Mandatory=$false, Position=5)]
[switch]$GetErrorInnerException = $true
)
Begin
{
## If function was called without specifying an error record, then choose the latest error that occured
If (-not $ErrorRecord)
{
If ($global:Error.Count -eq 0)
{
# The `$Error collection is empty
Return
}
Else
{
[array]$ErrorRecord = $global:Error[0]
}
}
## Define script block for selecting and filtering the properties on the error object
[scriptblock]$SelectProperty = {
Param
(
[Parameter(Mandatory=$true)]
[ValidateNotNullorEmpty()]
$InputObject,
[Parameter(Mandatory=$true)]
[ValidateNotNullorEmpty()]
[string[]]$Property
)
[string[]]$ObjectProperty = $InputObject | Get-Member -MemberType *Property | Select-Object -ExpandProperty Name
ForEach ($Prop in $Property)
{
If ($Prop -eq '*')
{
[string[]]$PropertySelection = $ObjectProperty
Break
}
ElseIf ($ObjectProperty -contains $Prop)
{
[string[]]$PropertySelection += $Prop
}
}
Write-Output $PropertySelection
}
# Initialize variables to avoid error if 'Set-StrictMode' is set
$LogErrorRecordMsg = $null
$LogErrorInvocationMsg = $null
$LogErrorExceptionMsg = $null
$LogErrorMessageTmp = $null
$LogInnerMessage = $null
}
Process
{
ForEach ($ErrRecord in $ErrorRecord)
{
## Capture Error Record
If ($GetErrorRecord)
{
[string[]]$SelectedProperties = &$SelectProperty -InputObject $ErrRecord -Property $Property
$LogErrorRecordMsg = $ErrRecord | Select-Object -Property $SelectedProperties
}
## Error Invocation Information
If ($GetErrorInvocation)
{
If ($ErrRecord.InvocationInfo)
{
[string[]]$SelectedProperties = &$SelectProperty -InputObject $ErrRecord.InvocationInfo -Property $Property
$LogErrorInvocationMsg = $ErrRecord.InvocationInfo | Select-Object -Property $SelectedProperties
}
}
## Capture Error Exception
If ($GetErrorException)
{
If ($ErrRecord.Exception)
{
[string[]]$SelectedProperties = &$SelectProperty -InputObject $ErrRecord.Exception -Property $Property
$LogErrorExceptionMsg = $ErrRecord.Exception | Select-Object -Property $SelectedProperties
}
}
## Display properties in the correct order
If ($Property -eq '*')
{
# If all properties were chosen for display, then arrange them in the order
# the error object displays them by default.
If ($LogErrorRecordMsg) {[array]$LogErrorMessageTmp += $LogErrorRecordMsg }
If ($LogErrorInvocationMsg) {[array]$LogErrorMessageTmp += $LogErrorInvocationMsg}
If ($LogErrorExceptionMsg) {[array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
}
Else
{
# Display selected properties in our custom order
If ($LogErrorExceptionMsg) {[array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
If ($LogErrorRecordMsg) {[array]$LogErrorMessageTmp += $LogErrorRecordMsg }
If ($LogErrorInvocationMsg) {[array]$LogErrorMessageTmp += $LogErrorInvocationMsg}
}
If ($LogErrorMessageTmp)
{
$LogErrorMessage = 'Error Record:'
$LogErrorMessage += "`n-------------"
$LogErrorMsg = $LogErrorMessageTmp | Format-List | Out-String
$LogErrorMessage += $LogErrorMsg
}
## Capture Error Inner Exception(s)
If ($GetErrorInnerException)
{
If ($ErrRecord.Exception -and $ErrRecord.Exception.InnerException)
{
$LogInnerMessage = 'Error Inner Exception(s):'
$LogInnerMessage += "`n-------------------------"
$ErrorInnerException = $ErrRecord.Exception.InnerException
$Count = 0
While ($ErrorInnerException)
{
$InnerExceptionSeperator = '~' * 40
[string[]]$SelectedProperties = &$SelectProperty -InputObject $ErrorInnerException -Property $Property
$LogErrorInnerExceptionMsg = $ErrorInnerException | Select-Object -Property $SelectedProperties | Format-List | Out-String
If ($Count -gt 0)
{
$LogInnerMessage += $InnerExceptionSeperator
}
$LogInnerMessage += $LogErrorInnerExceptionMsg
$Count++
$ErrorInnerException = $ErrorInnerException.InnerException
}
}
}
If ($LogErrorMessage) { $Output += $LogErrorMessage }
If ($LogInnerMessage) { $Output += $LogInnerMessage }
Write-Output $Output
If (Test-Path -Path 'variable:Output' ) { Clear-Variable -Name Output }
If (Test-Path -Path 'variable:LogErrorMessage' ) { Clear-Variable -Name LogErrorMessage }
If (Test-Path -Path 'variable:LogInnerMessage' ) { Clear-Variable -Name LogInnerMessage }
If (Test-Path -Path 'variable:LogErrorMessageTmp') { Clear-Variable -Name LogErrorMessageTmp }
}
}
End {}
}
This code:
try {
...
}
catch {
Write-Host $_.Exception.Message -Foreground "Red"
Write-Host $_.ScriptStackTrace -Foreground "DarkGray"
exit 1
}
Will echo an error in a following format:
No match was found for the specified search criteria and module names 'psake'.
at Get-InstalledModule<Process>, ...\PSModule.psm1: line 9251
at Import-ModuleThirdparty, ...\Import-ModuleThirdparty.psm1: line 3
at <ScriptBlock>, ...\index.ps1: line 13
Here's a way: Tracing the script stack
The core of it is this code:
1..100 | %{ $inv = &{ gv -sc $_ myinvocation }
I just figured it out. The $_ is the exception caught in the catch block.
$errorString= $_ | Out-String
You can also change the default formatting for the error object to include the stack trace. Basically, make your format file by copying the chunk for System.Management.Automation.ErrorRecord from $PSHOME\PowerShellCore.format.ps1xml and add your own
element that adds the trace. Then load it with Update-FormatData. For more details, I've just written a blog post about it: https://blogs.msdn.microsoft.com/sergey_babkins_blog/2016/12/28/getting-a-stack-trace-in-powershell/
Oh, one more thing: this doesn't propagate automatically into the remote sessions. The objects get formatted into strings on the remote side. For stack traces in the remote sessions you'll have to upload this file there and call Update-FormatData there again.
Stumbled upon this looking for a built in solution. I am going with simple solution. Just add trace block before using any powershell. This will ensure a call stack is shown. Down side of this is the stack will be displayed before the error message.
Trace {
$_.ScriptStackTrace
}
Maybe I've misunderstood something, but my issue here is that I've not been seeing powershell script stack traces for inner exceptions.
In the end I ended up searching $Global:Error for an exception's Error Eecord object to retrieve the script stack trace.
<#
.SYNOPSIS
Expands all inner exceptions and provides Script Stack Traces where available by mapping Exceptions to ErrorRecords
.NOTES
Aggregate exceptions aren't full supported, and their child exceptions won't be traversed like regular inner exceptions
#>
function Get-InnerErrors ([Parameter(ValueFrompipeline)] $ErrorObject=$Global:Error[0])
{
# Get the first exception object from the input error
$ex = $null
if( $ErrorObject -is [System.Management.Automation.ErrorRecord] ){
$ex = $ErrorObject.Exception
}
elseif( $ErrorObject -is [System.Exception] ){
$ex = $ErrorObject
}
else
{
throw "Unexpected error type for Get-InnerErrors: $($ErrorObject.GetType()):`n $ErrorObject"
}
Write-Debug "Walking inner exceptions from exception"
for ($i = 0; $ex; $i++, ($ex = $ex.InnerException))
{
$ErrorRecord = $null
if( $ex -is [System.Management.Automation.IContainsErrorRecord] ){
Write-Debug "Inner Exception $i : Skipping search for ErrorRecord in `$Global:Error, exception type implements IContainsErrorRecord"
$ErrorRecord = ([System.Management.Automation.IContainsErrorRecord]$ex).ErrorRecord
}
else {
# Find ErrorRecord for exception by mapping exception to ErrorRecrods in $Global:Error
ForEach( $err in $Global:Error ){# Need to use Global scope when referring from a module
if( $err -is [System.Management.Automation.ErrorRecord] ){
if( $err.Exception -eq $ex ){
$ErrorRecord = $err
Write-Debug "Inner Exception $i : Found ErrorRecord in `$Global:Error"
break
}
}
elseif( $err -is [System.Management.Automation.IContainsErrorRecord] ) {
if( $err -eq $ex -or $err.ErrorRecord.Exception -eq $ex ){
$ErrorRecord = $err.ErrorRecord
Write-Debug "Inner Exception $i : Found ErrorRecord in `$Global:Error"
break
}
}
else {
Write-Warning "Unexpected type in `$Global:Error[]. Expected `$Global:Error to always be an ErrorRecrod OR exception implementing IContainsErrorRecord but found type: $($err.GetType())"
}
}
}
if( -not($ErrorRecord) ){
Write-Debug "Inner Exception $i : No ErrorRecord could be found for exception of type: $($ex.GetType())"
}
# Return details as custom object
[PSCustomObject] #{
ExceptionDepth = $i
Message = $ex.Message
ScriptStackTrace = $ErrorRecord.ScriptStackTrace # Note ErrorRecord will be null if exception was not from Powershell
ExceptionType = $ex.GetType().FullName
ExceptionStackTrace = $ex.StackTrace
}
}
}
Example Usage:
function Test-SqlConnection
{
$ConnectionString = "ThisConnectionStringWillFail"
try{
$sqlConnection = New-Object System.Data.SqlClient.SqlConnection $ConnectionString;
$sqlConnection.Open();
}
catch
{
throw [System.Exception]::new("Sql connection failed with connection string: '$ConnectionString'", $_.Exception)
}
finally
{
if($sqlConnection){
$sqlConnection.Close();
}
}
}
try{
Test-SqlConnection
}
catch {
Get-InnerErrors $_
}
Example output:
ExceptionDepth : 0
Message : Sql connection failed with connection string: 'ThisConnectionStringWillFail'
ScriptStackTrace : at Test-SqlConnection, <No file>: line 11
at <ScriptBlock>, <No file>: line 23
ExceptionType : System.Exception
ExceptionStackTrace :
ExceptionDepth : 1
Message : Exception calling ".ctor" with "1" argument(s): "Format of the initialization string does not conform to specification starting at index 0."
ScriptStackTrace :
ExceptionType : System.Management.Automation.MethodInvocationException
ExceptionStackTrace : at System.Management.Automation.DotNetAdapter.AuxiliaryConstructorInvoke(MethodInformation methodInformation, Object[] arguments, Object[] originalArguments)
at System.Management.Automation.DotNetAdapter.ConstructorInvokeDotNet(Type type, ConstructorInfo[] constructors, Object[] arguments)
at Microsoft.PowerShell.Commands.NewObjectCommand.CallConstructor(Type type, ConstructorInfo[] constructors, Object[] args)
ExceptionDepth : 2
Message : Format of the initialization string does not conform to specification starting at index 0.
ScriptStackTrace :
ExceptionType : System.ArgumentException
ExceptionStackTrace : at System.Data.Common.DbConnectionOptions.GetKeyValuePair(String connectionString, Int32 currentPosition, StringBuilder buffer, Boolean useOdbcRules, String& keyname, String&
keyvalue)
at System.Data.Common.DbConnectionOptions.ParseInternal(Hashtable parsetable, String connectionString, Boolean buildChain, Hashtable synonyms, Boolean firstKey)
at System.Data.Common.DbConnectionOptions..ctor(String connectionString, Hashtable synonyms, Boolean useOdbcRules)
at System.Data.SqlClient.SqlConnectionString..ctor(String connectionString)
at System.Data.SqlClient.SqlConnectionFactory.CreateConnectionOptions(String connectionString, DbConnectionOptions previous)
at System.Data.ProviderBase.DbConnectionFactory.GetConnectionPoolGroup(DbConnectionPoolKey key, DbConnectionPoolGroupOptions poolOptions, DbConnectionOptions& userConnectionOptions)
at System.Data.SqlClient.SqlConnection.ConnectionString_Set(DbConnectionPoolKey key)
at System.Data.SqlClient.SqlConnection.set_ConnectionString(String value)
at System.Data.SqlClient.SqlConnection..ctor(String connectionString, SqlCredential credential)
There are cases where PowerShell doesn't seem to keep a backtrace, like calling a method or calling a function with .Invoke(). For that, Set-PSDebug -Trace 2 may come in handy. It will print every executed line of the running script.
Try flipping # on (1) and (2) and running WrapStackTraceLog({ function f{ 1/0 } ; & f }) # let's divide by zero
Function WrapStackTraceLog($func) {
try {
# return $func.Invoke($args) # (1)
return (& $func $args) # (2)
} catch {
Write-Host ('=' * 70)
Write-Host $_.Exception.Message
Write-Host ('-' * 70)
Write-Host $_.ScriptStackTrace
Write-Host ('-' * 70)
Write-Host "$StackTrace"
Write-Host ('=' * 70)
}
}
Branch (1) exception caught:
Exception calling "Invoke" with "1" argument(s): "Attempted to divide by zero."
Branch (2) is more informative:
at f, <No file>: line 1
at <ScriptBlock>, <No file>: line 1
at global:WrapStackTraceLog, <No file>: line 4
at <ScriptBlock>, <No file>: line 1
But, you can still trace your Invokes with tracing on, branch (1):
DEBUG: ! CALL function 'f'
DEBUG: 1+ WrapStackTraceLog({ function f{ >>>> 1/0 } ; & f })
DEBUG: 6+ >>>> Write-Host ('=' * 70)
======================================================================
DEBUG: 7+ >>>> Write-Host $_.Exception.Message
Exception calling "Invoke" with "1" argument(s): "Attempted to divide by zero."