How to convert $args to a string and execute? - powershell

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.

Related

How to pass a variable to new console window in Powershell

I want to pass a variable to a new console, but I don't know how.
$server = "server_name"
Start-Process Powershell {
$host.ui.RawUI.WindowTitle = “Get-Process”
Invoke-Command -ComputerName $server -ScriptBlock {
Get-Process
}
cmd /c pause
}
Error message:
Invoke-Command : Cannot validate argument on parameter 'ComputerName'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again
Start-Process only accepts (one or more) strings as arguments, not a script block ({ ... }).
While a script block is accepted, it is simply stringified, which results in its verbatim content being passed as the argument (sans { and }), which means that $server is retained as-is (not expanded), and the child process that runs your command doesn't have a variable by that name defined, causing Invoke-Command to fail due not receiving a value for -ComputerName.
Therefore, in order to incorporate variable values from the caller's scope, you must use string interpolation, using an expandable (double-quoted) string ("...") that encodes all arguments:[1]
$server = "server_name"
# Parameters -FilePath and -ArgumentList are positionally implied.
# For the resulting powershell.exe call, -Command is implied.
Start-Process powershell "
$host.ui.RawUI.WindowTitle = 'Get-Process'
Invoke-Command -ComputerName $server -ScriptBlock { Get-Process }
pause
"
A computer name ($server, in your case) doesn't contain spaces, but any variable values that do would require embedded enclosing quoting inside the overall "..." string, such as \`"$someVar\`" (`" escapes an " inside a "..." string in PowerShell, and the \ is additionally needed to escape the resulting verbatim " for the PowerShell CLI, powershell.exe).
For full robustness, additionally enclose the entire string value (implied -Command argument) in embedded "..." quoting ("`"...`"").
You can make this a bit easier by using the here-string form of an expandable string (#"<newline>...<newline>"#), inside of which you don't need to escape " chars.
Example of a fully robust call:
$someVar = 'A value with spaces'
Start-Process powershell #"
-NoProfile -Command "
# Echo the value of $someVar
Write-Output \"$someVar\"
pause
"
"#
Note the use of -NoProfile before -Command, which suppresses loading of the profile files, which can speed up the call and makes for a more predictable execution environment.
[1] Technically, -ArgumentList accepts an array of arguments, and while passing the pass-through arguments individually may be conceptually preferable, a long-standing bug unfortunately makes it better to encode all arguments in a single string - see this answer.
if you want to use param-
info
param([type]$p1 = , [type]$p2 = , ...)
or:
info
param(
[string]$server
)
Write-Host $a
./Test.ps1 "your server name"

Is -Args a correct substitute for -ArgumentList?

I came across some code using Start-Process in this form.
Start-Process <cmd> -Args <arguments>
On checking the Start-Process docs, it says Start-Process takes a parameter of -ArgumentList but doesn't actually mention -Args.
Is this shortening documented somewhere or is it just a known thing that you can shorten ArgumentList to Args? If so, do they behave in the same way?
Yes it's already documented on the same link.Args is an alias name for ArgumentList.
-ArgumentList
Specifies parameters or parameter values to use when this cmdlet starts the process. Arguments can be accepted as a single
string with the arguments separated by spaces, or as an array of
strings separated by commas. The cmdlet joins the array into a single
string with each element of the array separated by a single space.
The outer quotes of the PowerShell strings are not included when the
ArgumentList values are passed to the new process. If parameters or
parameter values contain a space or quotes, they need to be surrounded
with escaped double quotes. For more information, see
about_Quoting_Rules.
Type: String[]
Aliases: Args
Position: 1
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
-Args is an Alias for -ArgumentList as mentioned on Abdul Niyas P M answer, parameters in PowerShell can have many aliases. This can be particularly useful for example on functions that accept value from pipeline by property name.
See Benefits of Using Aliases
function SayHello {
param(
[parameter(ValueFromPipelineByPropertyName)]
[alias('alias1','alias2')]
[string]$Message
)
"Hello $Message"
}
PS /> [pscustomobject]#{ Message = 'World!' } | SayHello
Hello World!
PS /> [pscustomobject]#{ alias1 = 'World!' } | SayHello
Hello World!
PS /> [pscustomobject]#{ alias2 = 'World!' } | SayHello
Hello World!
If you want to know if a functions has aliases you can always use Get-Help:
PS /> Get-Help Start-Process -Parameter * |
Where-Object Aliases -NE 'none' |
Select-Object Name, Aliases
name aliases
---- -------
ArgumentList Args
Credential RunAs
FilePath PSPath
LoadUserProfile Lup
NoNewWindow nnw
RedirectStandardError RSE
RedirectStandardInput RSI
RedirectStandardOutput RSO
Other relevant documention:
about_Pipelines
about_Functions
about_Functions_Advanced_Parameters

A positional parameter cannot be found that accepts argument 't'

I am getting the following error
New-AzResourceGroup : A positional parameter cannot be found that accepts argument 't'.
At line:1 char:1
+ New-AzResourceGroup -Name #rgName -Location #location -Tag #{LoB="pla ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [New-AzResourceGroup], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.NewAzureResourceGrou
pCmdlet
while trying to create a new resource group with the following code. Where is the issue?
$rgName = "storage-dev-rg"
$location = "eastus"
New-AzResourceGroup -Name #rgName -Location #location -Tag #{LoB="platform"; CostCenter="IT"}
To quote your own answer:
The declared variables should be referenced using $, not with #.
about_Variables explains that in order to create and later reference variables in PowerShell, you prefix their name with sigil $ in both cases; i.e., $rgName and $location in your case.
You only ever prefix a variable name with sigil # if you want to perform splatting (see about_Splatting).
(The sigil # has other uses too, namely as #(...), the array-subexpression operator, and as #{ ... }, a hashtable literal, as also used in your command.)
Splatting is used to pass an array-like value stored in a variable as individual positional arguments or, more typically, to bind the entries of a hashtable containing parameter name-value pairs to the parameters so named - see this answer.
Since your variables contain strings and strings can be treated as an array-like collection of characters (via the System.Collections.IEnumerable interface), splatting a string variable effectively passes each character as a separate, positional argument.
PS> $foo = 'bar'; Write-Output #foo # same as: Write-Output 'b' 'a' 'r'
b
a
r
As for what you tried:
-Name #rgName, based on $rgName containing string 'storage-dev-rg', passed 's' - the 1st char only - to -Name, and the remaining characters as individual, positional arguments. 't', the 2nd character, was the first such positional argument, and since New-AzResourceGroup didn't expect any positional arguments, it complained about it.
I figured it out. The declared variables should be referenced using $, not with #.

How to use dash argument in Powershell?

I am porting a script from bash to PowerShell, and I would like to keep the same support for argument parsing in both. In the bash, one of the possible arguments is -- and I want to also detect that argument in PowerShell. However, nothing I've tried so far has worked. I cannot define it as an argument like param($-) as that causes a compile error. Also, if I decide to completely forego PowerShell argument processing, and just use $args everything appears good, but when I run the function, the -- argument is missing.
Function Test-Function {
Write-Host $args
}
Test-Function -- -args go -here # Prints "-args go -here"
I know about $PSBoundParameters as well, but the value isn't there, because I can't bind a parameter named $-. Are there any other mechanisms here that I can try, or any solution?
For a bit more context, note that me using PowerShell is a side effect. This isn't expected to be used as a normal PowerShell command, I have also written a batch wrapper around this, but the logic of the wrapper is more complex than I wanted to write in batch, so the batch wrapper just calls the PowerShell function, which then does the more complex processing.
I found a way to do so, but instead of double-hyphen you have to pass 3 of them.
This is a simple function, you can change the code as you want:
function Test-Hyphen {
param(
${-}
)
if (${-}) {
write-host "You used triple-hyphen"
} else {
write-host "You didn't use triple-hyphen"
}
}
Sample 1
Test-Hyphen
Output
You didn't use triple-hyphen
Sample 2
Test-Hyphen ---
Output
You used triple-hyphen
As an aside: PowerShell allows a surprising range of variable names, but you have to enclose them in {...} in order for them to be recognized; that is, ${-} technically works, but it doesn't solve your problem.
The challenge is that PowerShell quietly strips -- from the list of arguments - and the only way to preserve that token is you precede it with the PSv3+ stop-parsing symbol, --%, which, however, fundamentally changes how the arguments are passed and is obviously an extra requirement, which is what you're trying to avoid.
Your best bet is to try - suboptimal - workarounds:
Option A: In your batch-file wrapper, translate -- to a special argument that PowerShell does preserve and pass it instead; the PowerShell script will then have to re-translate that special argument to --.
Option B: Perform custom argument parsing in PowerShell:
You can analyze $MyInvocation.Line, which contains the raw command line that invoked your script, and look for the presence of -- there.
Getting this right and making it robust is nontrivial, however.
Here's a reasonably robust approach:
# Don't use `param()` or `$args` - instead, do your own argument parsing:
# Extract the argument list from the invocation command line.
$argList = ($MyInvocation.Line -replace ('^.*' + [regex]::Escape($MyInvocation.InvocationName)) -split '[;|]')[0].Trim()
# Use Invoke-Expression with a Write-Output call to parse the raw argument list,
# performing evaluation and splitting it into an array:
$customArgs = if ($argList) { #(Invoke-Expression "Write-Output -- $argList") } else { #() }
# Print the resulting arguments array for verification:
$i = 0
$customArgs | % { "Arg #$((++$i)): [$_]" }
Note:
There are undoubtedly edge cases where the argument list may not be correctly extracted or where the re-evaluation of the raw arguments causes side effect, but for the majority of cases - especially when called from outside PowerShell - this should do.
While useful here, Invoke-Expression should generally be avoided.
If your script is named foo.ps1 and you invoked it as ./foo.ps1 -- -args go -here, you'd see the following output:
Arg #1: [--]
Arg #2: [-args]
Arg #3: [go]
Arg #4: [-here]
I came up with the following solution, which works well also inside pipelines multi-line expressions. I am using the PowerShell Parser to parse the invocation expression string (while ignoring any incomplete tokens, which might be present at the end of $MyInfocation.Line value) and then Invoke-Expression with Write-Output to get the actual argument values:
# Parse the whole invocation line
$code = [System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.Line.Substring($MyInvocation.OffsetInLine - 1), [ref]$null, [ref]$null)
# Find our invocation expression without redirections
$myline = $code.Find({$args[0].CommandElements}, $true).CommandElements | % { $_.ToString() } | Join-String -Separator ' '
# Get the argument values
$command, $arguments = Invoke-Expression ('Write-Output -- ' + $myline)
# Fine-tune arguments to be always an array
if ( $arguments -is [string] ) { $arguments = #($arguments) }
if ( $arguments -eq $null ) { $arguments = #() }
Please be aware that the original values in the function call are reevaluated in Invoke-Expression, so any local variables might shadow values of the actual arguments. Because of that, you can also use this (almost) one-liner at the top of your function, which prevents the pollution of local variables:
# Parse arguments
$command, $arguments = Invoke-Expression ('Write-Output -- ' + ([System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.Line.Substring($MyInvocation.OffsetInLine - 1), [ref]$null, [ref]$null).Find({$args[0].CommandElements}, $true).CommandElements | % { $_.ToString() } | Join-String -Separator ' '))
# Fine-tune arguments to be always an array
if ( $arguments -is [string] ) { $arguments = #($arguments) }
if ( $arguments -eq $null ) { $arguments = #() }

Passing integer array as command line argument to powershell script

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"