December 8th, 2020

Announcing PowerShell Crescendo Preview.1

Jason Helmick
SR. PROGRAM MANAGER

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

Author

Jason Helmick
SR. PROGRAM MANAGER

Nice to meet you! I’m a Program Manager on the PowerShell team at Microsoft. My focus is on all things PowerShell including Predictive IntelliSense, Crescendo, DSC and PlatyPS. One favorite pastime is working with the rapidly growing PowerShell community.

21 comments

Discussion is closed. Login to edit/delete existing comments.

  • hspc pc

    I use a lot of native commands within powershell scripts. My main pain point is error handling as I have to use a lot of $lastexitcode checks. The primary issue I hope powershell could solve is consistent error handling.
    Transforming native commands to powershell cmdlets sounds like a good idea however it's unlikely that it will find good adoption as native CLI commands are more popular and are what you find on Sackoverflow or a...

    Read more
  • 颖 王

    This is a very gratifying new development!

  • Taylor

    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.

  • 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 ?

  • 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

  • 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....

    Read more
  • 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 }
    • Jason HelmickMicrosoft employee Author

      This was a bug that creeped in during release. We should have an update out shortly that removes the ternary operator from the proxy code. Thanks Luc!

      • Jason HelmickMicrosoft employee Author

        The blog is updated and Powershell Gallery has Crescendo 0.4.1 to remove generation of ternary. The output handler in the IPConfig example was also updated to remove the ternary operator to support Windows PowerShell 5.1

  • 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...

    Read more
  • Gonkers .

    Another abuse of JavaScript Object Notation. It was supposed to hold data, not be a DSL.

    • 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...

      Read more
      • Daniel Sturm

        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.

      • 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...

        Read more
  • Daniel Sturm

    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...

    Read more