Friday Fun: Job Announcer

announcer-blueLast week I came across some people having fun with the SAPI.SPVoice COM object. This is the object that lets your computer “speak”. If you’ve never played with this, it is very simple to do in PowerShell. The voice quality in Windows 7 and 8 is quite nice, as far as synthetic voices go.


$v = new-object -ComObject "SAPI.SPVoice"
$v.speak("Powershell rocks!")

This got me to wondering…what about using this as a job announcer? Instead of having to go back periodically to check the status of a background job, why not have the job notify me when it is finished? All I would need to do is add a few lines of code to the scriptblock to create the voice object and announce when the job is complete. Here’s a sample.


#define a scriptblock for the job
$sb = {
Param($jobname)
#find all errors and warning in the system eventlog since midnight yesterday
Get-Eventlog -LogName System -EntryType Error,Warning -After (Get-Date).AddHours(-24).Date
#notify that the job has finished
(New-Object -ComObject SAPI.SPVoice).Speak("Thank you for your patience. Your job $jobname has finished.") | Out-Null
}
#define a name for the job that can be passed to the scriptblock
$jobname = "Get Latest Eventlogs"
Start-job -Name $jobname -ScriptBlock $sb -ArgumentList $jobname

The scriptblock could simply announce that the job is finished. If I only have a single job, that could be fine. But I wanted the scriptblock to be “aware” of what job it was running so I could have a more meaningful announcement. Thus, I pass the job name to the scriptblock so the voice object can use it in the announcement. It probably wouldn’t be too difficult to build a wrapper function to incorporate the voice announcement with the scriptblock. But I’ll leave that for you. If you come up with something, I hope you’ll share it and let me know.


$v.speak("Enjoy and Happy New Year")

Friday Fun PowerShell Crypto

I’m a big fan of codes, ciphers and secret messages. I obviously am a big PowerShell fan as well. So why not mash these things together? Today’s Friday Fun is a PowerShell module I call PSCode. The module contains a few functions for encoding and decoding text. Now, before you get too excited, these won’t stand up to NSA scrutiny or even anyone proficient at code breaking, although I’d like to think I’ve made it a little challenging. Of course, the whole point of these Friday Fun posts is to learn a little PowerShell as well.

The basic encoding premise I’m using is a relative simple transposition. That is, replace S with E. But how to figure out what character substitute? There needs to be some “key” so that the message can later be decoded. My approach was to turn plaintext into its corresponding series of [CHAR] objects. I could have simply used the ToCharArray() method, but PowerShell is too helpful and I wanted to get the underlying integer value. My approach was to process each character of the word with a For statement.


for ($i=0;$i -lt $word.length;$i++) {
$x=$word[$i]
#get the current numeric value
$x=[char]::ConvertToUtf32("$x",0)

The trick here is to use the ConvertToUTF32() method of the [CHAR] class to get the integer value. From the console this is what it looks like:


PS S:\> [char]::ConvertToUtf32("P",0)
80

My substitution idea is to get another character based on some calculated offset. I could have simply said always get the current value plus 5. But then frequency analysis would break that pretty quickly. So my default algorithm, which can be modified via a parameter, is to calculate an offset based on the length of the current word. I take this value and perform a modulo operation using pi, adding 3 to it and rounding it to an integer. This guarantees an offset.


PS S:\> $word="PowerShell"
PS S:\> $word.length%[math]::pi
0.575222039230621
PS S:\> $word.length%[math]::pi+3
3.57522203923062
PS S:\> [int]$off=$word.length%[math]::pi+3
PS S:\> $off
4

This value is added to the [CHAR] value of the current letter. So in my example, the new value is 84. To get the corresponding character I invoke the ConvertFromUtf32() method and write the result to the pipeline.


PS S:\> [char]::ConvertFromUtf32(84)
T

Because the offset is based on a per word length, it varies throughout the plaintext. Thus each word would require a separate analysis, at least for most unclassified civilians. Anyway, the last piece is to use the -Join operator to assemble each character back into a word and write it to the pipeline. Here’s the ConvertTo-PSCode function.


Function ConvertTo-PSCode {
<# comment based help #>

[cmdletbinding()]

Param(
[Parameter(Position=0,Mandatory=$True,ValueFromPipeline=$True)]
[AllowEmptyString()]
[AllowNull()]
[string[]]$Text,
[ValidateNotNullorEmpty()]
[scriptblock]$Scriptblock={($word.length%[math]::pi)+3}
)

Begin {
#an array to hold encoded output
Write-Verbose "Starting $($myinvocation.mycommand)"
[email protected]()
}
Process {

#split text into words and lines
foreach ($line in $text) {
$newLine=""
Write-Verbose "Processing line: $line"
$NewLine+= foreach ($word in ($line.Split())) {
write-Verbose " Processing word: $word"
$chars = for ($i=0;$i -lt $word.length;$i++) {
$x=$word[$i]
#get the current numeric value
$x=[char]::ConvertToUtf32("$x",0)
#Calculate an offset. The default is the length of the
#word modulo pi rounded to an integer plus 3
[int]$off=&$Scriptblock
Write-Verbose "Using an offset of $off based on length $($word.length)"
[int]$y=$x+$off
#get the new value
[char]::ConvertFromUtf32($y)
} #for
#write the new "word" to the pipeline
$NewWord=$chars -join ""
$NewWord
} #foreach word
$Out+=$NewLine
} #foreach line
} #end process
End {
Write $Out
}
} #function

The function takes plaintext as a parameter or you can pipe to it. The offset algorithm is passed as a scriptblock. You can pass your own which I’ll show you in a moment.


PS S:\> convertto-pscode "I am the walrus"
M fr znk }grx{y
PS S:\> "I am the walrus" | convertto-pscode
M fr znk }grx{y

To decode the text, I can use the same technique. Except instead of adding the offset, I subtract it.


#rounded to an integer
[int]$off=&$Scriptblock
Write-Verbose "Using an offset of $off based on length $($word.length)"
[int]$y=$x-$off
#get the new value
[char]::ConvertFromUtf32($y)

The ConvertFrom-PSCode function also takes pipelined input which makes it very easy to test.


PS S:\> "I am the walrus" | convertto-pscode | convertfrom-pscode
I am the walrus

If I decide to use a different algorithm, I need to specify it for both functions. Depending on your formula you might end up with non-alpha characters.

As you can see in the figure the offset is the length of the word plus 5 which makes for some interesting code. Now for the really cool part. Because the convert functions write to the pipeline you can direct them to Out-File to save your results. Let’s say I have a simple script.


#requires -version 2.0

[cmdletbinding()]

Param(
[Parameter(Position=0)]
[ValidateNotNullorEmpty()]
[string]$name="m*"
)

write-host "$(Get-Date) Starting a sample script" -ForegroundColor Green
Write-verbose "getting services where name = $name"
Get-service -name $name | where {$_.status -eq "running"}

write-host "$(Get-Date) Ending a sample script" -ForegroundColor Green

I’ll encode the script using the defaults.


PS S:\> cat c:\work\abc.txt | convertto-pscode | out-file c:\work\secretscript.txt
PS S:\> cat C:\work\secretscript.txt
)xkw{oxky 2{jwxnts 846

_gqhpixfmrhmrk,-a

Vgxgs.
_Teveqixiv,TswmxmsrA4-a
_ZepmhexiRsxRyppsvIqtx},-a
xywnslb)sfrjB'r/'
-

{vmxi1lswx (*.Mkz3Jgzk/ Xyfwynsl e ygsvrk wgvmtx& 0IruhjurxqgFroru Lwjjs
Zulwh0yhuervh 'ljyynsl xjw{nhjx |mjwj reqi A *tgsk(
Ljy2xjw{nhj 2sfrj )sfrj € |mjwj (c2wxexyw 3kw &vyrrmrk&

{vmxi1lswx (*.Mkz3Jgzk/ Ktjotm e ygsvrk wgvmtx& 0IruhjurxqgFroru Lwjjs

What I thought would be extra fun would be a way to run this encoded script. So I wrote Invoke-PSCode.


Function Invoke-PSCode {

<# comment based help #>

[cmdletbinding()]
Param(
[Parameter(Position=0,Mandatory=$True,ValueFromPipeline=$True)]
[AllowEmptyString()]
[AllowNull()]
[string[]]$Text,
[ValidateNotNullorEmpty()]
[scriptblock]$Scriptblock={($word.length%[math]::pi)+3},
[hashtable]$Arguments
)
Begin {
Write-Verbose "Starting $($myinvocation.mycommand)"
#define an empty string to hold the contents of the decoded command
$a=""
}
Process {
foreach ($item in $text) {
#add each decoded line to the variable plus a line return
Write-Verbose "Decoding $item"
$a+="{0}
n" -f (ConvertFrom-PSCode -Text $item -Scriptblock $Scriptblock)
}
} #process

End {
Write-Verbose "Running command"
#create a scriptblock
$myCommand=$ExecutionContext.InvokeCommand.NewScriptBlock($a)
#splat the arguments to the script block
&$myCommand @Arguments
Write-Verbose "Ending $($myinvocation.mycommand)"
}
} #end function

The function takes each line of code and decodes it. Each decoded line is saved as part of a long string which is eventually turned into a scriptblock and executed. To pass parameters to the secret command, which you have to know in advance, Invoke-PSCode takes a hashtable of parameters and splats them to the scriptblock.

I haven’t tested this against all possible types of scripts and code samples but I thought it was fun.

The last little function in the module is something to quickly take a string and write its reverse to the pipeline. The business part is basically a single line that gets each character from a string in reverse order and writes it to the pipeline. But using the -join operator puts it all back together.


PS S:\> $item="PowerShell is fun!"
PS S:\> -join $(for ($i=$item.length;$i -ge 0;$i--) {$item[$i]})
!nuf si llehSrewoP

The For loop starts at the length and counts down to zero. The rest of the function is merely a wrapper to this core command.


PS S:\> "I am the walrus" | ConvertTo-ReverseString
surlaw eht ma I
PS S:\> "I am the walrus" | ConvertTo-ReverseString | convertto-Reversestring
I am the walrus

I suppose if I wanted I could even combine encoding and reversal.


PS S:\> $x = "PowerShell is fun for walruses." | ConvertTo-ReverseString | ConvertTo-PSCode
PS S:\> $x
4yky{xrg} xul t{l xn ppilWvi{sT
PS S:\> $x | ConvertFrom-PSCode | ConvertTo-ReverseString
PowerShell is fun for walruses.

Although I’d better not forget the order!

So have some fun with this and maybe learn something new about PowerShell. Download PSCode.psm1. This is a module so you’ll need to put it in a folder called PSCode in your modules directory or be sure to specify the full path when you import it.

Ymtxj }nu luxmkz yt yixovz gxk juuskj yt xkvkgz ymjnw {svo

Friday Fun: Expand Environmental Variables in PowerShell Strings

This week I was working on a project that involved using the %PATH% environmental variable. The challenge was that I have some entries that look like this: %SystemRoot%\system32\WindowsPowerShell\v1.0\. When I try to use that path in PowerShell, it complains because it doesn’t expand %SystemRoot%. What I needed was a way to replace it with the actual value, which I can find in the ENV: PSdrive, or reference as $env:systemroot. This seems reasonable enough. Take a string, use a regular expression to find the environmental variable, find the variable in ENV:, do a replacement, write the revised string back to the pipeline. So here I have Resolve-EnvVariable.

Function Resolve-EnvVariable {

[cmdletbinding()]
Param(
[Parameter(Position=0,ValueFromPipeline=$True,Mandatory=$True,
HelpMessage="Enter a string that contains an environmental variable like %WINDIR%")]
[ValidateNotNullOrEmpty()]
[string]$String
)

Begin {
Write-Verbose "Starting $($myinvocation.mycommand)"
} #Begin

Process {
#if string contains a % then process it
if ($string -match "%\S+%") {
Write-Verbose "Resolving environmental variables in $String"
#split string into an array of values
$values=$string.split("%") | Where {$_}
foreach ($text in $values) {
#find the corresponding value in ENV:
Write-Verbose "Looking for $text"
[string]$replace=(Get-Item env:$text -erroraction "SilentlyContinue").Value
if ($replace) {
#if found append it to the new string
Write-Verbose "Found $replace"
$newstring+=$replace
}
else {
#otherwise append the original text
$newstring+=$text
}

} #foreach value

Write-Verbose "Writing revised string to the pipeline"
#write the string back to the pipeline
Write-Output $NewString
} #if
else {
#skip the string and write it back to the pipeline
Write-Output $String
}
} #Process

End {
Write-Verbose "Ending $($myinvocation.mycommand)"
} #End
} #end Resolve-EnvVariable

The function takes a string as a parameter, or you can pipe into the function. The function looks to see if there is something that might be an environmental variable using a regular expression match.


if ($string -match "%\S+%") {

If there is an extra % character in the string, this won’t work so I’m assuming you have some control over what you provide as input. Now I need to get the match value. At first I tried using the Regex object. But when faced with a string like this “I am %username% and working on %computername%” it also tried to turn % and working on% as an environmental variable. I’m sure there’s a regex pattern that will work but I found it just as easy to split the string on the % character and trim off the extra space.


$values=$string.split("%") | Where {$_}

Now, I can go through each value and see if there is a corresponding environmental variable.


foreach ($text in $values) {
#find the corresponding value in ENV:
Write-Verbose "Looking for $text"
[string]$replace=(Get-Item env:$text -erroraction "SilentlyContinue").Value

I turned off the error pipeline to suppress errors about unfound entries. If something was found then I do a simple replace, otherwise, I re-use the original text.


if ($replace) {
#if found append it to the new string
Write-Verbose "Found $replace"
$newstring+=$replace
}
else {
#otherwise append the original text
$newstring+=$text
}

In essence I am building a new string adding the replacement values or original text. When finished I can write the new string, which has the variable replacements back to the pipeline.


Write-Verbose "Writing revised string to the pipeline"
#write the string back to the pipeline
Write-Output $NewString

Finally, I can pass strings that contain environmental variables to the function.


PS C:\> "I am %username% and working on %computername%" | resolve-envvariable
I am Jeff and working on SERENITY

This isn’t perfect. Look what happens if there is an undefined variable:


PS C:\> "I am %username% and working on %computername% with a %bogus% variable." | resolve-envvariable
I am Jeff and working on SERENITY with a bogus variable.

But as long as you are confident that variables are defined, then you can do things like this:


PS C:\> $env:path.split(";") | Resolve-EnvVariable | foreach { if (-Not (Test-Path $_)) {$_}}
c:\foo

Download Resolve-EnvVariable and let me know what you think. The download version includes comment based help.

Friday Fun: Another PowerShell Console Graph

Late last year I posted a demo script to create a horizontal bar graph in the PowerShell console. I liked it and many of you did as well. But I also wanted to be able to create a vertical bar graph, ie one with columns. This is much trickier since you have to tell PowerShell exactly where to “paint” the graph.

I’ve posted other articles on using the coordinates in the host and that is what I ended up doing for today’s Friday Fun. This demo script only works in the PowerShell console. It will run in the ISE but you won’t get the desired result. Let me post the code and then I’ll go through a few things.


Param([string]$computername=$env:computername)

Clear-Host

#get the data
$drives=Get-WmiObject -Class Win32_LogicalDisk -Filter "drivetype=3" -computername $computername

#define a set of colors for the graphs
[email protected]("Yellow","Magenta","Green","Cyan","Red")

#set cursor position
$Coordinate = New-Object System.Management.Automation.Host.Coordinates
$Coordinate.X= 10
$Coordinate.Y= [int]($host.ui.rawui.WindowSize.Height -5)

#save starting coordinates
$startY=$Coordinate.Y
$startX=$Coordinate.X

#counter for colors
$c=0

#adjust Y so we can write the caption
$Coordinate.Y+=1

foreach ($drive in $drives) {
#set the color to the first color in the array of colors
$color=$colors[$c]
$legend=$drive.DeviceID
#calculate used space value
$used=$Drive.Size - $Drive.FreeSpace
[int]$usedValue=($used/($drive.size))*10
#adjust for values less than 0 so something gets graphed
if ($usedValue -le 0) {
[int]$usedValue=($used/($drive.size))*50
}

#format usage as a percentage
$usedPer="{0:p2}" -f ($used/($drive.size))
#set the cursor to the new coordinates
$host.ui.rawui.CursorPosition=$Coordinate
#write the caption
write-host $legend -nonew
#move the Y coordinate up to start the graph
$coordinate.Y-=1

for ($i=$usedValue;$i -gt 0;$i--) {
$host.ui.rawui.CursorPosition=$Coordinate
#draw the color space for the graph
write-host " " -BackgroundColor $color -nonewline
#move Y up 1
$coordinate.y--
#repeat until we reach the $usedValue
}
#set new coordinate
$host.ui.rawui.CursorPosition=$Coordinate
#write the usage percentage at the top of the bar
write-host $usedPer -nonewline

#reset Y to where we started + 1
$Coordinate.Y=($startY+1)
#move X to the right
$coordinate.x+=8
#reset coordinates
$host.ui.rawui.CursorPosition=$Coordinate
#increment the color counter
$c++

#repeat for the next drive

} #foreach

#reset coordinates so we can write a legend
$coordinate.Y=$StartY+2
$coordinate.X=$startX
$host.ui.rawui.CursorPosition=$Coordinate
write-host ("Drive Usage for {0}" -f $drives[0].__SERVER)

#move cursor to bottom of the screen and write a blank line
$Coordinate.X=1
$coordinate.Y=[int]($host.ui.rawui.WindowSize.Height-2)
$host.ui.rawui.CursorPosition=$Coordinate
write-host ""

#your PowerShell prompt will now be displayed

This script gets drive usage and creates a vertical bar chart displaying disk utilization. Everyone loves a good drive space report and they always make good demos.

The script creates a System.Management.Automation.Host.Coordinates object, setting the value of X and Y that should be starting in the bottom left corner of the console.


#set cursor position
$Coordinate = New-Object System.Management.Automation.Host.Coordinates
$Coordinate.X= 10
$Coordinate.Y= [int]($host.ui.rawui.WindowSize.Height -5)
#save starting coordinates
$startY=$Coordinate.Y
$startX=$Coordinate.X

I’m also saving these values so I can reset. The script will have to “move” the cursor around the screen to draw the graphs. Once I calculate the values for each drive, I write a blank line using Write-Host with -Backgroundcolor to get the desired graphing effect.


for ($i=$usedValue;$i -gt 0;$i--) {
$host.ui.rawui.CursorPosition=$Coordinate
#draw the color space for the graph
write-host " " -BackgroundColor $color -nonewline
#move Y up 1
$coordinate.y--
#repeat until we reach the $usedValue
}

Notice after each write I move the Y point “up”, until I reach the limit of the current value. I set values as a percentage scaled to 10 so the graph doesn’t end up outside of the buffer. I also made an adjustment for low values that wouldn’t normally trigger a graph so that I get something written to the screen.


#calculate used space value
$used=$Drive.Size - $Drive.FreeSpace
[int]$usedValue=($used/($drive.size))*10
#adjust for values less than 0 so something gets graphed
if ($usedValue -le 0) {
[int]$usedValue=($used/($drive.size))*50
}

After drawing the graph I move the cursor position back to the beginning and write a legend.


#reset coordinates so we can write a legend
$coordinate.Y=$StartY+2
$coordinate.X=$startX
$host.ui.rawui.CursorPosition=$Coordinate
write-host ("Drive Usage for {0}" -f $drives[0].__SERVER)

Here’s the end result.

This script is really just a proof of concept. I haven’t created any functions to simply any of this or make it easy to use with other values. This is also a bit advanced so if you look at this and it makes your head hurt, don’t worry about it. You would only use something like this in special cases. Still, I’d like to know what you think, how it works for you and if you extend it to a more re-usable form.

Download demo-bargraph. The script will default to the local computer, but you can run specify any computer you want.


PS C:\Scripts\> .\demo-bargraph.ps1 Mycomputer

Enjoy and have fun.

Friday Fun: PowerShell ISE Function Finder

At the PowerShell Deep Dive in San Diego, I did a lightning session showing off something I had been working on. Sometimes I don’t know what possesses me, but I felt the need for a better way to navigate my PowerShell scripts files that had many functions. Some files, especially modules, can get quite long and contain a number of functions. When using the PowerShell ISE I wanted a faster way to jump to a function. The problem is I don’t always remember what I called a function or where it is in the file. It is very easy to jump to a particular line in the ISE using the Ctrl+G shortcut.

So I started with some basics.


$Path=$psise.CurrentFile.FullPath

I decided I’d use a regular expression pattern to find my functions. I write my functions like this:


Function Get-Foo {

Param()
...

So I came up with a regex pattern to match the first line and to include the Filter keyword as well.


[regex]$r="^(Function|Filter)\s\S+\s{"

I first thought of searching the content of the current file, but that won’t give me a line number. Then I thought of Select-String. With my regex pattern, I can get the content of the current file and pipe it to Select-String. The match object that comes out the other end of the pipeline includes a line number property (awesome) and the matching line. I decided to do a little string parsing on the later to drop off the trailing curly brace.


$list=get-content $path |
select-string $r | Select LineNumber,
@{Name="Function";Expression={$_.Line.Split()[1]}}

Because I’m in the ISE I felt the need to stay graphical, so my first thought was to pipe the results to Out-Gridview.


$list | out-gridview -Title $psise.CurrentFile.FullPath

Here’s a sample result.

Now I can see the function name and line number. In the ISE I can do Ctrl+G and jump to the function. Of course, if I modify the file and line numbers change I need to close the grid and re-run my command. But wait, there’s more….

I’ve never done much with the WPF and figured this would be a great opportunity to do something with the ShowUI module. I already had the data. All I had to do was create a form with ShowUI. This is what I ended up with.


[string]$n=$psise.CurrentFile.DisplayName
ScrollViewer -ControlName $n -CanContentScroll -tag $psise.CurrentFile.FullPath -content {
StackPanel -MinWidth 300 -MaxHeight 250
-orientation Vertical -Children {
#get longest number if more than one function is found
if ($count -eq 1) {
[string]$i=$list.Linenumber
}
else {
[string]$i=$list[-1].Linenumber
}
$l=$i.length
foreach ($item in $list) {

[string]$text="{0:d$l} {1}" -f $item.linenumber,$item.function

Button $text -HorizontalContentAlignment Left -On_Click {
#get the line number
[regex]$num="^\d+"
#parse out the line number
$goto = $num.match($this.content).value
#grab the file name from the tab value of the parent control
[string]$f= $parent | get-uivalue
#Open the file in the editor
psedit $f
#goto the selected line
$psise.CurrentFile.Editor.SetCaretPosition($goto,1)
#close the control
Get-ParentControl | Set-UIValue -PassThru |Close-Control
} #onclick
} #foreach
}
} -show

The result is a stack panel of buttons in a scroll control. The button shows the line number and function name.

When a button is clicked, the function gets the line number and automatically jumps to it. Originally I was leaving the control open, but this means the function is still running. And if I change the script the line numbers are off so I simply close the form after jumping to the function.

In the end, I packaged all of this as a script file that adds a menu choice. If ShowUI is available, the function will use it. Otherwise the function defaults to Out-GridView.


Function Get-ISEFunction {

[cmdletbinding()]
Param([string]$Path=$psise.CurrentFile.FullPath)

#import ShowUI if found and use it later in the functoin
if (Get-module -name ShowUI -listavailable) {
Import-Module ShowUI
$showui=$True
}
else {
Write-Verbose "Using Out-GridView"
$showui=$False
}

#define a regex to find "function | filter NAME {"
[regex]$r="^(Function|Filter)\s\S+\s{"

$list=get-content $path |
select-string $r | Select LineNumber,
@{Name="Function";Expression={$_.Line.Split()[1]}}

#were any functions found?
if ($list) {
$count=$list | measure-object | Select-object -ExpandProperty Count
Write-Verbose "Found $count functions"
if ($showui) {
<# display function list with a WPF Form from ShowUI Include file name so the right tab can get selected #>

[string]$n=$psise.CurrentFile.DisplayName
Write-Verbose "Building list for $n"

ScrollViewer -ControlName $n -CanContentScroll -tag $psise.CurrentFile.FullPath -content {
StackPanel -MinWidth 300 -MaxHeight 250

-orientation Vertical -Children {
#get longest number if more than one function is found
if ($count -eq 1) {
[string]$i=$list.Linenumber
}
else {
[string]$i=$list[-1].Linenumber
}
$l=$i.length
foreach ($item in $list) {

[string]$text="{0:d$l} {1}" -f $item.linenumber,$item.function
Write-Verbose $text
Button $text -HorizontalContentAlignment Left -On_Click {
#get the line number
[regex]$num="^\d+"
#parse out the line number
$goto = $num.match($this.content).value
#grab the file name from the tab value of the parent control
[string]$f= $parent | get-uivalue
#Open the file in the editor
psedit $f
#goto the selected line
$psise.CurrentFile.Editor.SetCaretPosition($goto,1)
#close the control
Get-ParentControl | Set-UIValue -PassThru |Close-Control
} #onclick
} #foreach
}
} -show

} #if $showui
else {
#no ShowUI module so use Out-GridView
$list | out-gridview -Title $psise.CurrentFile.FullPath
}
}
else {
Write-Host "No functions found in $($psise.CurrentFile.FullPath)" -ForegroundColor Magenta
}

} #close function

#Add to the Add-ons menu
$PSISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("List Functions",{Get-ISEFunction},$null)

#optional alias
set-alias gif get-isefunction

Now, I can click the List Functions menu choice and I’ll get a graphical list of any functions in the current file. I’m sure the regex could be tweaked. I’m also sure there are improvements I could make to the ShowUI code, but it works.

Download Get-ISEFunction and let me know what you think.