# cligen A Crystal shard that generates CLI parsers from class definitions using annotations and macros. Define your commands as classes; cligen builds the runtime parse tree. ## How It Works Subclass `CliGen::Command`, annotate your instance variables with `@[CliGen::Argument]` or `@[CliGen::Selection]`, and register the command with an `CliGen::App`. At compile time, macros inspect the annotations and generate typed `Flag(T)` objects; at runtime, `CliGen::App.process` walks the `CommandNode` tree to route arguments, populate your command instance, and dispatch to the right method. ## Installation 1. Add the dependency to your `shard.yml`: ```yaml dependencies: cligen: git: https://git.arcanium.tech/tristan/cligen ``` 2. Run `shards install` ## Usage ```crystal require "cligen" @[CliGen::CommandInfo(description: "Greet someone")] class Greet < CliGen::Command @[CliGen::Argument(short: "-n", long: "--name VALUE", description: "Name to greet")] @name : String = "world" argument(otherval : String = "abc", long: "--other", short: "-o", options: %w[ abc def ghi ], description: "This provides a way of setting the second string taht is printed" ) argument(myvars : Array(String) = [ "a" ], short: "-m", options: %w[ a b c ], description: "Provide multiple things to be printed out in the main function" ) def main puts "1) Hello, #{@name}!" puts "2) #{@otherval}" @myvars.each_with_index do |var, index| puts "%d) %s" % [ 3 + index, var ] end end end CliGen::App.process ``` ``` $ myapp greet --name Alice -m a a -m a,a,b 1) Hello, Alice! 2) abc 3) a 4) a 5) a 6) a 7) b ``` Full API documentation and design notes are in [`design.adoc`](design.adoc). ## Architecture As a short overview, this projects makes HEAVY use of Crystal macros to learn the shape of your project & command subclasses. Subclassing to `CliGen::Command` injects macros into your class that provides you user friendly DSLs/Macros for defining arguments/flags & subcommands. This is later used in the library `src/cligen/app/generate.cr` to generate a object graph of your commands and all annotated "arguments/flags" and stores them in a tree from the App object itself. This allows the project to "learn" your project & generate a command tree from the defined data. The MAJORITY of stdlib types (Int*, Float*, String, Bool & Time) are all supported in-place (as these are the primary data-types you might try to comsume from the CLI. However, custom data types are supported provided you extend the class's metaclass with `CliGen::Coercable` && `CliGen::Parsable` modules and define the `self.parse_args(args : Array(CliGen::Arg)` and `self.coerce(arg : String)` class methods. EX: ```crystal module MyModule class MyData extend CliGen::Parsable extend CliGen::Coercable @value : Int32 def initialize(value : String) @value = value.to_i32 end def self.parse_args(args : Array(CliGen::Arg)) arg = args.first # Mark the argument as processed arg.processed new(arg.value) end def self.coerce(arg : String) new(arg) end end end ``` Coercable Method: ----------------- ```crystal def self.coerce(arg : String) new(arg) end ``` The coerce method provides you the ability to parse a single string value into your class/datatype. This is generally only used when parsing from ENV VAR and when being used by parsing from an Array(T) type. This should only be used in the case you need a simple datatype that can be learned from a single string. Parsable Method: ---------------- ```crystal def self.parse_args(args : Array(CliGen::Arg)) arg = args.first # Mark the argument as processed (required) arg.processed new(arg.value) end ``` This method is used the most and is used for when using the bare class as the generic type in the Flag(T). With this the Flag(T) will collect all provided arguments (cli arguments that weren't determined to be flags or subcommands) and pass them to your parse_args method so that you can parse them how you see fit and determine if the args provided by the user are enough and to be able to raise if data is not provided correctly/in the right format. This gives the framework a way to allow you to extend the parser in your own custom way to allow for a "custom" format to be procesed. However, it's very strict and you MUST properly mark the arguments as processed so that the mainloop won't double-process arguments passed to your custom parser. However, this won't happen as in the Flag(T) I am doing a check to ensure that args were processed after passing it to your code. ## Development ```bash # Type-check without running crystal build src/cligen.cr --no-codegen # Run specs crystal spec ``` ## AI Assistance Disclosure This project uses [Claude Code](https://claude.ai/code) as a development aid — specifically for catching bugs, spotting typos, reviewing implementations, and talking through design decisions. All architecture decisions, code, and design are written by the author. Claude is used the way one might use a second pair of eyes on a diff, not as a code generator. `CLAUDE.md` at the repo root documents the project structure for Claude's context. `.claude/` holds project-level Claude Code settings. ## Contributors - [Tristan Ancelet](https://git.arcanium.tech/tristan) - creator and maintainer