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
Handlerthat represents the action that will be performed when the command is invoked. In this case, callingCommandRunner. CommandRunnerchecks if thefast-clinpm package exists (npm list --global fast-cli), if it doesn’t we install it and then run the speed test command.CommandRunneruses theSystem.Diagnostics.ProcessStartInfoclass 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.StandardInputstream. - 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.StandardOutputstream. - 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
speedCommandsubcommand to thecmdrRootCommand. - Finally, we invoke
cmdrRootCommand.
Note:
System.Diagnostics.Processclass 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--filenameor-fflag at execution time. This saves us some keystrokes while still allowing the users of the tool to use a custom value. - In the
ifstatement 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 (dateTimeand$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-Objectfunction. This prints the result to the terminal and also appends it to a file. The default file name isspeed-test.json. - In the
createCsvWithJqvariable, jq is used to create a CSV file by mapping the keys and values of the JSON object as rows and columns. - The
createSpeedTestCsvvariable 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
saveResultOptionis false. - Lastly, we’re adding
saveResultOptionandfileNameOptionas options to thespeedCommandcommand.
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.