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 (CheckGet-Verb
for valid verb names)Noun
: The name of the cmdlet nounOriginalName
: The original native command name and locationOriginalCommandElements
: Some native commands have additional mandatory switches to execute properly for a given scenarioOutputHandlers
: 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 cmdletOriginalName
: 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
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...
This is a very gratifying new development!
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.
In Powershell to get a directory listing it’s something like:
whereas in CMD.EXE it’s:
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 ?
dir is not a separate program. You’ll always need to create a cmd.exe instance. See Internal commands – Windows CMD – SS64.com
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
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....
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.
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!
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
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...
Another abuse of JavaScript Object Notation. It was supposed to hold data, not be a DSL.
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...
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.
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...
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...