How to pass a custom function inside a ForEach-Object -Parallel - powershell

I can't find a way to pass the function. Just variables.
Any ideas without putting the function inside the ForEach loop?
function CustomFunction {
Param (
$A
)
Write-Host $A
}
$List = "Apple", "Banana", "Grape"
$List | ForEach-Object -Parallel {
Write-Host $using:CustomFunction $_
}

The solution isn't quite as straightforward as one would hope:
# Sample custom function.
function Get-Custom {
Param ($A)
"[$A]"
}
# Get the function's definition *as a string*
$funcDef = ${function:Get-Custom}.ToString()
"Apple", "Banana", "Grape" | ForEach-Object -Parallel {
# Define the function inside this thread...
${function:Get-Custom} = $using:funcDef
# ... and call it.
Get-Custom $_
}
Note: This answer contains an analogous solution for using a script block from the caller's scope in a ForEach-Object -Parallel script block.
Note: If your function were defined in a module that is placed in one of the locations known to the module-autoloading feature, your function calls would work as-is with ForEach-Object -Parallel, without extra effort - but each thread would incur the cost of (implicitly) importing the module.
The above approach is necessary, because - aside from the current location (working directory) and environment variables (which apply process-wide) - the threads that ForEach-Object -Parallel creates do not see the caller's state, notably neither with respect to variables nor functions (and also not custom PS drives and imported modules).
As of PowerShell 7.2.x, an enhancement is being discussed in GitHub issue #12240 to support copying the caller's state to the parallel threads on demand, which would make the caller's functions automatically available.
Note that redefining the function in each thread via a string is crucial, as an attempt to make do without the aux. $funcDef variable and trying to redefine the function with ${function:Get-Custom} = ${using:function:Get-Custom} fails, because ${function:Get-Custom} is a script block, and the use of script blocks with the $using: scope specifier is explicitly disallowed in order to avoid cross-thread (cross-runspace) issues.
However, ${function:Get-Custom} = ${using:function:Get-Custom} would work with Start-Job; see this answer for an example.
It would not work with Start-ThreadJob, which currently syntactically allows you to do & ${using:function:Get-Custom} $_, because ${using:function:Get-Custom} is preserved as a script block (unlike with Start-Job, where it is deserialized as a string, which is itself surprising behavior - see GitHub issue #11698), even though it shouldn't. That is, direct cross-thread use of [scriptblock] instances causes obscure failures, which is why ForEach-Object -Parallel prevents it in the first place.
A similar loophole that leads to cross-thread issues even with ForEach-Object -Parallel is using a command-info object obtained in the caller's scope with Get-Command as the function body in each thread via the $using: scope: this too should be prevented, but isn't as of PowerShell 7.2.7 - see this post and GitHub issue #16461.
${function:Get-Custom} is an instance of namespace variable notation, which allows you to both get a function (its body as a [scriptblock] instance) and to set (define) it, by assigning either a [scriptblock] or a string containing the function body.

I just figured out another way using get-command, which works with the call operator. $a ends up being a FunctionInfo object.
EDIT: I'm told this isn't thread safe, but I don't understand why.
function hi { 'hi' }
$a = get-command hi
1..3 | foreach -parallel { & $using:a }
hi
hi
hi

So I figured out another little trick that may be useful for people trying to add the functions dynamically, particularly if you might not know the name of it beforehand, such as when the functions are in an array.
# Store the current function list in a variable
$initialFunctions=Get-ChildItem Function:
# Source all .ps1 files in the current folder and all subfolders
Get-ChildItem . -Recurse | Where-Object { $_.Name -like '*.ps1' } |
ForEach-Object { . "$($_.FullName)" }
# Get only the functions that were added above, and store them in an array
$functions = #()
Compare-Object $initialFunctions (Get-ChildItem Function:) -PassThru |
ForEach-Object { $functions = #($functions) + #($_) }
1..3 | ForEach-Object -Parallel {
# Pull the $functions array from the outer scope and set each function
# to its definition
$using:functions | ForEach-Object {
Set-Content "Function:$($_.Name)" -Value $_.Definition
}
# Call one of the functions in the sourced .ps1 files by name
SourcedFunction $_
}
The main "trick" of this is using Set-Content with Function: plus the function name, since PowerShell essentially treats each entry of Function: as a path.
This makes sense when you consider the output of Get-PSDrive. Since each of those entries can be used as a "Drive" in the same way (i.e., with the colon).

Related

Why Doesn't the Command Line Argument Work When Put Directly Into a Command in a Script?

I am struggling to understand why using $args[$i] directly in this command doesn't work. It gives a completely wrong answer.
$memory=$(Get-Process | Where-Object {$_.id -eq "$($args[$i])"} | select -expand VirtualMemorySize64)
However, putting the command line argument into another variable and using that one works.
$id=($args[$i])
$memory=$(Get-Process | Where-Object {$_.id -eq "$id"} | select -expand VirtualMemorySize64)
An explanation on why this is would be great.
Every script block ({ ... }) in PowerShell has its own copy of the automatic $args array in which positionally passed arguments are automatically collected.
Therefore, $args inside {$_.id -eq "$($args[$i])"} is not the same as $args at the script level, so you indeed need to save the script-level value in an auxiliary variable first, as in your 2nd snippet, which can be streamlined as follows:
# Must use aux. variable to access the script-level $args inside
# the Where-Object script block.
$id = $args[$i]
$memory = Get-Process | Where-Object { $_.id -eq $id } |
Select-Object -ExpandProperty VirtualMemorySize64
Note the absence of superfluous (...) and $(...), and the removal of quoting around "$id", given that the .Id property of process object is a number (type [int]).
Taking a step back, I suggest declaring parameters in your script, which is preferable to using $args - the variables holding the values of such parameters can be used without a problem in Where-Object script blocks.
Generally:
It is only meaningful to access $args inside a script block that you've invoked with arguments, which is not the case in a script block passed to Where-Object, where the input to the script block comes (only) from the pipeline, via the automatic $_ variable
By contrast, you can pass arguments to a script block, if you invoke it with &, the call operator, for instance: & { "[$args]" } 'foo' yields [foo].

Get All Functions In A PowerShell Script

I have a problem similar to this question. I want to get all the functions in a given PowerShell script but the difference being I don't want to execute the contents of the script and I don't want to execute the functions.
The intent is to be able to load all the functions into the runspace to be able to pull the comment-based help from each function for documentation purposes.
Does anyone have any magical tricks to just load the functions from a .ps1 without executing all the other code within that file?
I thought about using [System.Management.Automation.PSParser]::Tokenize() to parse the script file but that's a whole lot more work than I would like to do. If someone has something easier, I'd be delighted.
# I want to load this to get the comment-based help
Function Invoke-Stuff {
<#
.SYNOPSIS
Stuff doer
.DESCRIPTION
It does lots of stuff
.EXAMPLE
Invoke-Stuff
#>
Write-Host "Stuff was done"
}
# But I don't want to execute any of this
$Items = Get-ChildItem
$Items | ForEach-Object {
Invoke-Stuff
}
The AST is the way to go for static(ish) analysis. Here's how I would do what you described
$rs = [runspacefactory]::CreateRunspace()
$rs.Open()
# Get the AST of the file
$tokens = $errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
'MyScript.ps1',
[ref]$tokens,
[ref]$errors)
# Get only function definition ASTs
$functionDefinitions = $ast.FindAll({
param([System.Management.Automation.Language.Ast] $Ast)
$Ast -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
# Class methods have a FunctionDefinitionAst under them as well, but we don't want them.
($PSVersionTable.PSVersion.Major -lt 5 -or
$Ast.Parent -isnot [System.Management.Automation.Language.FunctionMemberAst])
}, $true)
# Add the functions into the runspace
$functionDefinitions | ForEach-Object {
$rs.SessionStateProxy.InvokeProvider.Item.Set(
'function:\{0}' -f $_.Name,
$_.Body.GetScriptBlock())
}
# Get help within the runspace.
$ps = [powershell]::Create().AddScript('Get-Help MyFunction')
try {
$ps.Runspace = $rs
$ps.Invoke()
} finally {
$ps.Dispose()
}
You could also use the $tokens from near the top if you want to go a purely static route. The comments won't be in the AST but they will be in the tokens.
Edit The method above actually loses comment help somewhere in the process, not because of the runspace but just because of how the function is assigned. Likely due to the comments not really being a part of the AST. In any case there is a more direct and more static way to obtain the help.
Instead of defining the functions, you can use the GetHelpContent method on FunctionDefinitionAst
$helpContent = $functionDefinitions | ForEach-Object { $_.GetHelpContent() }
This will return a CommentHelpInfo object for each function. It's important to note that this is not the same object returned by the Get-Help cmdlet. Most notably it does not distinguish between things like the code and the description in an example block. However, if you need the CBH to be parsed as normal you can get the comment block text and define your own fake version.
$helpContent = $functionDefinitions | ForEach-Object {
# Get the plain string comment block from the AST.
$commentBlock = $_.GetHelpContent().GetCommentBlock()
# Create a scriptblock that defines a blank version of the
# function with the CBH. You may lose some parameter info
# here, if you need that replace param() with
# $_.Body.ParamBlock.Extent.Text
$scriptBlock = [scriptblock]::Create(('
function {0} {{
{1}
param()
}}' -f $_.Name, $commentBlock))
# Dot source the scriptblock in a different scope so we can
# get the help content but still not pollute the session.
& {
. $scriptBlock
Get-Help $_.Name
}
}
Ideally, you'd have your functions in a module (or their own script file), which you could load. Your 'execution' script would then be it's own thing that you only run when you do want to execute the function, or you run the functions manually.
If you had your functions in a module, in one of the paths that PowerShell looks to for them, you'd be able to run this to see the functions:
Get-Command -Module Example -CommandType Function
Most of the time you wouldn't include the CommandType parameter, unless there was extra stuff in the module you didn't care about.
This module approach (or separation of function/execution) would be the only way to get your comment based help working as you'd expect it to work.
If you're happy to just see the names of your functions, you'd have to load the content of the script file, and check for lines starting with the function keyword. There's probably smarter ways to do this, but that's where my mind went to.
Just to be a bit more clear about what I'm getting at around separating the functions from the execution code, it'd look something like:
functions.ps1
# I want to load this to get the comment-based help
Function Invoke-Stuff {
<#
.SYNOPSIS
Stuff doer
.DESCRIPTION
It does lots of stuff
.EXAMPLE
Invoke-Stuff
#>
Write-Host "Stuff was done"
}
You can then freely load the function into your session, making the comment based help accessible.
execute.ps1
. .\path\functions.ps1
# But I don't want to execute any of this
$Items = Get-ChildItem
$Items | ForEach-Object {
Invoke-Stuff
}

Powershell - clarification about foreach

I am learning powershell and I need someone to give me an initial push to get me through the learning curve. I am familiar with programming and dos but not powershell.
What I would like to do is listing all files from my designated directory and pushing the filenames into an array. I am not very familiar with the syntax and when I tried to run my test I was asked about entering parameters.
Could someone please enlighten me and show me the correct way to get what I want?
This is what powershell asked me:
PS D:\ABC> Test.ps1
cmdlet ForEach-Object at command pipeline position 2
Supply values for the following parameters:
Process[0]:
This is my test:
[string]$filePath = "D:\ABC\*.*";
Get-ChildItem $filePath | foreach
{
$myFileList = $_.BaseName;
write-host $_.BaseName
}
Why was ps asking about Process[0]?
I would want to ps to list all the files from the directory and pipe the results to foreach where I put each file into $myFileList array and print out the filename as well.
Don't confuse foreach (the statement) with ForEach-Object (the cmdlet). Microsoft does a terrible job with this because there is an alias of foreach that points to ForEach-Object, so when you use foreach you have to know which version you're using based on how you're using it. Their documentation makes this worse by further conflating the two.
The one you're trying to use in your code is ForEach-Object, so you should use the full name of it to differentiate it. From there, the issue is that the { block starts on the next line.
{} is used in PowerShell for blocks of code related to statements (like while loops) but is also used to denote a [ScriptBlock] object.
When you use ForEach-Object it's expecting a scriptblock, which can be taken positionally, but it must be on the same line.
Conversely, since foreach is a statement, it can use its {} on the next line.
Your code with ForEach-Object:
Get-ChildItem $filePath | ForEach-Object {
$myFileList = $_.BaseName;
write-host $_.BaseName
}
Your code with foreach:
$files = Get-ChildItem $filePath
foreach ($file in $Files)
{
$myFileList = $file.BaseName;
write-host $file.BaseName
}

Is there an equivalent of "this" in powershell?

Basically I have this code:
$file = $web.GetFile("Pages/default.aspx")
$file.CheckOut()
and I was wondering if there is anyway to use a pipe and the powershell equivalent of this to rewrite it as:
$web.GetFile("Pages/default.aspx") | $this.CheckOut()
When I try this I get the error:
Expressions are only allowed as the first element of a pipeline.
I also tried using $_ instead of $this but got the same error.
Actually there is a $this in a few cases. You can create a ScriptProperty or ScriptMethod and attach it to an object, and $this will be the original object. You can then define these in types files (I'd recommend using the module EZOut, it makes life much easier) so that any time you see that type, you get that method.
For example:
$Web | Add-Member ScriptMethod EditFile { $this.Checkout() }
Hope this helps
What you're looking for is $_ and it represents the current object in the pipeline. However you can only access $_ in a scriptblock of a command that takes pipeline input e.g.:
$web.GetFile("Pages/default.aspx") | Foreach-Object -Process {$_.Checkout()}
However there are aliases for the Foreach-Object cmdlet {Foreach and %} and -Process is the default parameter so this can be simplified to:
$web.GetFile("Pages/default.aspx") | Foreach {$_.Checkout()}
One other point, the GetFile call appears to return a single file so in this case, the following would be the easiest way to go:
$web.GetFile("Pages/default.aspx").Checkout()
Of course, at this point you no longer have a variable containing the file object.
$_ is the variable for "current object" in powershell.
However, you aren't passing any data, this is just variable assignment. You can only use the pipeline if you manipulate the actual output of a command and use it as input down the pipeline.
I think what you want can be accomplish with nested parentheses:
($web.GetFile("Pages/default.aspx")).CheckOut()
In PS, anything you put inside parentheses gets treated as its own object, and you can apply methods to that inline without variable reassignment.
Assignment does silence the default output, but it does not prevent an object from being further referenced.
($file = $web.GetFile("Pages/default.aspx")).CheckOut()
Of course, it's much more common to either store the return value in a variable and do stuff with it or chain methods/properties/pipes.

How to transpose data in powershell

I have a file that looks like this:
a,1
b,2
c,3
a,4
b,5
c,6
(...repeat 1,000s of lines)
How can I transpose it into this?
a,b,c
1,2,3
4,5,6
Thanks
Here's a brute-force one-liner from hell that will do it:
PS> Get-Content foo.txt |
Foreach -Begin {$names=#();$values=#();$hdr=$false;$OFS=',';
function output { if (!$hdr) {"$names"; $global:hdr=$true}
"$values";
$global:names=#();$global:values=#()}}
-Process {$n,$v = $_ -split ',';
if ($names -contains $n) {output};
$names+=$n; $values+=$v }
-End {output}
a,b,c
1,2,3
4,5,6
It's not what I'd call elegant but should get you by. This should copy/paste correctly as-is. However if you reformat it to what is shown above you will need put back-ticks after the last curly on both the Begin and Process scriptblocks. This script requires PowerShell 2.0 as it relies on the new -split operator.
This approach makes heavy use of the Foreach-Object cmdlet. Normally when you use Foreach-Object (alias is Foreach) in the pipeline you specify just one scriptblock like so:
Get-Process | Foreach {$_.HandleCount}
That prints out the handle count for each process. This usage of Foreach-Object uses the -Process scriptblock implicitly which means it executes once for each object it receives from the pipeline. Now what if we want to total up all the handles for each process? Ignore the fact that you could just use Measure-Object HandleCount -Sum to do this, I'll show you how Foreach-Object can do this. As you see in the original solution to this problem, Foreach can take both a Begin scriptblock that is executed once for the first object in the pipeline and a End scripblock that executes when there are no more objects in the pipeline. Here's how you can total the handle count using Foreach-Object:
gps | Foreach -Begin {$sum=0} -Process {$sum += $_.HandleCount } -End {$sum}
Relating this back to the problem solution, in the Begin scriptblock I initialize some variables to hold the array of names and values as well as a bool ($hdr) that tells me whether or not the header has been output (we only want to output it once). The next mildly mind blowing thing is that I also declare a function (output) in the Begin scriptblock that I call from both the Process and End scriptblocks to output the current set of data stored in $names and $values.
The only other trick is that the Process scriptblock uses the -contains operator to see if the current line's field name has already been seen before. If so, then output the current names and values and reset those arrays to empty. Otherwise just stash the name and value in the appropriate arrays so they can be saved later.
BTW the reason the output function needs to use the global: specifier on the variables is that PowerShell performs a "copy-on-write" approach when a nested scope modifies a variable defined outside its scope. However when we really want that modification to occur at the higher scope, we have to tell PowerShell that by using a modifier like global: or script:.