I'm trying to make a simple function to create Variables in powershell with generated name
I'm using this code :
function createAnewVariable {
[CmdletBinding()]
Param(
[String]$Name
)
New-Variable -Name GenLink$Name -Value "AMIGA" -Option AllScope
}
createAnewVariable -Name "AutoVAR"
Outside a function it's working perfectly when i do a get-variable i can see my automatic created variables like $GenLinkName
but inside a function i don't have access to the varaible in the list and i can't call it !
oO
It's maybe simple but i don't understand why :)
Someone can tell me where i'm wrong please ?
Variables in PowerShell are scoped, and defaults to the current local scope, ie. the function body. The AllScope option does not make it available to antecedent scopes, it simply ensures PowerShell copies the variable into any new child scope created from the current scope.
To make the variable available in the parent scope, use -Scope 1 when calling New-Variable:
function createAnewVariable {
[CmdletBinding()]
Param(
[String]$Name
)
New-Variable -Name "GenLink$Name" -Value "AMIGA" -Scope 1
}
createAnewVariable -Name "AutoVAR"
# This is now available in the calling scope
$GenLinkAutoVAR
Be aware that if the function is module-scoped (eg. the function is part of a module), the parent scope will be the module scope, meaning all other functions in the same module will be able to see it too.
Related
I'm writing a PowerShell script that runs a couple of background jobs. Some of these background jobs will use the same set of constants or utility functions, like so:
$FirstConstant = "Not changing"
$SecondConstant = "Also not changing"
function Do-TheThing($thing)
{
# Stuff
}
$FirstJob = Start-Job -ScriptBlock {
Do-TheThing $using:FirstConstant
}
$SecondJob = Start-Job -ScriptBlock {
Do-TheThing $using:FirstConstant
Do-TheThing $using:SecondConstant
}
If I wanted to share variables (or, in this case, constants) in child scopes, I'd prefix the variable references with $using:. I can't do that with functions, though; running this code as-is returns an error:
The term 'Do-TheThing' is not recognized as the name of a cmdlet, function, script file, or operable program.
My question is this: How can my background jobs use a small utility function that I've defined in a higher scope?
If the function in the higher scope is in the same (non-)module scope in the same session, your code implicitly sees it, due to PowerShell's dynamic scoping.
However, background jobs run in a separate process (child process), so anything from the caller's scope must be passed explicitly to this separate session.
This is trivial for variable values, with the $using: scope, but less obvious for functions, but it can be made to work with a bit of duplication, by passing a function's body via namespace variable notation:
# The function to call from the background job.
Function Do-TheThing { param($thing) "thing is: $thing" }
$firstConstant = 'Not changing'
Start-Job {
# Define function Do-TheThing here in the background job, using
# the caller's function *body*.
${function:Do-TheThing} = ${using:function:Do-TheThing}
# Now call it, with a variable value from the caller's scope
Do-TheThing $using:firstConstant
} | Receive-Job -Wait -AutoRemoveJob
The above outputs 'thing is: Not changing', as expected.
Consider the following arbitrary function and test cases:
Function Foo-MyBar {
Param(
[Parameter(Mandatory=$false)]
[ScriptBlock] $Filter
)
if (!$Filter) {
$Filter = { $true }
}
#$Filter = $Filter.GetNewClosure()
Get-ChildItem "$env:SYSTEMROOT" | Where-Object $Filter
}
##################################
$private:pattern = 'T*'
Get-Help Foo-MyBar -Detailed
Write-Host "`n`nUnfiltered..."
Foo-MyBar
Write-Host "`n`nTest 1:. Piped through Where-Object..."
Foo-MyBar | Where-Object { $_.Name -ilike $private:pattern }
Write-Host "`n`nTest 2:. Supplied a naiive -Filter parameter"
Foo-MyBar -Filter { $_.Name -ilike $private:pattern }
In Test 1, we pipe the results of Foo-MyBar through a Where-Object filter, which compares the objects returned to a pattern contained in a private-scoped variable $private:pattern. In this case, this correctly returns all the files/folders in C:\ which start with the letter T.
In Test 2, we pass the same filtering script directly as a parameter to Foo-MyBar. However, by the time Foo-MyBar gets to running the filter, $private:pattern is not in scope, and so this returns no items.
I understand why this is the case -- because the ScriptBlock passed to Foo-MyBar is not a closure, so does not close over the $private:pattern variable and that variable is lost.
I note from comments that I previously had a flawed third test, which tried to pass {...}.GetNewClosure(), but this does not close over private-scoped variables -- thanks #PetSerAl for helping me clarify that.
The question is, how does Where-Object capture the value of $private:pattern in Test 1, and how do we achieve the same behaviour in our own functions/cmdlets?
(Preferably without requiring the caller to have to know about closures, or know to pass their filter script as a closure.)
I note that, if I uncomment the $Filter = $Filter.GetNewClosure() line inside Foo-MyBar, then it never returns any results, because $private:pattern is lost.
(As I said at the top, the function and parameter are arbitrary here, as a shortest-form reproduction of my real problem!)
The example given does not work because calling a function will enter a new scope by default. Where-Object will still invoke the filter script without entering one, but the scope of the function does not have the private variable.
There's three ways around this.
Put the function in a different module than the caller
Every module has a SessionState which has its own stack of SessionStateScopes. Every ScriptBlock is tied to the SessionState is was parsed in.
If you call a function defined in a module, a new scope is created within that module's SessionState, but not within the top level SessionState. Therefore when Where-Object invokes the filter script without entering a new scope, it does so on the current scope for the SessionState to which that ScriptBlock is tied.
This is a bit fragile, because if you want to call that function from your module, well you can't. It'll have the same issue.
Call the function with the dot source operator
You most likely already know the dot-source operator (.) for invoking script files without creating a new scope. That also works with command names and ScriptBlock objects.
. { 'same scope' }
. Foo-MyBar
Note, however, that this will invoke the function within the current scope of the SessionState that the function is from, so you cannot rely on . to always execute in the caller's current scope. Therefore, if you invoke functions associated with a different SessionState with the dot-source operator - such as functions defined in a (different) module - it may have unintended effects. Variables created will persist to future function invocations and any helper functions defined within the function itself will also persist.
Write a Cmdlet
Compiled commands (cmdlets) do not create a new scope when invoked. You can also use similar API's to what Where-Object use (though not the exact same ones)
Here's a rough implementation of how you could implement Where-Object using public API's
using System.Management.Automation;
namespace MyModule
{
[Cmdlet(VerbsLifecycle.Invoke, "FooMyBar")]
public class InvokeFooMyBarCommand : PSCmdlet
{
[Parameter(ValueFromPipeline = true)]
public PSObject InputObject { get; set; }
[Parameter(Position = 0)]
public ScriptBlock FilterScript { get; set; }
protected override void ProcessRecord()
{
var filterResult = InvokeCommand.InvokeScript(
useLocalScope: false,
scriptBlock: FilterScript,
input: null,
args: new[] { InputObject });
if (LanguagePrimitives.IsTrue(filterResult))
{
WriteObject(filterResult, enumerateCollection: true);
}
}
}
}
how does Where-Object capture the value of $private:pattern in Test 1
As can be seen in the source code for Where-Object in PowerShell Core, PowerShell internally invokes the filter script without confining it to its own local scope (_script is the private backing field for the FilterScript parameter, notice the useLocalScope: false argument passed to DoInvokeReturnAsIs()):
protected override void ProcessRecord()
{
if (_inputObject == AutomationNull.Value)
return;
if (_script != null)
{
object result = _script.DoInvokeReturnAsIs(
useLocalScope: false, // <-- notice this named argument right here
errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToCurrentErrorPipe,
dollarUnder: InputObject,
input: new object[] { _inputObject },
scriptThis: AutomationNull.Value,
args: Utils.EmptyArray<object>());
if (_toBoolSite.Target.Invoke(_toBoolSite, result))
{
WriteObject(InputObject);
}
}
// ...
}
how do we achieve the same behaviour in our own functions/cmdlets?
We don't - DoInvokeReturnAsIs() (and similar scriptblock invocation facilities) are marked internal and can therefore only be invoked by types contained in the System.Management.Automation assembly
I have a powershell function inside a file myfunc.ps1
function Set-Util ($utilPath ) {
if(Test-Path($utilPath) ){
$fullPath = Join-Path -Path $utilPath "util.exe"
set-alias MyUtil $fullPath
#echo "Path set to $fullPath"
}else {
throw (" Error: File not found! File path '$fullPath' does not exist.")
}
}
and I dot call it from command line with
. .\myfunc.ps1
then call
Set-Util somedirectory
The alias is being set correctly in the function, but I can't access it out here with
MyUtil
Should I export the alias because the scope is only in the method?
I tried doing so with
Export-ModuleMember but got an error saying the cmdlet can only be called from insdie a module.
You can't do that because the alias won’t be set until the function is called, and when the function is called the alias is scoped to the function scope, so when the function finishes running - the alias is gone.
If you want the alias to survive, specify use scope parameter with a value of 'global'
Aliases can only point to cmdlets, not to output of cmdlets (objects).
I'm using a function which creates some variables I'd like to use after the function has processed.
I've tried accessing them directly but I can't. How would I go about doing this?
Variables inside functions don't survive after the function has been run. If you want to access them after the function has processed, prefix them with a scope modifier.
PS> function test-var{ $script:var='foo' }
PS> test-var # excute the function
PS> $var #print var
foo
Type this in your console for more information:
PS> Get-Help about_Scopes
as Shay pointed out you can create what are called global variables inside the function scope that will be available at higher level scopes. However global variables are generally not a good idea and I'd like to suggest some alternatives for you.
This is from the wikipedia global variable page:
They are usually considered bad practice precisely because of their
non-locality: a global variable can potentially be modified from
anywhere (unless they reside in protected memory or are otherwise
rendered read-only), and any part of the program may depend on it.[1]
A global variable therefore has an unlimited potential for creating
mutual dependencies, and adding mutual dependencies increases
complexity.
Some alternatives:
Make the function return data needed by the caller. Powershell functions should generally return the data related to the noun in the Powershell function naming convention of Verb-Noun. If you need to return other data not associated to the noun consider making a second function.
function Get-Directories {
param ([string] $Path)
# Code to get or create objects here.
$dirs = Get-ChildItem -Path $Path | where {$_.PsIsContainer}
# Explicitly return data to the caller.
return $dirs
}
$myDirs = Get-Directories -Path 'C:\'
Use a reference variable. A reference passes a variable's address in memory to a function. When the function changes the variable's data it will be accessible outside of the function but the variable's scope will not of been changed.
function Get-Directories {
param ([string] $Path, [ref] $Directories)
$Directories.Value = Get-ChildItem -Path $Path | where {$_.PsIsContainer}
}
$myDirs = $null
Get-Directories -Path 'C:\' -Directories ([ref] $myDirs)
Hope this helps. Happy coding :-)
If you run your function with . , the function will execute in your scope, and all of the variables defined within the function will be available to the caller.
i.e.
. $func
$myPrivateVariable # Now Set in the parent scope.
If the function is in a module, you can also use this sort of trick to access the module's scope:
$m =Get-Module myModule
. $m { $myPrivateModuleVariable }
Hope this Helps
I've noticed that most Powershell advanced functions declare parameters of standard datatypes (string, int, bool, xml, array, hashtable, etc.) which map to specific .Net types.
How would one declare an advanced function parameter using another .Net data type? For example, here is a contrived example:
function Do-Something
{
[CmdletBinding()]
Param(
[System.Xml.XPathNodeList] $nodeList
)
Begin {}
Process
{
Foreach ($node in $nodeList)
{
Write-Host $node
}
}
End {}
}
# Prepare to call the function:
$xml = [xml](get-content .\employee.xml)
$nodeList = $xml.SelectNodes("//age")
# Call the function passing an XPathNodeList:
do-something $nodeList
Invoking this function results in the following runtime error:
Unable to find type [System.Xml.XPathNodeList]: make sure that the assembly
containing this type is loaded.
Can this be accomplished with LoadWithPartialName()? How?
Assuming this is possible, here's an ancillary question: would using non-standard types this way go against 'best practice'?
It is OK to use custom .NET types as long as you use something like the cmdlet Add-Type to load the assembly that defines the custom type. However in this case, the assembly, System.Xml, is already loaded. Your problem arises because the type you specified is a private type i.e. only visible within the System.Xml assembly.
PS> $nodeList.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
False False XPathNodeList System.Xml.XmlNodeList
Use its public base class instead:
[CmdletBinding()]
Param(
[Parameter()]
[System.Xml.XmlNodeList]
$nodeList
)
You shouldn't have any issues with using standard .NET objects as function parameters - the error you're getting is associated with unloaded assemblies, and that's where I'd look. Check your profile to make sure there's nothing unusual happening - see http://msdn.microsoft.com/en-us/library/bb613488%28v=vs.85%29.aspx for details.
If it really comes to it, you can use the following to load System.Xml (casting to Void to suppress the text output of loading):
[Void][System.Reflection.Assembly]::LoadWithPartialName("System.Xml")