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):
The Visibility property of a variable or alias determines whether you can see the item outside the container in which it was created.
[...]
A container could be a module, script, or snap-in.
Items that have private visibility can be viewed and changed only in the container in which they were created. If the container is added or imported, the items that have private visibility cannot be viewed or changed.
"
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 the VariablesToExport
entry in the associated module manifest, if present).
- Note: For functions, the logic is different, because - unlike variables - all functions are exported by default. To make functions effectively private, i.e. to hide them from consumers of your module, you must switch to explicit exporting: If your module has no manifest, use an
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 the FunctionsToExport
entry. Note that both Export-ModuleMember
and the FunctionsToExport
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, a FunctionsToExport = @( '*-*' )
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.:
$private:foo = 'bar' # create variable only visible in the very same scope
"in same scope: [$foo]" # -> 'in same scope: [bar]'
& { "in child scope: [$foo]" } # -> 'in child scope: []'
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 via Remove-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 the Get-Command
cmdlet; e.g.: (Get-Command myFunc).Visibility = 'Private'