How to pass a variable to new console window in Powershell - 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"

Related

Passing scriptblock as an argument to Invoke-Command scriptblock in powershell

I have a powershell script in which I have been trying to pass scriptblock(i.e $sb) as an argument to another scriptblock. However, I keep getting error that:
Cannot convert the "{get-member}" value of type "System.String" to
type "System.Management.Automation.ScriptBlock
a.psm1:
$sb = {get-member}
# $type = $sb.GetType().FullName => Returns scriptblock as data type
$result = Invoke-Command -Session "DC" -Scriptblock
{
Param(
[Parameter(Mandatory=$true)]
[scriptblock]
$sb
)
//Do some task
} -ArgumentList $sb
I am not able to figure out why $sb is treated as a string instead of Scriptblock?
The only way it works is changing the argument inside the Invoke-Command scriptblock to be of type string instead of Scriptblock.
I am not sure why scriptblocks gets implicitly converted to string while passing argument to Invoke-Command scriptblock.
When a script block (type [scriptblock], { ... } as a literal) is passed to code that executes out-of-process, such as during remoting (your case) or in background jobs, XML-based serialization and deserialization must be performed.
On deserialization in the target process, [scriptblock] instances indeed unexpectedly become strings.
Unfortunately and bewilderingly, this behavior has been declared by design(!) - see GitHub issue #11698.
Your only option is to pass the script block('s source code) as a string, and convert it back to a script block via [scriptblock]::Create(); a simple example, using a background job:
Start-Job {
param([string] $scriptBlockSourceCode) # Note the type: [string]
# Use [scriptblock]::Create() to turn the string into a script block,
# and execute it with &
& ([scriptblock]::Create($scriptBlockSourceCode))
} -ArgumentList { 'hi' } |
Receive-Job -Wait -AutoRemove

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

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 = #() }

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.

Powershell Start-Process to start Powershell session and pass local variables

Is there a way to use the Powershell Start-Process cmdlet to start a new Powershell session and pass a scriptblock with local variables (once of which will be an array)?
Example:
$Array = #(1,2,3,4)
$String = "This is string number"
$Scriptblock = {$Array | ForEach-Object {Write-Host $String $_}}
Start-Process Powershell -ArgumentList "$Scriptblock"
Thanks.
I'm pretty sure there's no direct way to pass variables from one PowerShell session to another. The best you can do is some workaround, like declaring the variables in the code you pass in -ArgumentList, interpolating the values in the calling session. How you interpolate the variables into the declarations in -ArgumentList depends on what types of variables. For an array and a string you could do something like this:
$command = '<contents of your scriptblock without the curly braces>'
Start-Process powershell -ArgumentList ("`$Array = echo $Array; `$String = '$String';" + $command)
I was able to get this to work by joining the array with "/" to create a string and entering the scriptblock into another .ps1 script with appropriate parameters and splitting the joined string back to an array within the second script and using
Start-Process Powershell -ArgumentList "&C:\script.ps1 $JoinedArray $String"
Ugly, but it's the only way I could get it to work. Thanks for all the replies.
You could wrap the contents of your script block in a function, and then call the function from the ArgumentList and pass in the variables as parameters to the function, as I do on this post.
$ScriptBlock = {
function Test([string]$someParameter)
{
# Use $someParameter to do something...
}
}
# Run the script block and pass in parameters.
$myString = "Hello"
Start-Process -FilePath PowerShell -ArgumentList "-Command & {$ScriptBlock Test('$myString')}"
The command line options for PowerShell.exe say that you should be able to pass arguments when using a script block by adding -args:
PowerShell.exe -Command { - | <script-block> [-args <arg-array>] | <string> [<CommandParameters>] }
However when I try to do that I get the following error:
-args : The term '-args' 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.
I added $MyInvocation | fl to the script block to see what was happening, and it looks like the -args is just appended to the deserialized commands in the script block (hence the error since -args is not a valid command). I also tried using GetNewClosure() and $Using:VariableName but those only appear to work when the script block is invoked (as opposed to this where we are using it to serialize/deserialize the commands).
The I was able to get it to work by wrapping it in a function like deadlydog's answer.
$var = "this is a test"
$scriptblock = {
$MyInvocation | fl #Show deserialized commands
function AdminTasks($message){
write-host "hello world: $message"
}
}
Start-Process powershell -ArgumentList '-noexit','-nologo','-noprofile','-NonInteractive','-Command',$scriptblock,"AdminTasks('$var')" -Verb runAs #-WindowStyle Hidden
#Output:
MyCommand :
$MyInvocation | fl #Show deserialized commands
function AdminTasks($message){
write-host hello world: $message
}
AdminTasks('this is a test')
BoundParameters : {}
UnboundArguments : {}
ScriptLineNumber : 0
OffsetInLine : 0
HistoryId : 1
ScriptName :
Line :
PositionMessage :
PSScriptRoot :
PSCommandPath :
InvocationName :
PipelineLength : 2
PipelinePosition : 1
ExpectingInput : False
CommandOrigin : Runspace
DisplayScriptPosition :
hello world: this is a test
Wrapping it in a script block and using $args[0] or $args[1] also works, just be aware that you many need to wrap the $var0 or $var1 in quotes if there are issues when it is deserialized and use `$ to prevent the $sb from being replaced with "" since that variable doesn't exist in the caller's scope:
$var0 = "hello"
$var1 = "world"
$scriptblock = {
$MyInvocation | fl #Show deserialized commands
$sb = {
write-host $args[0] $args[1]
}
}
Start-Process powershell -ArgumentList '-noexit','-nologo','-noprofile','-NonInteractive','-Command',$scriptblock,"& `$sb $var0 $var1"
If you want to pass objects that are serializable, but are not strings, I wrote up a solution: Is there a way to pass serializable objects to a PowerShell script with start-process?