Output from a Script in an Executable File
- Details
- Written by June Blender
- Last Updated: 13 June 2016
- Created: 17 December 2015
- Hits: 18574
This is the fourth article in a multi-part series about designing a Windows PowerShell script that will be packaged in an executable file.
- Passing Parameters to a Script in an Executable File explains how to use the special parsing features of PowerShell Studio and PrimalScript to make passing parameters easy for PowerShell users and authors.
- Parsing Parameters for a Script in an Executable File explains how to parse parameters manually for special uses.
- Displaying Help for a Script in an Executable File explains how to display help for a script in an executable file.
- Output from a Script in an Executable File explains how to manage string output from a script in an executable file.
When you write scripts in Windows PowerShell, you really don’t think much about the format of the output. Your script generates objects and they appear in the console. You learn quickly that you should never use any formatting commands on your output, because the format cmdlets return format objects, instead of the original objects. Also, the user is the best judge of the format they prefer and can easily format any objects your return.
But, all of those rules change when you package a script in an executable file. That’s because scripts in executable files always return strings — and only strings.
General Rule: Don’t format output objects
Let’s begin with the general rule. When a script generates output, don’t format it. Let the user do it. Otherwise, you return format objects that don’t have the properties and methods of the original objects.
For example, the Get-MyProcess.ps1 script returns the Process object (System.Diagnostics.Process) for the PowerShell Studio process.
Param ( [Parameter()] [String] [ValidateNotNullOrEmpty] $List ) if ($p = Get-Process 'PowerShell Studio') { if ($List) { $p | Format-List -Property * } else {$p} } |
PS C:\> .\Get-MyProcess.ps1 | Get-Member TypeName: System.Diagnostics.Process Name MemberType Definition ---- ---------- ---------- Handles AliasProperty Handles = Handlecount Name AliasProperty Name = ProcessName NPM AliasProperty NPM = NonpagedSystemMemorySize64 PM AliasProperty PM = PagedMemorySize64 SI AliasProperty SI = SessionId ...
But, if I use the List parameter, which tells the script to run the Format-List cmdlet, the object is a collection of Format objects, not a Process object.
PS C:\> .\Get-MyProcess.ps1 -List True | Get-Member TypeName: Microsoft.PowerShell.Commands.Internal.Format.FormatStartData Name MemberType Definition ---- ---------- ---------- Equals Method bool Equals(System.Object obj GetHashCode Method int GetHashCode() GetType Method type GetType() ToString Method string ToString() autosizeInfo Property Microsoft.PowerShell.Commands ClassId2e4f51ef21dd47e99d3c952918aff9cd Property string ClassId2e4f51ef21dd47e groupingEntry Property Microsoft.PowerShell.Commands pageFooterEntry Property Microsoft.PowerShell.Commands pageHeaderEntry Property Microsoft.PowerShell.Commands shapeInfo Property Microsoft.PowerShell.Commands TypeName: Microsoft.PowerShell.Commands.Internal.Format.GroupStartData Name MemberType Definition ---- ---------- ---------- Equals Method bool Equals(System.Object obj GetHashCode Method int GetHashCode() GetType Method type GetType() ToString Method string ToString() ClassId2e4f51ef21dd47e99d3c952918aff9cd Property string ClassId2e4f51ef21dd47e groupingEntry Property Microsoft.PowerShell.Commands shapeInfo Property Microsoft.PowerShell.Commands...
So, you never return formatted objects. Well, almost never.
Executable Files Return Strings — Always
Now, the exception to the do-not-format rule. When you package your script in an executable file, it always returns strings objects, regardless of the underlying object type, because it writes to stdout — standard output — which is always text.
The executable runtime uses the Out-String cmdlet, which does a pretty good job of preserving the appearance of the output, but the output loses the properties and methods of the original objects.
PS C:\> .\Get-MyProcess.exe #Looks the same... Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id SI ProcessName ------- ------ ----- ----- ----- ------ -- -- ----------- 2896 178 266168 315796 1170 926.55 4428 1 PowerShell Studio PS C:\> .\Get-MyProcess.exe | Get-Member #Just a good-looking string. TypeName: System.String Name MemberType Definition ---- ---------- ---------- Clone Method System.Object Clone(), CompareTo Method int CompareTo(System.O Contains Method bool Contains(string v CopyTo Method void CopyTo(int source EndsWith Method bool EndsWith(string v Equals Method bool Equals(System.Obj GetEnumerator Method System.CharEnumerator
In fact, in this case, it’s six good-looking strings, including blank lines.
PS C:\> (.\Get-MyProcess.exe).count 6 PS C:\> (.\Get-MyProcess.exe)[0] PS C:\> (.\Get-MyProcess.exe)[1] Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id SI ProcessName
If you run the Get-MyProcess.ps1 script, you can get the property values from the object, or call its methods, but the string does not have the Process object properties or methods.
PS C:\> (.\Get-MyProcess.ps1).PeakWorkingSet 359100416 PS C:\> (.\Get-MyProcess.exe).PeakWorkingSet PS C:\>
And the PowerShell options to format the properties and values into tables and lists have no apparent effect on strings.
PS C:\> .\Get-MyProcess.exe | Format-Table Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id SI ProcessName ------- ------ ----- ----- ----- ------ -- -- ----------- 3298 178 266168 315796 1170 926.61 4428 1 PowerShell Studio PS C:\> .\Get-MyProcess.exe | Format-List -Property * Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id SI ProcessName ------- ------ ----- ----- ----- ------ -- -- ----------- 3298 178 266168 315796 1170 926.61 4428 1 PowerShell Studio PS C:\> .\Get-MyProcess.exe | Format-List -Property * -Force Length : 0 Length : 156 Length : 156 Length : 156 Length : 0 Length : 0
New Exe Rule: Add a Format Option
When you’re writing scripts that are designed to be packaged in executable files, you might consider adding formatting options.
For example, the little Get-MyProcess.ps1 script (and its Get-MyProcess.exe variant) has a List parameter that runs the Format-List cmdlet and returns all properties and values of the object in a list.
The output of the executable file is still a string — it’s always a string — but now the user can see the properties and values.
PS C:\> .\Get-MyProcess.exe -List True Name : PowerShell Studio Id : 4428 PriorityClass : Normal FileVersion : 4.2.98.0 HandleCount : 3562 WorkingSet : 323387392 PagedMemorySize : 272556032 PrivateMemorySize : 272556032 VirtualMemorySize : 1226399744 TotalProcessorTime : 00:15:26.7187500 SI : 1 Handles : 3562 VM : 1226399744 WS : 323387392 PM : 272556032 NPM : 182704 ...
Save the output as XML or JSON
[Thanks to Joel Bennett (@Jaykul) and Rob Campbell (@mjolinor) for the idea.]
You can also add an option to save the output object in an XML or JSON file, instead of returning it as a string. Then, the user can convert the contents of the file to a more usable object type and include it in subsequent PowerShell commands.
In this version of the script, I’ve added two new parameters, XMLFilePath and JSONFilePath. The parameters take the path to an XML or JSON file. I don’t validate the input, because the output cmdlets return reasonably helpful error messages.
Because the List, XMLFilePath, and JSONFilePath parameters are exclusive, I put each in its own parameter set.
To preserve the default (no parameter) option, I made the ListSet parameter set the default and made its List parameter optional. When all parameters are strings, as in an exe, you must specify a default parameter set, because the parameter binding logic in Windows PowerShell cannot use types to figure out which parameter set you want.
(No, I did not memorize the syntax for all of this. I used the Function Builder. Prefer a video?).
Param ( [CmdletBinding(DefaultParameterSetName = 'ListSet')] [Parameter(ParameterSetName = 'ListSet')] [ValidateNotNullOrEmpty()] [String] $List, [Parameter(ParameterSetName = 'XMLSet', Mandatory = $true)] [String] $XMLFilePath, [Parameter(ParameterSetName = 'JSONSet', Mandatory = $true)] [String] $JSONFilePath ) |
The code is pretty simple. I used an IF/ELSEIF sequence to check the parameters and write the output as strings or save it as XML or JSON.
Because neither Export-Clixml nor Out-File (nor Set-Content) return any data, I always use Get-Item to return the new file to the user. If they don’t need it, they can always pipe it to Out-Null, but I bet it saves users an extra command to check the file.
if ($p = Get-Process 'PowerShell Studio') { if ($List) { $p | Format-List -Property * } elseif ($XMLFilePath) { $p | Export-Clixml -Path $XMLFilePath Get-Item $XMLFilePath } elseif ($JSONFilePath) { $p | ConvertTo-Json | Out-File $JSONFilePath Get-Item $JSONFilePath } else { $p } } |
Now, the user can run Get-MyProcess.exe, save the output in a file, import the output as a deserialized XML object (Import-Clixml) or convert it to a PSCustomObject (ConvertFrom-Json), and use it as needed.
PS C:\>.\Get-MyProcess.exe -XMLFilePath .\Scripts\SPSProcess.xml Directory: C:\Scripts Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 12/17/2015 2:33 PM 634284 SPSProcess.xml PS C:\> $p = Import-Clixml -Path .\Scripts\SPSProcess.xml PS C:\> $p Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id SI ProcessName ------- ------ ----- ----- ----- ------ -- -- ----------- 1506 176 287812 283776 1212 88.84 6776 1 PowerShell Studio PS C:\> $p.Handles 1506 PS C:\> $p | Format-List * Name : PowerShell Studio SI : 1 Handles : 1506 VM : 1271144448 WS : 290586624 PM : 294719488 NPM : 179984 ...
You can also use string-parsing techniques and cmdlets like Convert-String and Convert-FromString to convert the string output to a custom object.
Wrapping scripts in executable files is valuable, but remember to help the user with the string output.
June Blender is a technology evangelist at SAPIEN Technologies, Inc. You can reach her at This email address is being protected from spambots. You need JavaScript enabled to view it. or follow her on Twitter at @juneb_get_help.
For licensed customers, use the forum associated with your product in our Product Support Forums for Registered Customers.
For users of trial versions, please post in our Former and Future Customers - Questions forum.