Run PowerShell custom function in new window - powershell

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.

Related

How to escape the following regex, so its usable in PowerShell?

As far as I know, there is no way in PowerShell to execute an exe with parameters specified in a variable and direct the return of the exe into a variable. Therefore I am currently writing a small function to make this possible. But now I am stuck at the point that the parameters have to be passed individually when calling with &. (This is not necessary for all programs, but some programs cause problems if you pass all parameters as a string in a variable) Therefore I want to use a split to write the parameters passed to my function into an array. And then pass the array with the parameters in my exe call.
For this I have the following regex:
[^\s"']+|"([^"]*)"|'([^']*)'
This regex allows that single and double quotes are taken into account when passing parameters and that a text with spaces inside them is not split.
But unfortunately I don't have the slightest idea how to best escape this regex so that it doesn't cause any problems in the PowerShell script.
Here then still my function to make it a little easier to understand:
The function executes the file passed in the $Path parameter with the parameters from the $Arguments. Before the execution i try to split the $Arguments with the regex. As return of the function, you get an object with the ExitCode and the output of the executed file. Here you can see my attempt, but the quotes cause problems with the following code.
function Invoke-Process ($Path,$Arguments){
[PsObject[]] $ReturnValue = #()
$Params=$Arguments -split([regex]::escape([^\s"']+|"([^"]*)"|'([^']*)'))
$ExCode = 0
try {
$ProcOutput = & $Path $Params | out-string
}catch{
$ProcOutput = "Failed: $($_.Exception.Message)"
$ExCode = 1
}
$ReturnValue += [PsObject]#{ ExitCode = $ExCode; Output = $ProcOutput}
Return $ReturnValue
}
The function is called as follows:
$ExePath = "C:\arcconf\arcconf.exe"
$ExeParams = "getconfig 1"
$Output = Invoke-Process $ExePath $ExeParams
I hope you can understand my problem. I am also open to other ways of writing the function.
Greetings
There's nothing you need to escape - the pattern is perfectly valid.
All you need is a string literal type that won't treat the quotation marks as special. For this, I would suggest a verbatim here-string:
#'
This is a single-line string ' that " can have all sorts of verbatim quotation marks
'#
The qualifiers for a here-string is #' as the last token on the preceding line and '# as the first token on the following line (for an expandable here-string, use #" and "#).
Try running it with valid sample input:
$pattern = #'
[^\s"']+|"([^"]*)"|'([^']*)'
'#
'getconfig 1 "some string" unescaped stuff 123' |Select-String $pattern -AllMatches |ForEach-Object {$_.Matches.Value}
Which, as expected, returns:
gesture
1
"some string"
unescaped
stuff
123
As an alternative to a here-string, the best alternative is a regular verbatim string literal. The only character you need to escape is ', and the escape sequence is simply two in a row '', so your source code becomes:
$pattern = '[^\s"'']+|"([^"]*)"|''([^'']*)'''
Mathias R. Jessen's helpful answer answers your immediate question well.
Taking a step back:
there is no way in PowerShell to execute an exe with parameters specified in a variable and direct the return of the exe into a variable
No: PowerShell does support both of those things:
$output = someExternalProgram ... captures the stdout output from an external program in variable $output; use redirection 2>&1 to also capture stderr output; use >$null or 2>$null to selectively discard stdout or stderr output - see this answer for details.
someExternalProgram $someVariable ... passes the value of variable $someVariable as an argument to an external program; if $someVariable is an array (collection) of values, each element is passed as a separate argument. Instead of a variable reference, you may also use the output from a command or expression, via (...); e.g., someExternalProgram (1 + 2) passes 3 as the argument - see this answer for details.
Note: An array's elements getting passed as individual arguments only happens for external programs by default; to PowerShell-native commands, arrays are passed as a whole, as a single argument - unless you explicitly use splatting, in which case you must pass #someVariable rather than $someVariable. For external programs, #someVariable is effectively the same as $someVariable. While array-based splatting also works for PowerShell-native commands, for passing positional arguments only, the typical and more robust and complete approach is to use hashtable-based splatting, where the target parameters are explicitly identified.
A general caveat that applies whether or not you use variables or literal values as arguments: Up to at least PowerShell 7.2.x, passing empty-string arguments or arguments with embedded " chars. to external programs is broken - see this answer.
With the above in mind you can rewrite your function as follows, which obviates the need for - ultimately brittle - regex parsing:
function Invoke-Process {
# Split the arguments into the executable name / path and all
# remaining arguments.
$exe, $passThruArgs = $args
try {
# Call the executable with the pass-through arguments.
# and capture its *stdout* output.
$output = & $exe #passThruArgs
$exitCode = $LASTEXITCODE # Save the process' exit code.
} catch {
# Note: Only If $exe is empty or isn't a valid executable name or path
# does a trappable statement-terminating error occur.
# By contrast, if the executable can be called in principle, NO
# trappable error occurs if the process exit code is nonzero.
$exitCode = 127 # Use a special exit code
$output = $_.ToString() # Use the exception message
}
# Construct and output a custom object containing
# the captured stdout output and the process' exit code.
[pscustomobject] #{
ExitCode = $exitCode
Output = $output
}
}
Sample calls:
Invoke-Process cmd /c 'echo hi!'
# Equivalent, using an array variable for the pass-through arguments.
$argsForCmd = '/c', 'echo hi!'
Invoke-Process cmd #argsForCmd
Note:
Since Invoke-Process is a PowerShell command,
splatting (#argsForCmd) is necessary here in order to pass the array
elements as individual arguments, which inside the function are then reflected in the automatic $args variable variable.
The automatic $args variable is only available in simple functions, as opposed to advanced ones, which behave like cmdlets and therefore automatically support additional features, such as common parameters; to make your function and advanced one, replace the line $exe, $passThruArgs = $args at the top of the function with the following:
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $exe,
[Parameter(ValueFromRemainingArguments)]
[string[]] $passThruArgs
)

In Powershell what is the comma for, how does it work in this code example?

I just asked about a problem and the suggested code has some obscure points for me.
Specifically on this line of code
powershell.exe -c "Start-Process -Verb RunAs cmd /k, ('%~f0' -replace '[ &]', '^$0')"
I would like to know what the comma after the cmd /k represents.
What's this? How does it work? Is there a link to the documentation explaining this ?
What's is the $0 in the last part. '^$0')"
The Start-Process call embedded in your Windows PowerShell CLI (powershell.exe) call uses positional parameter binding, for brevity:
cmd, as the first positional argument, binds to the -FilePath parameter, i.e. the name or path of the executable to launch.
/k, (...), as the second positional argument, binds to the -ArgumentList parameter, which accepts an array of arguments to pass the target executable, whose elements are separated with ,
Start-Process then builds a command line behind the scenes, by joining the -FilePath argument and the -ArgumentList array elements with spaces, and launches it.
As an aside:
The way Start-Process builds the command line is actually broken, because no on-demand quoting and escaping is performed - see this answer for background information; in your specific case, however, the problem doesn't surface.
To work around the bug it is actually preferable in general to pass a single string to -ArgumentList that contains all arguments to pass; however, in this case that would have complicated quoting (you'd need an embedded "..." string or an explicit string-concatenation expression), so the simpler solution of enumerating the arguments with , was chosen.
See this answer for how to discover a given cmdlet's positional parameters; in short: invoke it with -? (Start-Process -?) and, in the relevant parameter set, look for the parameters whose names are enclosed in [...]; e.g.
Start-Process [-FilePath] <System.String> [[-ArgumentList] <System.String[]>] ...

MSIEXEC - Argumentlist with Variable

I am trying to start an installation via msiexec.
The following parameters are available and if I use this on that way, the installation will be correct.
This is the example from the vendor:
msiexec.exe /i ApexOne.msi MyServer="win2016-x64:8080|4343" MyDomain="Workgroup\Subdomain" /Lv+ "c:\temp\MSI.log"
This is my code:
Start-Process -FilePath "$env:systemroot\system32\msiexec.exe" -ArgumentList #('/i ".\agent.msi" MyServer="servername:8080|4344" MyDomain="999-Test"') -wait
But for automation I have to use the variable $domain to generate the correct domain.
I tried different codes, but nothing helped:
This is the code for my variable $domain
$domain='999-Test'
Example:
Start-Process -FilePath "$env:systemroot\system32\msiexec.exe" -ArgumentList #('/i ".\agent.msi" MyServer="servername:8080|4344" MyDomain=**"$domain"'**) -wait
I tried that also, but I don't know how to fix this failure.
I think the interpration of the variable is false, but I can not fix it.
-ArgumentList #('/i ".\agent.msi"','MyServer="servername:8080|4344"','MyDomain='$domain'')
Maybe someone can help me to fix this error?
Due to a long-standing bug in Start-Process, it is actually preferable to always pass all arguments for the target process encoded in a single string passed to -ArgumentList (-Args), rather than individually, as elements of an array - see this answer.
Doing so requires two things:
To incorporate variable values, you must use an expandable string, i.e. a double-quoted string, "...".
Any embedded quoting inside this overall "..." string must itself use double-quoting only, as CLIs on Windows generally only understand " (double quotes) and not also ' (single quotes) to have syntactic function. To embed " characters in a "..." string, escape them as `" (or as ""; inside a '...' string, you needn't escape them at all).
While this is somewhat cumbersome, it does have the advantage of allowing you to directly control the exact command line that the target process will see - and that enables you satisfy msiexec's picky syntax requirements where PROP="value with spaces" must be passed exactly with this value-only double-quoting (whereas PowerShell - justifiably - turns this argument into "PROP=value with spaces" behind the scenes, when it - of necessity - re-quotes arguments to synthesize the actual command line to use).
Therefore, use the following (using positional argument binding to -FilePath and -ArgumentList, respectively):
Start-Process -Wait $env:systemroot\system32\msiexec.exe "/i .\agent.msi MyServer=`"servername:8080|4344`" MyDomain=`"999-Test`""
Note: Strictly speaking, you do not need the embedded `"...`" quoting in your case, given that your property values have no embedded spaces.
The msiexec.exe program seems to have some peculiarities in its command-line parser. I recommend an approach like the following:
$msiFileName = ".\agent.msi"
$myServer = "servername:8080|4344"
$myDomain = "999-Test"
$argumentList = #(
'/i'
'"{0}"' -f $msiFileName
'MyServer="{0}"' -f $myServer
'MyDomain="{0}"' -f $myDomain
)
$startArgs = #{
"FilePath" = "msiexec.exe"
"ArgumentList" = $argumentList
"Wait" = $true
}
Start-Process #startArgs
This example creates an array of string arguments for the -ArgumentList parameter using single quotes (') and string formatting (-f). In the above example, each element in the $argumentList array is a separate string and contains the following elements:
/i
".\agent.msi"
MyServer="servername:8080|4344"
MyDomain="999-Test"
(Note that in the content of the array we are preserving the " characters.)
Next, we create a hashtable for the Start-Process cmdlet and call it using the # operator. (Creating a hashtable for a cmdlet's parameters is known as splatting and is a convenient way to build a large number of parameters for a cmdlet in a more readable and maintainable way.)

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.

PowerShell : How can I include single quotations in Argument?

I'd like to pass the argument include '' single quotations for PowerShell Invoke-Sqlcmd as follows.
Batch:
set table_name=XXXX
set extract_condition=start_dt='2016-10-01'
%PS_EXE% -ExecutionPolicy ByPass %PS_DIR%\Export_S3.ps1 -StrTableName %table_name% -StrCondition %extract_condition%
PowerShell:
$StrQry="select * from " + $StrTableName + " where " + $StrCondition + ";"
Invoke-Sqlcmd -Query $StrQry
It returns Operand Error, "date is not compatible with int", might be due to single quotation is not sent to PowerShell.
(It can work unless $StrCondition has no sinble quotation)
In actual case, we'd like to handle many tables with different extract conditions, so those variables should be read respectivelly from the list file in batch for loop.
I'd be appreciated if you would give us any advice.
Thank you.
To pass literal arguments to a script, use -File rather than the (implied) -Command parameter; also, double-quote them, to be safe:
%PS_EXE% -ExecutionPolicy ByPass -File "%PS_DIR%\Export_S3.ps1" -StrTableName "%table_name%" -StrCondition "%extract_condition%"
As an aside: In PowerShell Core, -File is now the default.
If you use -Command (possibly by default in Windows PowerShell), the arguments passed are interpreted by PowerShell as they would be from within PowerShell itself, and a token such as start_dt='2016-10-01', i.e., one without outer quoting, simply has the embedded single quotes stripped, which you can verify from within PowerShell itself:
PS> Write-Output start_dt='2016-10-01'
start_dt=2016-10-01 # !! single quotes were STRIPPED.
While enclosing the token as whole in "..." would have helped in this case, it is generally preferable to use -File if your intent is to pass literal values to a script - i.e., values that shouldn't be interpreted by PowerShell on receipt.