Not that long ago someone made a comment to me on Twitter about something I had shared related to PowerShell. He wanted to know more about the $MyInvocation variable. This is something that isn't well documented, yet can be very useful in your PowerShell scripting. Let's take a look at it in a bit more detail.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
The $MyInvocation variable is automatically created when you run a PowerShell script or function. Technically, it is a System.Management.Automation.InvocationInfo object. You can find Microsoft's documentation here. But let's explore this from a practical perspective.
Here's a simple demonstration function.
Function Get-Foo {
[cmdletbinding()]
Param(
[Parameter(Position = 0, Mandatory)]
[string]$Name,
[datetime]$Since = (Get-Date).AddHours(-24)
)
$MyInvocation
Write-Host "Getting $name since $since" -fore yellow
}
To get the most from $MyInvocation, I have found it is best to dot source your script file or import the module, into your PowerShell session. This function doesn't do anything other than show $MyInvocation.
Even though this is a rich object, there are probably only a handful of properties you will find useful in your PowerShell scripting. I use the MyCommand property all the time.
MyInvocation.Mycommand
Here's another version of the demo function.
Function Get-Foo {
[cmdletbinding()]
Param(
[Parameter(Position = 0, Mandatory)]
[string]$Name, [
datetime]$Since = (Get-Date).AddHours(-24)
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
} #begin
Process {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $Name"
#create a global copy for testing purposes
$global:mi = $myinvocation
Write-Host "Getting $name since $since" -fore yellow
} #process
End {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
}
This is the format of almost all of my PowerShell functions. The Begin and End script blocks have Verbose statements that reflect the command name. I like this approach because if I change the function name, I don't have to modify any other code.
Where this is truly useful is when I have functions calling other functions. These verbose statements make it easier to track the flow.
Function Get-Foo {
[cmdletbinding()]
Param(
[Parameter(Position = 0, Mandatory)]
[string]$Name, [
datetime]$Since = (Get-Date).AddHours(-24)
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
} #begin
Process {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $Name"
#create a global copy for testing purposes
$global:mi = $myinvocation
Write-Host "Getting $name since $since" -fore yellow
If (Test-Foo $name) {
$name
}
} #process
End {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
}
Function Test-Foo {
[cmdletbinding()]
Param([string]$Name)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
} #begin
Process {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Testing $name"
$True
} #process
End {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
}
The verbose output now shows when I am in one command, starting another, and returning to the original command. I have found this technique to be invaluable over the years.
Scripts
You can also use $MyInvocation with scripts.
#requires -version 5.1
Param(
[Parameter(Position = 0, Mandatory)]
[string]$Name,
[datetime]$Since = (Get-Date).AddHours(-24)
)
write-host "Starting $(Resolve-Path $myinvocation.InvocationName)" -ForegroundColor green
write-host "PSScriptroot is $PSScriptRoot" -ForegroundColor green
#create a global copy for testing purposes
$global:mi = $MyInvocation
Write-Host "Getting $name since $since" -fore yellow
write-host "Ending $($myinvocation.mycommand)" -ForegroundColor green
throw "I failed"
I can capture the script name from $MyInvocation. I'm taking things a step further by using Resolve-Path to convert any PSDrives or relative paths to a full filesystem path. $PSScriptRoot reflects the location of the current command. But here is where things get interesting.
InvocationInfo
My demo script intentionally threw an exception. Let's look at it.
This is the same type of object, but the properties get populated differently. Here I can see where the error occurred, on line 16. In fact, I even see the line text. This has possibilities.
Invoking Debugging
You can get the same result using functions. In fact, even better when you dot source the ps1 file with your function. This is the InvocationInfo from an exception object.
This doesn't tell me what function I ran, but it does show the command I was trying to run inside the function that failed, the line number and the source file!
With this information, I can add code to my script to edit the source file and jump right to the offending line. If you are still using the PowerShell ISE, you can automate it using the $PSISE object model. However, the PowerShell ISE is an editor and you can't get it to automatically run scripts. Instead, I have a helper function to create a temporary file with the required bits of information.
Function New-ISETemp {
[cmdletbinding()]
[Outputtype("System.IO.FileInfo")]
Param([string]$FilePath, [int]$LineNumber)
$content = @"
#Press F5 to Run This Script
`$f = `$psise.CurrentPowerShellTab.files.add("$FilePath")
`$psise.CurrentFile.Editor.Focus()
`$LineNumber = $LineNumber
`$f.Editor.SetCaretPosition(`$LineNumber,1)
`$f.editor.SelectCaretLine()
"@
$tmp = [system.io.path]::GetTempFileName() -replace "\.tmp$", ".ps1"
$content | Out-File -FilePath $tmp
#write the temp file as the function output
$tmp
}
In my function, I have code to open the temp file in the PowerShell ISE when using -Debug with my function.
Function Get-Foo2 {
[cmdletbinding()]
Param(
[Parameter(Position = 0, Mandatory)]
[string]$Name, [
datetime]$Since = (Get-Date).AddHours(-24)
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
} #begin
Process {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $Name"
#create a global copy of myinvocation for testing purposes
$global:mi = $myinvocation
Write-Host "Getting $name since $since" -fore yellow
Try {
Get-Service $name -ErrorAction Stop
}
Catch {
Write-Warning $_.exception.message
write-Warning $DebugPreference
#export for testing purposes
$_.invocationInfo | Export-Clixml d:\temp\demo-error.xml
if ($DebugPreference -match 'continue|inquire') {
$src = $_.invocationInfo.PSCommandPath
$line = $_.invocationInfo.ScriptLineNumber
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Debugging line $line"
$f = New-ISETemp -filepath $src -linenumber $line
powershell_ise $f
}
}
} #process
End {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
}
The PowerShell ISE is started with my temp file loaded.
All I need to do is press F5 and the source file is loaded and the offending line selected.
Invoke Debugging with VS Code
If you use VS Code as your editor, this is even easier. Here's the VSCode version of my function.
Function Get-Foo2 {
[cmdletbinding()]
Param(
[Parameter(Position = 0, Mandatory)]
[string]$Name, [
datetime]$Since = (Get-Date).AddHours(-24)
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
} #begin
Process {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $Name"
#create a global copy of myinvocation for testing purposes
$global:mi = $myinvocation
Write-Host "Getting $name since $since" -fore yellow
Try {
Get-Service $name -ErrorAction Stop
}
Catch {
Write-Warning $_.exception.message
write-Warning $DebugPreference
#export for testing purposes
$_.invocationInfo | Export-Clixml d:\temp\demo-error.xml
if ($DebugPreference -match 'continue|inquire') {
$src = $_.invocationInfo.PSCommandPath
$line = $_.invocationInfo.ScriptLineNumber
$goto = "{0}:{1}" -f $src, $line
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Debugging $goto"
#launch VS Code and jump to the line
code.cmd --goto $goto
}
} #catch
} #process
End {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
}
Now when I run the function with -Debug, VS Code loads the file and jumps directly to the line.
Summary
You may not need to incorporate this kind of recursive debugging in your code. But I think it demonstrates how you might take advantage of different properties of an InvocationInfo object. If you've come up with a useful way, I'd love to hear about it.
With creating my own logging script I found to use $MyInvocation.MyCommand.Name to create a log with the name of the script automagicaly.
That would be a great use.
Regardless of function nesting and current scope, this creates log file with the same name as the invoked script
$LogPath = “$([System.IO.Path]::GetFileNameWithoutExtension(((Get-Variable MyInvocation -Scope “Script”).Value).MyCommand.Name)).log”