Home > front end >  Powershell - How to pass variable into scriptblock
Powershell - How to pass variable into scriptblock

Time:12-01

I'm trying to understand and figure out how I can pass a variable into a scriptblock. In my below example script, when a new file is dropped into the monitored folder it executes the $action script block. But the $test1 variable just shows up blank. Only way I can make it work is by making it a global variable, but I don't really want to do that.

I've looked into this some and I'm more confused than when I started. Can anyone help me out or point me in the right direction to understand this?

$PathToMonitor = "\\path\to\folder"

$FileSystemWatcher = New-Object System.IO.FileSystemWatcher
$FileSystemWatcher.Path  = $PathToMonitor
$FileSystemWatcher.Filter  = "*.*"
$FileSystemWatcher.IncludeSubdirectories = $false

$FileSystemWatcher.EnableRaisingEvents = $true

$test1 = "Test variable"

$Action = {
    Write-Host "$test1"
}

$handlers = . {
    Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Created -Action $Action -SourceIdentifier FSCreateConsumer
}

try {
    do {
        Wait-Event -Timeout 5
    } while ($true)
}
finally {
    Unregister-Event -SourceIdentifier FSCreateConsumer
    
    $handlers | Remove-Job
    
    $FileSystemWatcher.EnableRaisingEvents = $false
    $FileSystemWatcher.Dispose()
}

CodePudding user response:

Hidden away in the documentation for Register-ObjectEvent, way down in the -Action parameter description is this little tidbit:

The value of the Action parameter can include the $Event, $EventSubscriber, $Sender, $EventArgs, and $Args automatic variables. These variables provide information about the event to the Action script block. For more information, see about_Automatic_Variables.

What this means is PowerShell automatically creates some variables that you can use inside the event handler scriptblock and it populates them when the event is triggered - for example:

$Action = {
    write-host ($Sender | format-list * | out-string)
    write-host ($EventArgs | format-list * | out-string)
}

When you create a file in the watched folder you'll see some output like this:


NotifyFilter          : FileName, DirectoryName, LastWrite
Filters               : {*}
EnableRaisingEvents   : True
Filter                : *
IncludeSubdirectories : False
InternalBufferSize    : 8192
Path                  : c:\temp\scratch
Site                  :
SynchronizingObject   :
Container             :




ChangeType : Created
FullPath   : c:\temp\scratch\New Text Document (3).txt
Name       : New Text Document (3).txt

If these contain the information you're after then you don't actually need to pass any parameters into the scriptblock yourself :-).

Update

If you still need to pass your own variables into the event you can use the -MessageData parameter of Register-ObjectEvent to be able to access it as $Event.MessageData inside your event scriptblock - for example:

$Action = {

    write-host ($EventArgs | format-list * | out-string)

    write-host "messagedata before = "
    write-host ($Event.MessageData | ConvertTo-Json)

    $Event.MessageData.Add($EventArgs.FullPath, $true)

    write-host "messagedata after = "
    write-host ($Event.MessageData | ConvertTo-Json)

}

$messageData = @{ };
$handlers = . {
    # note the -MessageData parameter
    Register-ObjectEvent `
        -InputObject      $FileSystemWatcher `
        -EventName        Created `
        -Action           $Action `
        -MessageData      $messageData `
        -SourceIdentifier FSCreateConsumer
}

which will output something like this when the event triggers:

ChangeType : Created
FullPath   : c:\temp\scratch\New Text Document (16).txt
Name       : New Text Document (16).txt



messagedata before =
{}
messagedata after =
{
  "c:\\temp\\scratch\\New Text Document (16).txt": true
}

$messageData is technically still a global variable but your $Action doesn't need to know about it anymore as it takes a reference from the $Event.

Note you'll need to use a mutable data structure if you want to persist changes - you can't just assign a new value to $Event.MessageData, and it'll possibly need to be thread-safe as well.

CodePudding user response:

The event action block runs on a background thread and can't resolve $test1 when dispatched.

One workaround is to explicitly read from and write to a globally-scoped variable (eg. Write-Host $global:test1), but a better solution is to ensure the $Action block "remembers" the value of $test1 for later - something we can accomplish with a closure.

We'll need to reorganize the code slightly for this, so start by replacing the $test1 string literal with a synchronized hashtable:

$test1 = [hashtable]::Synchronized(@{
  Value = "Test variable"
})

This will allow us to do 2 things:

  • we can modify the string value without changing the identity of the object stored in $test1,
  • string value can be modified by multiple background threads without any race conditions occuring

Now we just need to create the closure from the $Action block:

$Action = {
    Write-Host $test1.Value
}.GetNewClosure()

This will bind the value of $test1 (the reference to the synchronized hashtable we just created on the line above) to the $Action block, and it will therefore "remember" that $test1 resolves to the hashtable rather than attempt (and fail) to resolve it at runtime.

  • Related