Powershell script from another script with multiple positional arguments - powershell

There are already some questions about how to call one PS script with arguments from another like that one
Powershell: how to invoke a second script with arguments from a script
But I'm stuck in case I have first script with multiple positional parameters.
testpar2.ps1 calls testpars.ps1
#$arglist=(some call to database to return argument_string)
$arglist="first_argument second_argument third_argument"
$cmd=".\testpars.ps1"
& $cmd $arglist
$arglist variable should be populated with string from database. This string contains arguments for testpar.ps1.
testpars.ps1 looks like
echo argument1 is $args[0]
echo argument2 is $args[1]
echo arugment3 is $args[3]
# some_command_call $arg[0] $arg[1] $arg[2]
This arguments should be used in testpars.ps1 in some way, like to path them to some command.
But when i run testpars2.ps1 i got
argument1 is first_argument second_argument third argument
argument2 is
arugment3 is
It thinks that it is one argument, not a list of them.

As you can see, when you pass a string to a function, PowerShell treats that as a single value. This is usually good, because it avoids the problem in CMD of having to constantly quote and unquote strings. To get separate values, you need to split() the string into an array.
$arglistArray = $arglist.split()
Now you have an array of three strings, but they're still all passed as one parameter. PowerShell has an idea known as splatting to pass an array of values as multiple arguments. To use splatting, replace the $ with # in the argument list.
& $cmd #arglistArray
For more information on splatting, type Get-Help about_Splatting

Related

Powershell outputs vertically

I just wanted to run a simple command like echo #profile, but the output is vertical. I can theoretically read and understand the output, but it is a big unconvenience. How can I fix it?
You don't normally reference variables with an # symbol, you almost always use the $ to reference a variable's value by the variable name. You can also use the Variable: provider or Get-Variable, but I won't get into those here.
If you were to omit echo, you would actually get the following error message:
The splatting operator '#' cannot be used to reference variables in an expression.
'#var' can be used only as an argument to a command. To reference variables
in an expression use '$var'.
This is because using #var is a technique called Splatting, which is the practice of using an array or hashtable to provide the arguments to a command, function, or cmdlet. Note you cannot currently splat arguments to methods. Review my answer linked above for more information on how to actually splat arguments in several use cases.
As for why you get the vertical output, note that echo is actually an alias to Write-Output. Write-Output accepts positional arguments, for which it will output each object passed in on its own line. When you splat a string as an array of arguments to a function, it converts the string to an array of characters, so effectively you are passing in each character of #profile to Write-Output as its own argument, then spitting each element of the array back out. And when PowerShell displays an array directly to the console, it displays each element on its own line.
Note: Of the different Write- cmdlets, Write-Output is unique in that it will output each positional parameter on its own line as it returns an array for each argument you pass in. The other Write- cmdlets will instead join each element of the array into a single space-delimited string. The Write- cmdlets come into play when working with the different output streams in PowerShell. Here is another answer of mine which explains what the different output streams mean and how to write to them.
In addition, for displaying purely information text to the end user that does not need additional programmatic processing in your script or session, use Write-Host.
This is why you get the vertical output, because #profile is being converted to an array, then splatted into Write-Output as an array of characters, and Write-Output will write back all arguments of an array as individual elements of a new array. PowerShell will then display the new array with each element on its own line.
I suspect what you actually want to do is output $profile. You can use one of the following techniques (note that echo/Write-Output are often redundant to use):
# Use the alias
echo $profile
# Use Write-Output
Write-Output $profile
# Omit Write-Output entirely
$profile
# View one of the alternative profiles by name
# CurrentUserCurrentHost is the default
# and is most often the one you are looking for
$profile.CurrentUserCurrentHost
$profile.CurrentUserAllHosts
$profile.AllUsersCurrentHost
$profile.AllUsersAllHosts

Powershell seems to get the wrong number of elements in a param array

Assume I have (In file test.ps1):
param (
[string[]] $one
)
Write-Host $one.Count
If I do:
powershell -File test.ps1 -one "hello","cat","Dog"
I get:
1
But I expect:
3
Why?
"-one" is getting passed in as a whole string as the converting happens before calling the method.
You could alternatively call it like the following
powershell -Command {.\test.ps1 -one "hello","cat","Dog"}
To complement Kevin Smith's helpful answer:
The only way to pass an array to a PowerShell script via the PowerShell CLI (powershell.exe; pwsh for PowerShell Core) is to use -Commmand (-c).
By contrast, -File interprets the arguments as literal values, and does not recognize arrays, variable references ($foo), ...; in the case at hand, the script ends up seeing a single string with literal contents hello,cat,Dog (due to double-quote removal).
From inside PowerShell:
Use -Command with a script block ({ ... }), as shown in Kevin's answer, which not only simplifies the syntax (just use regular syntax inside the block), but produces type-rich output (not just strings, as with other external programs), because the target PowerShell instance uses the CLIXML serialization format to output its results, which the calling session automatically deserializes, the same way that PowerShell remoting / background jobs work (as with the latter, however, the type fidelity of the deserialization is invariably limited; see this answer).
Note, however, that from within PowerShell you generally don't need the CLI, which creates a (costly) child process, and can just invoke a *.ps1 script file directly:
.\test.ps1 -one hello, cat, Dog
From outside PowerShell - typically cmd.exe / a batch file - use -Command with a single, double-quoted string containing the PowerShell code to execute, given that using script blocks isn't supported from the outside.
powershell -Command ".\test.ps1 -one hello, cat, Dog"
Note that, with -Command, just as with direct invocation inside PowerShell, you must use .\test.ps1 rather than just test.ps1 in order to execute a file by that name in the current directory, which is a security feature.
Also note that with your simple argument values, "..."-enclosing them is optional, which is why the commands above use just hello, cat, Dog instead of "hello", "cat", "Dog"; in fact, using embedded " chars. in an overall "..." command string can get quite tricky - see this answer.

Run PowerShell custom function in new window

Function Check-PC
{
$PC = Read-Host "PC Name"
If($PC -eq "exit"){EXIT}
Else{
Write-Host "Pinging $PC to confirm status..."
PING -n 1 $PC
}
This is a snippet of a function I have written into a PowerShell script. I would like the function to run in a new instance of PowerShell, not in the main window.
Is it possible to run this in a separate process of PowerShell without writing it as a separate script and calling the script? Something like this:
$x= Start-Process powershell | Check-PC
I need to keep everything in one script if possible.
Note: It is the involvement of Start-Process that complicates the solution significantly - see below. If powershell were invoked directly from PowerShell, you could safely pass a script block as follows:
powershell ${function:Check-PC} # !! Does NOT work with Start-Process
${function:Check-PC} is an instance of variable namespace notation: it returns the function's body as a script block ([scriptblock] instance); it is the more concise and faster equivalent of Get-Content Function:Check-PC.
If you needed to pass (positional-only) arguments to the script block, you'd have to append -Args, followed by the arguments as an array (,-separated).
Start-Process solution with an auxiliary self-deleting temporary file:
See the bottom half of this answer to a related question.
Start-Process solution with -EncodedCommand and Base64 encoding:
Start-Process powershell -args '-noprofile', '-noexit', '-EncodedCommand',
([Convert]::ToBase64String(
[Text.Encoding]::Unicode.GetBytes(
(Get-Command -Type Function Check-PC).Definition
)
))
The new powershell instance will not see your current session's definitions (unless they're defined in your profiles), so you must pass the body of your function to it (the source code to execute).
(Get-Command -Type Function Check-PC).Definition returns the body of your function definition as a string.
The string needs escaping, however, in order to be passed to the new Powershell process unmodified.
" instances embedded in the string are stripped, unless they are either represented as \" or tripled (""").
(\" rather than the usual `" is needed to escape double quotes in this case, because PowerShell expects \" when passing a command to the powershell.exe executable.)
Similarly, if the string as a whole or a double-quoted string inside the function body ends in (a non-empty run of) \, that \ would be interpreted as an escape character, so the \ must be doubled.Thanks, PetSerAl.
The most robust way to bypass these quoting (escaping) headaches is to use a Base64-encoded string in combination with the -EncodedCommand parameter:
[Convert]::ToBase64String() creates a Base64-encoded string from a [byte[]] array.
[Text.Encoding]::Unicode.GetBytes() converts the (internally UTF-16 -
"Unicode") string to a [byte[]] array.
Note: To also pass arguments, you have two options:
You can "bake" them into the -EncodedCommand argument, assuming you can call a command to pass them to there - see below, which shows how to define your function as such in the new session, so you can call it by name with arguments.Thanks, Abraham Zinala
The advantage of this approach is that you can pass named arguments this way. The disadvantage is that you are limited to arguments that have string-literal representations.
You can use the (currently undocumented) -EncodedArguments parameter, to which you must similarly pass a Base64-encoded string, albeit based on the CLIXML representation of the array of arguments to pass
The advantage of this approach is that you can pass a wider array of data types, within the limits of the type fidelity that CLIXML serialization can provide - see this answer; the disadvantage is that only positional arguments are supported this way (although you could work around that by passing a hashtable that the target code then uses for splatting with the ultimate target command).
Here's a simplified, self-contained example, which uses Write-Output to echo the (invariably positional) arguments received:
$command = 'Write-Output $args'
$argList = 'foo', 42
Start-Process powershell -args '-noprofile', '-noexit',
'-EncodedCommand',
([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command))),
'-EncodedArguments',
([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes(
[System.Management.Automation.PSSerializer]::Serialize($argList)
)))
In case you want to pass the complete function, so it can be called by name in order to pass arguments as part of the command string, a little more work is needed.
# Simply wrapping the body in `function <name> { ... }` is enough.
$func = (Get-Command -Type Function Check-PC)
$wholeFuncDef = 'Function ' + $func.Name + " {`n" + $func.Definition + "`n}"
Start-Process powershell -args '-noprofile', '-noexit', '-EncodedCommand', `
([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes("$wholeFuncDef; Check-PC")))
As stated above, you can "bake" any arguments to pass to your function - assuming they can be represented as string literals - into the -EncodedCommand argument, simply by appending them inside the "$wholeFuncDef; Check-PC" string above; e.g.,
"$wholeFuncDef; Check-PC -Foo Bar -Baz Quux"
Start-Process solution with regex-based escaping of the source code to pass:
PetSerAl suggests the following alternative, which uses a regex to perform the escaping.
The solution is more concise, but somewhat mind-bending:
Start-Process powershell -args '-noprofile', '-noexit', '-Command', `
('"' +
((Get-Command -Type Function Check-PC).Definition -replace '"|\\(?=\\*("|$))', '\$&') +
'"')
"|\\(?=\\*("|$)) matches every " instance and every nonempty run of \ chars. - character by character - that directly precedes a " char. or the very end of the string.
\\ is needed in the context of a regex to escape a single, literal \.
(?=\\*("|$)) is a positive look-ahead assertion that matches \ only if followed by " or the end of the string ($), either directly, or with further \ instances in between (\\*). Note that since assertions do not contribute to the match, the \ chars., if there are multiple ones, are still matched one by one.
\$& replaces each matched character with a \ followed by the character itself ($&) - see this answer for the constructs you can use in the replacement string of a -replace expression.
Enclosing the result in "..." ('"' + ... + '"') is needed to prevent whitespace normalization; without it, any run of more than one space char. and/or tab char. would be normalized to a single space, because the entire string wouldn't be recognized as a single argument.
Note that if you were to invoke powershell directly, PowerShell would generally automatically enclose the string in "..." behind the scenes, because it does so for arguments that contain whitespace when calling an external utility (a native command-line application), which is what powershell.exe is - unlike the Start-Process cmdlet.
PetSerAl points out that the automatic double-quoting mechanism is not quite that simple, however (the specific content of the string matters with respect to whether automatic double-quoting is applied), and that the specific behavior changed in v5, and again (slightly) in v6.

What is the Powershell equivalent for bash $*?

In other words how can I get the command line of the script itself?
So, I know about $PSBoundParameters, but it is not the same. I just want to get the string containing the passed in parameters as is.
How do I do it?
See get-help about_Automatic_Variables.
$Args
Contains an array of the undeclared parameters and/or parameter
values that are passed to a function, script, or script block.
When you create a function, you can declare the parameters by using the
param keyword or by adding a comma-separated list of parameters in
parentheses after the function name.
In an event action, the $Args variable contains objects that represent
the event arguments of the event that is being processed. This variable
is populated only within the Action block of an event registration
command. The value of this variable can also be found in the SourceArgs
property of the PSEventArgs object (System.Management.Automation.PSEventArgs)
that Get-Event returns.
Example:
test.ps1
param (
)
Write-Output "Args:"
$args
Output:
PS L:\test> .\test.ps1 foo bar, 'this is extra'
Args:
foo
bar
this is extra
$MyInvocation.Line
Read about_Automatic_Variables:
$MyInvocation
Contains an information about the current command, such as the name,
parameters, parameter values, and information about how the command was
started, called, or "invoked," such as the name of the script that called
the current command.
$MyInvocation is populated only for scripts, function, and script blocks.
just want to get the string containing the passed in parameters as is.
You're looking for a powershell equivalent of "$#", not $*.
And the other answers in the thread are not equivalent. The best quick way I have found for this is:
$args | join-string -sep ' '
which can in turn be used for any string array you may have on hand, not just the $args array.

Calling powershell script with many args from cmd

I have a simple powershell script. It takes two parameters. Both are part of methods (Get-ResourcePool) which take string arguments.
If I call the function definition in the Powershell script, like so:
functName CI *FMS
That works fine.
The function call in Powershell is (and like so because this script will be called from outside):
FuncName $($args[0], $args[1])
I try to call this from Powershell editor, where I have the snapins I need all installed, like so:
C:\Script.ps1 A "*s"
Where scrpt is the name of my .ps1 file. There is one function. This, however, fails with an error that the argument is null or empty.
Any ideas why?
EDIT:
The function signature is:
function RevertToSnapshot($otherarg, $wildcard)
I use $wildcard here:
$SearchString = [System.String]::Concat("*",
$VMWildcard)
Name $SearchString (Name is a parameter to get-vm in powercli).
This calling style:
FuncName $($args[0], $args[1])
Will result in just one argument passed to FuncName - a single array with two elements. You probably want:
FuncName $args[0] $args[1]
In general, with PowerShell you call cmdlets, functions, aliases using space separated arguments and no parens. Calling .NET methods is the one exception to this rule where you have to use parens and commas to separate arguments e.g:
[string]::Concat('ab','c')