PowerShell: Error "A positional parameter cannot be found that accepts argument ..." - powershell

I have two PowerShell Scripts. The one is an initial script which does some work on JSON files and also has some constants in form of an array.
The second one is a function within a separate file.
In my script initial1.ps1 i have the following code:
$IDs=#("02921114-654b-4c28-a9d7-2ebd9ab0ada3",
"9045c61c-55bc-45b0-0000-ec9858b24867",
"011b0c6d-5678-4aaa-a833-e62111103f0a")
. 'D:\...\drop\Add-Folder.ps1' -AdlsAccountName $Accountname `
-PrincipalIds #IDs `
-Path $folderToCreate.path `
-AadGroupName $aadRoleName
My PowerShell script file named 'Add-Folder.ps1` looks like:
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True,Position=1)]
[string] $AdlsAccountName,
[Parameter(Mandatory=$True,Position=2)]
[string[]] $PrincipalIds,
[Parameter(Mandatory=$True,Position=3)]
[string] $Path,
[Parameter(Mandatory=$True,Position=4)]
[string] $AadGroupName
)
Write-Host "Hello from 'Add-AdlsFolderWithPermissions.ps1'!"
Write-Host $AdlsAccountName
Write-Host $PrincipalIds
Write-Host $Path
Write-Host $AadGroupName
But when i execute it i get the following error:
##[error]A positional parameter cannot be found that accepts argument '9045c61c-55bc-45b0-0000-ec9858b24867'.
Why happens this error or how can i pass the array of IDs to my function?

Use $IDs instead of #IDs. Otherwise the array will be expanded and attempted to be passed as multiple arguments. This is called splatting.
You can use this to collect either positional arguments to a command in an array, or named arguments in a hashtable, and then invoke the command with that splatted variable. It's never necessary or correct to use it when you actually want to pass an array as a single argument.

Using # with variable name causes parameter splatting, i.e. values in an array are being used as consecutive parameters for the script.

Related

Correct param type for piping from Get-Content (PowerShell)?

What is the correct type for piping all of the content from Get-Content?
My script:
param(
[Parameter(ValueFromPipeline)]
[???]$content
)
Write-Output $content
According to the docs for PowerShell 5.1, Get-Content returns "a collection of objects", but I'm not sure how to specify that in PowerShell. Without specifying a type for the [???], only the last line of the file is output.
Regarding your last comment:
When I specify [string] or [string[]], it is still only printing out the last line of the file. Is this related to missing the process block?
This is correct, otherwise your function, scriptblock or script is executed in the end block, if you want them to be able to process input from pipeline, you must add your logic in the process block.
Note, this assumes you want to use ValueFromPipeline which, in effect, converts it into an advanced function.
If you want it be able to process input from pipeline but also be compatible with positional binding and named parameter you would use [string[]] and a loop:
param(
[Parameter(ValueFromPipeline)]
[string[]] $Content
)
process {
foreach($line in $Content) {
$line
}
}
Then, assuming the above is called myScript.ps1, you would be able to:
Get-Content .\test.txt | .\myScript.ps1
And also:
.\myScript.ps1 (Get-Content .\test.txt)

PowerShell - accessing script parameters, sent from Windows cmd shell

Questions
How can you access the parameters sent to PowerShell script (.ps1 file)?
Can you access parameters A: by name, B: by position, C: a mix of either?
Context
I recently tried to write a PowerShell script (.ps1) that would be called from a Windows batch file (.bat) (or potentially cmd shell, or AutoHotKey script) - which would pass parameters into the .ps1 script for it to use (to display a toast notification). Thanks to the instructions on ss64.com, I have used $args to do this kind of thing in the past, however for some reason I could access the parameters this way (despite passing parameters, $args[0] = '' (empty string) and $args.Count = 0) so eventually had to remove all the $args code, and replace it with Param() script instead.
I'm still not quite sure why, but thought this is something I should get to the bottom of before I try to write my next script...
Code Example 1: Args (un-named parameters)
ToastNotificationArgs.ps1
-------------------------
Write-Debug "The script has been passed $($args.Count) parameters"
If (!$args[0]) { # Handle first parameter missing }
If (!$args[1]) { # Handle second parameter missing }
Import-Module -Name BurntToast
New-BurntToastNotification -Text "$args[0], $args[1]"
^ I thought the above code was correct, but like I say, I kept struggling to access the parameters for some reason and could not figure out why. (If anyone can spot what I was doing wrong, please shout!)
Is $args[] a valid approach? I assume so given it's use of ss64.com, but maybe there are some pitfalls / limitations I need to be aware of?
Code Example 2: Param (named parameters)
ToastNotificationParams.ps1
---------------------------
Param(
[Parameter(Mandatory=$false, Position=0, ValueFromPipeline=$true)] [string]$Title,
[Parameter(Mandatory=$false, Position=1, ValueFromPipeline=$true)] [string]$Message
)
Import-Module -Name BurntToast
New-BurntToastNotification -Text "$Title, $Message"
^ This was the only way to get my script working in the end. However when I passed the parameters in, my calling cmd script sent the parameters by position i.e. (pwsh.exe -File "ToastNotificationParams.ps1" "This is the title" "Message goes here") rather than by named pairs. (Not sure if this is best practice, but is how my script was initially intended to be used to left it for now).
While Param() got my script working this time (and I also realise the inherent dangers of position-based parameters), there are times when a position-based approach might be necessary (e.g. the number of parameter is unknown)...
Code Example 3: Hybrid
ToastNotificationMix.ps1
------------------------
Param(
[Parameter(Mandatory=$false, Position=0, ValueFromPipeline=$true)] [string]$Title
)
Import-Module -Name BurntToast
For ( $i = 1; $i -lt $args.count; $i++ ) {
New-BurntToastNotification -Text "$Title, $args[i]"
}
Is something like this valid?.. If not (or there is a better solution), any help would be greatly appreciated!
Thanks in advance!
The automatic $args variable is only available in simple (non-advanced) functions / scripts. A script automatically becomes an advanced one by using the [CmdletBinding()] attribute and/or at least one per-parameter [Parameter()] attribute.
Using $args allows a function/script to accept an open-ended number of positional arguments, usually instead of, but also in addition to using explicitly declared parameters.
But it doesn't allow passing named arguments (arguments prefixed by a predeclared target parameter name, e.g., -Title)
For robustness, using an advanced (cmdlet-like) function or script is preferable; such functions / scripts:
They require declaring parameters explicitly.
They accept no arguments other than ones that bind to declared parameters.
However, you can define a single catch-all parameter that collects all positional arguments that don't bind to any of the other predeclared parameters, using [Parameter(ValueFromRemainingArguments)].
Explicitly defined parameters are positional by default, in the order in which they are declared inside the param(...) block.
You can turn off this default with [CmdletBinding(PositionalBinding=$false)],
which then allows you to selectively enable positional binding, using the Position property of the individual [Parameter()] attributes.
When you call a PowerShell script via the PowerShell's CLI's -File parameter, the invocation syntax is fundamentally the same as when calling script from inside PowerShell; that is, you can pass named arguments and/or - if supported - positional arguments.
Constraints:
The arguments are treated as literals.
Passing array arguments (,-separated elements) is not supported.
If you do need your arguments to be interpreted as they would be from inside PowerShell, use the -Command / -c CLI parameter instead
See this answer for guidance on when to use -File vs. `-Command.
To put it all together:
ToastNotificationMix.ps1:
[CmdletBinding(PositionalBinding=$false)]
Param(
[Parameter(Position=0)]
[string]$Title
,
[Parameter(Mandatory, ValueFromRemainingArguments)]
[string[]] $Rest
)
Import-Module -Name BurntToast
foreach ($restArg in $Rest) {
New-BurntToastNotification -Text "$Title, $restArg"
}
You can then call your script from cmd.exe as follows, for instance (I'm using pwsh.exe, the PowerShell (Core) CLI; for Windows PowerShell, use powershell.exe):
Positional binding only:
:: "foo" binds to $Title, "bar" to $Rest
pwsh -File ./ToastNotificationMix.ps1 foo bar
Mix of named and positional binding:
:: "bar" and "baz" both bind to $Rest
pwsh -File ./ToastNotificationMix.ps1 -Title foo bar baz

How to provide options to script blocks?

I'm trying to learn about script blocks at https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_script_blocks?view=powershell-6.
I want to send a script block to an event registration with a command-line option to the registration command, like this:
$Block = {
Param($option)
Write-Host "The option was $option"
if ($option==ABC) {
Write-Host "ABC was specified"
}
}
But when I test this by appending
Invoke-Command -ScriptBlock $Block -option ABC
I get the error
Invoke-Command : A parameter cannot be found that matches parameter name 'option'.
When in doubt, read the documentation:
-ArgumentList
Supplies the values of local variables in the command. The variables in the command are replaced by these values before the command is run on the remote computer. Enter the values in a comma-separated list. Values are associated with variables in the order that they are listed. The alias for ArgumentList is "Args".
The values in ArgumentList can be actual values, such as "1024", or they can be references to local variables, such as "$max".
To use local variables in a command, use the following command format:
{param($<name1>[, $<name2>]...) <command-with-local-variables>} -ArgumentList <value> -or- <local-variable>
The "param" keyword lists the local variables that are used in the command. The ArgumentList parameter supplies the values of the variables, in the order that they are listed.
The first element from the -ArgumentList array becomes the value of the first named parameter in the scriptblock, the second array element becomes the value of the second named parameter, and so on.
Basically, your statement should look like this:
Invoke-Command -ScriptBlock $Block -ArgumentList 'ABC'
Also, the syntax of the comparison operation in your scriptblock is wrong. The equality comparison operator in PowerShell is -eq, not ==.
Remember that you can always check the documentation: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/invoke-command?view=powershell-6
$Block = {Param($option)
Write-Host "The option was $option"
if ($option -eq "ABC") // -eq instead of ==, string in quotations
{Write-Host "ABC was specified"}
}
Invoke-Command -ScriptBlock $Block -ArgumentList "ABC"
If there are more arguments you can list them in order. Using script blocks isn't something you want to do very often. It's better to create a function (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions?view=powershell-6)
You can use the call operator on scriptblocks too, and just provide the arguments after a space or -option:
& $block abc
The option was abc
ABC was specified
And you can later assign the scriptblock to a function. That's what a function is.
$function:myfunc = $block

Passing an object from one script to another

I am having an issue passing an array member to another script. I have a VM build script that pulls from a CSV, I end up with a $VM object with .Name, .CPU, .RAM, .IP, etc. I want to pass that VM object to another script (inside the new server) which can then act on it, but am unable to do so. I have been testing the correct syntax just to pass a simple array, like below, but am still not successful:
CSV:
Name,NumCPU,MemoryGB,IPAddress
JLTest01,2,4,172.24.16.25
Script1:
Function TestMe {
[CmdLetBinding()]
Param (
[Parameter(Mandatory, Position=1)]
[array]$arr
)
$arr | Out-GridView
}
TestMe
Calling Script:
$aVMs = Import-Csv -Path "PathToCsv"
foreach($VM in $aVMs) {
$command = "<path>\TestMe.ps1 " + "-arr $($VM)"
Invoke-Expression $command
}
This produces an error, which seems to be on parsing the array. The error states:
The term 'JLTest01' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:48 + ... \Desktop\TestMe.ps1 -arr #{Name=JLTest01; NumCPU ...
Just trying to figure out what I am doing wrong exactly, and what I need to do to pass the object to the second script.
Don't use Invoke-Expression (which is rarely the right tool and should generally be avoided for security reasons):
The stringification of the custom objects output by Import-Csv performed by $($VM) does not preserve the original objects; it results in a hashtable-like representation that isn't suitable for programmatic processing and breaks the syntax of the command line you're passing to Invoke-Expression.
Instead, just invoke your script directly:
$aVMs = Import-Csv -Path "PathToCsv"
.\TestMe.ps1 -arr $aVMs
Note that I'm passing $aVMs as a whole to your script, given that your -arr parameter is array-typed.
If you'd rather process the objects one by one, stick with the foreach approach (but then you should declare the type of your $arr parameter as [pscustomobject] rather than [array]):
$aVMs = Import-Csv -Path "PathToCsv"
foreach ($VM in $aVMs) {
.\TestMe.ps1 -arr $VMs
}
Another option is to declare $arr as accepting pipeline input, add a process block to your script, and then pipe $aVMs to your script ($aVMs | .\TestMe.ps1).
Also, don't nest a function of the same name inside your .ps1 script that you call from within the script, especially not without passing the arguments through; scripts can directly declare parameters, just like functions:
[CmdLetBinding()]
Param (
[Parameter(Mandatory, Position=1)]
[array]$arr
)
$arr | Out-GridView

Wrapper function in PowerShell: Pass remaining parameters

I’m trying to write a wrapper function in PowerShell that basically evaluates the first parameter and based on that runs a program on the computer. All the remaining parameters to the wrapper function should then be passed to the program that is ran as well.
So it should look something like this:
function test ( [string] $option )
{
if ( $option -eq 'A' )
{
Write-Host $args
}
elseif ( $option -eq 'B' )
{
. 'C:\Program Files\some\program.exe' $args
}
}
Now just adding $args does not work, so what do I have to do to make it work? Another option would probably be using Invoke-Expression, but it feels a bit like eval so I want to avoid if possible, and in addition I think doing it like that would limit me to string-only parameters right? If possible I would want to have the full support for the wrapped program/cmdlet - basically like a dynamic alias. Is that even possible?
This sort of does what you ask. You may run into trouble if you need to pass dash-prefixed options to the executable that conflict or cause ambiguity with the PowerShell common parameters. But this may get you started.
function Invoke-MyProgram
{
[CmdletBinding()]
Param
(
[parameter(mandatory=$true, position=0)][string]$Option,
[parameter(mandatory=$false, position=1, ValueFromRemainingArguments=$true)]$Remaining
)
if ($Option -eq 'A')
{
Write-Host $Remaining
}
elseif ($Option -eq 'B')
{
& 'C:\Program Files\some\program.exe' #Remaining # NOTE: # not $ (splatting)
}
}
What you have written does work. Note that what is there is $args is the unnamed arguments that are over and above the parameters expected by the function.
So if you call test as
test -option "A" 1 2 3
$args will have 1,2,3
Note that if you call test as
test -option "A" -other "B" 1 2 3
$args will have -other,B,1,2,3
Your solution works as-is for external programs (such as your C:\Program Files\some\program.exe example): you can always pass an array of values (which is what $args is) to an external program, and its elements will be passed as individual arguments (stringified, if necessary).
You can make your solution work with any command if you change $args to #args[1], to take advantage of a PowerShell parameter-passing technique called splatting:
function test ( [string] $option )
{
if ( $option -eq 'A' )
{
Write-Host $args
}
elseif ( $option -eq 'B' )
{
# Use #args to also support passing *named* arguments
# through to *PowerShell* commands.
& $someCommand #args
}
}
Caveats:
The automatic $args variable, which collects all arguments for which no parameter was declared, is only available in simple (non-advanced) functions and scripts; advanced functions and scripts - those that use the [CmdletBinding()] attribute and/or [Parameter()] attributes - require that all potential parameters be declared.
PowerShell has built-in magic that makes the automatic array variable $args also support passing named parameters through via splatting, which no custom array or collection supports.
By contrast, custom arrays and collections only support splatting positional (unnamed) arguments, which, however, covers all calls to external programs.
When calling PowerShell commands, this limitation is problematic, however: For instance, if you wanted to pass the named argument -Path C:\ through to the Set-Location cmdlet via splatting, using a custom collection parameter declared via ValueFromRemaining Arguments, as shown in OldFart's answer (Set-Location #Remaining), would not work; to support passing through named arguments (other than via #args, if available), you must use hashtable-based splatting.
Therefore, if your function is an advanced one and you need to support passing named arguments through to other PowerShell commands, a different approach is required: this answer shows two alternatives.
[1] With external programs, there is a corner case where #args behaves differently from $args, namely if the $args array contains --%, the stop-parsing symbol: #args recognizes it, $args treats it as a literal.