Announcing PowerShell Crescendo Preview.1

Jason Helmick

Jason

As a shell, PowerShell is intended to work with native commands in addition to cmdlets. However, native commands have their own unique syntax, sometimes containing many subcommands and parameters/switches, and are often like its own language. Wouldn’t it be great to have an option to leverage your PowerShell skills to use new commands or be even more proficient with native commands you are already using? As a PowerShell user, it would be great if those native commands supported PowerShell features like:

  • Clear naming convention – Verb-Noun
  • Consistent parameter naming for similar uses
  • Output consisting of objects
  • Common method of getting command help
  • Easy to work with objects in a pipeline
  • Easy to share using Modules

We are pleased to announce the first preview of PowerShell Crescendo, a framework to rapidly develop PowerShell cmdlets for native commands, regardless of platform.

Many of today’s modern native commands are complex, they themselves are mini-shells with their own mini-language containing sub levels, or sub contexts. If you’ve worked with kubectl, docker or netsh.exe, you have experienced the complexity of executing and automating these commands.

To make native commands have a more PowerShell-like feel and offer a similar experience, you could re-write the tool yourself, or if the tool uses REST, directly call the web APIs using AutoRest. These options work very well, but require more development experience and can be harder to maintain over time.

Crescendo provides the tools to easily wrap a native command to gain the benefits of PowerShell cmdlets. Wrapping native commands into Crescendo cmdlets can provide parameter handling like prompting for mandatory parameters and tab-completion for parameter values. Crescendo cmdlets can take the text output from the native application and parse it into objects. The output objects allow you to take advantage of all the post processing tools such as Sort-Object, Where-Object, etc.

What’s supported

Microsoft.PowerShell.Crescendo 0.4.1 Preview.1 is currently available for download from the PowerShell Gallery

Supported PowerShell versions for authoring a module with Crescendo:

  • PowerShell 7.0+

Supported PowerShell versions that can execute a Crescendo module:

  • Windows PowerShell 5.1+
  • PowerShell 7.0+

What’s next

Next/Future plans for preview.2 will be based on feedback and include a few items under investigation:

  • Create cmdlet aliases when generating a module
  • Investigate parsing man pages and documentation to improve automatic JSON generation

Installing Crescendo

To create a Crescendo cmdlet, you will need the module Microsoft.PowerShell.Crescendo from the PowerShell Gallery.

Install-Module Microsoft.PowerShell.Crescendo

Documentation

We’ve included the about_Crescendo and we’ll be adding more documentation in the future. Make sure to check out the Samples folder in the module.

Get-Help about_Crescendo

To author a Crescendo module, you create a JSON configuration file describing how you want the native command projected as a cmdlet. You need the Microsoft.PowerShell.Crescendo module to build the finished module containing the Crescendo proxy cmdlets. The Microsoft.PowerShell.Crescendo module runs on PowerShell 7+.

To consume a module built with Crescendo, you don’t need to have Microsoft.PowerShell.Crescendo installed. Simply download or install the generated module and you’re good to go. Because a Crescendo-built module is just like any other module, these are supported on Windows PowerShell 5.1 and above. Your work can help others in the community automate native commands. Fortunately, building and sharing Crescendo modules is easy when you publish to the PowerShell Gallery.

For more information about the concepts and design approach, see:

Examples

Creating a new cmdlet

This example builds on a common Linux packaging utility apt for displaying, installing and removing software. The properties Verb and Noun define the name of the new cmdlet Get-InstalledPackage.

The Crescendo module includes a schema definition file for assistance creating and editing JSON configuration files. The schema file Microsoft.PowerShell.Crescendo.Schema.json is included with the module. In the below example, the schema file is copied to a temporary working directory with the JSON file under development. The location of the schema file is defined in the JSON configuration and can be located anywhere.

Common properties defined by the schema:

  • Verb: The name of the cmdlet verb (Check Get-Verb for valid verb names)
  • Noun: The name of the cmdlet noun
  • OriginalName: The original native command name and location
  • OriginalCommandElements: Some native commands have additional mandatory switches to execute properly for a given scenario
  • OutputHandlers: The output handler captures the string output from the native command and transforms it into structured data (objects). As with other PowerShell cmdlets, this object output may be manipulated further down the pipeline.

Note: The example code contains comments (// <--) in JSON. They are for descriptive purposes only and should be removed.

{
    "$schema": "./Microsoft.PowerShell.Crescendo.Schema.json", // <-- Schema definition file
    "Verb": "Get", // <-- Cmdlet Verb
    "Noun":"InstalledPackage", // <-- Cmdlet Noun
    "OriginalName": "apt", // <-- Native command
    "OriginalCommandElements": ["-q","list","--installed"],
    "OutputHandlers": [ // <-- Add string output to an object
        {
            "ParameterSetName":"Default",
            "Handler": "$args[0]| select-object -skip 1 | %{$n,$v,$p,$s = "$_" -split ' '; [pscustomobject]@{ Name = $n -replace '/now'; Version = $v; Architecture = $p; State = $s.Trim('[]') -split ',' } }"
        }
    ]
}

The following example shows how to use the Get-InstalledPackage we defined in the JSON file.

PS> Get-InstalledPackage | Where-Object { $_.name -match "libc"}

Name        Version            Architecture State
----        -------            ------------ -----
libc-bin    2.31-0ubuntu9.1    amd64        {installed, local}
libc6       2.31-0ubuntu9.1    amd64        {installed, local}
libcap-ng0  0.7.9-2.1build1    amd64        {installed, local}
libcom-err2 1.45.5-2ubuntu1    amd64        {installed, local}
libcrypt1   1:4.4.10-10ubuntu4 amd64        {installed, local}

PS> Get-InstalledPackage | Group-Object -Property Architecture

Count Name   Group
----- ----   -----
   10 all    {@{Name=adduser; Version=3.118ubuntu2; Architecture=all; State=System.String[}, @{Name=debconf; V…
   82 amd64  {@{Name=apt; Version=2.0.2ubuntu0.1; Architecture=amd64; State=System.String[]}, @{Name=base-files…

Example with parameters

This example wraps the Windows command ipconfig.exe and illustrates how to define parameters. In addition to the properties in the above example, the schema supports a Parameters section to wrap and project descriptively named parameters to the new cmdlet.

  • Name: Name of parameter that appears in the new cmdlet
  • OriginalName: Name of original parameter that appears in the native command
{
    "$schema" : "./Microsoft.PowerShell.Crescendo.Schema.json",
    "Verb": "Get",
    "Noun": "IpConfig",
    "OriginalName":"c:/windows/system32/ipconfig.exe",
    "Description": "This will display the current IP configuration information on Windows",

    "Parameters": [ // <-- Parameter definition
        {
            "Name":"All", // <-- Name of parameter that appears in cmdlet
            "OriginalName": "/all", // <-- Name of original parameter that appears in native cmd
            "ParameterType": "switch",
            "Description": "This switch provides all ip configuration details" // <-- Help parameter
        },
        {
            "Name":"AllCompartments",
            "OriginalName": "/allcompartments",
            "ParameterType": "switch",
            "Description": "This switch provides compartment configuration details"
        }
    ],

    "OutputHandlers": [
        {
        "ParameterSetName": "Default",
        "Handler":"param ( $lines )
        $post = $false;
        foreach($line in $lines | ?{$_.trim()}) {
            $LineToCheck = $line | select-string '^[a-z]';
            if ( $LineToCheck ) {
                if ( $post ) { [pscustomobject]$ht |add-member -pass -typename $oName }
                $oName = ($LineToCheck -match 'Configuration') { 'IpConfiguration' } else {'EthernetAdapter'}
                $ht = @{};
                $post = $true
            }
            else {
                if ( $line -match '^   [a-z]' ) {
                    $prop,$value = $line -split ' :',2;
                    $pName = $prop -replace '[ .-]';
                    $ht[$pName] = $value.Trim()
                }
                else {
                    $ht[$pName] = .{$ht[$pName];$line.trim()}
                }
            }
        }
        [pscustomobject]$ht | add-member -pass -typename $oName"
        }
    ]
}

Export the proxy module from JSON configuration

Exporting a Crescendo JSON configuration file results in a PowerShell module that can be shared locally, on GitHub, and on the PowerShell Gallery. Crescendo includes the Export-CrescendoModule cmdlet that generates the proxy module from the JSON configuration file.

  • The ConfigurationFile parameter should include the name and path to the JSON configuration file.
  • The ModuleName parameter contains the name of the exported module.
Export-CrescendoModule -ConfigurationFile .get-ipconfig.crescendo.json -ModuleName Ipconfig.psm1

Using your Crescendo module

Once you have created the module with Crescendo, you can install and import it like any other module. Keep in mind you will need the original native command available wherever you install the Crescendo module.

PS> Import-Module .ipconfig.psm1
PS> Get-IpConfig -All | Select-Object -Property ipv4address, DefaultGateway, subnetmask

ipv4address  DefaultGateway                              subnetmask
-----------  --------------                              ----------

172.24.112.1                                             255.255.240.0
192.168.94.2 {fe80::145e:1779:5cfa:c2a5%8, 192.168.94.1} 255.255.255.0

How you can help

Our goal is to make it easier to convert your native commands to PowerShell cmdlets and receive the benefits that PowerShell provides. We value your ideas and feedback and hope you will give Crescendo a try, then stop by our GitHub repository and let us know of any issues or features you would like added.

For more information about PowerShell Crescendo issues and features, see: Crescendo on GitHub

Jason Helmick Program Manager, PowerShell

21 comments

Comments are closed. Login to edit/delete your existing comments

  • Avatar
    Jay Michaud

    This is an interesting idea, and I look forward to seeing how it develops. I am curious though: Why does it use JSON as a data format rather than PowerShell? It seems like a PowerShell hash table would be equivalent but would have the added benefit of being native PowerShell code.

  • Avatar
    ALIEN Quake

    Requiring JSON would be like shooting yourself in the foot:
    – It is too verbose
    – it offers no advantages over psd1 for such simple data
    – one tiny mistake makes everything blow up
    Please reconsider this!

  • Avatar
    Danstur

    The main reason I’d think Json would be a bad choice is because how you have to escape newlines in strings.

    Am I missing something here or is the second example not valid Json to begin with? Is this a mistake or did people realize that having to encode code in the script is a bad idea and decided to go with “almost-json”?

    I don’t mind using actual Json (just write a simple conversion from PS to JSON if you really have to write that much code), but something that is almost Json but not quite sounds like it’d a headache.

    • Avatar
      Dusk Banks

      I don’t mean to alarm you, but program source code is data. Program semantics in a language are encoded into data stored on a computer (sourcecode), to be interpreted later by a language compiler or interpreter, or anything else really. Crescendo’s current JSON encoding may be awkward, but it’s a valid use of a general-purpose data format. Lisps, at their core, have programs super obviously encoded as lists of lists & symbols, encoded for storage on disk in the S-expression format.

      JSON isn’t inherently a bad choice here. It depends on how you use it.

      • Avatar
        John Ludlow

        I agree, but there is an issue common to any scenario where some code is stored in a string and edited by hand – there’s no syntax highlighting or error checking on the code snippet to help spot mistakes. When you make a mistake the line numbers will be off because line 1 in the script block is not line 1 in the file.

        It’s easy to make a mistake and difficult to spot the mistakes you’ve made.

        Of course this can be mitigated somewhat by.moving as much of the complexity as possible to a separate script file.

        A better approach might have been to create a .psd1 rather than JSON, finish the PSD1 schemas story, and enable code in .psd1 files under certain circumstances.

        Or if they are gravitating towards JSON for everything, allow you to specify a script file and function name instead of a snippet of PowerShell in the JSON

      • Avatar
        Danstur

        Except that the current usage is not valid JSON and having to read larger scripts like the one shown in actually valid JSON format (i.e. all those pesky newlines replaced with \n and everything in a single line) is incredibly awkward.

  • Avatar
    Dan Ward

    Very cool idea! Over the years I’ve written a number of wrappers for native commands so having this utility will definitely save time in the future.

    In particular I wrote a wrapper for Docker (PS Gallery details at end) that returns PSObjects for images, ps and history – Docker commands that return tabular data. Some lessons learned that will help folks in writing their own wrappers:
    – Docker has a little known option for returning JSON via the format option. Use this to save yourself a lot of time: An example:
    docker images –format ‘{{ json . }}’
    – Docker’s date text format, at least at the time I wrote the tool, did not automatically convert with PowerShell’s Get-Date. So expect to have to write some custom code to wrapping properties and convert them properly (dates, bools, etc.).
    – Docker’s size properties (image Size and VirtualSite, for example) embed the unit of measurement in the value itself: 357MB, 15KB, etc. That’s no good. For those types of issues I ended up adding a new property in a consistent unit (KB, in this case). As a result the new object won’t match the old data perfectly but at least you will be able to properly sort / compare / measure in a pipeline.

    My wrapper, if you are interested, is in PSGallery as Invoke-Docker-PSObject and the source is at:
    https://github.com/DTW-DanWard/Invoke-Docker-PSObject

  • Avatar
    Luc Dekens

    I don’t think a Crescendo module can be used with PowerShell 5.1 as claimed in the article.
    The generated code contains the ternary operator.

    if ( $value -is [switch] ) { $__commandArgs += $value.IsPresent ? $param.OriginalName : $param.DefaultMissingValue }
  • Mike Loux
    Mike Loux

    This is wild. And it makes me laugh, as I, just yesterday, wrote a PowerShell wrapper to a .NET Core 3.1 console app (which uses the CommandLine.Parser library) simply so I could get the auto-completion features as well as SupportsShouldProcess. JSON does not bother me one bit; I will take it over XML (which I have used as a DSL in the past – Ant target files, anyone?) any day of the week. Looking forward to see how this progresses.

  • Avatar
    Schindler Christian

    Awesome idea. However I concur with others that JSON is not the best idea. I gave it a try and was successful with a simple command. One question: is it possible to specify multiple commands in one JSON file? Having a separate module for each an every command is cumbersome… Thanks Christian

  • Avatar
    Alan Bourke

    In Powershell to get a directory listing it’s something like:

    Get-ChildItem -Path c:\ -Recurse | Sort-Object Length -Descending | Select-Object length,name,directory -First 100 | Format-Table -AutoSize

    whereas in CMD.EXE it’s:

    dir c:\myfolder /os

    So if I understand it correctly, this would allow me (after creating the correct defintion file) to use the CMD syntax in Powershell without creating a CMD.EXE instance ?

  • Avatar
    Taylor Hutchison

    This is great. I’ll dissent from the other comments and say I like JSON as the format. I’m sure the arguments against JSON are sound and thoughtful, this is just my personal preference. I would like to see support for multiple cmdlets from one configuration file in a future release. Thank you for your work on this.