Variable scope in Powershell - powershell

I am new to Powershell and I have noticed something that I can't quite understand.
Given two scripts:
#Script 1
$test = "This is a test"
& ".\script2.ps1"
#Script 2
Write-Host $test
When I run script1 "This is a test" is printed.
My question is: why can script2 see the variable defined in script1 when the variable isn't defined as global?
The only reason I can think of is that script2 isn't called but just imported by '&' but I'm not sure this is corret.

Here & is the Call operator.
The call operator executes in a child scope and the calling scope is the parent scope. Quoting from the documentation,
The functions or scripts you call may call other functions, creating a
hierarchy of child scopes whose root scope is the global scope.
Unless you explicitly make the items private, the items in the
parent scope are available to the child scope.

Related

Global constants in Powershell

I'm refactoring some of my older PS scripts to a) improve them b) clean up c) modularize.
In the script I'm working now there are 10-15 functions that work on specific directory - let's call it work directory. Currently it's defined globally and loaded from a config file; it never changes after initialization (does that make it a constant?).
I want to wrap some of the functions in a separate module. The question is: should I rewrite them so the variable is passed explicitly as a parameter, or can I leave it as is, with the assumption that every script I use this module (library?) in will have this variable initialized? If the latter, how to make sure the module can "detect" the variable is uninitialized and throw some error?
And, last but not least, currently it's just a variable - should I use some specific construct so that it's obvious it is global, and not to be modified?
should I rewrite them so the variable is passed explicitly as a parameter
As long as there's no legitimate use case for overriding it in a single call, I wouldn't pass it as a parameter.
If your functions are packaged as a module, I'd strongly recommend utilizing module-scoped variables rather than globals.
Assuming you're talking about a script module, this is as simple as:
Set-Variable -Scope Script -Name ModuleTargetDirectory -Value $config.TargetDirectory
from inside the module file or a module function that runs during import (the script: scope is the same as module-scope inside a module), and then in the consuming functions:
function Get-Something
{
# ...
$targetDirectory = $script:ModuleTargetDirectory
# ...
}
Or wrap the entire config storage in a private helper method:
# don't export this function
function Get-MyModuleConfig
{
# assuming we stored a dictionary or custom object with the config options in a module-scoped variable named `config`
return $script:config
}
And then always just call $config = Get-MyModuleConfig in the begin block of functions that need access to the config data

Can I reuse variables defined in Powershell validation blocks?

I've made a powershell script which validates some parameters. In the process of validation I need to create some strings. I also need these strings later in the script.
To avoid rebuilding the same strings again, can I reuse variables defined within validation blocks? Perhaps I can use functions in validation blocks somehow? Or maybe global variables? I'm not sure what's possible here, or what's good practice.
Example:
Test.ps1
Function Test {
param(
[string]
[Parameter(Mandatory=$true)]
$thing1
[string]
[Parameter(Mandatory=$true)]
$thing2
[string]
[Parameter(Mandatory=$true)]
[ValidateScript({
$a = Get-A $thing1
$b = Get-B $thing2
$c = $a + $b
$d = Get-D $c
if(-not($d -contains $_)) {
throw "$_ is not a valid value for the thing3 parameter."
}
return $true
})]
$thing3
)
# Here I'd like to use $c
# At worst, calling Get-A and Get-B again may be expensive
# Or it could just be annoying duplication of code
}
Bonus question, if this is possible, could I reuse those variables in a subsequent validation block?
You could use a byref varliable.
This will affect the variable being passed to it so you could both have a return value and a parameter affected by the execution of your function.
About Ref
You can pass variables to functions by reference or by value.
When you pass a variable by value, you are passing a copy of the data.
In the following example, the function changes the value of the
variable passed to it. In PowerShell, integers are value types so they
are passed by value. Therefore, the value of $var is unchanged outside
the scope of the function.
Function Test{
Param($thing1,$thing2,[ref]$c)
$c.Value = new-guid
return $true
}
#$ThisIsC = $null
test -c ([ref] $ThisIsC)
Write-Host $ThisIsC -ForegroundColor Green
Alternatively, you can use the $script or the $global scope.
For a simple script to quickly expose your variable, the $scriptscope will do just that. A byref parameter might be easier for the end-user if you intend to distribute your function by making it clear you need to pass a reference parameter.
See About Scopes documentation.
Scopes in PowerShell have both names and numbers. The named scopes
specify an absolute scope. The numbers are relative and reflect the
relationship between scopes.
Global: The scope that is in effect when PowerShell starts. Variables
and functions that are present when PowerShell starts have been
created in the global scope, such as automatic variables and
preference variables. The variables, aliases, and functions in your
PowerShell profiles are also created in the global scope.
Local: The current scope. The local scope can be the global scope or
any other scope.
Script: The scope that is created while a script file runs. Only the
commands in the script run in the script scope. To the commands in a
script, the script scope is the local scope.
Private: Items in private scope cannot be seen outside of the current
scope. You can use private scope to create a private version of an
item with the same name in another scope.
Numbered Scopes: You can refer to scopes by name or by a number that
describes the relative position of one scope to another. Scope 0
represents the current, or local, scope. Scope 1 indicates the
immediate parent scope. Scope 2 indicates the parent of the parent
scope, and so on. Numbered scopes are useful if you have created many
recursive scopes.

get powershell variable name from actual variable

I am trying to figure out how to get the name of a powershell variable from the object, itself.
I'm doing this because I'm making changes to an object passed by reference into a function, so I don't know what the object will be and I am using the Set-Variable cmdlet to change that variable to read only.
# .__NEEDTOGETVARNAMEASSTRING is a placeholder because I don't know how to do that.
function Set-ToReadOnly{
param([ref]$inputVar)
$varName = $inputVar.__NEEDTOGETVARNAMEASSTRING
Set-Variable -Name $varName -Option ReadOnly
}
$testVar = 'foo'
Set-ToReadOnly $testVar
I've looked through a lot of similar questions and can't find anything that answers this specifically. I want to work with the variable entirely inside of the function--I don't want to rely on passing in additional information.
Also, while there may be easier/better ways of setting read-only, I have been wanting to know how to reliably pull the variable name from a variable for a long time, so please focus solving that problem, not my application of it in this example.
Mathias R. Jessen's helpful answer explains why the originating variable cannot be reliably determined if you only pass its value.
The only robust solution to your problem is to pass a variable object rather than its value as an argument:
function Set-ToReadOnly {
param([psvariable] $inputVar) # note the parameter type
$inputVar.Options += 'ReadOnly'
}
$testVar = 'foo'
Set-ToReadOnly (Get-Variable testVar) # pass the variable *object*
If your function is defined in the same scope as the calling code - which is not true if you the function is defined in a (different) module - you can more simply pass just the variable name and retrieve the variable from the parent / an ancestral scope:
# Works ONLY when called from the SAME SCOPE / MODULE
function Set-ToReadOnly {
param([string] $inputVarName)
# Retrieve the variable object via Get-Variable.
# This will implicitly look up the chain of ancestral scopes until
# a variable by that name is found.
$inputVar = Get-Variable $inputVarName
$inputVar.Options += 'ReadOnly'
}
$testVar = 'foo'
Set-ToReadOnly testVar # pass the variable *name*
As noted in this answer to a similar question, what you're asking (resolving the identity of a variable based on its value) can not be done reliably:
The simple reason being that contextual information about a variable
being referenced as a parameter argument will have been stripped away
by the time you can actually inspect the parameter value inside the
function.
Long before the function is actually called, the parser will have
evaluated the value of every single parameter argument, and
(optionally) coerced the type of said value to whatever type is
expected by the parameter it's bound to.
So the thing that is ultimately passed as an argument to the function
is not the variable $myVariable, but the (potentially coerced) value
of $myVariable.
What you could do for reference types is simply go through all variables in the calling scope and check if they have the same value:
function Set-ReadOnlyVariable {
param(
[Parameter(Mandatory=$true)]
[ValidateScript({ -not $_.GetType().IsValueType })]
$value
)
foreach($variable in Get-Variable -Scope 1 |Where-Object {$_.Value -ne $null -and $_.Value.Equals($value)}){
$variable.Options = $variable.Options -bor [System.Management.Automation.ScopedItemOptions]::ReadOnly
}
}
But this will set every single variable in the callers scope with that value to readonly, not just the variable you referenced, and I'd strongly recommend against this kind of thing - you're most likely doing something horribly wrong if you need to do this

Allowing extra parameters on a Powershell advanced function

Environmental note: I'm currently targetting PowerShell 5.1 because 6 has unrelated limitations I can't work around yet.
In the Powershell module I'm writing, there is one main function that's sort of a conglomeration of a bunch of the smaller functions. The main function has a superset of the smaller function's parameters. The idea is that calling the main function will call each smaller function with the necessary parameters specified on the main. So for example:
function Main { [CmdletBinding()] param($A,$B,$C,$D)
Sub1 -A $A -B $B
Sub2 -C $C -D $D
}
function Sub1 { [CmdletBinding()] param($A,$B)
"$A $B"
}
function Sub2 { [CmdletBinding()] param($C,$D)
"$C $D"
}
Explicitly specifying the sub-function parameters is both tedious and error prone particularly with things like [switch] parameters. So I wanted to use splatting to make things easier. Instead of specifying each parameter on the sub-function, I'll just splat $PSBoundParameters from the parent onto each sub-function like this:
function Main { [CmdletBinding()] param($A,$B,$C,$D)
Sub1 #PSBoundParameters
Sub2 #PSBoundParameters
}
The immediate problem with doing this is that the sub-functions then start throwing an error for any parameter they don't have defined such as, "Sub1 : A parameter cannot be found that matches parameter name 'C'." If I remove the [CmdletBinding()] declaration, things work but I lose all the benefits of those subs being advanced functions.
So my current workaround is to add and additional parameter on each sub-function that uses the ValueFromRemainingArguments parameter attribute like this:
function Sub1 { [CmdletBinding()]
param($A,$B,[Parameter(ValueFromRemainingArguments)]$Extra)
"$A $B"
}
function Sub2 { [CmdletBinding()]
param($C,$D,[Parameter(ValueFromRemainingArguments)]$Extra)
"$C $D"
}
Technically, this works well enough. The sub-functions get their specific params and the extras just get ignored. If I was writing this just for me, I'd move on with my life and be done with it.
But for a module intended for public consumption, there's an annoyance factor with that -Extra parameter being there. Primarily, it shows up in Get-Help output which means I have to document it even if just to say, "Ignore this."
Is there an extra step I can take to make that extra parameter effectively invisible to end users? Or am I going about this all wrong and there's a better way to allow for extra parameters on an advanced function?
My usual approach is to export only "wrapper" functions that call internal (i.e., not user-facing) functions in the module.

Is there a simple way to pass specific *named* PowerShell parameters through directly to a called function?

I am sure I read somewhere that there is an easy way to pass named parameters from a calling function to a called function without explicitly naming and specifying each parameter.
This is more than just reusing the position; I'm interested in the case where the name of the passed parameters is the same in some cases, but not in others.
I also think there is a way that is not dependent on position.
function called-func {
param([string]$foo, [string]$baz, [string]$bar)
write-debug $baz
write-host $foo,$bar
}
function calling-func {
param([int]$rep = 1, [string]$foo, [string]$bar)
1..$rep | %{
called-func -foo $foo -bar $bar -baz $rep ## <---- Should this be simpler?
}
}
calling-func -rep 10 -foo "Hello" -bar "World"
What would the method be, and is there a link?
I thought it might have been Jeffrey Snover, but I'm not sure.
In PowerShell v2 (which admittedly you may not be ready to move to yet) allows you to pass along parameters without knowing about them ahead of time:
called-func $PSBoundParameters
PSBoundParameters is a dictionary of all the parameters that were actually provided to your function. You can remove parameters you don't want (or add I suppose).
Well, I think I was confusing a blog post I read about switch parameters. As far as I can tell the best way is to just reuse the parameters like so:
called-func -foo:$foo -bar:$bar
How about
called-func $foo $bar