One question for you is here ;)
I have this function:
function Set-DbFile {
param(
[Parameter(ValueFromPipeline=$true)]
[System.IO.FileInfo[]]
$InputObject,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[scriptblock]
$Properties
)
process {
$InputObject | % {
Write-Host `nInside. Storing $_.Name
$props = & $Properties
Write-Host ' properties for the file are: ' -nonew
write-Host ($props.GetEnumerator()| %{"{0}-{1}" -f $_.key,$_.Value})
}
}
}
Look at the $Properties
. It should be evaluated for each file and then the file and the properties should be processed further.
Example how to use it might be:
Get-ChildItem c:\windows |
? { !$_.PsIsContainer } |
Set-DbFile -prop {
Write-Host Creating properties for $_.FullName
@{Name=$_.Name } # any other properties based on the file
}
When I copy & paste function Set-dbFile
to command line and run the example snippet, everything is fine.
However, when I store the function in a module, import it and run the example, the $_
variable is empty. Does anybody know why? And how to solve it? (other solutions are welcome as well)
Results for function defined in a script/typed in commandline:
Inside. Storing adsvw.ini
Creating properties for C:\windows\adsvw.ini
properties for the file are: Name-adsvw.ini
Inside. Storing ARJ.PIF
Creating properties for C:\windows\ARJ.PIF
properties for the file are: Name-ARJ.PIF
....
Results for function defined in module:
Inside. Storing adsvw.ini
Creating properties for
properties for the file are: Name-
Inside. Storing ARJ.PIF
Creating properties for
properties for the file are: Name-
....
The problem here is down to scope hierarchy. If you define two functions like...
Then F2 will inherit the variable scope of F1 since it's called from F1's scope. If you define function F2 in a module and export the function the $test variable is not available since the module has it's own scope tree. See the Powershell Language Specification (Section 3.5.6):
In your case the current node variable is defined in the local scope and hence it will not survive into the module scope since it's in a different tree with a different scope root (apart from global variables).
To quote the text on the GetNewClosure() method in the Powershell Language Specification (Section 4.3.7):
...hence GetNewClosure() works a treat since it bridges the local scope/module divide. I hope this helps.
Looks like
GetNewClosure()
is as good a work around as any, but it changes the way the script block sees those variables. Passing$_
to the scriptblock as an argument works, too.It has nothing to do with normal scope issues (e.g., global vs local), but it appears like that at first. Here's my very simplified reproduction and some explanation following:
script.ps1
for normal dot-sourcing:Module\MyTest\MyTest.psm1
for importing:Calls and output:
So I started hunting around since this piqued my curiosity, and I found a few interesting things.
This Q&A, which also features a link to this bug report is pretty much the exact same topic, as are some other blog articles I ran across. But while it was reported as a bug, I disagree.
The about_Scopes page has this to say (w:
Now I understand the behavior, but it was the above and a few more experiments that led me to it:
$message
in the scriptblock to$local:message
then all 3 tests have a blank space, because$message
is not defined in the scriptblock's local scope.$global:message
, all 3 tests printoutside
.$script:message
, the first 2 tests printoutside
and the last printsinside
.Then I also read this in
about_Scopes
:$((get-variable -name message -scope 1).value)
in order to attempt getting the value from the immediate parent scope, what happens? We still getoutside
rather thaninside
.At this point it was clear enough to me that sessions and modules have their own declaration scope or context of sorts, at least for script blocks. The script blocks act like anonymous functions in the environment in which they're declared until you call
GetNewClosure()
on them, at which point they internalize copies of the variables they reference of the same name in the scope whereGetNewClosure()
was called (using locals first, up to globals). A quick demonstration:I hope this helps.
Addendum: Regarding design.
JasonMArcher's comment made me think about a design issue with the scriptblock being passed into the module. In the code of your question, even if you use the
GetNewClosure()
workaround, you have to know the name of the variable(s) where the scriptblock will be executed in order for it to work.On the other hand, if you used parameters to the scriptblock and passed
$_
to it as an argument, the scriptblock does not need to know the variable name, it only needs to know that an argument of a particular type will be passed. So your module would use$props = & $Properties $_
instead of$props = & $Properties.GetNewClosure()
, and your scriptblock would look more like this:See CosmosKey's answer for further clarification.
I believe you need to call getnewclosure() on that script block before you run it. Called from a script file or module, script blocks are evaluated at compile time. When you work from the console, there is no "compile time". It's evaluated at run time, so it behaves differenly there than when it's in the module.