Passing integer array as command line argument to powershell script - powershell

I have the following powershell script:
param (
[Parameter(Mandatory=$true)][int[]]$Ports
)
Write-Host $Ports.count
foreach($port in $Ports) {
Write-Host `n$port
}
When I run the script with $ powershell -File ./test1.ps1 -Ports 1,2,3,4 it works (but not as expected):
1
1234
When I try to use larger numbers, $ powershell -File .\test.ps1 -Ports 1,2,3,4,5,6,10,11,12, the script breaks entirely:
test.ps1 : Cannot process argument transformation on parameter 'Ports'. Cannot convert value "1,2,3,4,5,6,10,11,12" to type "System.Int32[]". Error: "Cannot convert value "1,2,3,4,5,6,10,11,12" to type "System.Int32". Error: "Input
string was not in a correct format.""
+ CategoryInfo : InvalidData: (:) [test.ps1], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,test.ps1
It seems like powershell is trying to process any numbers passed via the Ports param as a single number, though I'm not sure why this is happening, or how to get past it.

The issue is that a parameter passed through powershell.exe -File is a [string].
So for your first example,
powershell -File ./test1.ps1 -Ports 1,2,3,4
$Ports is passed as [string]'1,2,3,4' which then attempts to get cast to [int[]]. You can see what happens with:
[int[]]'1,2,3,4'
1234
Knowing that it will be an just a regular [int32] with the comma's removed means that casting 1,2,3,4,5,6,10,11,12 will be too large for [int32] which causes your error.
[int[]]'123456101112'
Cannot convert value "123456101112" to type "System.Int32[]". Error: "Cannot convert value "123456101112" to type "System.Int32". Error: "Value was either too
large or too small for an Int32.""
To continue using -file you could parse the string yourself by splitting on commas.
param (
[Parameter(Mandatory=$true)]
$Ports
)
$PortIntArray = [int[]]($Ports -split ',')
$PortIntArray.count
foreach ($port in $PortIntArray ) {
Write-Host `n$port
}
But luckily that is unnecessary because there is also powershell.exe -command. You can call the script and use the PowerShell engine to parse the arguments. This would correctly see the Port parameter as an array.
powershell -Command "& .\test.ps1 -Ports 1,2,3,4,5,6,10,11,12"

Related

How to convert $args to a string and execute?

I'd like to pass all arguments that were given to the script and execute.
For example, given execute.ps1 script:
Invoke-Expression $($args[0])
I can run:
.\execute.ps1 hostname
myhostname
Same with two parameters with this script:
Invoke-Expression "$($args[0]) $($args[1])"
and executing it by:
.\execute.ps1 echo foo
foo
How I can make the script universal to support unknown number of arguments?
For example:
.\execute.ps1 echo foo bar buzz ...
I've tried the following combinations which failed:
Invoke-Expression $args
Invoke-Expression : Cannot convert 'System.Object[]' to the type 'System.String' required by parameter 'Command'. Specified method is not supported.
Invoke-Expression [system.String]::Join(" ", $args)
Invoke-Expression : A positional parameter cannot be found that accepts argument 'System.Object[]'.
Invoke-Expression $args.split(" ")
Invoke-Expression : Cannot convert 'System.Object[]' to the type 'System.String' required by parameter 'Command'. Specified method is not supported.
Invoke-Expression [String] $args
Invoke-Expression : A positional parameter cannot be found that accepts argument 'System.Object[]'.
I recommend Bill_Stewart's answer to avoid issues with the command itself (first argument) having spaces. But even with that answer, you would have to be careful to individual quote arguments, with the caveat that that itself itself a complicated thing.
You can just do:
Invoke-Expression "$args"
By default converting to a string that way will join it with spaces (technically, join it with the default output field separator, which is the value of $OFS, which defaults to a space).
You can also do a manual join as in Wayne's answer.
$args is a whitespace delimited array of strings created from the imput
Invoke-Expression -Command "$($args -join " ")"
Re-joining it with a whitespace character, and passing it to invoke-expression as a string works for me.
My recommendation would be to avoid Invoke-Expression and use & instead. Example:
$command = $args[0]
$params = ""
if ( $args.Count -gt 1 ) {
$params = $args[1..$($args.Count - 1)]
}
& $command $params
Of course, parameters containing spaces would still need to contain embedded quotes.

ValidateScript unexpectedly returning false when condition is true

I have a PowerShell function I'm writing to build and execute a variety of logman.exe commands for me so I don't have to reference the provider GUIDs and type up the command each time I want to capture from a different source. One of the parameters is the file name and I am performing some validation on the parameter. Originally I used -match '.+?\.etl$' to check that the file name had the .etl extension and additionally did some validation on the path. I later decided to remove the path validation but neglected to change the validation attribute to ValidatePattern.
What I discovered was that while it worked perfectly on the machine I was using to author and validate it, on my Server 2016 Core machine it seemed to misbehave when calling the function but that if I just ran the same check at the prompt it worked as expected.
The PowerShell:
[Parameter(ParameterSetName="Server", Mandatory=$true)]
[Parameter(ParameterSetName="Client", Mandatory=$true)]
[ValidateScript({$FileName -match '.+?\.etl$'}]
[string] $FileName = $null
The Output:
PS C:\Users\Administrator> Start-TBLogging -ServerLogName HTTPSYS -FileName ".\TestLog.etl"
PS C:\Users\Administrator> Start-TBLogging : Cannot validate argument on parameter 'FileName'. The "$FileName -match '.+?\.etl$'" validation script
for the argument with value ".\TestLog.etl" did not return a result of True. Determine why the validation script failed,
and then try the command again.
At line:1 char:50
+ Start-TBLogging -ServerLogName HTTPSYS -FileName ".\TestLog.etl"
+ ~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [Start-TBLogging], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Start-TBLogging
Trying it manually worked:
PS C:\Users\Administrator> $FileName = ".\TestLog.etl"
PS C:\Users\Administrator> $FileName -match '.+?\.etl$'
True
After changing the function to use ValidatePattern it works just fine everywhere but I was wondering if anyone could shed light on the discontinuity.
As Joshua Shearer points out in a comment on a question, you must use automatic variable $_ (or its alias form, $PSItem), not the parameter variable to refer to the argument to validate inside [ValidateScript({ ... })].
Therefore, instead of:
# !! WRONG: The argument at hand has NOT yet been assigned to parameter
# variable $FileName; by design, that assignment
# doesn't happen until AFTER (successful) validation.
[ValidateScript({ $FileName -match '.+?\.etl$' }]
[string] $FileName
use:
# OK: $_ (or $PSItem) represents the argument to validate inside { ... }
[ValidateScript({ $_ -match '.+?\.etl$' })]
[string] $FileName
As briantist points out in another comment on the question, inside the script block $FileName will have the value, if any, from the caller's scope (or its ancestral scopes).

Consume $args while also using parameter set names

Consider the following toy example script test.ps1:
Param(
[Parameter(ParameterSetName='readfile',Position=0,Mandatory=$True)]
[string] $FileName,
[Parameter(ParameterSetName='arg_pass',Mandatory=$True)]
[switch] $Ping
)
if ($Ping.isPresent) {
&$env:ComSpec /c ping $args
} else {
Get-Content $FileName
}
The desired effect would be that
.\test.ps1 FILE.TXT
displays the contents of FILE.TXT and
.\test.ps1 -Ping -n 5 127.0.0.1
pings localhost 5 times.
Unfortunately, the latter fails with the error
A parameter cannot be found that matches parameter name 'n'.
At line:1 char:18
+ .\test.ps1 -Ping -n 5 127.0.0.1
+ ~~
+ CategoryInfo : InvalidArgument: (:) [test.ps1], ParameterBindingException
+ FullyQualifiedErrorId : NamedParameterNotFound,test.ps1
This is just a minimal example, of course.
In general, I am looking for a way to introduce a [switch] parameter to my script that lives inside its own parameter set and when that switch is present, I want to consume all remaining arguments from the commandline and pass them on to another commandline application. What would be the way to do this in PowerShell?
You can use the ValueFromRemainingArguments parameter attribute. I would also recommend specifying a default parameter set name in CmdletBinding. Example:
[CmdletBinding(DefaultParameterSetName="readfile")]
param(
[parameter(ParameterSetName="readfile",Position=0,Mandatory=$true)]
[String] $FileName,
[parameter(ParameterSetName="arg_pass",Mandatory=$true)]
[Switch] $Ping,
[parameter(ParameterSetName="arg_pass",ValueFromRemainingArguments=$true)]
$RemainingArgs
)
if ( $Ping ) {
ping $RemainingArgs
}
else {
Get-Content $FileName
}
(Aside: I don't see a need for & $env:ComSpec /c. You can run commands in PowerShell without spawning a copy of cmd.exe.)

PS 4.0 Parameters with Space - Catch Error

I'm executing script in batch and passing 5 parameters separated by a comma via Java. PS 4.0 accepts parameters without the "".
[CmdletBinding()]
Param (
[Parameter(Position = 0)]
[string[]] $inpParms = $(throw 'Failure : This is required.')
)
I invoke the script like
myScript.ps1 user,pwd,Server Name, DLName,Action
It errors out due to the space in "Server Name". Throws error which spits back the parameters:
A positional parameter cannot be found that accepts argument 'System.Object[]'.
At line:1 char:1
+ .\myScript.ps1 user,pwd,Server Name, DLName,Create
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [ExchangeDL.ps1], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,ExchangeDL.ps1
Is there a way to trap this error so I don't spit sensitive info in logs?
While you don't have to quote arguments that don't have shell metacharacters in them, a space is a metacharacter, so you have two choices:
`-quote the metacharacters individually:
./myScript.ps1 user, pwd, Server` Name, DLName, Create
Or use "..." (interpolating) or '...' (literal) to enclose entire arguments as needed:
./myScript.ps1 user, pwd, 'Server Name', DLName, Create
Generally, though, your script will be more maintainable if you define individual parameters rather than a single array.
If you want to catch an incorrect invocation attempt, use a Try / Catch statement:
Try {
./myScript.ps1 user, pwd, Server Name, DLName, Action
} Catch {
Throw "Invocation of ./myScript.ps1 failed."
}
By not using the information from the statement-terminating error that the incorrect invocation produced (accessible as $_ in the Catch block) in the Throw statement, the original command line is not leaked.

Powershell: Args/params not being populated

I have a PowerShell script:
param (
[Parameter(Mandatory=$true)][string]$input,
[Parameter(Mandatory=$true)][string]$table
)
Write-Host "Args:" $Args.Length
Get-Content $input |
% { [Regex]::Replace($_, ",(?!NULL)([^,]*[^\d,]+[^,]*)", ",'`$1'") } |
% { [Regex]::Replace($_, ".+", "INSERT INTO $table VALUES (`$1)") }
The Write-Host part is for debugging.
I run it as .\csvtosql.ps1 mycsv.csv dbo.MyTable (from powershell shell), and get
Args: 0
Get-Content : Cannot bind argument to parameter 'Path' because it is an empty s
tring.
At C:\temp\csvtosql.ps1:7 char:12
+ Get-Content <<<< $input |
+ CategoryInfo : InvalidData: (:) [Get-Content], ParameterBinding
ValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorEmptyStringNotAl
lowed,Microsoft.PowerShell.Commands.GetContentCommand
I get exactly the same error with any parameters that I pass, also the same error if I try to use named parameters.
What can cause parameters not to be passed in?
UPDATE: PowerShell ISE asks me for these parameters using GUI prompts, then gives me the same error about them not being passed in.
Unless you marked a parameter with the ValueFromRemainingArguments attribute (indicates whether the cmdlet parameter accepts all the remaining command-line arguments that are associated with this parameter), Args is "disabled". If all you need is the arguments count call the special variable:
$PSBoundParameters.Count
Do not mix. Make use of $args or parameters.
Also do note that $input is a special variable, don't declare it as a parameter. http://dmitrysotnikov.wordpress.com/2008/11/26/input-gotchas/
You're calling your script with positional parameters (i.e. unnamed) and PowerShell doesn't know how to map them to your script parameters. You need to either call your script using the parameter names:
.\csvtosql.ps1 -input mycsv.csv -table dbo.MyTable
or update your script to specify your preferred order of positional parameters:
param (
[Parameter(Mandatory=$true,Position=0)]
[string]
$input,
[Parameter(Mandatory=$true,Position=1)]
[string]
$table
)