I write the following into a script module (i.e. a psm1 file) and do Import-Module for that file:
New-Variable -Name 'mytest' -Value 'Foo' -Scope 'Global'
Now I can access this variable in my PowerShell session. $mytest
yields Foo
, for example.
Alternatively, I write the following into a script module and do Import-Module for that file:
New-Variable -Name 'mytest' -Value 'Foo' -Scope 'Global' -Visibility Private
Now, the variable $mytest
is not accessible in my PowerShell session anymore, fair enough.
But if I do . {$mytest}
, I can get access on my "private" variable again - how comes? Just in case you would ask, & {$mytest}
works the same, btw.
Note 1: Please be aware, that I don't ask about the (pseudo-)scope private, but about the visibility option private, which is a difference: The scope private blocks visibility "down the invocation hierarchy", whereas the visibility option private should block visibility "up the invocation hierarchy" (at least I have understood it that way).
Note 2: The global scope in the example above is only there, to be able to access the variable $mytest
from the PowerShell session in the first attempt. Otherwise, even "free" public variables in a module ("free" meaning here, variables outside all scriptblocks) would not be accessible from the PowerShell session, except if you would use Export-ModuleMember -Variable 'mytest'
(you would be forced to explicitly export everything else from the module, too, then). But this wouldn't change anything on the result.
The good news is: I don't think the variable visibility feature is relevant in practice, at least not for PowerShell code that runs in a regular PowerShell session (typically in a console (terminal)).
As of this writing, the documentation is confusing, and seems inconsistent with the actual behavior.
about_Scopes: talks about how visibility relates to containers rather than scopes (emphasis added):
Snap-ins were binary-only module predecessors from v1, and are no longer relevant.
Scripts don't have state except while they're running, so the idea of accessing its variables from the outside generally doesn't apply (see below re dot-sourcing).
Modules do have state, but all variables in a module are hidden by default from the outside, without needing to set special variable properties. Conversely, you must explicitly request export of a variable via
Export-ModuleMember -Variable
(and also via theVariablesToExport
entry in the associated module manifest, if present).Export-ModuleMember -Function ...
call at the bottom of your script module to select the functions to export explicitly. If you do have a manifest (a satellite*.psd1
file), it is sufficient to list the functions of interest in theFunctionsToExport
entry. Note that bothExport-ModuleMember
and theFunctionsToExport
module-manifest entry accept name patterns (wildcards); thus, for instance, if you adopt a convention of using the verb-noun naming convention only for public functions in your module, aFunctionsToExport = @( '*-*' )
entry would automatically limit exporting to the functions implicitly designated as public.Therefore all visibility needs should be met even without the visibility property (set to
Private
):As stated, modules hide their variables by default, but allow explicit exporting; similarly, what functions are visible from the outside can be controlled with explicit exporting (any functions you do not export are implicitly hidden, though without explicit exporting all are visible).
When a script or function runs, it can hide its variables from descendent scopes via the
Private
option / "scope"; e.g.:If a script is designed for dot-sourcing, i.e. to be loaded directly into the current scope with the
.
operator and you selectively want to hide variables created in it, the simplest solution is to enclose such variables in& { ... }
in order to create them in a child scope that is discarded when the script block is exited; alternatively, but less robustly, you can also clean up explicitly viaRemove-Variable
As for the visibility property getting enforced, as of PowerShell 7.1:
Dot-sourcing a script from the global scope seems to be the only scenario in which invisible variables then cannot directly be accessed in the caller's scope.
Running a command such as
New-Variable -Name 'mytest' -Value 'Foo' -Scope 'Global' -Visibility Private
directly at the interactive prompt is tantamount to placing it in a script and dot-sourcing that script.Attempting to access such a variable in the global scope directly then results in a statement-terminating error:
PermissionDenied: Cannot access the variable '$mytest' because it is a private variable.
As you've observed, this restriction is trivially bypassed by accessing the variable via a script block, whether as
. { $mytest }
or as& { $mytest }
- this works with hidden commands as well.Dot-sourcing a script in a non-global scope, such as from inside another script, doesn't seem to enforce the restriction at all.
I don't know what the original design intent was, but:
I suspect that we're dealing with a bug that doesn't consistently enforce invisibility.
When it is enforced, it is curious that an invisible variable isn't simply treated as if it didn't exist, but an error is reported instead; by contrast, an invisible command indeed presents as if it didn't exist.[1]
The documentation of the feature as it relates to commands,
[System.Management.Automation.CommandInfo]
's.Visible
property, suggests that the feature is designed for out-of-runspace access (emphasis added)."Indicates if the command is to be allowed to be executed by a request external to the runspace."
The implication is that visibility isn't designed for intra-session use, given that a regular PowerShell session is confined to a single runspace - except if you explicitly create additional runspaces via the PowerShell SDK.
[1] As you've discovered, it is possible to control the visibility of commands - which includes functions - too, namely via a command's
[System.Management.Automation.CommandInfo]
representation, such as returned by theGet-Command
cmdlet; e.g.:(Get-Command myFunc).Visibility = 'Private'