# Crescendo

By
,
Microsoft
Published 2022-08-27

I felt like I was in dream-land having a chat with Jason and Jim
I felt like I was in dream-land having a chat with Jason and Jim

# 📢 Crescendo

When I look at the above krə-shĕn′dō it makes me think of a special move you could do on Street Fighter 2. Something like krə-shĕn′dō Kick looking up this word I find:-

So the one chance I had to ask the brains behind this, aka either Jason or Jim about why they called it Crescendo I totally fluffed and didn't ask. Which would be a perfect reason to have another chat with the delightful Jason Helmick and Jim Truher. Jim Truher has been on the Powershell team since Jeffery Snover v1 days! So to be speaking to such a key player in the Powershell world this was crazy. Then Jason Helmick the Powershell PM on the Powershell team, like damn did I hit the jackpot? I was gutted that I had some technical issues which prevented my camera working, but damn I wanted a screen shot for proof I really did talk to these two guys. So just believe me it really did happen. Honest!

Why on earth would Microsoft want to talk to a little tiny fish in the sea like me? Well sadly it wasn't to offer me an amazing work from home job to create weird and wonderful modules (drats) it was to talk about Crescendo. Still despite no job offer, (which there never was I just made this up) I was still over the moon to be speaking to these guys about this totally awesome Crescendo Module

Lets cover what Crescendo is and why you as someone who obviously it into Information Technology and use Powershell to do some or all of your job. Well Ladies and Gentlemen...Crescendo in a nutshell allows you to make any command line executable into a Powershell Module to share with the masses. Hmmm I hear you ask, but why would I want to do that when I can already run a non-powershell command-line-executable from a Powershell script anyways yes dear reader, you are correct you could invoke a native CLI from a script, but can you then easily access the output of that using the Powershell pipeline? The output is most likely plain string data that is returned not an actual object you could inspect so Crescendo bridges that gap and has the capability of making the output into objects.

Official documentation on what Crescendo is if you do not believe me, or if what I wrote doesn't make sense to you:-

📢 Official documentation:-
https://docs.microsoft.com/en-us/powershell/utility-modules/crescendo/overview?source=recommendations&view=ps-modules

# What got me into Crescendo? 🤓

So I am really lucky not to have just got to speak to Jason and Jim, but over the years I have spoken to a fair amount of MVP people. Trust people there is a reason Microsoft gives the people they give an MVP. They are genuinely great people who do amazing work. Anyways I saw this tweet from Adam Driscoll about this Sysinternals Module he had done using Crescendo. I know all about the Sysinternals tools and one of the particular tools he did handles I was recently using to un-stick the not responding Powershell ISE sessions I was having when trying to cancel code from running. So I looked at the Github repo then was shocked when I read a few lines basically explaining how he did the module and how you could add to it. I felt like a kind in a candy shop all the CLI programs I was thinking about that I too could now make Crescendo modules out of. Boom that alone was enough to sell this to me on a plate. Right so step one for me, was I needed to install Powershell 7 to be able to install the Crescendo module yep I was still running Powershell v5 as it did everything I needed until now.

I guess as I explained I felt like a kid in a candy shop, and I just wanted to eat those sweets, when in hind-sight I should have read the instructions, as although I was able to build these Crescendo modules very easily using the Github repo template Mr Driscoll had kindly provided, I did not know about Crescendo other than it could make the magic of making this old-skool CLI or even nu-skool CLI run as if it was a native Powershell cmdlet or function.

Let us demystify what Crescendo does and does not magically do, as I know from personal experiance you try to find this information there is either a lot to read, or a lot to watch. I want to stream-line the goodness into your brain, so please keep reading.

Just hope Jason and Jim did not regret the call
Just hope Jason and Jim did not regret the call

# Demystify Crescendo 🧐

I felt I was late to this party, as Crescendo has been about for a little while now, and I had only just discovered it, via a tweet, which I am super glad I read. However I did not build my Crescendo modules using the proper method and this although got me into Crescendo I didn't fully appreciate what it is actually doing, or how to properly use it.

Do you want the good news or the bad news?

Bad News
Good News

It is not real magic, there is no magic involved

It will not automatically generate all the parameters from the chosen executable you wish to covert

It will not automatically make the output into an object output for Powershell

It is not complicated to understand

It will automatically build a very complicated PSM1 file for you

Editing the JSON file is very easy and has built in intellisense.

Using the JSON file is not difficult and will help populate the end module with the attribute-value pairs specified.

Trust this will all become clear shortly if this is your first glance at Crescendo, I just do not want to mention JSON later on and you be like what the heck is JSON

Why does Crescendo not make every CLI into objects then? Well dear reader, Crescendo can only make the CLI into outputted Powershell objects if the original CLI you are wrapping supports some sort of output other than just text. Newer CLI do maily support JSON output or others may support CSV output. So for me this was something I didn't grasp at the start. For instance the Robocopy module I made, robocopy being a really old CLI does not support JSON or CSV output, so I cannot make the returned data from this into Powershell objects, so you can view the properties or methods of particular objects. It is just string data. So why bother doing that one then? Simple it is written in the blog I done, but these old-skool CLI had very interesting parameters that didn't make sense unless I read the help for each one. So for me just to be able to give simple param names is enough, and to remind all the young-ones out there that these old but quality CLI can still come in use, just a case of knowing if they exist.

Think about it folks, I am not just talking Windows built in CLI you could convert. How about anyone of these CLI you could make into a Crescendo module Azure cli, AWS cli, Docker cli, Kubernetes cli, Microsoft PowerApps cli, Jenkins cli, Github cli, vSphere cli...the list just goes on and on. All these different CLIs that exist out there all doing different things, but we could bring all this goodness to the Powershell gallery by making these into Crescendo modules. Which then means if any of the above mentioned have quirky parameter sets you could make the CLI a lot easier for everyone else by making decent easy to understand parameters. Secondly as most of these are modern CLI then they should support outputting the format into something like JSON or CSV to then allow Crescendo to make this into proper objects when you query the CLI you are converting. Please note there is like one-line of code you have to add in the JSON file for this magic to happen.

Yes you will have to manually construct all the parameters you want to use from the original CLI into the JSON file. Crescendo will not magically go ah I see it has all these params I will automatically insert these into the JSON file. However that being said, once you right your first parameter, you can pretty much copy and paste that small code block to use for the rest of the parameters you wish to include.

# In a nutshell 🥜

So I created a folder named SpeedTest-CLI within that folder I created the following .PS1 file, to be able to create the JSON file which you will need to edit.

Import-Module Microsoft.Powershell.Crescendo
$NewConfiguration = @{
    '$schema' = 'https://aka.ms/PowerShell/Crescendo/Schemas/2021-11'
    Commands  = @()
}
$parameters = @{
    Verb         = 'Initialize'
    Noun         = 'SpeedTest'
    OriginalName = "librespeed-cli.exe"
}
$NewConfiguration.Commands += New-CrescendoCommand @parameters
$NewConfiguration | ConvertTo-Json -Depth 3 | Out-File .\SpeedTest.json

JSON file that this generated by the above command. Don't worry this can be trimmed down.

{
  "$schema": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11",
  "Commands": [
    {
      "Verb": "Initialize",
      "Noun": "SpeedTest",
      "OriginalName": "librespeed-cli.exe",
      "OriginalCommandElements": null,
      "Platform": [
        "Windows",
        "Linux",
        "MacOS"
      ],
      "Elevation": null,
      "Aliases": null,
      "DefaultParameterSetName": null,
      "SupportsShouldProcess": false,
      "ConfirmImpact": null,
      "SupportsTransactions": false,
      "NoInvocation": false,
      "Description": null,
      "Usage": null,
      "Parameters": [],
      "Examples": [],
      "OriginalText": null,
      "HelpLinks": null,
      "OutputHandlers": null
    }
  ]
}

Trim the JSON file down a bit to make it less daunting to start modifying it, see much better.

{
  "$schema": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11",
  "Commands": [
    {
      "Verb": "Initialize",
      "Noun": "SpeedTest",
      "OriginalName": "librespeed-cli.exe",
      "OriginalCommandElements": null,
      "Platform": ["Windows"],
      "Description": null,
      "Parameters": [],
      "OutputHandlers": null
    }
  ]
}

Now I added the --json into OriginalCommandElements to make this parameter automatically run each time the command is run. Then I gave it a Description, after that I added the parameters, then most importantly as I wanted objects, and this was using JSON output, I used one line of code into the OutputHandlers to make it a true Powershell object the data this CLI returned. Below is the final JSON file finished. I just wanted to show you the steps on this JSON file as this is a critical process to understand to actually make the end module.

{
  "Commands": [
    {
      "Verb": "Initialize",
      "Noun": "SpeedTest",
      "OriginalName": ".\\librespeed-cli.exe",
      "OriginalCommandElements": ["--json"],
      "Platform": [
        "Windows"
      ],
      "Description": "Performs an internet speed test",
      "Parameters": [
        {
            "ParameterType": "switch",
            "OriginalName"  : "--ipv4",
            "OriginalPosition": 0,
            "Name"          : "IPv4Only"
        },
        {
            "ParameterType": "switch",
            "OriginalName"  : "--ipv6",
            "OriginalPosition": 1,
            "Name"          : "IPv6Only"
        },
        {
            "ParameterType": "switch",
            "OriginalName"  : "--no-download",
            "OriginalPosition": 2,
            "Name"          : "NoDownload"
        },
        {
            "ParameterType": "switch",
            "OriginalName"  : "--no-upload",
            "OriginalPosition": 3,
            "Name"          : "NoUpload"
        },
        {
            "ParameterType": "switch",
            "OriginalName"  : "--list",
            "OriginalPosition": 5,
            "Name"          : "ListServers"
        },
        {
            "ParameterType": "int",
            "OriginalName"  : "--server",
            "OriginalPosition": 6,
            "Name"          : "Server"
        }
      ],
      "OutputHandlers": [
        {
          "Handler": "$args[0] | ConvertFrom-Json -ErrorAction SilentlyContinue | Add-Member -TypeName SpeedTest -passthru",
          "HandlerType": "Inline",
          "ParameterSetName": "Default",
          "StreamOutput": false
        }
      ]
    }
  ],
  "$schema": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11"
}

So the big-takeaway lessons to be learnt, is the below code does the intellisense within VSCode

'$schema' = 'https://aka.ms/PowerShell/Crescendo/Schemas/2021-11'

also this original executable I wanted to convert aka librespeed-cli.exe did indeed support JSON output format. I found this out by actually looking at the help for this executable, which showed it supported both CSV and JSON

   --csv                          Suppress verbose output, only show basic information in CSV
                                  format. Speeds listed in bit/s and not affected by --bytes
                                   (default: false)
   --csv-delimiter CSV_DELIMITER  Single character delimiter (CSV_DELIMITER) to use in
                                  CSV output. (default: ",")
   --csv-header                   Print CSV headers (default: false)
   --json                         Suppress verbose output, only show basic information
                                  in JSON format. Speeds listed in bit/s and not
                                  affected by --bytes (default: false)

I needed this original CLI to always include the --json parameter everytime it was run, which in-turn I could then use as Powershell objects in the module

"OriginalCommandElements": ["--json"],

By using the above tiny bit of code in the JSON file now means whenever you run this within Powershell it will always use this parameter. This will allow it to return proper objects, we just need to do one last thing for this to actually work by using the following line of code in the JSON file:-

"Handler": "$args[0] | ConvertFrom-Json -ErrorAction SilentlyContinue | Add-Member -TypeName SpeedTest -passthru"

Boom. Without that line of code specified, the magic of making this CLI into a Powershell Object to allow you to use the pipline, would not have happened.

Do not worry readers you only need one Crescendo cmdlet to export this into a magically generated module file.

Export-CrescendoModule -ConfigurationFile .\SpeedTest.json -ModuleName SpeedTest-CLI -Force

As me and Jason are good mates now I strongly recommend you watch his videos which explain this in better detail than I wrote here.

# Videos To Watch 🎞️

Seriously check out the videos below to let Jason ease you into Crescendo

# Magical PSM1 file 🧙‍♂️

Why is this so magical? Well reader it is because Crescendo automatically did this for me using that end JSON file. Check out all this code

# Module created by Microsoft.PowerShell.Crescendo
class PowerShellCustomFunctionAttribute : System.Attribute { 
    [bool]$RequiresElevation
    [string]$Source
    PowerShellCustomFunctionAttribute() { $this.RequiresElevation = $false; $this.Source = "Microsoft.PowerShell.Crescendo" }
    PowerShellCustomFunctionAttribute([bool]$rElevation) {
        $this.RequiresElevation = $rElevation
        $this.Source = "Microsoft.PowerShell.Crescendo"
    }
}



function Initialize-SpeedTest
{
[PowerShellCustomFunctionAttribute(RequiresElevation=$False)]
[CmdletBinding()]

param(
[Parameter()]
[switch]$IPv4Only,
[Parameter()]
[switch]$IPv6Only,
[Parameter()]
[switch]$NoDownload,
[Parameter()]
[switch]$NoUpload,
[Parameter()]
[switch]$ListServers,
[Parameter()]
[int]$Server
    )

BEGIN {
    $__PARAMETERMAP = @{
         IPv4Only = @{
               OriginalName = '--ipv4'
               OriginalPosition = '0'
               Position = '2147483647'
               ParameterType = 'switch'
               ApplyToExecutable = $False
               NoGap = $False
               }
         IPv6Only = @{
               OriginalName = '--ipv6'
               OriginalPosition = '1'
               Position = '2147483647'
               ParameterType = 'switch'
               ApplyToExecutable = $False
               NoGap = $False
               }
         NoDownload = @{
               OriginalName = '--no-download'
               OriginalPosition = '2'
               Position = '2147483647'
               ParameterType = 'switch'
               ApplyToExecutable = $False
               NoGap = $False
               }
         NoUpload = @{
               OriginalName = '--no-upload'
               OriginalPosition = '3'
               Position = '2147483647'
               ParameterType = 'switch'
               ApplyToExecutable = $False
               NoGap = $False
               }
         ListServers = @{
               OriginalName = '--list'
               OriginalPosition = '5'
               Position = '2147483647'
               ParameterType = 'switch'
               ApplyToExecutable = $False
               NoGap = $False
               }
         Server = @{
               OriginalName = '--server'
               OriginalPosition = '6'
               Position = '2147483647'
               ParameterType = 'int'
               ApplyToExecutable = $False
               NoGap = $False
               }
    }

    $__outputHandlers = @{
        Default = @{ StreamOutput = $False; Handler = { $args[0] | ConvertFrom-Json -ErrorAction SilentlyContinue | Add-Member -TypeName SpeedTest -passthru } }
    }
}

PROCESS {
    $__boundParameters = $PSBoundParameters
    $__defaultValueParameters = $PSCmdlet.MyInvocation.MyCommand.Parameters.Values.Where({$_.Attributes.Where({$_.TypeId.Name -eq "PSDefaultValueAttribute"})}).Name
    $__defaultValueParameters.Where({ !$__boundParameters["$_"] }).ForEach({$__boundParameters["$_"] = get-variable -value $_})
    $__commandArgs = @()
    $MyInvocation.MyCommand.Parameters.Values.Where({$_.SwitchParameter -and $_.Name -notmatch "Debug|Whatif|Confirm|Verbose" -and ! $__boundParameters[$_.Name]}).ForEach({$__boundParameters[$_.Name] = [switch]::new($false)})
    if ($__boundParameters["Debug"]){wait-debugger}
    $__commandArgs += '--json'
    foreach ($paramName in $__boundParameters.Keys|
            Where-Object {!$__PARAMETERMAP[$_].ApplyToExecutable}|
            Sort-Object {$__PARAMETERMAP[$_].OriginalPosition}) {
        $value = $__boundParameters[$paramName]
        $param = $__PARAMETERMAP[$paramName]
        if ($param) {
            if ($value -is [switch]) {
                 if ($value.IsPresent) {
                     if ($param.OriginalName) { $__commandArgs += $param.OriginalName }
                 }
                 elseif ($param.DefaultMissingValue) { $__commandArgs += $param.DefaultMissingValue }
            }
            elseif ( $param.NoGap ) {
                $pFmt = "{0}{1}"
                if($value -match "\s") { $pFmt = "{0}""{1}""" }
                $__commandArgs += $pFmt -f $param.OriginalName, $value
            }
            else {
                if($param.OriginalName) { $__commandArgs += $param.OriginalName }
                $__commandArgs += $value | Foreach-Object {$_}
            }
        }
    }
    $__commandArgs = $__commandArgs | Where-Object {$_ -ne $null}
    if ($__boundParameters["Debug"]){wait-debugger}
    if ( $__boundParameters["Verbose"]) {
         Write-Verbose -Verbose -Message librespeed-cli.exe
         $__commandArgs | Write-Verbose -Verbose
    }
    $__handlerInfo = $__outputHandlers[$PSCmdlet.ParameterSetName]
    if (! $__handlerInfo ) {
        $__handlerInfo = $__outputHandlers["Default"] # Guaranteed to be present
    }
    $__handler = $__handlerInfo.Handler
    if ( $PSCmdlet.ShouldProcess("librespeed-cli.exe $__commandArgs")) {
    # check for the application and throw if it cannot be found
        if ( -not (Get-Command -ErrorAction Ignore "librespeed-cli.exe")) {
          throw "Cannot find executable 'librespeed-cli.exe'"
        }
        if ( $__handlerInfo.StreamOutput ) {
            & "librespeed-cli.exe" $__commandArgs | & $__handler
        }
        else {
            $result = & "librespeed-cli.exe" $__commandArgs
            & $__handler $result
        }
    }
  } # end PROCESS

<#


.DESCRIPTION
Performs an internet speed test

.PARAMETER IPv4Only



.PARAMETER IPv6Only



.PARAMETER NoDownload



.PARAMETER NoUpload



.PARAMETER ListServers



.PARAMETER Server




#>
}

Obviously the last thing I do before publishing my Crescendo modules is to make sure my help file for the module is all in-place and makes total-sense I why I done the module and how to use it.

# I hope this helps? 🤗

I got some big plans for Crescendo as I do think it is an amazing module. Just currently I am limited in the CLIs I can use, but once I get using something big that I can share I certainly will do. I also hope that this has also demystified Crescendo for you, and shown you how to implement it on an existing CLI out there.

Thanks for reading if you got this far
Thanks for reading if you got this far