- BaseCommandNode (abstract) / CommandNode(T) split for heterogeneous tree - Flag(T) with compile-time type branching, Parsable/Coercable modules - Arg double-process invariant enforcement - App.generate macro builds CommandNode tree from Command subclasses - Command#initialize generated via macro finished, populates ivars from handler - Reserved -h/--help enforcement in Flag#check! - MatchType::Help added, SubCommand dispatch fixed - design.adoc: added Planned Features (markdown docs, bash completion) - Makefile: tabs, .DEFAULT_GOAL, doc_show target Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.0 KiB
Crytal Cli Generator
This document outlines the overall design of the CliGen shard & it’s underlying classes/objects & their usecases.
Architecture
The desire for this project is to provide a framework for generating commandline arg-parses & command dispatch built into a class/object.
The idea is to (much like JSON::Serializable & YAML::Serializable) is to use macros to help defining a Command object and abstract the command-line away from the codebase needing input from the user at init time.
Command Objects
The core of this codebase is the CliGen::Command object.
This is the object that is meant to provide you the hook into being able to utilize the codebase. It is where you are able to define all of your expecetd arguments/flags & any selectables (aka, instance variable that you want the user to choose one value for. ex: -f|--format json|yaml|ecr).
To make this all possible we load the base class with macros that help define your variables & inform the framework how it needs to handle arguments being presented to your command object setters.
require "cligen"
module MyModule
class MyCommand < CliGen::Command
argument(myvar : Int32 = 23,
long: "--myvar VAR",
short: "-m",
description: "This tells the utility how many times to do thing",
validate: ->(v : Int32) : Bool { (1..23).includes?(v) }
)
selection(output_format : String = "ecr",
long: "--format FORMAT",
short: "-f",
description: "Inform the utility what output you want the data in",
options: %w[ json yaml ecr ]
)
DO_THING_EXAMPLES = [
"myutil mycommand do_thing --myvar 5"
]
subcommand do_thing : Int32,
description: "Do the THING",
examples: MyModule::MyCommand::DO_THING_EXAMPLES \
do
output = 0
@myvar.times do |i|
puts "thing done %i times" % [ i + 1 ]
output += i
end
output
end
end
CliGen::App.process(ARGV)
end
The examples like above provide a "DSL-esk" way of defining: - Instance Variables, - Short/Long flags - Description of the flags (used in the help output as well) - A verification proc/lambda for doing ad-hoc checks of the value provided by the user (essentially allowing you to implement your own option: key like in selection) - Selections (currently compile-time and will open it up to runtime collecting of options later on based on defined annotations in the class) - define subcommands of this current command
How it works
Using crystal macros, you define the shape (arguments/flags, selections, work functions/subcommands, etc
Planned Features
Markdown Documentation Generation
Since all command metadata is present in annotations at compile time (@[CliGen::CommandInfo], @[CliGen::SubCommand], @[CliGen::Argument], @[CliGen::Selection]), the framework can walk the same structures that generate.cr already walks and render them into a Markdown document instead of a CommandNode tree.
The generation would be driven by a macro finished block (similar to generate.cr) that emits a self.generate_docs class method on App. This method walks every Command subclass and its annotations to produce a structured document.
Proposed output structure:
# <AppName> ## Commands ### mycommand <CommandInfo description> #### Flags | Flag | Short | Type | Default | Description | |------|-------|------|---------|-------------| | --myvar VAR | -m | Int32 | 23 | ... | #### Subcommands - `do_thing` — <description> - Examples: ...
Implementation notes:
-
Driven by a
--generate-docsglobal flag or a dedicated class method -
ECR templates (already pulled in) are the natural rendering mechanism
-
The same annotation data powers both runtime help output and the doc generator, keeping them in sync automatically
Bash Autocompletion Generation
Since all command names and flag names are known at compile time, a complete bash completion script can be generated statically — no runtime --completions endpoint needed.
The approach is a hidden --generate-completion bash flag (potentially extended to zsh/fish later) that prints a ready-to-install completion script to stdout.
Proposed completion script shape:
_myapp() {
local cur="${COMP_WORDS[COMP_CWORD]}"
local prev="${COMP_WORDS[COMP_CWORD-1]}"
case "${COMP_WORDS[1]}" in
mycommand)
COMPREPLY=($(compgen -W "--myvar -m --format -f" -- "$cur"))
;;
*)
COMPREPLY=($(compgen -W "mycommand myothercommand" -- "$cur"))
;;
esac
}
complete -F _myapp myapp
Implementation notes:
-
Script body generated at compile time via a
macro finishedwalk ofCommand.subclasses -
@[CliGen::Selection]options (%w[json yaml ecr]) can be included as valid completions for their flag -
Install path:
myapp --generate-completion bash > ~/.bash_completion.d/myappor printed with instructions -
Same annotation data used by the doc generator, so both stay in sync with the command definition