Calling a powershell function with a global hotkey - powershell

I want to use a function from my Powershell script by triggering a global hotkey (the combination Ctrl+Shift+F12) which is registered and unregistered by my script. I need to access a .NET object created by my script. In pseudo code:
$object_i_need = New-Object SomeClass
register_hotkey "Ctrl+Shift+F12" hotkey_func
function hotkey_func { do_something_with $object_i_need }
wait_for_keypress
unregister_hotkey
Is this possible somehow?

If the .Net object supports Add_Keydown (like System.Windows.Forms.Form) you can do something like this...
$objWhatever.KeyPreview = $True
$objWhatever.Add_KeyDown({
if ($_.KeyCode -eq "Enter"){
hotkey_func{}
}
})

Related

Use local functions in PowerShell Job

I am writing custom cmdlets to perform a task. One of the cmdlets depends on the other ones and is taking a really long time, so I wanted to perform that task inside a job. Well guess what, you can't because the other cmdlets are not available inside that Job's scope. Why? The other languages out there like C++, Java, C# allow you to use variables, objects, functions from whitin the same scope, why isn't this available in PowerShell? Why is it so decoupled? I feel like it makes it harder for developers. Maybe I don't get the topic, but I would like to do something like this:
function Write-Yes {
Write-Host "yes"
}
function Write-No {
Write-Host "no"
}
function Write-Random {
$result = #($true, $false) | Get-Random
if ($result) {
Write-Yes
}
else {
Write-No
}
}
Start-Job -ScriptBlock { Write-Random }
This is not possible. You have to do some hacks like providing the scriptblock of the function as the argument and call it using the call operator or something like this. Or even, use Import-Module to reimport the same file that you are working in. This feels overly complicated. The only module that I saw that is able to do something like this is PoshRSJob that allows you to name the cmdlets that will be used inside the job and it will create them dynamically for you, with some lexical parsing and again, overly complicated things.
Why are the things like they are and is there any way to do what I'm trying in the example in an elegat way?
Start-Job is very limited and slow. It was implemented very poorly imho and I never use it. You can use runspaces for fast and lightweight "background jobs", and import functions and variables from your current session.
Example:
function Write-Yes { "yes" }
function Write-No { "no" }
function Write-Random {
if ($true, $false | Get-Random) {
Write-Yes
}
else {
Write-No
}
}
# setup session and import functions
$session = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
"Write-Yes", "Write-No", "Write-Random" | foreach {
$session.Commands.Add((
New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry $_, (Get-Content "Function:\$_")
))
}
# setup separate powershell instance
$job = [Powershell]::Create($session)
[void]$job.AddScript({ Write-Random })
# start async
$asyncResult = $job.BeginInvoke()
# do stuff ...
# wait for completion
$job.EndInvoke($asyncResult)
$job.Dispose()
But in general, Powershell is not made for complex parallel processing. In general, it's best to put everything inside a script file and run that, as a task or background job etc.

PowerShell scriptblock scope weirdness

I am using Invoke-Command on the local (not remote) computer to run a scriptblock that includes a function. Some of the variables defined at the "root" of the scriptblock are available in the function, and some require me to scope them as Global:.
So in quasi-code, this fails:
{
function test() {
if (test-path $filepath.trim('t')) {
$taskFolder.gettasks(1) |out-file '$env:temp\tasks.txt'
}
}
$filepath = '$env:temp\test.txt'
$TaskService = new-object -ComObject('Schedule.Service')
$TaskService.connect()
$TaskFolder = $TaskService.GetFolder('\')
test
}
with a "You cannot call a method on a null-valued expression." (i.e. $filepath -eq $null).
However, this works:
{
function test() {
if (test-path $Global:filepath.trim('t')) {
$taskFolder.gettasks(1) |out-file '$env:temp\tasks.txt'
}
}
$Global:filepath = '$env:temp\test.txt'
$TaskService = new-object -ComObject('Schedule.Service')
$TaskService.connect()
$TaskFolder = $TaskService.GetFolder('\')
test
}
even though $TaskFolder is not Global.
I can't seem to replicate the problem with short, simple code like this here, but the effect of ONLY globally scoping a couple variables makes all the difference.
I initially noticed the issue when I was attempting to use the variables with Script: scope and that was failing. I assumed that was because this was a script block and not an actual script. Changing to Global: resolved the problem and then I noticed the non-scoped $taskfolder variable.
I am not asking about scheduled tasks! I only included it in the example because that is the variable that is working fine without being assigned a global scope.
Obviously, I'm not understanding something about scoping, invoking, or maybe the Schedule Service.
Can someone enlighten me?

How can I pass a PowerShell function to a .NET method that takes a Func<T1, T2>?

I need to create a .NET object in PowerShell. The signature for the constructor is:
public ObjCTor(TClient param1, Func<TClient, IInterface> param2)
and the class is defined as:
public class ObjCTor<TClient> : IOtherInt1, IOtherInt2 where TClient : ConcreteBase
The first parameter is easy enough. I'm stumped on the second. How can I pass a PowerShell function as Func<T1, T2>?
UPDATED
PowerShell can convert a ScriptBlock to a delegate, so something like this might work fine:
$sb = { param($t1) $t1 }
New-Object "ObjCTor[CClient, Func[CClient,IInterface]]" -ArgumentList $param1,$sb
Note that PowerShell cannot deduce generic type arguments from a ScriptBlock. In your case, ObjCTor looks like a generic class so type deduction isn't a factor, but if you were calling a generic method, things could get messy.
The syntax supplied by Jason wasn't quiet correct. This works:
$= New-Object "ObjCTor[TClient]" -ArgumentList $param1,$functionToCall
I was also missing the $args[0] as a parameter in my scriptblock/delegate. This is what I should've had:
$functionToCall = { Get-Blah $args[0] }

How does name lookup work in Powershell script blocks?

User cashfoley has posted what appears to be a fairly elegant set of code at codeplex for a "module" called PSClass.
When I dot-source the psclass code into some code of my own, I am able to write code like:
$Animal = New-PSClass Animal {
constructor {
param( $name, $legs )
# ...
}
method -override ToString {
"A $($this.Class.ClassName) named $($this.name) with $($this.Legs) Legs"
}
}
When I tried to create a module out of the PSClass code, however, I started getting errors. The constructor and method names are no longer recognized.
Looking at the actual implementation, what I see is that constructor, method, etc. are actually nested functions inside the New-PSClass function.
Thus, it seems to me that when I dot-source the PSClass.ps1 file, my script-blocks are allowed to contain references to functions nested inside other local functions. But when the PSClass code becomes a module, with the New-PSClass function exported (I tried both using a manifest and using Export-ModuleMember), the names are no longer visible.
Can someone explain to me how the script blocks, scoping rules, and visibility rules for nested functions work together?
Also, kind of separately, is there a better class definition protocol for pure Powershell scripting? (Specifically, one that does not involve "just write it in C# and then do this...")
The variables in your script blocks don't get evaluated until they are executed. If the variables in the script block don't exist in the current scope when the block is executed, the variables won't have any values. Script blocks aren't closures: they don't capture the context at instantiation time.
Remove-variable FooBar
function New-ScriptBlock
{
$FooBar = 1
$scriptBlock = {
Write-Host "FooBar: $FooBar"
}
$FooBar = 2
& $scriptBlock # Outputs FooBar: 2 because $FooBar was set to 2 before invocation
return $scriptBlock
}
function Invoke-ScriptBlock
{
param(
$ScriptBlock
)
& $ScriptBlock
}
$scriptBlock = New-ScriptBlock
& $scriptBlock # Prints nothing since $FooBar doesn't exist in this scope
$FooBar = 3
Invoke-ScriptBlock $scriptBlock # Prints $FooBar: 3 since FooBar set to 3

Can I decorate advanced PowerShell functions with my own custom attributes?

For example:
function TestThis()
{
[MySpecialCustomAttribute]
[CmdletBinding()]
Param(...)
Process{...}
}
Yes, of course you can!
Any type derived from Attribute which allows UsageType.All (or UsageType.Class) can be used on the function itself (i.e. above Param)
Any type derived from Attribute which allows UsageType.Property or UsageType.Field usage can be used on parameters themselves or variables.
It's not uncommon to just be lazy and use UsageType.All (e.g. the built in OutputType attribute does that).
You can even write them in PowerShell classes now
using namespace System.Management.Automation
class ValidateFileExistsAttribute : ValidateArgumentsAttribute {
[void] Validate([object]$arguments, [EngineIntrinsics]$engineIntrinsics) {
if($null -eq $arguments) {
throw [System.ArgumentNullException]::new()
}
if(-not (Test-Path -Path "$arguments" -Type Leaf)) {
throw [System.IO.FileNotFoundException]::new("The specified path is not a file: '$arguments'")
}
}
}
See more examples on Kevin Marquette's blog.
There's an older example here showing how to do it in PowerShell 4 and earlier using Add-Type, although it's a little out of date now, because the particular example it shows has been integrated into PowerShell 6 and is no longer needed 😉
There are also videos