PowerShell Reflection: Define a MIME type or subty

2019-05-29 08:58发布

问题:

It's possible in PowerShell to define an output type on scripts. Consider myScript.ps1:

[OutputType([String])]
param(
    [string]$name
)

The following returns String:

(Get-Command .\myScript.ps1).OutputType.Name

But I would like to specify that a script returns text/json or text/xml. What would be a good way of doing that?

Inventing types for OutputType (e.g. [String.JSON]) does not work.

回答1:

There are two independent, but complementary mechanisms for declaring output types:

Important: Both ways of declaring output types are informative only and aren't enforced by PowerShell at runtime.

  • Mechanism A: Using the OutputType attribute above the param() declaration in a script or function, as in the question:

    • Always use this mechanism, and, if necessary, supplement with mechanism B.

    • Only recognizes full type names of .NET types or the name of PowerShell type accelerators and no up-front validation is performed, but if an unrecognized type is encountered at invocation time:

      • if originally specified as a string (e.g., 'System.Text.Encoding'): it is quietly ignored.
      • if originally specified as a type literal (e.g., [System.Text.Encoding]): the function call breaks on invocation
    • This strict, type-based integration enables tab completion / IntelliSense (command line / Visual Studio Code)

    • As for when you may want to use mechanism B too:

      • If the .NET type names don't tell the full story, as in the question.

      • If a function outputs multiple types and you want to verbally describe what types are output when.

  • [Supplemental] Mechanism B: Using the .OUTPUTS section of comment-based help:

    • Accepts free-form descriptions; while referencing actual type names makes sense, doing so is not enforced.

    • While you can use this mechanism alone, doing so forgoes the advantages of tab completion and IntelliSense.

    • Therefore, use it to supplement mechanism A, if needed, but note that:

      • If both mechanisms are used, Get-Help only shows the mechanism B definitions.

      • Since the two mechanisms are independent, you must manually ensure that the free-form information specified is complete and consistent with the mechanism A declarations.

To examine the output-type information with human eyeballs, use (Get-Help <cmd>).returnvalues (caveat: requires help to be defined, such as via comment-based help) or read the OUTPUTS section of the output from Get-Help -Full <cmd>.
This will either show the free-form .OUTPUTS content or, in its absence, the full type names of the [OutputType[]]-declared types.
For programmatic access, use (Get-Command <cmd>).OutputType, which returns [System.Management.Automation.PSTypeName] instances whose .Type property contains the actual type.

Details below, starting with the answer to the original question.


Mechanism A: Using OutputType attribute(s) above params():

You may only specify .NET types as arguments to the OutputType attribute, so strings such as text/json or text/xml that reflect MIME types won't work.

If you want string output, you've already chosen the closest approximation of your MIME types in terms of .NET types: [OutputType([String])]

You may specify multiple types in a single [OutputType()] attribute, or you may use individual attributes.
You must use individual attributes if you want to map output types to specific parameter sets (e.g., [OutputType([string], ParameterSetName='NameOnly')]).
As of Windows PowerShell v5.1 / PowerShell Core v6.0.1, however, this information is neither used by tab completion / IntelliSense nor reflected in the output from Get-Help -Full.

Note: For a type to be recognized by the OutputType attribute at invocation time,

  • use either the full type name (e.g., [System.Text.RegularExpressions.Match] rather than just [Match]) or the name of a PowerShell type accelerator, such as [regex].

    • When in doubt, type [<fullTypeName>] at the prompt and see if it is recognized as a type; additionally you may choose between specifying the type as a string (e.g., 'System.Text.Encoding') or as a type literal (e.g., (e.g., [System.Text.Encoding]), which has behavioral implications - see below.
  • if the specified type isn't present at invocation time, e.g., because the assembly containing the type hasn't been loaded, the behavior depends on how the output was declared:

    • if originally specified as a string: it is quietly ignored.
    • if originally specified as a type literal: the function call breaks on invocation

Beyond that,

  • either: simply describe the specific types of strings that your cmdlet outputs in its help text, such as via mechanism B described below,

  • or: create custom .NET types whose names reflect the desired conceptual type, and specify them in the OutputType attribute - see below.

As stated, despite its constrained nature, the OutputType attribute is purely informative at runtime - it is, however, used for tab completion and IntelliSense (Visual Studio Code).


Example of using a custom type:

# Define an empty custom type for the sole purpose for being able to use
# it with the OutputType attribute.
# Note: On first call, this may take a second or two, as the code is being
#       compiled.
Add-Type @'
  namespace org.example {
   public class text_json {}
  }
'@

function foo {
  # Reference the custom type defined above; full type name required.
  [OutputType([org.example.text_json])]
  param(
      [string]$name
  )
}

You then get:

> (Get-Command foo).OutputType.Name
org.example.text_json

Note that the [System.Management.Automation.PSTypeName] instances that .OutputType outputs are not the same as the [type] instances you get when you inspect a type directly:

The .Name property of [System.Management.Automation.PSTypeName] corresponds to the .FullName property of [type], so you get the full type name if the type is recognized (available in the session); otherwise, it is the name as originally specified.


Mechanism B: Using the .OUTPUTS section in comment-based help:

Conceptual help topic Get-Help about_Comment_Based_Help describes how section .OUTPUTS inside comment-based help for scripts and functions can be used to list and describe output types.

Note: Similarly, the .INPUTS section can be used to describe supported input types, though that is arguably less interesting, given that specifying input types is an integral part of parameter declaration and documentation. By and large, .INPUTS functions analogously to .OUTPUTS, with only differences mentioned below.

An .OUTPUTS section uses the following format suggested by the examples in the help topic, but note that the text is ultimately free-form, and no structure is enforced.

<type-name> <optional-description>

Even though the help topic (as of PSv5) doesn't mention it, it seems that in the event of multiple output types, each should be described in its own .OUTPUTS section.
That said, the free-form format allows you to describe multiple output-type descriptions in a single section.

Example, taking advantage of the free-form format to describe the output in terms of MIME types:

<#
.SYNOPSIS
Does stuff.

.OUTPUTS
text/json. In case of x, returns a JSON [string].

.OUTPUTS
text/xml. In case of y, returns an XML [string].
#>
function foo {
  param()
}

Note that when using Get-Help to view the help as a whole, the (aggregated) .OUTPUTS (and .INPUTS) sections are only shown with Get-Help -Full.

Querying that information programmatically essentially yields the OUTPUTS section from
Get-Help -Full verbatim (with individual .OUTPUTS sections in the source concatenated with an empty line in between, and extra trailing empty lines):

> (Get-Help foo).returnvalues

text/json. In case of x, returns a JSON [string].

text/xml. In case of y, returns an XML [string].

To access the descriptions individually, by index:

> (Get-Help foo).returnvalues.returnvalue[0].type.name
text/json. In case of x, returns a JSON [string].

However, given the free-form nature of the descriptions and that they're intended for human consumption, this granular access may not be needed.

That said, using this form returns the text without extra whitespace, so (Get-Help foo).returnvalues.returnvalue.type.name can be used to return all text without empty lines.

This works analogously for the .INPUTS sections:
(Get-Help foo).inputTypes.inputType.type.name



回答2:

One neat way is using the PowerShell help comment syntax.

<# 
 .Synopsis
  Returns a list of files.

 .Description
  ...

 .Inputs
 text/csv

 .Outputs
 text/json[]

#>

You can access this information from the Get-Help object:

$cmd = Get-Help -Name .\components\FileList.ps1
"Input Type: " + $cmd.inputTypes.inputType[0].type.name
"Output Type: " + $cmd.returnValues.returnValue[0].type.name

Results in:

Input Type: text/csv
Output Type: text/json[]

Also works on standard cmdlets:

(Get-Help Get-Date).returnValues[0].returnValue[0].type.name

Returns:

System.DateTime or System.String