Asked 1 month ago by VoidCaptain249
How Can I Retrieve a PowerShell Script's Exit Code When Invoking It from C#?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by VoidCaptain249
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
I'm calling a PowerShell script from my C# application and need to capture its exit code, but the PSObject collection returned by Invoke() is always empty.
I tried the following C# code:
CSHARPusing (PowerShell ps = PowerShell.Create()) { ps.AddScript(File.ReadAllText(buildScript.ps1)); var psResults = ps.Invoke(); foreach (PSObject psObj in psResults) { var result = psObj.ToString()); } ps.Dispose; }
The PowerShell script (buildScript.ps1) I used for testing is:
POWERSHELLStart-Sleep -Seconds 30 Write-Host "Exit code is : 25" exit 25
Despite the script including an exit statement, I don't get any exit code in my C# application. What can I do to correctly capture the exit code of the PowerShell script from within C#?
Comments 0
•Answers 3
•Views 126
Answer by TychoAI Agent • 1 month ago
When you call a PowerShell script via the PowerShell class using AddScript and Invoke, the script's exit statement (and anything sent to Write-Host) won’t appear in the collection returned by Invoke. This is because:
• The exit command terminates the runspace but does not return a pipeline object.
• Write-Host writes directly to the host instead of writing to the standard output (pipeline).
To get a meaningful result (such as an exit code), you have two common approaches:
Instead of writing to the host and exiting, modify your script so that it sends the desired exit code to the output stream. For example, change your script from:
POWERSHELLStart-Sleep -Seconds 30 Write-Host "Exit code is : 25" exit 25
…to something like:
POWERSHELLStart-Sleep -Seconds 30 Write-Output 25
In your C# code, the call to Invoke will return a collection with one PSObject containing the number 25:
CSHARPusing (PowerShell ps = PowerShell.Create()) { ps.AddScript(File.ReadAllText("buildScript.ps1")); var psResults = ps.Invoke(); foreach (PSObject psObj in psResults) { // Should output "25" if the script returns that value Console.WriteLine($"Script returned: {psObj}"); } }
If you need to use the script’s exit code (that is, the process return code), consider starting PowerShell.exe as an external process using System.Diagnostics.Process. This way, you can capture the exit code from the process itself. For example:
CSHARPusing System.Diagnostics; ProcessStartInfo psi = new ProcessStartInfo { FileName = "powershell.exe", Arguments = "-File \"buildScript.ps1\"", RedirectStandardOutput = true, UseShellExecute = false, // Required for redirection CreateNoWindow = true }; using (Process process = Process.Start(psi)) { process.WaitForExit(); int exitCode = process.ExitCode; Console.WriteLine($"Script exit code: {exitCode}"); }
Choose the approach that best fits your scenario. If you simply need to capture an exit-like value within your C# code, returning the value as an object from the script (approach 1) is simplest. If you require the process exit code (for example, as part of a build or test harness that checks the external process’s exit status), running PowerShell as an external process (approach 2) is the way to go.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by SupernovaMariner362 • 1 month ago
Use .AddCommand()
rather than .AddScript()
; .AddCommand()
allows direct invocation of *.ps1
files by file path, and reflects their exit code in the automatic $LASTEXITCODE
variable.[1]
After execution, you can invoke .Runspace.SessionStateProxy.GetVariable("LASTEXITCODE")
on your System.Management.Automation.PowerShell
instance to obtain the value of this variable.
Therefore:
CSHARP// Create an initial default session state. var iss = System.Management.Automation.Runspaces.InitialSessionState.CreateDefault2(); // Windows only: // Set the session state's script-file execution policy // (for the current session (process) only). iss.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Bypass; using (PowerShell ps = PowerShell.Create(iss)) { ps.AddCommand(@"/path/to/your/buildScript.ps1"); // Invoke synchronously and process the success output. // To retrieve output from other streams, use ps.Streams later. foreach (PSObject psObj in ps.Invoke()) { Console.WriteLine(psObj.ToString()); } // Obtain the exit code. int exitCode = (int)ps.Runspace.SessionStateProxy.GetVariable("LASTEXITCODE"); Console.WriteLine($"Exit code: {exitCode}"); }
[1] The reasons for preferring .AddCommand()
over .AddScript()
are:
(a) You can use script file paths that contain spaces and other metacharacters as-is (whereas .AddScript()
would require use of embedded quoting and &
, the call operator)
(b) You can pass
richly typed parameter values via .AddArgument()
/ .AddParameter()
/ .AddParameters()
(whereas .AddScript()
would require you to "bake" the parameter values as string literals into the single string argument passed to it).
In short: .AddScript()
is for executing arbitrary PowerShell source code, whereas .AddCommand()
is for executing a single command by name or path, such as a *.ps1
file. It is important to know this distinction, because AddScript()
will only behave like .AddCommand()
in the simplest of cases: with a space-less *.ps1
path that is also free of other metacharacters, to which no arguments need be passed.
See this answer for more information.
[2] Note, however, that if your machine's / user account's execution policy is controlled by GPOs, a process-level override will not work; see this answer for details.
No comments yet.
Answer by EtherealMariner878 • 1 month ago
Normally you shouldn't rely on exit codes from PowerShell script, but to answer your question, with your current implementation you can query $LASTEXITCODE
automatic variable before disposing your PowerShell instance, however, for this to work you will need to pass-in the script path as .AddScript(...)
argument, instead of reading the script content via File.ReadAllText(...)
. Preferably you should use an absolute path but relative might work
You should also handle the Write-Host
output in your code, that output you can find it in ps.Streams.Information
, it will not be output from .Invoke()
.
Alternatively, you could subscribe to DataAdding
event:
POWERSHELLps.AddScript(@".uildScript.ps1"); ps.Streams.Information.DataAdding += (s, e) => { InformationRecord info = (InformationRecord)e.ItemAdded; Console.WriteLine(info.MessageData); };
In summary you can do:
CSHARPint exitCode = 0; using (PowerShell ps = PowerShell.Create()) { ps.AddScript(@".uildScript.ps1"); var psResults = ps.Invoke(); foreach (PSObject psObj in psResults) { var result = psObj.ToString(); } // if not using the event driven approach if (ps.Streams is { Information.Count: > 0 }) { foreach (InformationRecord information in ps.Streams.Information) { // handle information output here... } } if (ps.HadErrors) { ps.Commands.Clear(); exitCode = ps .AddScript("$LASTEXITCODE") .Invoke<int>() .FirstOrDefault(); } } // Do something with exitCode
No comments yet.
No comments yet.