|
| 1 | +# Understanding Scoping & Context |
| 2 | + |
| 3 | +Each powershell process can have multiple runspaces, each runspace has its own session state and scopes, session states and scopes can't be accessed across runspaces. |
| 4 | + |
| 5 | +## Scope |
| 6 | + |
| 7 | +- Scope nesting: each scope can have parent and child scopes, function and scriptblock creates its own scope in the hierarchy. |
| 8 | + ```ps1 |
| 9 | + function bar { echo $foo } |
| 10 | + function foo { |
| 11 | + $foo = 'foo' # defaults to $local: scope |
| 12 | + & bar # bar can read the context of foo because it's the child scope of foo's |
| 13 | + } |
| 14 | + ``` |
| 15 | +- Named scopes: |
| 16 | + - `$global:`: top level parent scope. |
| 17 | + - `$local:`: current scope, barely useful. |
| 18 | + - `$script:`: scope of the nearest script invoked, falls back to `$global:` if no script was found. |
| 19 | + - the default scope of a script file is `$script:` |
| 20 | + - `$script:` is the most magical scope |
| 21 | + - sourcing a script can treat scoped members within it as declared locally. |
| 22 | + - there's no way to prevent sourcing members from script as locals, even with `$private:`. |
| 23 | + - `$private:`: an accessor keyword to mark only accessible to current scope. |
| 24 | + - it looks like a scope name, but just an option to current scope. |
| 25 | + - use `New-Alias -Option Private` to create one private alias, variable and function can use direct `$private:` accessor. |
| 26 | + ```ps1 |
| 27 | + function foo { |
| 28 | + $foo = 'foo' |
| 29 | + $private:bar = 'bar' # [!code highlight] |
| 30 | + & bar |
| 31 | + } |
| 32 | + function bar { |
| 33 | + Write-Output "bar is null? $($null -eq $bar)" # [!code highlight] |
| 34 | + Write-Output $foo |
| 35 | + } |
| 36 | + & foo # bar is null? True foo |
| 37 | + ``` |
| 38 | + - `$using:`: represents a **copy** of that variable value, expanding it to remote command or background job which runs on a different process. |
| 39 | + - you can't re-assign or alter the original value within the process because it's a copy. |
| 40 | + - powershell transfer the value by xml-based serialization from process to process or remote to local mutually. |
| 41 | + ```ps1 |
| 42 | + $foo = 1 |
| 43 | + $wrapped = Get-Variable foo |
| 44 | + # thread job can alter the value by [psvariable] instance |
| 45 | + # this is not possible for background job or remote command! |
| 46 | + Start-ThreadJob { ($using:wrapped).Value += 1 } | Receive-Job -Wait # [!code highlight] |
| 47 | + $foo # 2 |
| 48 | + ``` |
| 49 | + - driver scopes: containers created from **PSDrive**, may also be accessed by path syntax. |
| 50 | + - `$env:` environment variables for **current scope** |
| 51 | + - `$function:`: functions declared for **current scope** |
| 52 | + - `$alias:`: alias declared for **current scope** |
| 53 | + - `$variable:`: variables declared for **current scope** |
| 54 | +
|
| 55 | +## Module Scope |
| 56 | +
|
| 57 | +Module has their own scope even after being imported to a runspace. That is, for example, the parent scope of a function/scriptblock imported from module is the module scope. |
| 58 | +
|
| 59 | +```ps1 |
| 60 | +# inside module Foo |
| 61 | +$foo = 'foo from module' |
| 62 | +function foo { |
| 63 | + $foo # points to $foo from module scope |
| 64 | +} |
| 65 | +# variable $foo is not exported |
| 66 | +Export-ModuleMember -Function foo |
| 67 | +
|
| 68 | +# during a pwsh session |
| 69 | +Import-Module Foo |
| 70 | +$foo = 'foo from global' |
| 71 | +& foo # foo from module # [!code highlight] |
| 72 | +``` |
| 73 | + |
| 74 | +## ScriptBlock Context Injection |
| 75 | + |
| 76 | +Extra members can be injected into the context of a scriptblock using `ScriptBlock.InvokeWithContext` method. |
| 77 | +So you can reference the functions or variable within the scriptblock. |
| 78 | + |
| 79 | +- Parameters |
| 80 | + - `functions: IDictionary[string, scriptblock]`: functions to inject |
| 81 | + - `variables: List[psvariable]`: variables to inject |
| 82 | + - `params args: object[]`: arguments for the scriptblock |
| 83 | + |
| 84 | +```ps1 |
| 85 | +{ foofunc $foo; }.InvokeWithContext(@{ foofunc = { echo $args[0] } }, [psvariable]::new('foo', 123)) |
| 86 | +# 123 |
| 87 | +``` |
| 88 | + |
| 89 | +> [!NOTE] |
| 90 | +> See [ScriptBlock.InvokeWithContext Method](https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.scriptblock.invokewithcontext?view=powershellsdk-1.1.0) |
| 91 | +
|
| 92 | +### Delay-bind Parameter |
| 93 | + |
| 94 | +Context injection is useful when implementing a cmdlet with delay-bind parameters such as `Foreach-Object -Process` and `Where-Object -Filter`, where you can access `$_` within the given scriptblock. |
| 95 | +But the given scriptblock doesn't share the same scope of `process` block of a pipeline cmdlet **when the cmdlet came from a module**, because the scriptblock was created from global scope while the `process` block was inside the module scope. |
| 96 | +The solution is obvious that to use `ScriptBlock.InvokeWithContext` to inject `$_` into the context of the given scriptblock. |
| 97 | + |
| 98 | +```ps1 |
| 99 | +function all { |
| 100 | + param ( |
| 101 | + [Parameter(ValueFromPipeline)] |
| 102 | + [psobject]$InputObject, |
| 103 | + [Parameter(Position = 1, Mandatory)] |
| 104 | + [scriptblock]$Condition |
| 105 | + ) |
| 106 | +
|
| 107 | + process { |
| 108 | + if (-not $Condition.InvokeWithContext($null, [psvariable]::new('_', $_))) { # [!code highlight] |
| 109 | + $false |
| 110 | + break |
| 111 | + } |
| 112 | + } |
| 113 | +
|
| 114 | + end { $true } |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +> [!IMPORTANT] |
| 119 | +> It's worth noting that `Where-Object` and `Foreach-Object` executes the given scriptblock in a global context, meaning that you can alter the members from global scope. |
| 120 | +> But it's not available for cmdlet using `ScriptBlock.InvokeWithContext`, the context is always local to the scriptblock itself, should use `$script:` scope to access members instead. |
| 121 | +>```ps1 |
| 122 | +>$foo = 1 |
| 123 | +>1..5 | foreach { $foo++ } |
| 124 | +>$foo # 6 |
| 125 | +> |
| 126 | +>$foo = 1 |
| 127 | +>1..5 | any { $foo++ } |
| 128 | +>$foo # 1 |
| 129 | +> |
| 130 | +>$script:foo = 1 |
| 131 | +>1..5 | any { $script:foo++ } |
| 132 | +>$foo # 6 |
| 133 | +>``` |
0 commit comments