Building a CLI Tool Aggregator with C#: Creating a hello world .NET tool
Coming from a Python background, switching to C# was daunting. The syntax was more verbose, with more filler code. Setting up projects was also more involved in comparison to Python.
Over time, I’ve come to appreciate and even understand the language better. I’m starting this series (and hopefully more 😅🤞🏾) as a way to document and share my learnings. It started as an attempt to share a new thing I’d learned and became an attempt at building something I’d find useful.
I settled on building a CLI tool aggregator that allows me to customize my favorite CLI commands. One of the things I was most curious about was building a database of my internet speeds over time.
From here on out, I’ll be referring to the CLI tool we’ll be building as “Commander” and using it from the CLI by typing cmdr (I’m shortening this for the sake of convenience).
Early disclaimer: Months into writing/building this I discovered that speedtest.net does most of what I wanted commander to do (save for detecting my connection type). I figured folks trying to build a DotNet tool might still find this useful.
What I’d like commander
to do
- Run my favorite CLI commands using command verbs of my choosing. e.g instead of
git pull
, I can runcmdr gp
instead. - Save the output of the CLI command I’m most interested in (
speed-test
) as a JSON file on my computer. - Add extra fields to this JSON output (
dateTime
andconnectionType
). I’d like to know the exact time I ran the command and if my internet connection was wired (ethernet cable) or wireless (WiFi). - Upload every run of this command to a cloud database. I’d probably do something uncomplicated like updating an online spreadsheet.
- Build a live chart with the data.
We’ll be building the first objective using C#. The result is a console application that can be packaged and installed as a NuGet Package. To make this less verbose, I’ll assume this is not your first time using C# or the .NET CLI. If this is, please check out the official documentation on how to set up your development environment if you’d like to follow along:
Note: For this project, I’m using Visual Studio 2022 with C# 10 which was a part of the .NET 6 release. Read more on C# 10.
Creating a hello world .NET tool
What is a .NET tool?
The .NET CLI lets you create a console application as a tool, which others can install and run. DotNet tools are NuGet packages that are installed from the .NET CLI. For more information about tools, check out the .NET tools overview.
I’ll be creating different checkpoints for the development of commander. The first is a hello world .NET tool that uses the System.CommandLine
package to handle our CLI “inputs”. These inputs are called arguments. We’ll talk more about this in the next post.
We’ll be able to give the tool an input (a name) and then it says hello back. This checkpoint is inspired by this dotnet walkthrough.
Here, We’re using the .NET CLI command-line tool to set up a console application.
|
|
- The
dotnet new
keyword sets up a sample console application. - The
dotnet add
keyword adds the System.CommandLine package to our project. The package is still in Beta hence theprerelease
flag.
After running the commands above, you’ll have a simple application that prints out Hello, World!
when you run the application.
Building out commander
: first checkpoint
This involves:
- Making our project packable a.k.a the output of our project should be a NuGet package.
- Install this package globally on our computer i.e running
cmdr
should print outHello, World
. - Use System.CommandLine to accept a name (
--name
) as input so we can printHello, Mercy!
if the--name
input is included in the command.
For the first step our Commander.csproj
file needs to look like this:
|
|
From the official definitions:
- PackAsTool indicates if the NuGet package should be configured as a .NET tool suitable for use with
dotnet tool install
(this is the command for installing .NET tools). It’s what we are most interested in here. - GeneratePackageOnBuild is a “nice to have”. Adding it results in a new
.nupkg
(NuGet Package) generated every time I build the project. The alternative is running thenuget pack
command manually instead. - ToolCommandName specifies the command that’ll invoke the tool after it’s installed.
- PackageId is a case-insensitive NuGet package identifier, which must be unique across nuget.org or whatever gallery the NuGet package will reside in. IDs may not contain spaces or characters that are not valid for a URL and generally follow .NET namespace rules. This is important when we’re publishing our NuGet package to a gallery so it can be easily discovered and used.
- Description is a long description of the NuGet package for UI display.
For the second step:
The tool can be installed globally with:
|
|
By default, NuGet attempts to find the package in package sources we’ve already added to the NuGet Package Manager. The --add-source
flag’s value points to the location of the NuGet package that gets generated when we build the project.
The output of running this is:
|
|
Because we installed the tool globally, running cmdr
from any terminal window will print Hello, World!
.
More information can be found here: How to manage .NET tools.
For the third step, we’d like to accept a name
input.
Our Program.cs
file now looks like this:
|
|
The following is happening:
- We’re using System.CommandLine to create an option (
--name
) that’ll be used to accept aname
input. This option is also required when the parent command is invoked.Note: When an option is required and the parent command (
cmdr
) is invoked without it, an error occurs. This won’t happen here because we’re setting a default value for name. - Setting the default value
SetDefaultValue
=World
to maintain the out-of-box experience we got when we started building out commander. - Making the root command aware of the
nameOption
. You can think of an option as a symbol defining a named parameter that’ll hold a value for that parameter. - Adding a description for the root command.
- Passing the
name
input we’re getting from the command line to the root command’s handler when we invoke thecmdr
command.
We can update our tool to this new version by running:
|
|
Running cmdr
results in:
|
|
Running cmdr --name "Mercy Markus"
results in:
|
|
Dotnet tools also come with help options out of the box.
Running cmdr --help
results in:
|
|
In the next post, we’ll modify commander to include a speed check subcommand i.e cmdr speed
outputs our network speed.