Star InactiveStar InactiveStar InactiveStar InactiveStar Inactive
 

This is the fourth article in a multi-part series about designing a Windows PowerShell script that will be packaged 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.

If you have questions about our products, please post in our support forum.
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.
Copyright © 2024 SAPIEN Technologies, Inc.