Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 777a1b992a | |||
| a17053f45c | |||
| 6535753fa3 | |||
| 09389cb5e9 | |||
| d1746602b1 | |||
| 21462029e3 | |||
| 4a0dcb8b59 | |||
| 8780be3a42 | |||
| fd510469ac | |||
| f55cc25f2d | |||
| b05f49c2e8 | |||
| d3ca70ea9e | |||
| 697a0db3af | |||
| a1a9c6fc04 | |||
| ffde5165f7 | |||
| 53f510278c | |||
| e6d9306e69 | |||
| 18c5a559b1 | |||
| 28c45fae9e | |||
| f889aae5e1 | |||
| e90d89689c | |||
| 9b3d823bc7 | |||
| d02eded273 | |||
| fc592fee37 | |||
| 47b4bfa494 | |||
| 1a2da86048 | |||
| 37c5083ae2 | |||
| 976ed10f75 | |||
| ea8ca62a63 | |||
| 0c7849dc7c | |||
| 47df89a6c1 | |||
| afd7ba7c7b | |||
| 8447a1ffbb | |||
| 4af08f8efe | |||
| 1085335c88 | |||
| 19ea00ef72 | |||
| 8d17de7434 | |||
| 405303e4f8 | |||
| 29327a3e83 | |||
| ac5711c0df | |||
| 519c8dcd44 | |||
| bc0f56fda9 |
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(crystal build*)",
|
||||||
|
"Bash(crystal spec*)",
|
||||||
|
"Bash(crystal run*)",
|
||||||
|
"Bash(shards install*)",
|
||||||
|
"Bash(shards update*)",
|
||||||
|
"Bash(git status*)",
|
||||||
|
"Bash(git diff*)",
|
||||||
|
"Bash(git log*)",
|
||||||
|
"Bash(git add*)",
|
||||||
|
"Bash(git commit*)",
|
||||||
|
"Bash(git push*)",
|
||||||
|
"Bash(git rm*)",
|
||||||
|
"Bash(rm -f test)",
|
||||||
|
"Bash(ls*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*.cr]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
*
|
||||||
|
!CLAUDE.md
|
||||||
|
!lib/
|
||||||
|
!lib/cligen
|
||||||
|
!src/
|
||||||
|
!src/**
|
||||||
|
!shard.yml
|
||||||
|
!README.md
|
||||||
|
!design.adoc
|
||||||
|
!docs/
|
||||||
|
!docs/**
|
||||||
|
!Makefile
|
||||||
|
!LICENSE
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
# CliGenerator
|
# cligen
|
||||||
|
|
||||||
This is a crystal project to manage setting up OptionParser objects based around "Command" objects and arguments you define inside them.
|
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
|
## Installation
|
||||||
|
|
||||||
@@ -14,12 +18,47 @@ This is a crystal project to manage setting up OptionParser objects based around
|
|||||||
|
|
||||||
2. Run `shards install`
|
2. Run `shards install`
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```crystal
|
```crystal
|
||||||
require "cligen"
|
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"
|
||||||
|
|
||||||
|
def main
|
||||||
|
puts "Hello, #{@name}!"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
CliGen::App.process
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
$ myapp --name Alice
|
||||||
|
Hello, Alice!
|
||||||
|
```
|
||||||
|
|
||||||
|
Full API documentation and design notes are in [`design.adoc`](design.adoc).
|
||||||
|
|
||||||
|
## 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
|
## Contributors
|
||||||
|
|
||||||
- [Tristan Ancelet](https://git.arcanium.tech/tristan) - creator and maintainer
|
- [Tristan Ancelet](https://git.arcanium.tech/tristan) - creator and maintainer
|
||||||
|
|||||||
+4
-6
@@ -44,10 +44,8 @@ module MyModule
|
|||||||
"myutil mycommand do_thing --myvar 5"
|
"myutil mycommand do_thing --myvar 5"
|
||||||
]
|
]
|
||||||
|
|
||||||
subcommand do_thing : Int32,
|
subcommand do_thing, description: "Do the THING", examples: MyModule::MyCommand::DO_THING_EXAMPLES do
|
||||||
description: "Do the THING",
|
|
||||||
examples: MyModule::MyCommand::DO_THING_EXAMPLES \
|
|
||||||
do
|
|
||||||
output = 0
|
output = 0
|
||||||
|
|
||||||
@myvar.times do |i|
|
@myvar.times do |i|
|
||||||
@@ -55,11 +53,11 @@ module MyModule
|
|||||||
output += i
|
output += i
|
||||||
end
|
end
|
||||||
|
|
||||||
output
|
puts "Total : %i" % output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
CliGen::App.process(ARGV)
|
CliGen::App.process
|
||||||
end
|
end
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
..
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
require "./cligen/exceptions"
|
||||||
|
require "./cligen/coercable"
|
||||||
require "./cligen/parsable"
|
require "./cligen/parsable"
|
||||||
require "./cligen/annotations"
|
require "./cligen/annotations"
|
||||||
require "./cligen/format"
|
require "./cligen/format"
|
||||||
@@ -10,6 +12,11 @@ require "./cligen/app"
|
|||||||
module CliGen
|
module CliGen
|
||||||
VERSION = "0.1.0"
|
VERSION = "0.1.0"
|
||||||
|
|
||||||
|
macro override_help_template(filepath)
|
||||||
|
{% raise "ERROR : CliGen.override_help_template : File(#{filepath}) doesn't exist" unless file_exists?(filepath) %}
|
||||||
|
CliGen::HELP_OVERRIDE_TEMPLATE = {{`readlink -f #{filepath}`.strip.stringify}}
|
||||||
|
end
|
||||||
|
|
||||||
APPNAME = File.basename(PROGRAM_NAME)
|
APPNAME = File.basename(PROGRAM_NAME)
|
||||||
|
|
||||||
record AdditionalDefaultFlag,
|
record AdditionalDefaultFlag,
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ module CliGen
|
|||||||
annotation Selection
|
annotation Selection
|
||||||
end
|
end
|
||||||
|
|
||||||
|
annotation PreRunCommand
|
||||||
|
end
|
||||||
|
|
||||||
annotation SubCommand
|
annotation SubCommand
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+17
-5
@@ -14,17 +14,29 @@ module CliGen
|
|||||||
@@instance = self
|
@@instance = self
|
||||||
end
|
end
|
||||||
|
|
||||||
def check! : Nil
|
def check!
|
||||||
CliGen::GLOBAL_FLAGS.each(&.check!)
|
super
|
||||||
# @flags is the amalgamation of all child flags — checked by the commands themselves
|
end
|
||||||
check_for_duplicates!([CliGen::GLOBAL_FLAGS, @flags].flatten)
|
|
||||||
@commands.each(&.check!)
|
def self.handle_command_raises(&) : Nil
|
||||||
|
begin
|
||||||
|
yield
|
||||||
|
rescue e : CliGen::RuntimeError
|
||||||
|
abort e.message
|
||||||
|
rescue e : CliGen::ConfigurationError
|
||||||
|
abort e.message
|
||||||
|
rescue e : CliGen::HelpRequestedError
|
||||||
|
puts e.message
|
||||||
|
exit 0
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Convenience entry point; defaults to ARGV
|
# Convenience entry point; defaults to ARGV
|
||||||
def self.process(args : Array(String) = ARGV.to_a) : Nil
|
def self.process(args : Array(String) = ARGV.to_a) : Nil
|
||||||
generate if @@instance.nil?
|
generate if @@instance.nil?
|
||||||
|
handle_command_raises do
|
||||||
@@instance.not_nil!.process(args)
|
@@instance.not_nil!.process(args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -27,10 +27,13 @@ module CliGen
|
|||||||
env_var: {% if anno[:env_var] %} {{anno[:env_var]}} {% else %} {{"#{cmd.name.split("::").last.upcase.id}_#{var.name.upcase}"}} {% end %},
|
env_var: {% if anno[:env_var] %} {{anno[:env_var]}} {% else %} {{"#{cmd.name.split("::").last.upcase.id}_#{var.name.upcase}"}} {% end %},
|
||||||
description: {{ anno[:description] }},
|
description: {{ anno[:description] }},
|
||||||
default: {% unless var.default_value.nil? %} {{var.default_value}} {% else %} nil {% end %},
|
default: {% unless var.default_value.nil? %} {{var.default_value}} {% else %} nil {% end %},
|
||||||
validate: {% if anno[:validate] %} {{anno[:validate]}} {% else %} nil {% end %},
|
validate: {% if anno[:validation] %} {{anno[:validation]}} {% else %} nil {% end %},
|
||||||
on_match: {% if anno[:on_match] %} {{anno[:on_match]}} {% else %} nil {% end %}
|
on_match: {% if anno[:on_match] %} {{anno[:on_match]}} {% else %} nil {% end %},
|
||||||
|
options: {% if anno[:options] %} {{anno[:options]}} {% else %} nil {% end %},
|
||||||
|
delimiter: {% if anno[:delimiter] %} {{anno[:delimiter]}} {% else %} "," {% end %},
|
||||||
|
format: {% if anno[:format] %} {{anno[:format]}} {% else %} nil {% end %}
|
||||||
)
|
)
|
||||||
{% debug %}
|
{% debug if env("DEBUG") %}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
# Keep a copy of every flag on the root for global matching
|
# Keep a copy of every flag on the root for global matching
|
||||||
@@ -44,7 +47,7 @@ module CliGen
|
|||||||
post_run_commands: cmd_post_run_cmds,
|
post_run_commands: cmd_post_run_cmds,
|
||||||
description: {{cmd_info[:description]}}
|
description: {{cmd_info[:description]}}
|
||||||
)
|
)
|
||||||
{% debug %}
|
{% debug if env("DEBUG") %}
|
||||||
{% end %}
|
{% end %}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
|
|||||||
+49
-1
@@ -24,12 +24,60 @@ module CliGen
|
|||||||
def initialize(@value, @index)
|
def initialize(@value, @index)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.flag?(val : String) : Bool
|
||||||
|
if val =~ CliGen::Regex::FLAG_REGEX
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def flag?(val : String = @value) : Bool
|
||||||
|
Arg.flag?(val)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.int?(val : String) : Bool
|
||||||
|
if val =~ CliGen::Regex::INT
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def int?(val : String = @value) : Bool
|
||||||
|
Arg.int?(val)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.uint?(val : String) : Bool
|
||||||
|
if val =~ CliGen::Regex::UINT
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def uint?(val : String = @value) : Bool
|
||||||
|
Arg.uint?(val)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.float?(val : String) : Bool
|
||||||
|
if val =~ CliGen::Regex::FLOAT
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def float?(val : String = @value) : Bool
|
||||||
|
Arg.float?(val)
|
||||||
|
end
|
||||||
|
|
||||||
# This serves as a trigger that tells the object that it has been processed
|
# This serves as a trigger that tells the object that it has been processed
|
||||||
#
|
#
|
||||||
# This will raise an exception if it is re-called after already having been
|
# This will raise an exception if it is re-called after already having been
|
||||||
# processed.
|
# processed.
|
||||||
def processed
|
def processed
|
||||||
raise "ERROR : CliGen::Arg(index: #{@index}, value: #{@value})#processed : This arg was re-processed" if @processed
|
raise CliGen::ArgReprocessedError.new("CliGen::Arg(index: #{@index}, value: #{@value})#processed : This arg was re-processed") if @processed
|
||||||
@processed = true
|
@processed = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module CliGen::Coercable
|
module CliGen::Coercable
|
||||||
abstract def coerce(arg : String) : self
|
abstract def coerce(arg : String)
|
||||||
end
|
end
|
||||||
|
|||||||
+8
-18
@@ -4,32 +4,22 @@ require "time"
|
|||||||
require "./annotations"
|
require "./annotations"
|
||||||
require "./command/argument"
|
require "./command/argument"
|
||||||
require "./command/selection"
|
require "./command/selection"
|
||||||
require "./command/trigger"
|
require "./command/help_template"
|
||||||
require "./command/subcommand"
|
require "./command/subcommand"
|
||||||
|
require "./command/def_init"
|
||||||
|
require "./command/define_command_initializer"
|
||||||
|
|
||||||
module CliGen
|
module CliGen
|
||||||
class Command
|
class Command
|
||||||
macro inherited
|
macro inherited
|
||||||
{% verbatim do %}
|
{% verbatim do %}
|
||||||
macro finished
|
macro finished
|
||||||
def initialize(*, handler : CliGen::BaseCommandNode)
|
{% anno = @type.annotation(CliGen::CommandInfo) %}
|
||||||
{% verbatim do %}
|
{% raise "" unless anno %}
|
||||||
{% for var in @type.instance_vars %}
|
{% if anno[:def_init] %}
|
||||||
{% anno = (var.annotation(CliGen::Argument) || var.annotation(CliGen::Selection)) %}
|
def_init
|
||||||
{% if anno %}
|
|
||||||
{% raise "ERROR : #{@type.name}#initialize : Argument '#{var.name}' cannot be a nilable type (#{var.type}) — flags always resolve to a concrete value" if var.type.union? %}
|
|
||||||
if flg = handler.flags.find{|f| f.var == {{var.name.stringify}} && f.long == {{anno[:long]}}}
|
|
||||||
@{{var.id}} = flg.as(CliGen::Flag({{var.type}})).value!
|
|
||||||
else
|
|
||||||
raise "ERROR : {{@type.name}}\#{{@def.name}} : No flag found for \"{{var.name}}\"?"
|
|
||||||
end
|
|
||||||
{% else %}
|
|
||||||
{% raise "ERROR : #{@type.name}#{@def.name} : Instance Variable(#{var.name}) is not handled by CliGen and does not have a default value" unless var.default_value %}
|
|
||||||
@{{var.id}} = {{var.default_value}}
|
|
||||||
{% end %}
|
{% end %}
|
||||||
{% end %}
|
define_command_initializer
|
||||||
{% end %}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,41 +1,80 @@
|
|||||||
module CliGen
|
module CliGen
|
||||||
class Command
|
class Command
|
||||||
macro argument(variable, long, description, short = nil, validation = nil, on_match = nil)
|
macro argument(variable, description, long = nil, short = nil, validation = nil, on_match = nil, def_setter = false, def_getter = false, options = nil, delimiter = ",", format = nil, allow_no_verification = false)
|
||||||
|
{% raise "ERROR : CliGen::Command.argument : def_setter must be a Bool" unless def_setter.is_a? BoolLiteral %}
|
||||||
{% raise "ERROR : CliGen::Command.argument : First argument (#{variable}) must be a TypeDeclaration (ex: '<var> : <type> [= val]')" unless variable.is_a? TypeDeclaration %}
|
{% raise "ERROR : CliGen::Command.argument : First argument (#{variable}) must be a TypeDeclaration (ex: '<var> : <type> [= val]')" unless variable.is_a? TypeDeclaration %}
|
||||||
{% name = variable.var %}
|
{% name = variable.var %}
|
||||||
{% type = variable.type %}
|
{% type = variable.type %}
|
||||||
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided delimiter must be a string" unless delimiter.is_a? StringLiteral %}
|
||||||
{% if short %}
|
{% if short %}
|
||||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided short must be a string" unless short.is_a? StringLiteral %}
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided short must be a string" unless short.is_a? StringLiteral %}
|
||||||
{% end %}
|
{% end %}
|
||||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided long must be a string" unless long.is_a? StringLiteral || long == nil %}
|
{% if long %}
|
||||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : You must provide a short or long" unless long || short %}
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided long must be a string" unless long.is_a? StringLiteral %}
|
||||||
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided long must match --[a-zA-Z0-9-_]+" unless long =~ /^--[a-zA-Z0-9-_]+/ %}
|
||||||
|
{% else %}
|
||||||
|
{% long = "--#{name.downcase}" %}
|
||||||
|
{% end %}
|
||||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : You must provide a description" unless description %}
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : You must provide a description" unless description %}
|
||||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided description must be a String" unless description.is_a? StringLiteral %}
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided description must be a String" unless description.is_a? StringLiteral || description.is_a? StringInterpolation %}
|
||||||
{% unless on_match.nil? %}
|
{% unless on_match.nil? %}
|
||||||
{% puts on_match %}
|
{% puts "DEBUG : #{@type.name}.argument(#{name}) : OnMatch:\n\tid: #{on_match}\n\treturn_type: #{on_match.return_type}\n\tinput_vars: #{on_match.args}" if env("DEBUG")%}
|
||||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided on_match must be a Proc" unless on_match.is_a? ProcLiteral %}
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided on_match must be a Proc" unless on_match.is_a? ProcLiteral %}
|
||||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided on_match return type must be a Bool" unless on_match.return_type == Nil %}
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided on_match return type must be Nil" unless on_match.return_type.resolve == Nil %}
|
||||||
{% end %}
|
{% end %}
|
||||||
{% unless validation.nil? %}
|
{% unless validation.nil? %}
|
||||||
|
{% puts "DEBUG : #{@type.name}.argument(#{name}) : Validation:\n\tid: #{validation}\n\treturn_type: #{validation.return_type}\n\tinput_vars: #{validation.args}" if env("DEBUG")%}
|
||||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation must be a Proc" unless validation.is_a? ProcLiteral %}
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation must be a Proc" unless validation.is_a? ProcLiteral %}
|
||||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation return type must be a Bool" unless validation.return_type == Bool %}
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation return type must be a Bool" unless validation.return_type.resolve == Bool %}
|
||||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation provided validation must have an input variable" if validation.args.empty? %}
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation provided validation must have an input variable" if validation.args.empty? %}
|
||||||
{% arg = validation.args.first %}
|
{% arg = validation.args.first %}
|
||||||
{% unless arg.restriction == type %}
|
{% unless arg.restriction == type %}
|
||||||
{% example = "->(#{arg.name} : #{type}) : #{type} { #{validation.body} }" %}
|
{% example = "->(#{arg.name} : #{type}) : Bool { #{validation.body} }" %}
|
||||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation input value must be #{type}. EX: #{example}" %}
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation input value must be #{type}. EX: #{example}" %}
|
||||||
{% end %}
|
{% end %}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
{% if options %}
|
||||||
|
{% options = options.resolve if options.is_a? Path %}
|
||||||
|
{% if options.is_a? Call %}
|
||||||
|
{% elsif options.is_a? ArrayLiteral %}
|
||||||
|
{% else %}
|
||||||
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided options must be an ArrayLiteral or a runtime method call to retrieve data" %}
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
{% if format %}
|
||||||
|
{% format = format.resolve if format.is_a? Path %}
|
||||||
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided format must be a RegexLiteral" unless format.is_a? RegexLiteral %}
|
||||||
|
{% end %}
|
||||||
|
{% if type.resolve <= Array && ! allow_no_verification %}
|
||||||
|
{% elem = type.type_vars.first %}
|
||||||
|
{% unless elem == Int32 %}
|
||||||
|
{% if format.nil? && options.nil? %}
|
||||||
|
{% raise "ERROR : CliGen::Command.argument(#{name}) : When providing custom data types for Array(T) or using Array(String) you must provide a format or options for argument filtering so that parsing can be done deterministically" %}
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
|
||||||
@[CliGen::Argument(short: {{short}}, long: {{long}}, description: {{description}}, validation: {{validation}}, on_match: {{on_match}})]
|
{% if type.resolve < Array && ! options.nil? %}
|
||||||
|
@[CliGen::Argument(short: {{short}}, long: {{long}}, description: {{description}}, validation: {{validation}}, on_match: {{on_match}}, options: [{{options}}], delimiter: {{delimiter}}, format: {{format}})]
|
||||||
|
{% else %}
|
||||||
|
@[CliGen::Argument(short: {{short}}, long: {{long}}, description: {{description}}, validation: {{validation}}, on_match: {{on_match}}, options: {{options}}, delimiter: {{delimiter}}, format: {{format}})]
|
||||||
|
{% end %}
|
||||||
@{{variable}}
|
@{{variable}}
|
||||||
|
|
||||||
|
{% if def_getter %}
|
||||||
|
def {{variable.var}}
|
||||||
|
@{{name}}
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
{% if def_setter %}
|
||||||
def {{variable.var}}= (value : {{type}})
|
def {{variable.var}}= (value : {{type}})
|
||||||
{% unless validation.nil? %}
|
{% unless validation.nil? %}
|
||||||
raise "ERROR : #{@type.name}##{@def.name} : Provided value #{value} is not passing validation" unless {{validation}}.call(value)
|
raise CliGen::ValidationError.new("#{@type.name}##{@def.name} : Provided value #{value} failed validation") unless {{validation}}.call(value)
|
||||||
{% end %}
|
{% end %}
|
||||||
@{{name}} = value
|
@{{name}} = value
|
||||||
end
|
end
|
||||||
|
{% end %}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
module CliGen
|
||||||
|
class Command
|
||||||
|
macro def_init
|
||||||
|
def initialize
|
||||||
|
{% verbatim do %}
|
||||||
|
{% for var in @type.instance_vars %}
|
||||||
|
{% raise "ERROR : Can't define a default initializer if #{var.name} doesn't have a default" if var.default_value.nil? && !var.type.nilable? %}
|
||||||
|
@{{var.name}} = {{var.default_value}}
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
def after_initialize
|
||||||
|
@@instance = self
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get
|
||||||
|
@@instance ||= new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
module CliGen
|
||||||
|
class Command
|
||||||
|
macro define_command_initializer
|
||||||
|
def initialize(*, handler : CliGen::BaseCommandNode)
|
||||||
|
{% verbatim do %}
|
||||||
|
{% for var in @type.instance_vars %}
|
||||||
|
{% anno = (var.annotation(CliGen::Argument) || var.annotation(CliGen::Selection)) %}
|
||||||
|
{% if anno %}
|
||||||
|
{% raise "ERROR : #{@type.name}#initialize : Argument '#{var.name}' cannot be a nilable type (#{var.type}) — flags always resolve to a concrete value" if var.type.union? %}
|
||||||
|
if flg = handler.flags.find{|f| f.var == {{var.name.stringify}} && f.long == {{anno[:long]}}}
|
||||||
|
flg.validate!
|
||||||
|
@{{var.id}} = flg.as(CliGen::Flag({{var.type}})).value!
|
||||||
|
else
|
||||||
|
raise CliGen::FlagNotFoundError.new("{{@type.name}}\#{{@def.name}} : No flag found for \"{{var.name}}\"")
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
{% raise "ERROR : #{@type.name}#{@def.name} : Instance Variable(#{var.name}) is not handled by CliGen and does not have a default value" unless var.default_value %}
|
||||||
|
@{{var.id}} = {{var.default_value}}
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
{% if @type.has_method? :after_initialize %}
|
||||||
|
after_initialize
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
module CliGen
|
||||||
|
class Command
|
||||||
|
macro help_template(filepath)
|
||||||
|
{% raise "ERROR : CliGen::Command.help_template : #{filepath} does not exist" unless file_exists? filepath %}
|
||||||
|
HELP_TEMPLATE = {{`readlink -f #{filepath}`.strip.stringify}}
|
||||||
|
{% puts "DEBUG : #{@type.name} : Set HELP_TEMPLATE to #{filepath}" if env("DEBUG") %}
|
||||||
|
{% debug if env("DEBUG") %}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,14 +1,21 @@
|
|||||||
module CliGen
|
module CliGen
|
||||||
class Command
|
class Command
|
||||||
macro selection(variable, short, long, description, options)
|
macro selection(variable, description, options, short = nil, long = nil, validation = nil, on_match = nil)
|
||||||
{% raise "ERROR : CliGen::Command.selection : First selection must be a TypeDeclaration (ex: '<var> : <type> [= val]')" unless variable.is_a? TypeDeclaration %}
|
{% raise "ERROR : CliGen::Command.selection : First selection must be a TypeDeclaration (ex: '<var> : <type> [= val]')" unless variable.is_a? TypeDeclaration %}
|
||||||
{% raise "ERROR : CliGen::Command.selection : Provided short must be a string" unless short.is_a? StringLiteral || string == nil %}
|
{% if short %}
|
||||||
{% raise "ERROR : CliGen::Command.selection : Provided long must be a string" unless long.is_a? StringLiteral || long == nil %}
|
{% raise "ERROR : CliGen::Command.selection : Provided short must be a string" unless short.is_a? StringLiteral %}
|
||||||
|
{% end %}
|
||||||
|
{% long = "--#{variable.var}" if long.nil? %}
|
||||||
|
{% raise "ERROR : CliGen::Command.selection : Provided long must be a flag format" unless long =~ /^--[a-zA-Z0-9-_]+/ %}
|
||||||
|
{% raise "ERROR : CliGen::Command.selection : Provided long must be a string" unless long.is_a? StringLiteral %}
|
||||||
{% raise "ERROR : CliGen::Command.selection : You must provide a short or long" unless long || short %}
|
{% raise "ERROR : CliGen::Command.selection : You must provide a short or long" unless long || short %}
|
||||||
{% raise "ERROR : CliGen::Command.selection : You must provide a description" unless description %}
|
{% raise "ERROR : CliGen::Command.selection : You must provide a description" unless description %}
|
||||||
{% raise "ERROR : CliGen::Command.selection : Provided description must be a String" unless description.is_a? StringLiteral %}
|
{% raise "ERROR : CliGen::Command.selection : Provided description must be a String" unless description.is_a? StringLiteral %}
|
||||||
{% raise "ERROR : CliGen::Command.selection : You must provide options" unless options %}
|
{% options = options.resolve if options.is_a? Path %}
|
||||||
{% raise "ERROR : CliGen::Command.selection : Provided options must be " unless options %}
|
{% raise "ERROR : CliGen::Command.selection : Provided options must be an ArrayLiteral" unless options.is_a? ArrayLiteral %}
|
||||||
|
|
||||||
|
@[CliGen::Argument(short: {{short}}, long: {{long}}, description: {{description}}, validation: {{validation}}, on_match: {{on_match}}, options: {{options}}, delimiter: "-")]
|
||||||
|
@{{variable}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
module CliGen
|
|
||||||
class Command
|
|
||||||
macro trigger(short, long, argument = nil, &on_match)
|
|
||||||
{% raise "ERROR : CliGen::Command.trigger : Provided short must be a string" unless short.is_a? StringLiteral %}
|
|
||||||
{% raise "ERROR : CliGen::Command.trigger : Provided long must be a string" unless long.is_a? StringLiteral %}
|
|
||||||
{% raise "ERROR : CliGen::Command.trigger : Must provide a block for on_match trigger" unless on_match %}
|
|
||||||
{% name = long.gsub(/--/, "") %}
|
|
||||||
|
|
||||||
@[CliGen::Trigger(short: {{short}}, long: {{long}}, argument: {{argument}})]
|
|
||||||
{% if argument %}
|
|
||||||
def self.__cligen_trigger__{{name}}__({{name}} : {{argument}}) : Nil
|
|
||||||
{{on_match.body}}
|
|
||||||
end
|
|
||||||
{% else %}
|
|
||||||
def self.__cligen_trigger__{{name}}__ : Nil
|
|
||||||
{{on_match.body}}
|
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
+93
-26
@@ -33,10 +33,6 @@ module CliGen
|
|||||||
@flags = (flags + CliGen::GLOBAL_FLAGS + @commands.flat_map(&.flags)).uniq
|
@flags = (flags + CliGen::GLOBAL_FLAGS + @commands.flat_map(&.flags)).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
def help : String
|
|
||||||
ECR.render("src/cligen/template/cmd_help.ecr")
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_for_duplicates!(flags : Array(BaseFlag)) : Nil
|
def check_for_duplicates!(flags : Array(BaseFlag)) : Nil
|
||||||
shorts = flags.compact_map(&.short)
|
shorts = flags.compact_map(&.short)
|
||||||
short_duplicates = [] of String
|
short_duplicates = [] of String
|
||||||
@@ -67,18 +63,31 @@ module CliGen
|
|||||||
message += "\nShort:\n%s" % short_duplicates.map { |f| "- #{f}" }.join("\n")
|
message += "\nShort:\n%s" % short_duplicates.map { |f| "- #{f}" }.join("\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
raise error_buffer % [@name, message]
|
raise CliGen::DuplicateFlagError.new(error_buffer % [@name, message])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(flag_long : String) : BaseFlag?
|
def get(*, long : String) : BaseFlag?
|
||||||
@flags.find{|f| f.long_key == flag_long}
|
@flags.find{|f| f.long_key == long}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(*, short : String) : BaseFlag?
|
||||||
|
@flags.find{|f| f.short == short}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_flag_raises(&) : Nil
|
||||||
|
begin
|
||||||
|
yield
|
||||||
|
rescue e : CliGen::RuntimeError
|
||||||
|
abort e.message
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_match(arg : String)
|
def find_match(arg : String)
|
||||||
if subcommand?(arg)
|
if subcommand?(arg)
|
||||||
return CliGen::MatchType::SubCommand
|
return CliGen::MatchType::SubCommand
|
||||||
end
|
end
|
||||||
|
|
||||||
case arg
|
case arg
|
||||||
when "-h", "--help"
|
when "-h", "--help"
|
||||||
CliGen::MatchType::Help
|
CliGen::MatchType::Help
|
||||||
@@ -90,8 +99,6 @@ module CliGen
|
|||||||
end
|
end
|
||||||
when CliGen::Regex::FLAG_WITH_ARG
|
when CliGen::Regex::FLAG_WITH_ARG
|
||||||
CliGen::MatchType::FlagWithArg
|
CliGen::MatchType::FlagWithArg
|
||||||
when CliGen::Regex::SHORT_WITH_INLINE_ARG
|
|
||||||
CliGen::MatchType::ShortWithInlineArg
|
|
||||||
when CliGen::Regex::FLAG_MULTIPLE_SHORT
|
when CliGen::Regex::FLAG_MULTIPLE_SHORT
|
||||||
CliGen::MatchType::FlagMultipleShort
|
CliGen::MatchType::FlagMultipleShort
|
||||||
else
|
else
|
||||||
@@ -111,6 +118,10 @@ module CliGen
|
|||||||
subcommands.any?{|f| f.name == arg}
|
subcommands.any?{|f| f.name == arg}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def flag?(arg : String) : Bool
|
||||||
|
@flags.any?(&.matches?(arg))
|
||||||
|
end
|
||||||
|
|
||||||
# Converts String array to Arg array and hands off to the typed process method
|
# Converts String array to Arg array and hands off to the typed process method
|
||||||
def process(args : Array(String)) : Nil
|
def process(args : Array(String)) : Nil
|
||||||
new_args = args.each_with_index.map { |arg, i| CliGen::Arg.new(value: arg, index: i) }.to_a
|
new_args = args.each_with_index.map { |arg, i| CliGen::Arg.new(value: arg, index: i) }.to_a
|
||||||
@@ -143,12 +154,35 @@ module CliGen
|
|||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def verbose? : Bool
|
||||||
|
@verbose_flag ||= get(long: "--verbose").not_nil!.as(Flag(Bool))
|
||||||
|
@verbose_flag.not_nil!.value!
|
||||||
|
end
|
||||||
|
|
||||||
|
def help : String
|
||||||
|
{% begin %}
|
||||||
|
{% if T.has_constant? "HELP_TEMPLATE" %}
|
||||||
|
{% puts "#{T} was found to have HELP_TEMPLATE defined using this instead" if env("DEBUG") %}
|
||||||
|
ECR.render({{T.constant("HELP_TEMPLATE")}})
|
||||||
|
{% elsif CliGen.has_constant? "HELP_OVERRIDE_TEMPLATE" %} # If we have a global override use it
|
||||||
|
{% puts "Global override found. Using" if env("DEBUG") %}
|
||||||
|
ECR.render({{CliGen::HELP_OVERRIDE_TEMPLATE}})
|
||||||
|
{% else %}
|
||||||
|
{% puts "No type overrided help output. Using default" if env("DEBUG") %}
|
||||||
|
ECR.render("lib/cligen/src/cligen/template/cmd_help.ecr")
|
||||||
|
{% end %} # otherwise
|
||||||
|
{% debug if env("DEBUG") %}
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
def check! : Nil
|
def check! : Nil
|
||||||
@flags.each(&.check!)
|
@flags.each(&.check!)
|
||||||
check_for_duplicates!(@flags)
|
check_for_duplicates!(@flags)
|
||||||
@commands.each(&.check!)
|
@commands.each(&.check!)
|
||||||
raise "ERROR : CommandNode({{T}})#check! : {{T}} has no subcommands and no #main defined" \
|
{% unless T == Nil %}
|
||||||
|
raise CliGen::MissingDispatchError.new("CommandNode({{T}})#check! : {{T}} has no subcommands and no #main defined") \
|
||||||
if subcommands.empty? && !{{T.has_method?(:main)}}
|
if subcommands.empty? && !{{T.has_method?(:main)}}
|
||||||
|
{% end %}
|
||||||
end
|
end
|
||||||
|
|
||||||
def process(args : Array(CliGen::Arg)) : Nil
|
def process(args : Array(CliGen::Arg)) : Nil
|
||||||
@@ -170,55 +204,80 @@ module CliGen
|
|||||||
{% for cls in CliGen::Command.subclasses %}
|
{% for cls in CliGen::Command.subclasses %}
|
||||||
when CliGen::CommandNode({{cls.name}})
|
when CliGen::CommandNode({{cls.name}})
|
||||||
match.as(CommandNode({{cls}})).process(args.reject(&.processed?))
|
match.as(CommandNode({{cls}})).process(args.reject(&.processed?))
|
||||||
|
exit 0
|
||||||
{% end %}
|
{% end %}
|
||||||
else
|
else
|
||||||
raise "ERROR : Couldn't find thing"
|
raise CliGen::UnknownCommandNodeError.new("CommandNode({{T}})#process : matched a BaseCommandNode that isn't a known CommandNode(T)")
|
||||||
end
|
end
|
||||||
{% end %}
|
{% end %}
|
||||||
passed_execution = true
|
passed_execution = true
|
||||||
|
|
||||||
when BaseFlag
|
when BaseFlag
|
||||||
|
# Ensuring we catch any exceptions to have them abort with the
|
||||||
|
# error message if caught
|
||||||
|
handle_flag_raises do
|
||||||
if match.requires_arg?
|
if match.requires_arg?
|
||||||
match.process(args.reject(&.processed?))
|
match.process(args.reject(&.processed?).take_while{|v| find_match(v.value) == CliGen::MatchType::NoMatch})
|
||||||
else
|
else
|
||||||
match.process
|
match.process
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
when MatchType::SubCommand
|
when MatchType::SubCommand
|
||||||
raise "ERROR : CommandNode({{T}})#process : Subcommand(#{matched_subcommand}) was already matched" if matched_subcommand
|
raise CliGen::InternalError.new("CommandNode({{T}})#process : subcommand '#{matched_subcommand}' was already matched — duplicate subcommand token") if matched_subcommand
|
||||||
matched_subcommand = arg.value
|
matched_subcommand = arg.value
|
||||||
|
|
||||||
when MatchType::Help
|
when MatchType::Help
|
||||||
abort help
|
raise CliGen::HelpRequestedError.new(help)
|
||||||
|
|
||||||
when MatchType::FlagWithArg
|
when MatchType::FlagWithArg
|
||||||
if regex_match = CliGen::Regex::FLAG_WITH_ARG.match(arg.value)
|
if regex_match = CliGen::Regex::FLAG_WITH_ARG.match(arg.value)
|
||||||
case flag_match = find_match(regex_match["flag"])
|
case flag_match = find_match(regex_match["flag"])
|
||||||
when BaseFlag
|
when BaseFlag
|
||||||
|
handle_flag_raises do
|
||||||
flag_match.process([CliGen::Arg.new(value: regex_match["arg"], index: arg.index)])
|
flag_match.process([CliGen::Arg.new(value: regex_match["arg"], index: arg.index)])
|
||||||
else
|
|
||||||
raise "ERROR : CommandNode(#{@name}).process : No flag matched '#{regex_match["flag"]}'"
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
raise "Oh good, you broke regex. How the hell did it match in find_match but not above? What the hell is going on"
|
raise CliGen::UnknownCommandNodeError.new("CommandNode(#{@name}).process : No flag matched '#{regex_match["flag"]}'")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise CliGen::RegexInvariantError.new("CommandNode(#{@name}).process : FLAG_WITH_ARG matched in find_match but failed on re-match — this is a framework bug")
|
||||||
end
|
end
|
||||||
|
|
||||||
when MatchType::ShortWithInlineArg
|
|
||||||
abort "#{CliGen::APPNAME}: invalid flag '#{arg.value}' — inline values are not supported. Did you mean '#{arg.value[0..1]} #{arg.value[2..]}' ?"
|
|
||||||
|
|
||||||
when MatchType::FlagMultipleShort
|
when MatchType::FlagMultipleShort
|
||||||
arg.value.gsub(/^-/, "").chars.map { |c| "-#{c}" }.each do |flag|
|
val = arg.value.gsub(/^-/,"")
|
||||||
|
|
||||||
|
# If the next character in the series is a flag assume the remaining are flags as well.
|
||||||
|
if flag?("-#{val[1]}")
|
||||||
|
chars = val.chars
|
||||||
|
chars.map { |c| "-#{c}" }.each_with_index do |flag, index|
|
||||||
case match = find_match(flag)
|
case match = find_match(flag)
|
||||||
when BaseFlag
|
when BaseFlag
|
||||||
abort "#{CliGen::APPNAME}: cannot bundle flag that requires an argument: #{flag}" if match.requires_arg?
|
# If this is the last flag in the series let it process others
|
||||||
|
handle_flag_raises do
|
||||||
|
## If this is the last flag provided in the clump do the thing
|
||||||
|
if index == chars.size - 1
|
||||||
|
if match.requires_arg?
|
||||||
|
match.process(args.reject(&.processed?).take_while{|v| find_match(v.value) == CliGen::MatchType::NoMatch})
|
||||||
|
else
|
||||||
match.process
|
match.process
|
||||||
when MatchType::NoMatch
|
|
||||||
raise "ERROR : CommandNode(#{@name}).process : No flag match for '#{flag}'"
|
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
raise CliGen::FlagBundleError.new("#{CliGen::APPNAME}: cannot bundle '#{flag}' — it requires an argument") if match.requires_arg?
|
||||||
|
match.process
|
||||||
|
end
|
||||||
|
end
|
||||||
|
when MatchType::NoMatch
|
||||||
|
raise CliGen::UnknownFlagError.new("#{CliGen::APPNAME}: unknown flag '#{flag}'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# In this case we assume the characters following the flag is the argument
|
||||||
|
raise CliGen::FlagArgumentError.new("#{CliGen::APPNAME}: inline flag arguments are not supported — did you mean '-#{val[0]} #{val[1..]}'?")
|
||||||
end
|
end
|
||||||
|
|
||||||
when MatchType::NoMatch
|
when MatchType::NoMatch
|
||||||
raise "ERROR : CommandNode(#{@name}).process : No command, subcommand or flag match for '#{arg.value}'"
|
raise CliGen::HelpRequestedError.new("#{CliGen::APPNAME}: unknown token '#{arg.value}'\n\n#{help}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -227,6 +286,9 @@ module CliGen
|
|||||||
{% unless T == Nil %}
|
{% unless T == Nil %}
|
||||||
unless passed_execution
|
unless passed_execution
|
||||||
cls = T.new(handler: self.as(CliGen::BaseCommandNode))
|
cls = T.new(handler: self.as(CliGen::BaseCommandNode))
|
||||||
|
{% for cmd in T.methods.select(&.annotation(CliGen::PreRunCommand)) %}
|
||||||
|
cls.{{cmd.name}}
|
||||||
|
{% end %}
|
||||||
{% subcmds = T.methods.select(&.annotation(CliGen::SubCommand)) %}
|
{% subcmds = T.methods.select(&.annotation(CliGen::SubCommand)) %}
|
||||||
{% begin %}
|
{% begin %}
|
||||||
case matched_subcommand
|
case matched_subcommand
|
||||||
@@ -238,11 +300,16 @@ module CliGen
|
|||||||
{% if T.has_method?(:main) %}
|
{% if T.has_method?(:main) %}
|
||||||
cls.main
|
cls.main
|
||||||
{% else %}
|
{% else %}
|
||||||
raise "ERROR : CommandNode({{T}})#{{@def.name}} : No subcommand matched and no #main defined"
|
puts "ERROR : CommandNode({{T}})\#{{@def.name}} : No subcommand matched and no #main defined"
|
||||||
|
puts help
|
||||||
{% end %}
|
{% end %}
|
||||||
|
exit 0
|
||||||
end
|
end
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
{% else %}
|
||||||
|
puts help
|
||||||
|
exit 0
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
module CliGen
|
||||||
|
# Base for all CliGen exceptions
|
||||||
|
class Error < Exception; end
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Internal errors — framework invariant violations, should never reach users
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class InternalError < Error; end
|
||||||
|
|
||||||
|
# Arg#processed was called a second time on the same Arg
|
||||||
|
class ArgReprocessedError < InternalError; end
|
||||||
|
|
||||||
|
# A token matched FLAG_WITH_ARG in find_match but the regex failed on re-match
|
||||||
|
class RegexInvariantError < InternalError; end
|
||||||
|
|
||||||
|
# A BaseCommandNode was matched but couldn't be cast to any known CommandNode(T)
|
||||||
|
class UnknownCommandNodeError < InternalError; end
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Configuration errors — shard consumer wired something up incorrectly
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ConfigurationError < Error; end
|
||||||
|
|
||||||
|
# -h or --help was used as a flag short/long (reserved for internal help)
|
||||||
|
class ReservedFlagError < ConfigurationError; end
|
||||||
|
|
||||||
|
# Duplicate short or long flags detected during check!
|
||||||
|
class DuplicateFlagError < ConfigurationError; end
|
||||||
|
|
||||||
|
# A CommandNode(T) has no subcommands and no #main defined
|
||||||
|
class MissingDispatchError < ConfigurationError; end
|
||||||
|
|
||||||
|
# No flag was found in the handler for a Command ivar during initialize
|
||||||
|
class FlagNotFoundError < ConfigurationError; end
|
||||||
|
|
||||||
|
# A flag that requires an argument was processed with an empty argv
|
||||||
|
class FlagMissingArgumentError < ConfigurationError; end
|
||||||
|
|
||||||
|
# A Parsable type's parse_args did not mark any args as processed
|
||||||
|
class ParseableInvariantError < ConfigurationError; end
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Runtime errors — bad user input at the CLI level
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class RuntimeError < Error; end
|
||||||
|
|
||||||
|
# A required flag was not provided and has no env var or default to fall back on
|
||||||
|
class MissingRequiredFlagError < RuntimeError; end
|
||||||
|
|
||||||
|
# A setter's validation proc rejected the provided value
|
||||||
|
class ValidationError < RuntimeError; end
|
||||||
|
|
||||||
|
# A flag token was provided where a value argument was expected
|
||||||
|
class FlagArgumentError < RuntimeError; end
|
||||||
|
|
||||||
|
# A provided value doesn't satisfy type or format requirements (wrong type, bad format, invalid bool/date string)
|
||||||
|
class InvalidFlagValueError < RuntimeError; end
|
||||||
|
|
||||||
|
# A provided value is not in the flag's allowed options list
|
||||||
|
class InvalidOptionError < RuntimeError; end
|
||||||
|
|
||||||
|
# An unrecognised flag token was encountered during parsing
|
||||||
|
class UnknownFlagError < RuntimeError; end
|
||||||
|
|
||||||
|
# A flag that requires an argument was included in a short-flag bundle
|
||||||
|
class FlagBundleError < RuntimeError; end
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Help signal — not an error; exit 0 after printing
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Raised when -h/--help is matched; carries the rendered help string
|
||||||
|
class HelpRequestedError < Error; end
|
||||||
|
end
|
||||||
+200
-42
@@ -1,6 +1,14 @@
|
|||||||
require "./arg"
|
require "./arg"
|
||||||
|
|
||||||
module CliGen
|
module CliGen
|
||||||
|
# To be able to store metadata for use in the help output
|
||||||
|
record FlagMeta,
|
||||||
|
type : String,
|
||||||
|
array : Bool,
|
||||||
|
format : String?,
|
||||||
|
default : String,
|
||||||
|
options : Array(String)?
|
||||||
|
|
||||||
abstract class BaseFlag
|
abstract class BaseFlag
|
||||||
getter var : String
|
getter var : String
|
||||||
getter short : String?
|
getter short : String?
|
||||||
@@ -8,17 +16,21 @@ module CliGen
|
|||||||
getter long_key : String
|
getter long_key : String
|
||||||
getter env_var : String
|
getter env_var : String
|
||||||
getter description : String
|
getter description : String
|
||||||
|
getter delimiter : String
|
||||||
|
getter meta : FlagMeta
|
||||||
|
|
||||||
def initialize(
|
def initialize(
|
||||||
@var : String,
|
@var : String,
|
||||||
@short : String?,
|
@short : String?,
|
||||||
@long : String,
|
@long : String,
|
||||||
@env_var : String,
|
@env_var : String,
|
||||||
@description : String
|
@description : String,
|
||||||
|
@delimiter : String,
|
||||||
|
@meta : FlagMeta
|
||||||
)
|
)
|
||||||
# if the user provides just a "--long" I want the @long_key to match it
|
# if the user provides just a "--long" I want the @long_key to match it
|
||||||
if @long.includes?(" ")
|
if @long =~ /\s|=/
|
||||||
@long_key = @long.split(" ").first
|
@long_key = @long.split(/\s|=/).first
|
||||||
else
|
else
|
||||||
@long_key = @long
|
@long_key = @long
|
||||||
end
|
end
|
||||||
@@ -40,6 +52,7 @@ module CliGen
|
|||||||
@options : Array(T)?
|
@options : Array(T)?
|
||||||
@validate : (T -> Bool)?
|
@validate : (T -> Bool)?
|
||||||
@on_match : Proc(Nil)?
|
@on_match : Proc(Nil)?
|
||||||
|
@format : ::Regex?
|
||||||
|
|
||||||
def initialize(
|
def initialize(
|
||||||
var : String,
|
var : String,
|
||||||
@@ -47,12 +60,37 @@ module CliGen
|
|||||||
long : String,
|
long : String,
|
||||||
env_var : String,
|
env_var : String,
|
||||||
description : String,
|
description : String,
|
||||||
|
delimiter : String = ",",
|
||||||
@default : T? = nil,
|
@default : T? = nil,
|
||||||
@options : Array(T)? = nil,
|
@options : Array(T)? = nil,
|
||||||
@validate : (T -> Bool)? = nil,
|
@validate : (T -> Bool)? = nil,
|
||||||
@on_match : Proc(Nil)? = nil
|
@on_match : Proc(Nil)? = nil,
|
||||||
|
@format : ::Regex? = nil
|
||||||
)
|
)
|
||||||
super(var, short, long, env_var, description)
|
{% unless T.class.has_method? :to_s %}
|
||||||
|
{% raise "ERROR : Flag(#{T}) : Error your flag type must have a to_s method" %}
|
||||||
|
{% end %}
|
||||||
|
{% if T < Array %}
|
||||||
|
{% elem = T.type_vars.first %}
|
||||||
|
{% raise "ERROR : Flag(#{T}) : Error your flag subtype #{elem} must have a to_s method" unless elem.class.has_method? :to_s %}
|
||||||
|
{% end %}
|
||||||
|
unless @default.nil?
|
||||||
|
{% if T < Array %}
|
||||||
|
default = "[" + @default.not_nil!.map(&.to_s).join(", ") + "]"
|
||||||
|
{% else %}
|
||||||
|
default = @default.not_nil!.to_s
|
||||||
|
{% end %}
|
||||||
|
else
|
||||||
|
default = ""
|
||||||
|
end
|
||||||
|
meta = FlagMeta.new(
|
||||||
|
type: {{T.stringify}},
|
||||||
|
array: {{ T < Array ? true : false }},
|
||||||
|
options: {% if T < Array %} @options.try(&.first.map(&.to_s)) {% else %} @options.try(&.map(&.to_s)) {% end %},
|
||||||
|
default: default,
|
||||||
|
format: @format.try(&.source)
|
||||||
|
)
|
||||||
|
super(var, short, long, env_var, description, delimiter, meta)
|
||||||
end
|
end
|
||||||
|
|
||||||
def requires_arg? : Bool
|
def requires_arg? : Bool
|
||||||
@@ -61,49 +99,114 @@ module CliGen
|
|||||||
|
|
||||||
def process(argv : Array(Arg) = [] of Arg) : Nil
|
def process(argv : Array(Arg) = [] of Arg) : Nil
|
||||||
if requires_arg?
|
if requires_arg?
|
||||||
raise "ERROR : Flag({{T}}) : Array requires an argument but provided array is empty" if argv.empty?
|
raise CliGen::FlagMissingArgumentError.new("Flag(#{T}, long: #{@long_key}) : requires an argument but provided array is empty") if argv.empty?
|
||||||
|
raise CliGen::FlagArgumentError.new("Flag(#{T}, long: #{@long_key}) : a flag token was provided where a value was expected (got: #{argv.first.value})") if argv.first.flag?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
{% if T == Bool %}
|
{% if T == Bool %}
|
||||||
@value = true
|
@value = true
|
||||||
{% elsif T <= Array %}
|
{% elsif T < Array %}
|
||||||
{% raise "ERROR : Flag(#{T}) : You cannot define multiple types of array entries" if T.type_vars.size > 1 %}
|
{% raise "ERROR : Flag(#{T}) : You cannot define multiple types of array entries" if T.type_vars.size > 1 %}
|
||||||
{% elem = T.type_vars.first %}
|
{% elem = T.type_vars.first %}
|
||||||
argv.each do |arg|
|
argv.each do |arg|
|
||||||
break if arg.value.starts_with('-')
|
break if arg.flag?
|
||||||
{% if elem == Int32 %}
|
unless @format.nil?
|
||||||
(@value ||= [] of Int32) << arg.value.to_i
|
unless arg.value.includes?(@delimiter)
|
||||||
{% elsif elem == String %}
|
unless arg.value =~ @format
|
||||||
(@value ||= [] of String) << arg.value
|
puts "DEBUG : Flag({{T}}, long: #{@long_key}) : #{arg.value} was not found to be matching the defined filter #{@format}. So breaking from parse loop" if ENV["DEBUG"]?
|
||||||
{% elsif elem < CliGen::Coercable %}
|
break
|
||||||
if arg.value.includes?(",")
|
end
|
||||||
(@value ||= [] of {{elem}}) += arg.value.split(",").map{|i| {{elem}}.coerce(i)}
|
end
|
||||||
|
end
|
||||||
|
{% if elem < Int %}
|
||||||
|
{% int_case = elem.stringify =~ /^UInt/ ? "uint?".id : "int?".id %}
|
||||||
|
if arg.value.includes?(@delimiter)
|
||||||
|
@value = (@value || T.new) + arg.value.split(@delimiter).map do |val|
|
||||||
|
val = val.strip
|
||||||
|
unless arg.{{int_case}}(val)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{val}' is not a valid {{elem}}")
|
||||||
|
end
|
||||||
|
{{elem}}.new(val)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
(@value ||= [] of {{elem}}) << {{elem}}.coerce(arg.value)
|
unless arg.{{int_case}}
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{arg.value}' is not a valid {{T}}")
|
||||||
|
end
|
||||||
|
(@value ||= T.new) << {{elem}}.new(arg.value)
|
||||||
|
end
|
||||||
|
{% elsif elem < Float %}
|
||||||
|
if arg.value.includes?(@delimiter)
|
||||||
|
@value = (@value || T.new) + arg.value.split(@delimiter).map do |val|
|
||||||
|
val = val.strip
|
||||||
|
unless CliGen::Arg.float?(val)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{val}' is not a valid {{T}}")
|
||||||
|
end
|
||||||
|
{{elem}}.new(val)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
unless arg.float?
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{arg.value}' is not a float")
|
||||||
|
end
|
||||||
|
(@value ||= T.new) << {{elem}}.new(arg.value)
|
||||||
|
end
|
||||||
|
{% elsif elem == String %}
|
||||||
|
if arg.value.includes?(@delimiter)
|
||||||
|
@value = (@value || [] of String) + arg.value.split(@delimiter).map { |v|
|
||||||
|
unless @format.nil?
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{v}' does not match required format /#{@format.not_nil!.source}/") unless v =~ @format
|
||||||
|
end
|
||||||
|
v
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if ! @format.nil? && arg.value !~ @format
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{arg.value}' does not match required format /#{@format.not_nil!.source}/")
|
||||||
|
end
|
||||||
|
(@value ||= [] of String) << arg.value
|
||||||
|
end
|
||||||
|
{% elsif elem.class < CliGen::Coercable %}
|
||||||
|
if arg.value.includes?(@delimiter)
|
||||||
|
@value = (@value || [] of {{elem}}) + arg.value.split(@delimiter).map { |i| {{elem}}.coerce(i) }
|
||||||
|
else
|
||||||
|
@value = (@value || [] of {{elem}}) + [({{elem}}.coerce(arg.value))]
|
||||||
end
|
end
|
||||||
{% else %}
|
{% else %}
|
||||||
{% raise "ERROR : Flag(#{T}) : #{elem} is not a coercable type. If you wish to coerce it from a bare string include CliGen::Coercable & implement the class method" %}
|
{% raise "ERROR : Flag(#{T}) : #{elem} is not a coercable type. If you wish to coerce it from a bare string extend with CliGen::Coercable & implement the class method" %}
|
||||||
{% end %}
|
{% end %}
|
||||||
arg.processed
|
arg.processed
|
||||||
end
|
end
|
||||||
{% elsif T == Int32 %}
|
{% elsif T < Int %}
|
||||||
@value = argv.first.value.to_i
|
{% int_case = T.stringify =~ /^UInt/ ? "uint?".id : "int?".id %}
|
||||||
|
unless argv.first.{{int_case}}
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag(#{T}, long: #{@long_key}) : '#{argv.first.value}' is not a valid {{T}}")
|
||||||
|
end
|
||||||
|
@value = T.new(argv.first.value)
|
||||||
|
argv.first.processed
|
||||||
|
{% elsif T < Float %}
|
||||||
|
unless argv.first.float?
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag(#{T}, long: #{@long_key}) : '#{argv.first.value}' is not a valid {{T}}")
|
||||||
|
end
|
||||||
|
@value = T.new(argv.first.value)
|
||||||
argv.first.processed
|
argv.first.processed
|
||||||
{% elsif T == Time %}
|
{% elsif T == Time %}
|
||||||
@value = parse_time(argv.first.value)
|
@value = parse_time(argv.first.value)
|
||||||
argv.first.processed
|
argv.first.processed
|
||||||
{% elsif T == String %} # String
|
{% elsif T == String %} # String
|
||||||
|
if ! @format.nil? && argv.first.value !~ @format
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag(#{T}, long: #{@long_key}) : '#{argv.first.value}' does not match required format /#{@format.not_nil!.source}/")
|
||||||
|
end
|
||||||
|
|
||||||
@value = argv.first.value
|
@value = argv.first.value
|
||||||
argv.first.processed
|
argv.first.processed
|
||||||
{% elsif T < CliGen::Parsable %}
|
{% elsif T.class < CliGen::Parsable %}
|
||||||
processed = argv.select(&.processed?)
|
processed = argv.select(&.processed?)
|
||||||
@value = T.parse_args(argv)
|
@value = T.parse_args(argv)
|
||||||
post_processed = argv.select(&.processed?)
|
post_processed = argv.select(&.processed?)
|
||||||
if processed == post_processed
|
if processed == post_processed
|
||||||
raise "ERROR : Flag({{T}}, long: #{@long_key})#process : Your {{T}}#process_args did not mark processed args as processed. Please check your code"
|
raise CliGen::ParseableInvariantError.new("Flag({{T}}, long: #{@long_key})#process : {{T}}#parse_args did not mark any args as processed")
|
||||||
end
|
end
|
||||||
{% else %}
|
{% else %}
|
||||||
{% raise "ERROR : Flag({{T}}, long: #{@long_key})#process : Generic Type #{T} is not supported. To add support you must include CliGen::Parsable & implement the class method" %}
|
{% raise "ERROR : Flag({{T}}#process : Generic Type #{T} is not supported. To add support you must extend with CliGen::Parsable & implement the class method" %}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
validate!
|
validate!
|
||||||
@@ -122,7 +225,7 @@ module CliGen
|
|||||||
|
|
||||||
v ||= @default
|
v ||= @default
|
||||||
|
|
||||||
raise "#{CliGen::APPNAME}: required flag #{@long_key} was not provided" if v.nil?
|
raise CliGen::MissingRequiredFlagError.new("#{CliGen::APPNAME}: required flag #{@long_key} was not provided") if v.nil?
|
||||||
v.not_nil!
|
v.not_nil!
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -147,50 +250,105 @@ module CliGen
|
|||||||
v = value!
|
v = value!
|
||||||
|
|
||||||
if opts = @options
|
if opts = @options
|
||||||
abort "#{CliGen::APPNAME}: '#{v}' is not a valid value for #{@long_key} (valid: #{opts.join(", ")})" unless opts.includes?(v)
|
{% if T < Array %}
|
||||||
|
v.each do |v2|
|
||||||
|
raise CliGen::InvalidOptionError.new("#{CliGen::APPNAME}: '#{v2}' is not a valid value for #{@long_key} (valid: #{opts.join(", ")})") unless opts.first.includes?(v2)
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
raise CliGen::InvalidOptionError.new("#{CliGen::APPNAME}: '#{v}' is not a valid value for #{@long_key} (valid: #{opts.join(", ")})") unless opts.includes?(v)
|
||||||
|
{% end %}
|
||||||
end
|
end
|
||||||
|
|
||||||
if check = @validate
|
if check = @validate
|
||||||
abort "#{CliGen::APPNAME}: validation failed for #{@long_key}" unless check.call(v)
|
raise CliGen::ValidationError.new("#{CliGen::APPNAME}: validation failed for #{@long_key} (got: #{v})") unless check.call(v)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def check! : Nil
|
def check! : Nil
|
||||||
raise "ERROR : Flag({{T}}, long: #{@long})#check! : -h is reserved for internal help usage" if @short == "-h"
|
raise CliGen::ReservedFlagError.new("Flag({{T}}, long: #{@long})#check! : -h is reserved for internal help") if @short == "-h"
|
||||||
raise "ERROR : Flag({{T}}, long: #{@long})#check! : --help is reserved for internal help usage" if @long_key == "--help"
|
raise CliGen::ReservedFlagError.new("Flag({{T}}, long: #{@long})#check! : --help is reserved for internal help") if @long_key == "--help"
|
||||||
end
|
end
|
||||||
|
|
||||||
private def coerce(raw : String) : T
|
private def coerce(raw : String) : T
|
||||||
{% if T == Bool %}
|
{% if T == Bool %}
|
||||||
case raw
|
case raw
|
||||||
when "t","true","1"
|
when /^(t|true|y|yes|1)$/i
|
||||||
true
|
true
|
||||||
when "f","false","0"
|
when /^(f|false|n|no|0)$/i
|
||||||
false
|
false
|
||||||
else
|
else
|
||||||
raise "ERROR"
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{raw}' is not a valid boolean (expected: t/f/true/false/y/n/yes/no/1/0)")
|
||||||
end
|
end
|
||||||
{% elsif T == Int32 %}
|
{% elsif T < Int %}
|
||||||
raw.to_i
|
{% int_case = T.stringify =~ /^UInt/ ? "uint?".id : "int?".id %}
|
||||||
|
unless CliGen::Arg.{{int_case}}(raw)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{raw}' is not a valid {{T}}")
|
||||||
|
end
|
||||||
|
T.new(raw)
|
||||||
|
{% elsif T < Float %}
|
||||||
|
unless CliGen::Arg.float?(raw)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{raw}' is not a valid {{T}}")
|
||||||
|
end
|
||||||
|
T.new(raw)
|
||||||
{% elsif T == Time %}
|
{% elsif T == Time %}
|
||||||
parse_time(raw)
|
parse_time(raw)
|
||||||
{% elsif T <= Array %}
|
{% elsif T < Array %}
|
||||||
|
if raw.includes?(@delimiter)
|
||||||
{% elem = T.type_vars.first %}
|
{% elem = T.type_vars.first %}
|
||||||
{% if elem == Int32 %}
|
raw.split(@delimiter).map do |val|
|
||||||
raw.split(',').map(&.to_i)
|
unless @format.nil?
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{val}' does not match required format /#{@format.not_nil!.source}/") unless val =~ @format
|
||||||
|
end
|
||||||
|
|
||||||
|
{% if elem < Int %}
|
||||||
|
val = val.strip
|
||||||
|
{% int_case = elem.stringify =~ /^UInt/ ? "uint?".id : "int?".id %}
|
||||||
|
unless CliGen::Arg.{{int_case}}(val)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{val}' is not a valid {{elem}}")
|
||||||
|
end
|
||||||
|
{{elem}}.new(val)
|
||||||
|
{% elsif elem < Float %}
|
||||||
|
unless CliGen::Arg.float?(val)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{val}' is not a valid {{elem}}")
|
||||||
|
end
|
||||||
|
{{elem}}.new(val)
|
||||||
{% elsif elem == String %}
|
{% elsif elem == String %}
|
||||||
raw.split(',')
|
val
|
||||||
{% elsif elem < CliGen::Coercable %}
|
{% elsif elem.class < CliGen::Coercable %}
|
||||||
raw.split(',').map{|i| {{elem}}.coerce(i)}
|
{{elem}}.coerce(val)
|
||||||
{% else %}
|
{% else %}
|
||||||
{% raise "ERROR : Flag(#{T}) : #{elem} is not a coercable type. If you wish to coerce it from a bare string include CliGen::Coercable & implement the class method" %}
|
{% raise "ERROR : Flag(#{T}) : #{elem} is not a coercable type. If you wish to coerce it from a bare string extend CliGen::Coercable & implement the class method" %}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if ! @format.nil? && raw !~ @format
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{raw}' does not match required format /#{@format.not_nil!.source}/")
|
||||||
|
end
|
||||||
|
{% if elem < Int %}
|
||||||
|
{% int_case = elem.stringify =~ /^UInt/ ? "uint?".id : "int?".id %}
|
||||||
|
unless CliGen::Arg.{{int_case}}(raw)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{raw}' is not a valid {{elem}}")
|
||||||
|
end
|
||||||
|
[ {{elem}}.new(raw) ]
|
||||||
|
{% elsif elem < Float %}
|
||||||
|
unless CliGen::Arg.float?(raw)
|
||||||
|
raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{raw}' is not a valid {{elem}}")
|
||||||
|
end
|
||||||
|
[ {{elem}}.new(raw) ]
|
||||||
|
{% elsif elem == String %}
|
||||||
|
[ raw ]
|
||||||
|
{% elsif elem.class < CliGen::Coercable %}
|
||||||
|
[ {{elem}}.coerce(raw) ]
|
||||||
|
{% else %}
|
||||||
|
{% raise "ERROR : Flag(#{T}) : #{T} is not a coercable type. If you wish to coerce it from a bare string extend CliGen::Coercable & implement the class method" %}
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
{% elsif T == String %} # String
|
{% elsif T == String %} # String
|
||||||
raw
|
raw
|
||||||
{% elsif T < CliGen::Coercable %}
|
{% elsif T.class < CliGen::Coercable %}
|
||||||
T.coerce(raw)
|
T.coerce(raw)
|
||||||
{% else %}
|
{% else %}
|
||||||
{% raise "ERROR : Flag(#{T}) : #{T} is not a coercable type. If you wish to coerce it from a bare string include CliGen::Coercable & implement the class method" %}
|
{% raise "ERROR : Flag(#{T}) : #{T} is not a coercable type. If you wish to coerce it from a bare string extend CliGen::Coercable & implement the class method" %}
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -199,7 +357,7 @@ module CliGen
|
|||||||
CliGen::Regex::INPUT_DATETIME_REGEX,
|
CliGen::Regex::INPUT_DATETIME_REGEX,
|
||||||
CliGen::Regex::INPUT_DATE_REGEX
|
CliGen::Regex::INPUT_DATE_REGEX
|
||||||
]
|
]
|
||||||
abort "#{CliGen::APPNAME}: invalid date/time format '#{raw}' (expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)" unless formats.any? { |f| raw.match(f) }
|
raise CliGen::InvalidFlagValueError.new("#{CliGen::APPNAME}: invalid date/time format '#{raw}' (expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)") unless formats.any? { |f| raw.match(f) }
|
||||||
if raw.match(formats[0])
|
if raw.match(formats[0])
|
||||||
Time.parse_local(raw, CliGen::Format::INPUT_DATETIME_FORMAT)
|
Time.parse_local(raw, CliGen::Format::INPUT_DATETIME_FORMAT)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ require "./flag"
|
|||||||
module CliGen
|
module CliGen
|
||||||
GLOBAL_FLAGS = [] of BaseFlag
|
GLOBAL_FLAGS = [] of BaseFlag
|
||||||
|
|
||||||
|
GLOBAL_FLAGS << Flag(Bool).new(
|
||||||
|
var: "",
|
||||||
|
short: "-v",
|
||||||
|
long: "--verbose",
|
||||||
|
env_var: "VERBOSE",
|
||||||
|
default: false,
|
||||||
|
description: "Enable verbose output from program & help output"
|
||||||
|
)
|
||||||
|
|
||||||
macro add_global_flag(type, long, description, env_var = nil, short = nil, validation = nil, &on_match)
|
macro add_global_flag(type, long, description, env_var = nil, short = nil, validation = nil, &on_match)
|
||||||
{% raise "ERROR : CliGen.add_global_flag : type must be a TypeNode" unless type.is_a? TypeNode %}
|
{% raise "ERROR : CliGen.add_global_flag : type must be a TypeNode" unless type.is_a? TypeNode %}
|
||||||
{% raise "ERROR : CliGen.add_global_flag : long must begin a StringLiteral" unless long.is_a? StringLiteral %}
|
{% raise "ERROR : CliGen.add_global_flag : long must begin a StringLiteral" unless long.is_a? StringLiteral %}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module CliGen::Parseable
|
module CliGen::Parsable
|
||||||
abstract def parse_args(args : Array(CliGen::Arg)) : self
|
abstract def parse_args(args : Array(CliGen::Arg))
|
||||||
end
|
end
|
||||||
|
|||||||
+6
-3
@@ -1,9 +1,12 @@
|
|||||||
module CliGen::Regex
|
module CliGen::Regex
|
||||||
FLAG_REGEX=/^(-[a-zA-Z]|--[a-zA-Z-_]+)$/
|
FLAG_REGEX=/^(-[a-zA-Z]|--[a-zA-Z-_0-9]+)$/
|
||||||
FLAG_WITH_ARG=/^(?<flag>(-[a-zA-Z]|--[a-zA-Z-_]+))="?(?<arg>\S+?)"?$/
|
FLAG_WITH_ARG=/^(?<flag>(-[a-zA-Z]|--[a-zA-Z-_]+))="?(?<arg>\S+?)"?$/
|
||||||
FLAG_MULTIPLE_SHORT=/^-[a-zA-Z]+$/
|
FLAG_MULTIPLE_SHORT=/^-[a-zA-Z0-9]+/
|
||||||
SHORT_WITH_INLINE_ARG=/^-[a-zA-Z][a-zA-Z0-9]+$/
|
|
||||||
|
|
||||||
INPUT_DATETIME_REGEX = /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/
|
INPUT_DATETIME_REGEX = /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/
|
||||||
INPUT_DATE_REGEX = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/
|
INPUT_DATE_REGEX = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/
|
||||||
|
|
||||||
|
FLOAT = /^[-+]?[[:digit:]]+(\.[[:digit:]]+)?$/
|
||||||
|
UINT = /^[[:digit:]]+$/
|
||||||
|
INT = /^[-+]?[[:digit:]]+$/
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,21 +3,36 @@ Command: <%= @name %>
|
|||||||
Description: <%= @description %>
|
Description: <%= @description %>
|
||||||
<%- end -%>
|
<%- end -%>
|
||||||
|
|
||||||
|
<%- unless @flags.empty? -%>
|
||||||
|
<%- len = @flags.map{|f| f.short.nil? ? f.long.size : "#{f.short},#{f.long}".size}.max + 5 -%>
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
---------------------------------------------------------------
|
---------------------------------------------------------------
|
||||||
<%- @flags.each do |flag| -%>
|
<%- @flags.each do |flag| -%>
|
||||||
<%- unless flag.short.nil? -%>
|
<%- if flag.short.nil? -%>
|
||||||
<%= "%-15s %s" % ["#{flag.short.not_nil!.strip},#{flag.long.strip}", flag.description.strip] %>
|
<%- flags = [flag.long.strip] -%>
|
||||||
<%- else -%>
|
<%- else -%>
|
||||||
<%= "%-15s %s" % [flag.long.strip, flag.description.strip] %>
|
<%- flags = [flag.short,flag.long.strip] -%>
|
||||||
|
<%- end -%>
|
||||||
|
<%= "%-#{len}s %s" % [flags.join(","), flag.description.strip] %><%= flag.meta.options.nil? ? "" : " (valid: #{flag.meta.options.not_nil!.join(", ")})" %><%= flag.meta.default.empty? ? "" : " (default: #{flag.meta.default.not_nil!})" %>
|
||||||
|
<%- if verbose? -%>
|
||||||
|
<%= "%-#{len}s %s" % ["", "Type: #{flag.meta.type}"] %>
|
||||||
|
<%= "%-#{len}s %s" % ["", "ENV VAR: #{flag.env_var}"] %>
|
||||||
|
<%- unless flag.meta.format.nil? -%>
|
||||||
|
<%= "%-#{len}s %s" % ["", "Valid Format: #{flag.meta.format}"] %>
|
||||||
|
<%- end -%>
|
||||||
|
<%- if flag.meta.array -%>
|
||||||
|
<%= "%-#{len}s %s" % ["", "Delimiter: #{flag.delimiter}"] %>
|
||||||
|
<%- end -%>
|
||||||
<%- end -%>
|
<%- end -%>
|
||||||
|
|
||||||
|
<%- end -%>
|
||||||
<%- end -%>
|
<%- end -%>
|
||||||
<%- unless @commands.empty? -%>
|
<%- unless @commands.empty? -%>
|
||||||
Other Commands
|
Other Commands
|
||||||
---------------------------------------------------------------
|
---------------------------------------------------------------
|
||||||
<%- @commands.each do |command| -%>
|
<%- @commands.each do |command| -%>
|
||||||
<%= "%-10s %s" % [ command.name, command.description ] %>
|
<%= "%-#{len}s %s" % [ command.name, command.description ] %>
|
||||||
<%- end -%>
|
<%- end -%>
|
||||||
|
|
||||||
<%- end -%>
|
<%- end -%>
|
||||||
@@ -25,7 +40,7 @@ Other Commands
|
|||||||
SubCommands of <%= @name %>:
|
SubCommands of <%= @name %>:
|
||||||
---------------------------------------------------------------
|
---------------------------------------------------------------
|
||||||
<%- subcommands.each do |cmd| -%>
|
<%- subcommands.each do |cmd| -%>
|
||||||
<%= "%-10s %s" % [cmd.name, cmd.description] %>
|
<%= "%-#{len}s %s" % [cmd.name, cmd.description] %>
|
||||||
<%- end -%>
|
<%- end -%>
|
||||||
|
|
||||||
<%- cmds = subcommands.select{|c| ! c.examples.nil? } -%>
|
<%- cmds = subcommands.select{|c| ! c.examples.nil? } -%>
|
||||||
|
|||||||
Reference in New Issue
Block a user