Kingpin.Net is a free interpretation of the glorious Kingpin golang project, found here.
Using coding fluent style, you can easily build a consistent commandline interface for your tool. Kingpin.Net supports type safety on arguments and flags, and supports nested commands.
Install the Kingpin.Net nuget package using the following command in the package manager console window
PM> Install-Package KingpinNet
The Nuget package can be found here
- Fluent style API
- Rich support for commmands, sub-commands, arguments and flags
- Deep integration into Microsoft.Extensions.Configuration
- Type safe arguments and flags
- Beautiful console help
- POSIX Style short flags
- Customizable console help using Liquid syntax (rendered with Scriban)
- Context sensitive help output
- TAB Auto-completion on ZSH, Bash and Powershell
- Arguments containing lists of values
Here is the two major ways to add rich support for command line aruments into your application
Value-taking flags accept two fully interchangeable forms:
myapp --host=localhost # equals form
myapp --host localhost # space form
myapp -h=localhost # short flag, equals form
myapp -h localhost # short flag, space form
myapp --items alpha,beta # list flags take the space form tooRules:
--flag=valueand--flag valueare interchangeable, as are-f=valueand-f value. Both forms run the same type validation, so--port abcfails exactly like--port=abc.- Boolean flags never consume the next token.
--verbosesets it totrue, and--verbose=falsesets it tofalse;--verbose falseleavesfalseto be parsed as a command or an argument. - A token starting with
-is never taken as a value.--host --port=1fails with--host need a value, and so does a trailing--hostat the end of the command line. Dash-leading values must use the equals form:--offset=-5. - The space form takes the next token verbatim, even when that token is also a declared
command.
myapp --host deploysetshostto"deploy"and does not run thedeploycommand. Booleans are exempt, somyapp --verbose deploystill runsdeploy.
In order just to get the simplest command line parsing up and running
class Program
{
static void Main(string[] args)
{
Kingpin.Version("0.0.1");
Kingpin.Author("Joe Malone");
Kingpin.ExitOnHelp();
Kingpin.ShowHelpOnParsingErrors();
FlagItem debug = Kingpin.Flag("debug", "Enable debug mode.").IsBool();
FlagItem timeout = Kingpin.Flag("timeout", "Timeout waiting for ping.")
.IsRequired().Short('t').IsDuration();
ArgumentItem ip = Kingpin.Argument("ip", "IP address to ping.").IsRequired().IsIp();
ArgumentItem count = Kingpin.Argument("count", "Number of packets to send").IsInt();
var result = Kingpin.Parse(args);
Console.WriteLine($"Would ping: {ip} with timeout {timeout} and count {count} with debug = {debug}");
Console.ReadLine();
}
}Integrating with the configuration system build into .NET Core is equally easy. Just add .AddKingpinNetCommandLine(args) to your configuration builder
class Program
{
static void Main(string[] args)
{
Kingpin.Version("1.0").Author("Peter Andersen").ApplicationName("curl")
.ApplicationHelp("An example implementation of curl.");
Kingpin.ShowHelpOnParsingErrors();
var get = Kingpin.Command("get", "GET a resource.").IsDefault();
get.Argument("url", "Retrieve a URL.").IsDefault();
var post = Kingpin.Command("post", "POST a resource.");
post.Argument("url", "URL to POST to.").IsRequired().IsUrl();
var configuration = new ConfigurationBuilder().AddEnvironmentVariables()
.AddKingpinNetCommandLine(args).Build();
switch (configuration["command"])
{
case "get:url":
Console.WriteLine($"Getting URL {configuration["get:url"]}");
break;
case "post":
Console.WriteLine($"Posting to URL {configuration["post:url"]}");
break;
}
Console.ReadLine();
}
}KingpinNet supports auto completion on all the three major terminals. Just run the following commands with your tool:
For ZSH:
eval "$({Your-tool-executable} --suggestion-script-zsh)"
For Bash:
eval "$({Your-tool-executable} --suggestion-script-bash)"
For Powershell:
iex "$({Your-tool-executable} --suggestion-script-pwsh)"
After you run the script, you are able to have TAB auto complete on your tool.
KingpinNet apps emit a YAML description of their command tree designed for LLM agents to consume directly, alongside the
human-rendered --help. Two global flags are registered automatically (parallel to --help) once Initialize() runs:
--help-ai— writes the YAML to stdout.--help-ai-file— writes a Markdown-wrapped copy to<exename>-ai-help.mdnext to the executable. Pass an explicit path with--help-ai-file=/path/to/file.mdto override.
Both flags are available at the root and on every subcommand; invoking on a subcommand scopes the output to that
subtree. The human --help output gains a single footer line —
For agents: run with --help-ai for a machine-readable description. — so an LLM that reads --help first can discover
the AI variant without external docs.
Five fluent methods on KingpinApplication (and the static Kingpin facade) populate the root-level sections of the
YAML — each is emitted only when non-empty:
Kingpin
.ExitCode(0, "Success")
.ExitCode(1, "Network or protocol error")
.Example("Fetch a URL", "curl-ai get url https://example.com")
.Note("All commands accept --timeout to bound network operations.")
.Prefer(
rule: "Always pass --timeout explicitly",
when: "Non-interactive or scripted invocations",
why: "Without --timeout, curl-ai blocks indefinitely on dead peers.")
.Avoid(
rule: "Don't use --insecure",
unless: "Hitting a trusted endpoint with a known-bad certificate during dev",
why: "Disables TLS verification globally; opens the request to MITM.");Per-flag/argument, .Unit("seconds") declares what a value means and .Caution("...") surfaces destructive-flag
warnings inline:
Kingpin.Flag("timeout", "Set connection timeout.")
.Short('t').IsInt().Unit("seconds").Default("5");
Kingpin.Flag("insecure", "Skip TLS certificate verification.")
.Short('k').IsBool()
.Caution("Disables TLS verification; only use against trusted hosts you control.");Running the bundled Examples/CurlAi with --help-ai produces, in part:
command: curl-ai
summary: An example implementation of curl exercising the AI-friendly help surface.
synopsis: "curl-ai [commands] [flags]"
global_flags:
- long: --timeout
short: "-t"
type: int
takes_value: true
unit: seconds
default: "5"
required: false
help: Set connection timeout.
- long: --insecure
short: "-k"
type: bool
takes_value: false
required: false
help: Skip TLS certificate verification.
caution: Disables TLS verification; only use against trusted hosts you control.
commands:
- path: [get, url]
summary: Retrieve a URL.
synopsis: "curl-ai get url <arguments>"
argument:
- type: url
takes_value: true
required: true
help: URL to GET.
exit_codes:
0: Success
1: Network or protocol error
prefer:
- rule: Always pass --timeout explicitly
when: Non-interactive or scripted invocations
why: Without --timeout, curl-ai blocks indefinitely on dead peers.
conventions:
flag_form: "--flag=value and --flag value are interchangeable for value-taking flags (same for -f=value and -f value); booleans are bare or --flag=false and never consume the next token; values starting with - require the = form"
inheritance: Commands inherit flags from each ancestor and from global_flagsThe full set of fields, the inheritance rule, and the format of every section are documented in specs/roadmap.md
under "Phase 3 — AI-Friendly Help".
--flag valueand-f valueare now accepted everywhere--flag=valueand-f=valuewere, includingListOfStringflags- Both forms run the same type validation, so error messages and behaviour are unchanged
- Boolean flags are deliberately exempt and never consume the next token
- Tokens starting with
-are still rejected as values; dash-leading values need the=form --help-aiconventions.flag_formand the rendered help now describe both forms
- New
--help-aiand--help-ai-fileglobal flags emit a YAML description of the command tree - Fluent API for declaring
ExitCode,Example,Note,Prefer, andAvoidsections - Per-flag/argument
.Unit(...)and.Caution(...)for machine-readable hints - Hand-rolled, trim/AOT-safe YAML emitter (no
YamlDotNetruntime dependency) Examples/CurlAi/exercises the full surface end-to-end
- Replaced DotLiquid with Scriban for trimming-safe help rendering
- Library is now marked
IsAotCompatiblewith trim, AOT, and single-file analyzers enabled - Help rendering no longer uses reflection — models are built as explicit
ScriptObject/ScriptArrayvalues Environment.ProcessPathreplacesAssembly.Locationto support single-file publish- CI now gates on AOT and single-file publish of an example app
- Resolved inconsistencies in
BaseItem.csandParser.cs - Tightened parser edge-case test coverage
- Target framework aligned to
net10.0
- 1.2
- Added space-separated flag values:
--flag valueand-f valueare interchangeable with--flag=valueand-f=valuefor every value-taking flag type, including lists. Booleans stay bare-or-=only, and dash-leading values still require the=form.
- Added space-separated flag values:
- 1.1.15
- Added argument containing list of values
- 1.1
- Various clean ups
- 1.0
- Removed TT templates help generation
- Added default DotLiquid help template
- Cleaned up IConsole usage
- 0.9
- Added auto completion scripts for ZSH, Bash and Powershell
- 0.8
- Added first attempt on auto completions for PowerShell, BASH and ZSH
- 0.7
- Added KingpinNet.UI
- Added ProgressBar and Spinner widgets
- Removed all references to Microsofts static Console object and used the mockable IConsole interface instead
- Updated tests
- 0.6
- Bug fixes
- 0.5
- Bug fixes
- 0.4
- Added parse method to the KingpinApplication class
- 0.2
- Added support for Linux newlines
- Added documentation
- Refactored the help flag code
- 0.1
- Initial project structure setup
- Help on nested commands
- Added example applications
- Added template help using T4 templates
A flag is declared with app.Flag("name", "help") and may be given a POSIX short name with
.Short('f'). Value-taking flags accept --flag=value and --flag value interchangeably, and
-f=value and -f value interchangeably; both forms are validated identically against the
flag's type. Boolean flags never consume the following token — they are set with a bare
--flag or explicitly with --flag=false. Any token starting with - is rejected as a value,
so a value that begins with a dash must use the equals form (--offset=-5), and a value-taking
flag with nothing usable after it fails with --<name> need a value. Because the space form
takes the next token verbatim, --host deploy sets host to "deploy" even when deploy is a
declared command.
- Check out this fantastic ASCII font library https://github.com/drewnoakes/figgle