Powershell Start-Process with Splatting - powershell

I want to call a "PS App Deployment Toolkit"-package (Link) from a PowerShell-Script with arguments.
The mentioned "PS App Deployment Toolkit"-package is a powershell-script, which I want to call with parameters. (Call a .ps1 from a .ps1)
I want to use splatting for the parameters.
I want to wait for the script to end.
I want to get the exit-code from the script.
Here is my code, which is not working:
$PSADToolKitInstallWrapper = "C:\Temp\MyPackage\PS-AppDeploy.ps1"
$PSADToolKitParameters = #{
"DeploymentType" = "Uninstall";
"DeployMode" = "Interactive";
"AllowRebootPassThru" = $True;
"TerminalServerMode" = $False;
"DisableLogging" = $False;
}
$InstallStatus = Start-Process -FilePath "PowerShell.exe" -ArgumentList $PSADToolKitInstallWrapper #PSADToolKitParameters -Wait -PassThru
Write-Host "Exit-Code: $( $InstallStatus.ExitCode )"
This Line would work fine, but I want to set the Parameters like in the example above:
$InstallStatus = Start-Process -FilePath "PowerShell.exe" -ArgumentList "$PSADToolKitInstallWrapper","-DeploymentType Install -DeployMode Silent -AllowRebootPassThru -TerminalServerMode" -Wait -PassThru
Could you please assist me to get this working?
Thank you!

I don't think you need to try so hard. Why run powershell.exe from inside a PowerShell script? You're already running PowerShell. Just run the command line you want:
$PSADToolKitParameters = #{
"DeploymentType" = "Uninstall"
"DeployMode" = "Interactive"
"AllowRebootPassThru" = $True
"TerminalServerMode" = $False
"DisableLogging" = $False
}
C:\Temp\MyPackage\PS-AppDeploy.ps1 #PSADToolKitParameters
If the path and/or filename to the script you want to run contains spaces, then call it with the invocation operator (&) and quote the filename; example:
& "C:\Temp\My Package\PS-AppDeploy.ps1" #PSADToolKitParameters
Checking the results of the script depends on what the script returns. If it returns an output object, then you can simply assign it:
$output = C:\Temp\MyPackage\PS-AppDeploy.ps1 ...
If the script runs an executable that sets an exit code, you check the value of the $LASTEXITCODE variable (this is analogous to the %ERRORLEVEL% dynamic variable in cmd.exe).

Related

How to call powershell script from other powershell script and script is assigned in the powershell object instead of the file

I am writing a PowerShell script and I have another PowerShell script.
I know, we can use below code if it is stored in the path
$scriptPath = "D:\Ashish\powershell\script.ps1"
$argumentList = "asdf fgh ghjk"
$output =Invoke-Expression "& `"$scriptPath`" $argumentList"
but my PowerShell is stored in the object instead of a file. I am using the below code
$argumentList = "asdf fgh ghjk"
$logPath = "C:\AshishG\powershell\script21.txt"
$x = 'Write-Host "Hello script2" return "script2"' #This is my powershell script
//Write code here to call this script($x) with params and store the return value in the other object and also store the logs in $logpath
The one way could be to store the PowerShell to the script.ps1 but I think, there should be some way to call it from the PowerShell object itself?
Please share your suggestions.
Seems like you're looking for a script block:
$argumentList = "asdf fgh ghjk"
$logPath = "C:\AshishG\powershell\script21.txt"
$x = {
param($arguments, $path)
"Arguments: $arguments"
"Path: $path"
}
A script block can be executed using the call operator &:
& $x -arguments $argumentList -path $logPath
Or the dot sourcing operator .:
. $x -arguments $argumentList -path $logPath
.Invoke(..) method works too however it's not commonly used and not recommended in this context. See this answer for more information:
$x.Invoke($argumentList, $logPath)
Yet another option is to call the [scriptblock]::Create(..) method, if the script is stored in strings this is the recommended alternative over Invoke-Expression which should be avoided.. This is also very useful for example when we need to pass a function to a different scope. Thanks #mklement0 for the reminder on this one :)
$argumentList = "asdf fgh ghjk"
$logPath = "C:\AshishG\powershell\script21.txt"
$x = #'
"Arguments: $argumentList"
"Path: $logPath"
'#
& ([scriptblock]::Create($x))
# Or:
$scriptblock = [scriptblock]::Create($x)
& $scriptblock

How to run a command with options and store exit code in a variable

Hello Stack Overflow experts
I am currently trying to run an exe file from a PowerShell script, and would like to store the exit code in a variable.
If run from the Windows command line on its own, the syntax is as follows:
C:\MyProgram\mycommand.exe --option1=value1 --option2=value2 --option3=value3
In Powershell, I can run the above using the call operator:
$myExe="C:\MyProgram\mycommand.exe"
$options = #(
"--option1=value1"
"--option2=value2"
"--option3=value3"
)
& $myExe #options
but I'm not sure how to assign the exit code returned by the exe file into a variable.
So far, this is what I have tried:
Tried the following syntax:
$myVariable | & $myExe #options
but the variable does not get assigned a value
Used Tee-Object
& $myExe #options | Tee-Object $myVariable
but I get a ParameterBindingValidationException error
Created a System.Diagnostics.Process object
$ps = New-Object System.Diagnostics.Process
$ps.StartInfo.FileName = "C:\MyProgram\mycommand.exe"
$ps.StartInfo.Arguments = "--option1=value1 --option2=value2 --option3=value3"
$ps.StartInfo.RedirectStandardOutput = $True
$ps.StartInfo.UseShellExecute = $False
$ps.Start()
$ps.WaitForExit()
which allows me to get the exit code by calling $ps.ExitCode, but the exe is not running correctly.
No errors are thrown, but it looks like the options are not being read correctly.
Any deas on how this can be achieved?
After the executable exits, PowerShell will populate the $LASTEXITCODE variable with the exit code:
$myExe="C:\MyProgram\mycommand.exe"
$options = #(
"--option1=value1"
"--option2=value2"
"--option3=value3"
)
$outputFromExe = & $myExe #options
return [pscustomobject]#{
Program = $myExe
ExitCode = $LASTEXITCODE
Output = $outputFromExe
}

how can I run my powershell script in line?

I want to execute powershell inline , but when I try to execute everything. I got Out-Null : A positional parameter cannot be found that accepts argument 'do'.
$OutputString = 'Hello World'
$WScript = New-Object -ComObject 'wscript.shell'
$WScript.Run('notepad.exe') | Out-Null
do {
Start-Sleep -Milliseconds 100
}
until ($WScript.AppActivate('notepad'))
$WScript.SendKeys($OutputString)
inline powershell
powershell -Command $OutputString = 'Hello World';$WScript = New-Object -ComObject 'wscript.shell';$WScript.Run('notepad.exe') | Out-Null ;do{ Start-Sleep -Milliseconds 100};until($WScript.AppActivate('notepad'));$WScript.SendKeys($OutputString);
I want to get the same result inline , but I got this issue
Your code had 3 problems:
a missing ; before the do statement (multiple statements placed on a single line must be ;-separated in PowerShell)
you've later added that, which makes your command inconsistent with the described symptom.
a missing until keyword after do { ... }
you've later added that, but with a preceding ;, which breaks the do statement.
unescaped use of |, which means that cmd.exe itself interpreted it, not PowerShell as intended (I'm assuming you're calling this from cmd.exe, otherwise there'd be no need to call PowerShell's CLI via powershell.exe).
| must be escaped as ^| to make cmd.exe pass it through to PowerShell.
Fixing these problems yields the following command for use from cmd.exe:
# From cmd.exe / a batch file:
powershell -Command $OutputString = 'Hello World'; $WScript = New-Object -ComObject 'wscript.shell'; $WScript.Run('notepad.exe') ^| Out-Null; do{ Start-Sleep -Milliseconds 100} until ($WScript.AppActivate('notepad')); $WScript.SendKeys($OutputString)
If you do want to run your code from PowerShell, you can just invoke it directly, as shown in the first snippet in your question - there's no need to create another PowerShell process via powershell.exe
If, for some reason, you do need to create another PowerShell process, use the script-block syntax ({ ... }) to pass the command, in which case no escaping is needed:
# From PowerShell
powershell -Command { $OutputString = 'Hello World'; $WScript = New-Object -ComObject 'wscript.shell'; $WScript.Run('notepad.exe') | Out-Null; do{ Start-Sleep -Milliseconds 100} until ($WScript.AppActivate('notepad')); $WScript.SendKeys($OutputString) }

Wrapping an executable with nullable parameters

In cmd I can write a batch file like so, which will provide an alias for an executable.
SuperUtil.bat
call Mine.Library.SuperUtil.exe %*
So I can call
SuperUtil -t SomeParam
This is my attempt of a PowerShell equivalent
Start-Process Mine.Library.SuperUtil.exe -NoNewWindow -ArgumentList $args
However when I call it with no parameters I get an error.
Start-Process : Cannot validate argument on parameter 'ArgumentList'.
The argument is null, empty, or an element of the argument collection
contains a null value. Supply a collection that does not contain any
null values and then try the command again.
I've tried the following but doesn't pass the arguments, plus it's quite verbose:
if ($args -ne $Null)
{
Start-Process Mine.Library.SuperUtil.exe -NoNewWindow -ArgumentList $args -Wait
}
else
{
Start-Process Mine.Library.SuperUtil.exe -NoNewWindow -Wait
}
So I want to optionally pass the arguments or not, in order to explore the command line options.
Use splatting if you want to provide optional parameter arguments:
$params = #{}
if($args){
$params['ArgumentList'] = $args
}
Start-Process Mine.Library.SuperUtil.exe -NoNewWindow -Wait #params
If the $params hashtable is empty by the time Start-Process is invoked, it'll simply be ignored.
You can also squash the other parameter arguments into the hashtable if you want:
$params = #{
FilePath = 'Mine.Library.SuperUtil.exe'
NoNewWindow = $true
Wait = $true
}
if($args){
$params['ArgumentList'] = $args
}
Start-Process #params
Makes maintenance of the script easy (but obviously still quite verbose compared to the batch file alternative)
Read 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.
Therefore, check if ($args.Count -ne 0) rather than if ($args -ne $Null) as the automatic variable $args is always an array (and never $Null).

Is there a way to pass serializable objects to a PowerShell script with start-process?

I know about PowerShell jobs, but I want to use start-process and pass a serializable object to the new powershell process. Is there any way to do this?
It seems that using start-process you have to provide a string argument list which won't cut it for me. I'm trying to get a PSCredential from one process to another (or a SecureString, I'll take either one). Maybe this circumvents security.
UPDATE - adding the solution I used after seeing help from others (using solution from #PetSerAl)
I wrote two test scripts: a parent script and a child script. The parent script calls the child script.
Parent Script:
$securePassword = ConvertTo-SecureString "testpassword" -AsPlainText -Force
$cred = New-Object PSCredential("testuser", $securePassword)
$credSerial = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([Management.Automation.PSSerializer]::Serialize($cred)))
$psFile = "C:\repos\Test\PowerShell Scripts\KirkTestChild.ps1"
$p1 = "-someparam ""this is a test!!!"""
$p2 = "-cred ""$credSerial"""
$proc = Start-Process PowerShell.exe -PassThru:$true -Argument "-File ""$($psFile)""", $p1, $p2
Write-Host "ID" $proc.Id
Write-Host "Has Exited" $proc.HasExited
Start-Sleep -Seconds 15
Write-Host "Has Exited" $proc.HasExited
Child Script:
Param(
$someParam,
$cred
)
Write-Host "someParam: $($someParam)"
Write-Host "cred (raw): $($cred)"
$realCred=[Management.Automation.PSSerializer]::Deserialize([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($cred)))
Write-Host "cred user: $($realCred.UserName)"
Write-Host "start"
Start-Sleep 5
Write-Host "ending"
Start-Sleep 5
Yes. As PetSerAl wrote in a comment, you can use the PSSerializer class to handle that:
$ser = [System.Management.Automation.PSSerializer]::Serialize($credential)
$cred = [System.Management.Automation.PSSerializer]::Deserialize($ser)
I don't know if your process will understand the CliXML format though; you don't specify what process you're starting.
Since this will produce XML which is multi-line, it may not work to pass this to a process as a parameter which is why PetSerAl is including the code to Base64 encode the resulting XML.
You might also be able to pass the XML over STDIN instead of Base64 encoding, which also ensures that you won't accidentally hit the command line size limit.
You can pass a scriptblock as an argument from a parent PS process to a child PS process. The result of the child process executing the script is serialized and returned to the parent. (Run powershell -? to get help on this feature). So if the direction you want to move the credential is from child to parent then this is an alternative, or if you want to move data in both directions, then you could combine the previous answers with this approach. (The OP doesn't say which direction).
EDIT
I'm assuming OP wants to start another Powershell process - because he said "new Powershell process". And he wants to pass a PSCredential from either the parent to the child or vice versa. I'd guess the former. Based on PetSerAl's solution he could serialize the cred as CliXML. It would have new lines. Besides potentially causing problems with passing the arguments to Powershell, the newlines will cause the Deserialize to fail if they are in the middle of a value. So to avoid those problems the whole script can be base64 encoded and passed via the -EncodedCommand parameter. It'll look something like this:
$xmlStr = [management.automation.psserializer]::Serialize($cred)
$scriptStr = "$liveCred = " +
"[management.automation.psserializer]::Deserialize('$xmlstr');" +
"# PS statements to do something with $liveCred"
$inB64 = [Convert]::ToBase64String( [Text.Encoding]::UTF8.GetBytes($scriptStr))
powershell -EncodedCommand $inB64
If OP needs something back from the script that ran in the child PS, and wanted to use the scriptblock feature mentioned earlier in this answer, I don't he could use this approach, because that feature is related to the -Command parameter. Rather newlines would need to be removed and escaping concerns might come into play.
Here's a variation that uses a ScriptBlock instead of script files. See the post by Greg Bray at Powershell Start-Process to start Powershell session and pass local variables.
$securePassword = ConvertTo-SecureString 'testpassword' -AsPlainText -Force
$cred = New-Object PSCredential( 'testuser', $securePassword )
$scriptBlockOuter = {
$sb = {
Param(
[Parameter( Mandatory, Position = 0 )]
[String] $someParam,
[Parameter( Mandatory, Position = 1 )]
[String] $credSerial
)
$cred = [System.Management.Automation.PSSerializer]::Deserialize( [System.Text.Encoding]::UTF8.GetString( [System.Convert]::FromBase64String( $credSerial )))
Write-Host "someParam: $someParam"
Write-Host "cred user: $($cred.UserName)"
Write-Host 'start'
Start-Sleep 5
Write-Host 'ending'
Start-Sleep 5
}
}
$p1 = 'this is a test!!!'
$credSerial = [System.Convert]::ToBase64String( [System.Text.Encoding]::UTF8.GetBytes( [System.Management.Automation.PSSerializer]::Serialize( $cred )))
$proc = Start-Process PowerShell -PassThru -ArgumentList '-Command', $scriptBlockOuter, '& $sb', '-someParam', "'$p1'", '-credSerial', "'$credSerial'"
Write-Host 'ID' $proc.Id
Write-Host 'Has Exited' $proc.HasExited
Start-Sleep -Seconds 15
Write-Host 'Has Exited' $proc.HasExited
Using this ConvertTo-Expression cmdlet:
Parent script:
$Expression = $Cred | ConvertTo-Expression
$credSerial = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Expression))
Child script:
$Expression = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($credSerial))
$Cred = Invoke-Expression $Expression # or safer: &([ScriptBlock]::Create($Expression))