Building a CLI Tool Aggregator with C#: Beyond Hello World
System.CommandLine beyond printing “Hello world”
Our starting point is getting commander
to run a speed test command and because we need commander
to work with multiple commands we’ll be moving away from using the SetHandler
function of our rootCommand object to invoke a command. We’ll use individual command objects instead and invoke the commands by calling the CommandHandler.Create()
method on them.
To use the CommandHandler
class, we need to install the System.CommandLine.NamingConventionBinder
package to our project.
|
|
Our Program.cs file
class needs to be modified for cmdr speed
to print out the results of a speed test to the terminal.
|
|
The following new things are happening:
- We introduce
speedCommand
, a command object that’ll run a speed test when invoked withcmdr speed
. - We create a
Handler
that represents the action that will be performed when the command is invoked. In this case, callingCommandRunner
. CommandRunner
checks if thefast-cli
npm package exists (npm list --global fast-cli
), if it doesn’t we install it and then run the speed test command.CommandRunner
uses theSystem.Diagnostics.ProcessStartInfo
class to start a PowerShell process that’ll run our commands.- We’re setting some defaults for the process that gets started. These are:
- Redirecting the standard input: Gets or sets a value indicating whether the input for an application is read from the
System.Diagnostics.Process.StandardInput
stream. - Redirecting the standard output: Gets or sets a value that indicates whether the textual output of an application is written to the
System.Diagnostics.Process.StandardOutput
stream. - Redirecting the standard error: Gets or sets a value that indicates whether the error output of an application is written to the System.Diagnostics.Process.StandardError stream.
- Working Directory: Gets or sets the working directory for the process to be started. Here we’re setting it to the default UserProfile (in my case
Environment.SpecialFolder.UserProfile
=C:\Users\mercymarkus
). This is so our speed test results get saved to a known location and we’re able to build a database.
- Redirecting the standard input: Gets or sets a value indicating whether the input for an application is read from the
- After the process is started, we print out every output to the console, wait for the process to exit, and then close the process. If it exits with an exitCode that isn’t 0, we throw an exception and print it out to the terminal (0 means our code ran without any problems).
- After the handler is created, we add the
speedCommand
subcommand to thecmdrRootCommand
. - Finally, we invoke
cmdrRootCommand
.
Note:
System.Diagnostics.Process
class provides access to local and remote processes and enables you to start and stop local system processes.
We’ll update cmdr
using dotnet tool update --global --add-source ./bin/Debug --version 1.0.0 Commander
and run cmdr speed
to test these changes.
The result is:
|
|
Handle JSON data with jq
For handling our output as a JSON object, we’ll be using a nifty library called jq. You can download it here. I downloaded the jq 1.6 executable for windows.
jq is like sed for JSON data - you can use it to slice, filter, map and transform structured data with the same ease that sed, awk, grep and friends let you play with text.
Remember the previous output from running the cmdr speed
command in the last section? We’ll be using the jq library to filter out the fields we’re interested in (downloadSpeed, uploadSpeed, and latency) as well as adding extra fields we’d like to collect (dateTime and connectionType) and then export this JSON as a CSV we’re using to create a database of our speed test results over time.
speedCommand
now looks like this:
|
|
The additional things happening are:
- We’ve added an option called
saveResultOption
. It’s a Boolean command line option we’re using to toggle between 2 states; printing the output of the speed test command to the terminal or printing and then saving it to a JSON file which gets exported as a CSV. - We’ve also added a
fileNameOption
, a string option that customizes the speed test result filenames. We’re setting a default value(getDefaultValue: () => "speed-results"
) so we skip adding the--filename
or-f
flag at execution time. This saves us some keystrokes while still allowing the users of the tool to use a custom value. - In the
if
statement block, the following happens:- We’re creating a filter that includes all the fields we’d like added to our JSON object. This happens in
var constructSpeedTestObject
. - We’re using jq to filter the JSON object (
var filterSpeedTestOutput
) and passing the fields we’re creating as arguments (dateTime
and$connectionType
). - The arguments format the current dateTime value and invoke the
GetConnectionType()
method (we’ll talk about this in the next section). - We run the speed test command next and pass the output to PowerShell’s
Tee-Object
function. This prints the result to the terminal and also appends it to a file. The default file name isspeed-test.json
. - In the
createCsvWithJq
variable, jq is used to create a CSV file by mapping the keys and values of the JSON object as rows and columns. - The
createSpeedTestCsv
variable gets the contents of thespeed-test.json
, creates the CSV using jq, and then saves the output asspeed-test.csv
- Lastly, CommandRunner runs the speed test command and saves the output as a CSV.
- We’re creating a filter that includes all the fields we’d like added to our JSON object. This happens in
- The else block executes the speed test command (without saving to a JSON/CSV file) if the
saveResultOption
is false. - Lastly, we’re adding
saveResultOption
andfileNameOption
as options to thespeedCommand
command.
Get network connection type
We’re using the NetworkInterface.GetAllNetworkInterfaces()
method to get the network connection type. It returns an object that describes the network interfaces available on our local computer. We can then iterate through the interfaces that are operational and are either wireless or ethernet interfaces.
We have no interest in vEthernet, hence the exclusion.
vEthernet switches allow network access for virtual machines and other aspects of Hyper-V.
|
|
Extra: Change commands to LowerCase
This additional change was added to avoid CMDR SPEED
from not executing. I noticed that the root command was case-insensitive but not the subcommands i.e CMDR
or CmDr
work but not CMDR SPEED
or CmDr Speed
or other variants.
Here’s why commands, option names, and aliases are case-sensitive by default.
Output of cmdr Speed
before adding and calling the ChangeCommandsToLowerCase()
method:
|
|
The ChangeCommandsToLowerCase()
method:
|
|
This method is called right before invoking cmdrRootCommand
. It takes in the same arguments as the root command, iterates through the list of arguments, and changes them to lower case.
While building this, I discovered that there were a bunch of CLI tools that do similar things. Some honorable mentions are:
- speedtest.net: It saves your speed test results if you create an account. It’s web-based and works directly in the browser and also has a CLI tool. The downside for me is that I can’t save speed test results from the mobile app or CLI tool to my account. It has to be from the browser and that’s not ideal because I reach for my terminal more frequently than a browser.
- internet speed continuous monitor npm package: It’s an npm package that logs the internet speed every x interval.
Finally, if you got this far and you’re wondering where the CLI tool aggregation is happening as the title says, I’ll cover this in the next post 🙏🏾. This was getting too long.
I plan to save the speed test results in some online storage location (undecided on which) and update a live chart of the results in real-time as well.