Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "externalTerminal",
"console": "integratedTerminal",
},
{
"name": "PowerShell Launch Current File",
Expand All @@ -27,13 +27,6 @@
"script": "${file}",
"cwd": "${workspaceFolder}"
},
{
"name": ".NET FullCLR Attach",
"type": "clr",
"request": "attach",
"processId": "${command:pickProcess}",
"justMyCode": true,
},
{
"name": ".NET CoreCLR Attach",
"type": "coreclr",
Expand Down
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,24 @@ $message = 'world!'

## `-Variables`, `-Functions`, `-ModuleNames`, and `-ModulePaths` Parameters

- [`-Variables` Parameter](./docs/en-US/Invoke-Parallel.md#-variables): Pass variables directly to parallel runspaces.
- [`-Variables` Parameter](./docs/en-US/Invoke-Parallel.md#-variables): Pass variables directly using hashtables, multiple dictionaries, or wildcard patterns to automatically match caller variables.

```powershell
# Using a hashtable or dictionary
'hello ' | Invoke-Parallel { $_ + $msg } -Variables @{ msg = 'world!' }
# hello world!

# Using wildcards to import matching caller variables
$appUser = 'John Doe'
$appRole = 'Admin'
0..5 | Invoke-Parallel { "Processing $_ $appUser ($appRole)" } -Variables 'app*'
```

- [`-Functions` Parameter](./docs/en-US/Invoke-Parallel.md#-functions): Use local functions in parallel scopes without redefining them.
- [`-Functions` Parameter](./docs/en-US/Invoke-Parallel.md#-functions): Use local functions in parallel scopes by name or wildcard pattern without redefining them.

```powershell
function Get-Message {param($MyParam) $MyParam + 'world!' }
'hello ' | Invoke-Parallel { Get-Message $_ } -Functions Get-Message
# hello world!
function Get-Greeting { param($s) "Hello $s" }
function Get-Farewell { param($s) "Goodbye $s" }
0..5 | Invoke-Parallel { Get-Greeting $_; Get-Farewell$_ } -Functions Get-*
```

- [`-ModuleNames` Parameter](./docs/en-US/Invoke-Parallel.md#-modulenames): Import system-installed modules into parallel runspaces by name, using modules discoverable via `$env:PSModulePath`.
Expand Down
69 changes: 52 additions & 17 deletions docs/en-US/Invoke-Parallel.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Invoke-Parallel
[-InputObject <Object>]
[-ThrottleLimit <Int32>]
[-TimeoutSeconds <Int32>]
[-Variables <Hashtable>]
[-Variables <IDictionary[]>]
[-Functions <String[]>]
[-ModuleNames <String[]>]
[-ModulePaths <String[]>]
Expand Down Expand Up @@ -67,7 +67,29 @@ This example demonstrates the [`-Variables` parameter](#-variables), which passe
the parallel scope using a hashtable. The key `message` in the hashtable defines the variable name available within the
script block, serving as an alternative to the `$using:` scope modifier.

### Example 3: Adding to a thread-safe collection with `$using:`
### Example 3: Import variables using wildcards

```powershell
$appUser = 'John Doe'
$appRole = 'Admin'

0..5 | Invoke-Parallel { "Processing $_ for $appUser ($appRole)" } -Variables app*
```

This example uses wildcards with `-Variables` to dynamically import all caller variables starting with `app` (in this case `$appUser` and `$appRole`) into the parallel scope.

### Example 4: Passing multiple dictionaries to `-Variables`

```powershell
$dict1 = @{ Server = 'localhost' }
$dict2 = @{ Port = 8080 }

0..5 | Invoke-Parallel { "Connecting to $Server:$Port" } -Variables $dict1, $dict2
```

This example demonstrates passing an array of dictionaries to `-Variables`. Key/value pairs from both dictionaries are merged into the parallel scope.

### Example 5: Adding to a thread-safe collection with `$using:`

```powershell
$dict = [System.Collections.Concurrent.ConcurrentDictionary[int, object]]::new()
Expand All @@ -78,7 +100,7 @@ $dict[$PID]
This example uses a thread-safe dictionary to store process objects by ID, leveraging the `$using:` modifier for
variable access.

### Example 4: Adding to a thread-safe collection with `-Variables`
### Example 6: Adding to a thread-safe collection with `-Variables`

```powershell
$dict = [System.Collections.Concurrent.ConcurrentDictionary[int, object]]::new()
Expand All @@ -88,18 +110,18 @@ $dict[$PID]

Similar to Example 3, this demonstrates the same functionality using `-Variables` instead of `$using:`.

### Example 5: Using the `-Functions` parameter
### Example 7: Using the `-Functions` parameter with wildcards

```powershell
function Greet { param($s) "$s hey there!" }
function Get-Greeting { param($s) "Hello $s" }
function Get-Farewell { param($s) "Goodbye $s" }

0..10 | Invoke-Parallel { Greet $_ } -Functions Greet
0..5 | Invoke-Parallel { Get-Greeting $_; Get-Farewell $_ } -Functions Get-*
```

This example imports a local function `Greet` into the parallel scope using [`-Functions` parameter](#-functions),
allowing its use within the script block.
This example imports all functions matching the wildcard pattern `Get-*` from the local session into the parallel scope using the [`-Functions` parameter](#-functions).

### Example 6: Setting a timeout with `-TimeoutSeconds`
### Example 8: Setting a timeout with `-TimeoutSeconds`

```powershell
0..10 | Invoke-Parallel { Start-Sleep 1 } -TimeoutSeconds 3
Expand All @@ -108,7 +130,7 @@ allowing its use within the script block.
This example limits execution to 3 seconds, stopping all running script blocks and ignoring unprocessed input once the
timeout is reached.

### Example 7: Creating new runspaces with `-UseNewRunspace`
### Example 9: Creating new runspaces with `-UseNewRunspace`

```powershell
0..3 | Invoke-Parallel { [runspace]::DefaultRunspace.InstanceId } -ThrottleLimit 2
Expand All @@ -133,7 +155,7 @@ timeout is reached.
This example contrasts default runspace reuse with the `-UseNewRunspace` switch, showing unique runspace IDs for each
invocation in the latter case.

### Example 8: Using the `-ModuleNames` parameter
### Example 10: Using the `-ModuleNames` parameter

```powershell
Import-Csv users.csv | Invoke-Parallel { Get-ADUser $_.UserPrincipalName } -ModuleNames ActiveDirectory
Expand All @@ -142,7 +164,7 @@ Import-Csv users.csv | Invoke-Parallel { Get-ADUser $_.UserPrincipalName } -Modu
This example imports the `ActiveDirectory` module into the parallel scope using `-ModuleNames`, enabling the
`Get-ADUser` cmdlet within the script block.

### Example 9: Using the `-ModulePaths` parameter
### Example 11: Using the `-ModulePaths` parameter

```powershell
$moduleDir = Join-Path $PSScriptRoot "CustomModule"
Expand All @@ -160,10 +182,14 @@ function to be used in the parallel script block.

### -Functions

Specifies an array of function names from the local session to include in the runspaces’
Specifies an array of function names or wildcard patterns from the local session to include in the runspaces’
[Initial Session State](https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.initialsessionstate).
This enables their use within the parallel script block.

> [!NOTE]
>
> If a specified name or wildcard pattern fails to match any function in the caller's scope, a terminating error is thrown.

> [!TIP]
>
> This parameter is the recommended way to make local functions available in the parallel scope.
Expand All @@ -179,7 +205,7 @@ Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
Accept wildcard characters: True
```

### -InputObject
Expand Down Expand Up @@ -270,8 +296,17 @@ Accept wildcard characters: False

### -Variables

Provides a hashtable of variables to make available in the parallel scope. Keys define the variable names within the
script block.
Provides dictionaries (`IDictionary`), variable names, or wildcard patterns to make matching caller variables available in the parallel scope.

You can supply arguments in multiple forms:

- __Dictionaries__: Pass one or more dictionaries (e.g., hashtables). Keys specify variable names in the parallel scope. Duplicate keys across multiple dictionaries are ignored.
- __Variable Names or Wildcard Patterns__: Pass strings containing variable names or wildcard expressions (e.g., `*`, `foo*`, `[ab]*`). __Non-built-in__ variables in the caller scope matching the pattern will be imported automatically.

> [!NOTE]
>
> - When resolving variables by string/wildcard pattern, variables holding a `ScriptBlock` value are silently skipped.
> - If a string or wildcard pattern does not match any variable in the caller's scope, a terminating error is thrown.

> [!TIP]
>
Expand All @@ -287,7 +322,7 @@ Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
Accept wildcard characters: True
```

### -ModuleNames
Expand Down
2 changes: 1 addition & 1 deletion module/PSParallelPipeline.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
RootModule = 'bin/netstandard2.0/PSParallelPipeline.dll'

# Version number of this module.
ModuleVersion = '1.2.5'
ModuleVersion = '1.3.0'

# Supported PSEditions
# CompatiblePSEditions = @()
Expand Down
11 changes: 5 additions & 6 deletions src/PSParallelPipeline/Commands/InvokeParallelCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,15 @@ public sealed class InvokeParallelCommand : PSCmdlet, IDisposable

[Parameter]
[ValidateNotNullOrEmpty]
[VariableTransformation]
[SupportsWildcards]
[Alias("vars")]
public Hashtable? Variables { get; set; }
public IDictionary[]? Variables { get; set; }

[Parameter]
[ValidateNotNullOrEmpty]
[ArgumentCompleter(typeof(CommandCompleter))]
[SupportsWildcards]
[Alias("funcs")]
public string[]? Functions { get; set; }

Expand Down Expand Up @@ -83,11 +86,7 @@ protected override void BeginProcessing()

protected override void ProcessRecord()
{
if (_worker is null)
{
return;
}

if (_worker is null) return;
InputObject.ThrowIfInputObjectIsScriptBlock(this);

try
Expand Down
122 changes: 122 additions & 0 deletions src/PSParallelPipeline/ExceptionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System;
using System.IO;
using System.Management.Automation;
using Microsoft.PowerShell.Commands;

namespace PSParallelPipeline;

internal static class ExceptionExtensions
{
private const string ScriptBlockNotSupported =
"Passed-in script block variables are not supported, and can result in undefined behavior.";

private static readonly PSArgumentException PassedInVariableCannotBeScriptBlock = new(ScriptBlockNotSupported);

private static readonly PSArgumentException UsingVariableCannotBeScriptBlock = new(
$"A $using: variable cannot be a script block. {ScriptBlockNotSupported}");

private static readonly PSArgumentException InputObjectCannotBeScriptBlock = new(
$"Piped input object cannot be a script block. {ScriptBlockNotSupported}");

extension(Exception exception)
{
internal void WriteTimeoutError(PSCmdlet cmdlet) =>
cmdlet.WriteError(new ErrorRecord(
new TimeoutException("Timeout has been reached.", exception),
"TimeOutReached",
ErrorCategory.OperationTimeout,
cmdlet));

internal PSOutputData CreateProcessingTaskError() =>
PSOutputData.CreateError(new ErrorRecord(
exception, "ProcessingTask", ErrorCategory.NotSpecified, null));
}

extension(object? value)
{
internal bool IsNotScriptBlock() =>
value is not ScriptBlock and not PSObject { BaseObject: ScriptBlock };

internal void ThrowIfInputObjectIsScriptBlock(PSCmdlet cmdlet)
{
if (value.IsNotScriptBlock()) return;

ErrorRecord error = new(
InputObjectCannotBeScriptBlock,
nameof(InputObjectCannotBeScriptBlock),
ErrorCategory.InvalidType, value);

cmdlet.ThrowTerminatingError(error);
}
}

extension(PSCmdlet cmdlet)
{
internal void ThrowIfVariableIsScriptBlock(object? value)
{
if (value.IsNotScriptBlock()) return;

ErrorRecord error = new(
PassedInVariableCannotBeScriptBlock,
nameof(PassedInVariableCannotBeScriptBlock),
ErrorCategory.InvalidType, value);

cmdlet.ThrowTerminatingError(error);
}

internal void ThrowIfUsingValueIsScriptBlock(object? value)
{
if (value.IsNotScriptBlock()) return;

ErrorRecord error = new(
UsingVariableCannotBeScriptBlock,
nameof(UsingVariableCannotBeScriptBlock),
ErrorCategory.InvalidType, value);

cmdlet.ThrowTerminatingError(error);
}

internal void ThrowFunctionNotFoundError(string function)
{
Exception ex = new CommandNotFoundException(
$"Could not find any function matching the name or pattern '{function}'.");
ErrorRecord error = new(ex, "FunctionNotFound", ErrorCategory.ObjectNotFound, function);
cmdlet.ThrowTerminatingError(error);
}
}

extension(ProviderInfo provider)
{
internal void ThrowIfInvalidProvider(string path, PSCmdlet cmdlet)
{
if (provider.ImplementingType == typeof(FileSystemProvider)) return;

ErrorRecord error = new(
new NotSupportedException(
$"The resolved path '{path}' is not a FileSystem path but '{provider.Name}'."),
"NotFileSystemPath",
ErrorCategory.InvalidArgument,
path);

cmdlet.ThrowTerminatingError(error);
}
}

extension(string path)
{
internal void ThrowIfNotDirectory(PSCmdlet cmdlet)
{
if (Directory.Exists(path)) return;

ErrorRecord error = new(
new ArgumentException(
$"The specified path '{path}' does not exist or is not a directory. " +
"The path must be a valid directory containing one or more PowerShell modules."),
"NotDirectoryPath",
ErrorCategory.InvalidArgument,
path);

cmdlet.ThrowTerminatingError(error);
}
}
}
Loading