12

I have a PowerShell script that accepts 3 named parameters. Please let me know how to pass the same from command line. I tried below code but same is not working. It assigns the entire value to P3 only. My requirement is that P1 should contain 1, P2 should 2 and P3 should be assigned 3.

Invoke-Command -ComputerName server -FilePath "D:\test.ps1" -ArgumentList  {-P1 1 -P2 2 -P3 3}

Ihe below is script file code.

Param (
    [string]$P3,
    [string]$P2,
    [string]$P1
)
Write-Output "P1 Value :" $P1
Write-Output "P2 Value:" $P2
Write-Output "P3 Value :" $P3
Matt
  • 45,022
  • 8
  • 78
  • 119
Parveen Kumar
  • 419
  • 3
  • 7
  • 20
  • possible duplicate of [How do I pass named parameters with Invoke-Command?](http://stackoverflow.com/questions/4225748/how-do-i-pass-named-parameters-with-invoke-command) – Loïc MICHEL Jan 06 '15 at 09:42

7 Answers7

19

One option:

$params = @{
P1 = 1
P2 = 2 
P3 = 3
}

$ScriptPath = 'D:\Test.ps1'

$sb = [scriptblock]::create(".{$(get-content $ScriptPath -Raw)} $(&{$args} @params)")

Invoke-Command -ComputerName server -ScriptBlock $sb
mjolinor
  • 66,130
  • 7
  • 114
  • 135
  • 1
    Yes I am able to achieve the result using the above code. – Parveen Kumar Jan 06 '15 at 17:50
  • 1
    Can you please let me know how to achieve the same using C# code as my requirement is to run this PowerShell script from C#. – Parveen Kumar Jan 06 '15 at 17:55
  • Sorry, don't know C# well enough to convert that for you. – mjolinor Jan 06 '15 at 18:02
  • This code works really well until you have an array as a parameter. When that happens you just get "System.String[]" as the value since the param block effectively gets a .ToString() applied to it when it gets expanded. I have yet to find a reliable way of avoiding this, if anyone has a solution i would love to see it! – CarlR Feb 16 '17 at 22:21
  • @CarlR - Try P3 = "@('a','b','c')" – mjolinor Feb 17 '17 at 16:19
  • It works well. To pass string with spaces you have to wrap value with additional apostrophe. E.g. `P1 = "'My string parameter'"` – sephirot Mar 01 '17 at 14:49
5

The code by mjolinor works great, but it took me several minutes to understand it.

The code makes a simple thing - generates a content of script block with built-in parameters:

&{
    Param (
        [string]$P3,
        [string]$P2,
        [string]$P1
    )
    Write-Output "P1 Value:" $P1
    Write-Output "P2 Value:" $P2
    Write-Output "P3 Value:" $P3
} -P1 1 -P2 2 -P3 3

Then this script block is passed to Invoke-Command.

To simplify the code:

".{$(get-content $ScriptPath -Raw)} $(&{$args} @params)"

$scriptContent = Get-Content $ScriptPath -Raw
$formattedParams = &{ $args } @params
# The `.{}` statement could be replaced with `&{}` here, because we don't need to persist variables after script call.
$scriptBlockContent = ".{ $scriptContent } $formattedParams"
$sb = [scriptblock]::create($scriptBlockContent)

Let's make a basic C# implementation:

void Run()
{
    var parameters = new Dictionary<string, string>
    {
        ["P1"] = "1",
        ["P2"] = "2",
        ["P3"] = "3"
    };

    var scriptResult = InvokeScript("Test.ps1", "server", parameters)
    Console.WriteLine(scriptResult);
}

string InvokeScript(string filePath, string computerName, Dictionary<string, string> parameters)
{
    var innerScriptContent = File.ReadAllText(filePath);
    var formattedParams = string.Join(" ", parameters.Select(p => $"-{p.Key} {p.Value}"));
    var scriptContent = "$sb = { &{ " + innerScriptContent + " } " + formattedParams + " }\n" +
        $"Invoke-Command -ComputerName {computerName} -ScriptBlock $sb";

    var tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".ps1");
    File.WriteAllText(tempFile, scriptContent);

    var psi = new ProcessStartInfo
        {
            FileName = "powershell",
            Arguments = $@"-ExecutionPolicy Bypass -File ""{tempFile}""",
            RedirectStandardOutput = true,
            UseShellExecute = false
        };

    var process = Process.Start(psi);
    var responseText = process.StandardOutput.ReadToEnd();

    File.Delete(tempFile);

    return responseText;
}

The code generates a temporary script and executes it.

Example script:

$sb = {
    &{
        Param (
            [string]$P3,
            [string]$P2,
            [string]$P1
        )
        Write-Output "P1 Value:" $P1
        Write-Output "P2 Value:" $P2
        Write-Output "P3 Value:" $P3
     } -P1 1 -P2 2 -P3 3
}
Invoke-Command -ComputerName server -ScriptBlock $sb
Der_Meister
  • 4,771
  • 2
  • 46
  • 53
  • Could you please explain what `$(&{args})` does? I'm trying to understand how that combines with `@params` to give you a string like "-P1 1 -P2 2 -P3 3". – batbrat May 02 '19 at 17:11
  • Working from the inside out, {$args} is a script block that simply returns whatever arguments it's been given. &{args} executes the script block. Wrapping it in $() - $(&{$args}) makes it a subexpression, so that it expands within the string. Because the argument given was a hash table, the output is a series of key/value pairs - the result of the splatting operation as it was passed to the script block. – mjolinor May 03 '19 at 01:56
  • I love this solution and your explanation is great. However, What if your variable is a string with spaces in, how can I encapsulate the args like so: "-P2 'This is P2' -P1 'This Is P1' -P3 'This is P3'" – tommylux Apr 04 '22 at 16:01
  • Can you better this? IF ($StrArgs -is [Hashtable]) { $formattedParams = Foreach ($i in $StrArgs.keys) { "-${i}: `"$($StrArgs.$i)`"" } } Else { $formattedParams = $StrArgs } – tommylux Apr 04 '22 at 16:22
1

Here's a simple solution:

[PowerShell]::Create().AddCommand('D:\test.ps1').AddParameters(@{ P1 = 1; P2 = 2; P3 = 3 }).Invoke()

Here's output:

PS C:\Windows\system32> [PowerShell]::Create().AddCommand('D:\test.ps1').AddParameters(@{ P1 = 1; P2 = 2; P3 = 3 }).Invoke()
P1 Value :
1
P2 Value:
2
P3 Value :
3
Haoshu
  • 832
  • 7
  • 19
1

If you are trying to use the -FilePath with named parameters (-P1 1 -P2 2), then I found this will work. Use a script block to run the file, instead of the using -FilePath.

Invoke-Command -ComputerName server -ScriptBlock {& "D:\test.ps1" -P1 1 -P2 2 -P3 3}
Barry MSIH
  • 3,525
  • 5
  • 32
  • 53
0

Use a hashtable :

icm -ComputerName test -ScriptBlock{$args} -ArgumentList @{"p1"=1;"p2"=2;"p3"=3} 
Loïc MICHEL
  • 24,935
  • 9
  • 74
  • 103
  • can you please provide more details on this. – Parveen Kumar Jan 06 '15 at 08:55
  • 7
    This doesn't work—the entire hashtable is passed as the first argument. The docs say the arguments need to be a comma-separated list (non-associative array) ([source](http://web.archive.org/web/20180927151353/https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/invoke-command?view=powershell-6)). – Eric Eskildsen Sep 27 '18 at 15:20
0

Use a hashtable, indeed!

#TestPs1.ps1
Param (
    [string]$P3,
    [string]$P2,
    [string]$P1
)
Write-Output "P1 Value :" $P1
Write-Output "P2 Value:" $P2
Write-Output "P3 Value :" $P3
$params = @{
    P3 = 3
    P2 = 2
}
#(just to prove it doesn't matter which order you put them in)
$params["P1"] = 1;
#Trhough the use of the "Splat" operator, we can add parameters directly onto the module
& ".\TestPs1.ps1" @params

outputs:

P1 Value :
1
P2 Value:
2
P3 Value :
3
Max Hay
  • 230
  • 2
  • 11
0

If you're willing to skip Invoke-Command altogether...

Your script could look like this:

([string]$args).split('-') | %{ 
    if ($_.Split(' ')[0].ToUpper() -eq "P1") { $P1 = $_.Split(' ')[1] } 
    elseif ($_.Split(' ')[0].ToUpper() -eq "P2") { $P2 = $_.Split(' ')[1] }
    elseif ($_.Split(' ')[0].ToUpper() -eq "P3") { $P3 = $_.Split(' ')[1] } 
}

Write-Output "P1 Value :" $P1
Write-Output "P2 Value :" $P2
Write-Output "P3 Value :" $P3

And you would call it like this:

D:\test.ps1 -P1 1 -P2 2 -P3 3

b-frid
  • 116
  • 8