Compare commits
34 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 |
+1
-1
@@ -1 +1 @@
|
||||
../src/
|
||||
..
|
||||
@@ -1,3 +1,4 @@
|
||||
require "./cligen/exceptions"
|
||||
require "./cligen/coercable"
|
||||
require "./cligen/parsable"
|
||||
require "./cligen/annotations"
|
||||
|
||||
@@ -14,6 +14,9 @@ module CliGen
|
||||
annotation Selection
|
||||
end
|
||||
|
||||
annotation PreRunCommand
|
||||
end
|
||||
|
||||
annotation SubCommand
|
||||
end
|
||||
end
|
||||
|
||||
+18
-6
@@ -14,17 +14,29 @@ module CliGen
|
||||
@@instance = self
|
||||
end
|
||||
|
||||
def check! : Nil
|
||||
CliGen::GLOBAL_FLAGS.each(&.check!)
|
||||
# @flags is the amalgamation of all child flags — checked by the commands themselves
|
||||
check_for_duplicates!([CliGen::GLOBAL_FLAGS, @flags].flatten)
|
||||
@commands.each(&.check!)
|
||||
def check!
|
||||
super
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
# Convenience entry point; defaults to ARGV
|
||||
def self.process(args : Array(String) = ARGV.to_a) : Nil
|
||||
generate if @@instance.nil?
|
||||
@@instance.not_nil!.process(args)
|
||||
handle_command_raises do
|
||||
@@instance.not_nil!.process(args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,7 +28,10 @@ module CliGen
|
||||
description: {{ anno[:description] }},
|
||||
default: {% unless var.default_value.nil? %} {{var.default_value}} {% 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 if env("DEBUG") %}
|
||||
{% end %}
|
||||
|
||||
+31
-7
@@ -24,36 +24,60 @@ module CliGen
|
||||
def initialize(@value, @index)
|
||||
end
|
||||
|
||||
def flag? : Bool
|
||||
if @value =~ CliGen::Regex::FLAG_REGEX
|
||||
def self.flag?(val : String) : Bool
|
||||
if val =~ CliGen::Regex::FLAG_REGEX
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def int? : Bool
|
||||
if @value =~ /^[[:digit:]]+$/
|
||||
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 float? : Bool
|
||||
if @value =~ /^[[:digit:]]+(\.[[:digit:]]+)?$/
|
||||
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 will raise an exception if it is re-called after already having been
|
||||
# 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
|
||||
end
|
||||
end
|
||||
|
||||
+8
-22
@@ -6,34 +6,20 @@ require "./command/argument"
|
||||
require "./command/selection"
|
||||
require "./command/help_template"
|
||||
require "./command/subcommand"
|
||||
require "./command/def_init"
|
||||
require "./command/define_command_initializer"
|
||||
|
||||
module CliGen
|
||||
class Command
|
||||
macro inherited
|
||||
{% verbatim do %}
|
||||
macro finished
|
||||
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]}}}
|
||||
@{{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 %}
|
||||
|
||||
{% if @type.has_method? :after_initialize %}
|
||||
after_initialize
|
||||
{% end %}
|
||||
{% end %}
|
||||
end
|
||||
{% anno = @type.annotation(CliGen::CommandInfo) %}
|
||||
{% raise "" unless anno %}
|
||||
{% if anno[:def_init] %}
|
||||
def_init
|
||||
{% end %}
|
||||
define_command_initializer
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
module CliGen
|
||||
class Command
|
||||
macro argument(variable, description, long = nil, short = nil, validation = nil, on_match = nil, def_setter = false)
|
||||
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 %}
|
||||
{% name = variable.var %}
|
||||
{% type = variable.type %}
|
||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided delimiter must be a string" unless delimiter.is_a? StringLiteral %}
|
||||
{% if short %}
|
||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided short must be a string" unless short.is_a? StringLiteral %}
|
||||
{% end %}
|
||||
@@ -15,7 +16,7 @@ module CliGen
|
||||
{% long = "--#{name.downcase}" %}
|
||||
{% end %}
|
||||
{% 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? %}
|
||||
{% 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 %}
|
||||
@@ -32,14 +33,44 @@ module CliGen
|
||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation input value must be #{type}. EX: #{example}" %}
|
||||
{% 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}}
|
||||
|
||||
{% if def_getter %}
|
||||
def {{variable.var}}
|
||||
@{{name}}
|
||||
end
|
||||
{% end %}
|
||||
|
||||
{% if def_setter %}
|
||||
def {{variable.var}}= (value : {{type}})
|
||||
{% 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 %}
|
||||
@{{name}} = value
|
||||
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
|
||||
@@ -1,14 +1,21 @@
|
||||
module CliGen
|
||||
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 : Provided short must be a string" unless short.is_a? StringLiteral || string == nil %}
|
||||
{% raise "ERROR : CliGen::Command.selection : Provided long must be a string" unless long.is_a? StringLiteral || long == nil %}
|
||||
{% if short %}
|
||||
{% 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 description" unless description %}
|
||||
{% 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 %}
|
||||
{% raise "ERROR : CliGen::Command.selection : Provided options must be " unless options %}
|
||||
{% options = options.resolve if options.is_a? Path %}
|
||||
{% 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
|
||||
|
||||
+81
-32
@@ -29,7 +29,7 @@ module CliGen
|
||||
@pre_run_commands : Array(RunCommand),
|
||||
@post_run_commands : Array(RunCommand),
|
||||
@description : String? = nil
|
||||
)
|
||||
)
|
||||
@flags = (flags + CliGen::GLOBAL_FLAGS + @commands.flat_map(&.flags)).uniq
|
||||
end
|
||||
|
||||
@@ -63,12 +63,24 @@ module CliGen
|
||||
message += "\nShort:\n%s" % short_duplicates.map { |f| "- #{f}" }.join("\n")
|
||||
end
|
||||
|
||||
raise error_buffer % [@name, message]
|
||||
raise CliGen::DuplicateFlagError.new(error_buffer % [@name, message])
|
||||
end
|
||||
end
|
||||
|
||||
def get(flag_long : String) : BaseFlag?
|
||||
@flags.find{|f| f.long_key == flag_long}
|
||||
def get(*, long : String) : BaseFlag?
|
||||
@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
|
||||
|
||||
def find_match(arg : String)
|
||||
@@ -87,8 +99,6 @@ module CliGen
|
||||
end
|
||||
when CliGen::Regex::FLAG_WITH_ARG
|
||||
CliGen::MatchType::FlagWithArg
|
||||
when CliGen::Regex::SHORT_WITH_INLINE_ARG
|
||||
CliGen::MatchType::ShortWithInlineArg
|
||||
when CliGen::Regex::FLAG_MULTIPLE_SHORT
|
||||
CliGen::MatchType::FlagMultipleShort
|
||||
else
|
||||
@@ -108,7 +118,7 @@ module CliGen
|
||||
subcommands.any?{|f| f.name == arg}
|
||||
end
|
||||
|
||||
def flag?(arg : String) : BaseFlag
|
||||
def flag?(arg : String) : Bool
|
||||
@flags.any?(&.matches?(arg))
|
||||
end
|
||||
|
||||
@@ -144,6 +154,11 @@ module CliGen
|
||||
{% 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" %}
|
||||
@@ -154,7 +169,7 @@ module CliGen
|
||||
ECR.render({{CliGen::HELP_OVERRIDE_TEMPLATE}})
|
||||
{% else %}
|
||||
{% puts "No type overrided help output. Using default" if env("DEBUG") %}
|
||||
ECR.render("lib/cligen/cligen/template/cmd_help.ecr")
|
||||
ECR.render("lib/cligen/src/cligen/template/cmd_help.ecr")
|
||||
{% end %} # otherwise
|
||||
{% debug if env("DEBUG") %}
|
||||
{% end %}
|
||||
@@ -164,8 +179,10 @@ module CliGen
|
||||
@flags.each(&.check!)
|
||||
check_for_duplicates!(@flags)
|
||||
@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)}}
|
||||
{% end %}
|
||||
end
|
||||
|
||||
def process(args : Array(CliGen::Arg)) : Nil
|
||||
@@ -187,55 +204,80 @@ module CliGen
|
||||
{% for cls in CliGen::Command.subclasses %}
|
||||
when CliGen::CommandNode({{cls.name}})
|
||||
match.as(CommandNode({{cls}})).process(args.reject(&.processed?))
|
||||
exit 0
|
||||
{% end %}
|
||||
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 %}
|
||||
passed_execution = true
|
||||
|
||||
when BaseFlag
|
||||
if match.requires_arg?
|
||||
match.process(args.reject(&.processed?))
|
||||
else
|
||||
match.process
|
||||
# Ensuring we catch any exceptions to have them abort with the
|
||||
# error message if caught
|
||||
handle_flag_raises do
|
||||
if match.requires_arg?
|
||||
match.process(args.reject(&.processed?).take_while{|v| find_match(v.value) == CliGen::MatchType::NoMatch})
|
||||
else
|
||||
match.process
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
when MatchType::Help
|
||||
abort help
|
||||
raise CliGen::HelpRequestedError.new(help)
|
||||
|
||||
when MatchType::FlagWithArg
|
||||
if regex_match = CliGen::Regex::FLAG_WITH_ARG.match(arg.value)
|
||||
case flag_match = find_match(regex_match["flag"])
|
||||
when BaseFlag
|
||||
flag_match.process([CliGen::Arg.new(value: regex_match["arg"], index: arg.index)])
|
||||
handle_flag_raises do
|
||||
flag_match.process([CliGen::Arg.new(value: regex_match["arg"], index: arg.index)])
|
||||
end
|
||||
else
|
||||
raise "ERROR : CommandNode(#{@name}).process : No flag matched '#{regex_match["flag"]}'"
|
||||
raise CliGen::UnknownCommandNodeError.new("CommandNode(#{@name}).process : No flag matched '#{regex_match["flag"]}'")
|
||||
end
|
||||
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::RegexInvariantError.new("CommandNode(#{@name}).process : FLAG_WITH_ARG matched in find_match but failed on re-match — this is a framework bug")
|
||||
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
|
||||
arg.value.gsub(/^-/, "").chars.map { |c| "-#{c}" }.each do |flag|
|
||||
case match = find_match(flag)
|
||||
when BaseFlag
|
||||
abort "#{CliGen::APPNAME}: cannot bundle flag that requires an argument: #{flag}" if match.requires_arg?
|
||||
match.process
|
||||
when MatchType::NoMatch
|
||||
raise "ERROR : CommandNode(#{@name}).process : No flag match for '#{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)
|
||||
when BaseFlag
|
||||
# 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
|
||||
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
|
||||
|
||||
when MatchType::NoMatch
|
||||
abort "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
|
||||
|
||||
@@ -244,6 +286,9 @@ module CliGen
|
||||
{% unless T == Nil %}
|
||||
unless passed_execution
|
||||
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)) %}
|
||||
{% begin %}
|
||||
case matched_subcommand
|
||||
@@ -255,12 +300,16 @@ module CliGen
|
||||
{% if T.has_method?(:main) %}
|
||||
cls.main
|
||||
{% else %}
|
||||
abort help
|
||||
#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 %}
|
||||
exit 0
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
{% else %}
|
||||
puts help
|
||||
exit 0
|
||||
{% 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
|
||||
+191
-38
@@ -1,6 +1,14 @@
|
||||
require "./arg"
|
||||
|
||||
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
|
||||
getter var : String
|
||||
getter short : String?
|
||||
@@ -8,13 +16,17 @@ module CliGen
|
||||
getter long_key : String
|
||||
getter env_var : String
|
||||
getter description : String
|
||||
getter delimiter : String
|
||||
getter meta : FlagMeta
|
||||
|
||||
def initialize(
|
||||
@var : String,
|
||||
@short : String?,
|
||||
@long : 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 @long =~ /\s|=/
|
||||
@@ -40,6 +52,7 @@ module CliGen
|
||||
@options : Array(T)?
|
||||
@validate : (T -> Bool)?
|
||||
@on_match : Proc(Nil)?
|
||||
@format : ::Regex?
|
||||
|
||||
def initialize(
|
||||
var : String,
|
||||
@@ -47,12 +60,37 @@ module CliGen
|
||||
long : String,
|
||||
env_var : String,
|
||||
description : String,
|
||||
delimiter : String = ",",
|
||||
@default : T? = nil,
|
||||
@options : Array(T)? = 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
|
||||
|
||||
def requires_arg? : Bool
|
||||
@@ -61,27 +99,74 @@ module CliGen
|
||||
|
||||
def process(argv : Array(Arg) = [] of Arg) : Nil
|
||||
if requires_arg?
|
||||
raise "ERROR : Flag(#{T}, long: #{@long_key}) : Array requires an argument but provided array is empty" if argv.empty?
|
||||
abort "ERROR : Flag(#{T}, long: #{@long_key}) : Provided argument was a flag (#{argv.first.value})" if argv.first.flag?
|
||||
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
|
||||
|
||||
|
||||
{% if T == Bool %}
|
||||
@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 %}
|
||||
{% elem = T.type_vars.first %}
|
||||
argv.each do |arg|
|
||||
break if arg.flag?
|
||||
{% if elem == Int32 %}
|
||||
abort "ERROR : Flag({{T}}) : Provided arguemnt(#{arg.value}) was not an integer" unless arg.int?
|
||||
(@value ||= [] of Int32) << arg.value.to_i
|
||||
unless @format.nil?
|
||||
unless arg.value.includes?(@delimiter)
|
||||
unless arg.value =~ @format
|
||||
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"]?
|
||||
break
|
||||
end
|
||||
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
|
||||
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 %}
|
||||
{% delim = elem.has_constant?("DELIMITER") ? elem.constant("DELIMITER") : ',' %}
|
||||
if arg.value.includes?({{delim}})
|
||||
@value = (@value || [] of {{elem}}) + arg.value.split({{delim}}).map{|i| {{elem}}.coerce(i)}
|
||||
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
|
||||
@@ -90,13 +175,27 @@ module CliGen
|
||||
{% end %}
|
||||
arg.processed
|
||||
end
|
||||
{% elsif T == Int32 %}
|
||||
@value = argv.first.value.to_i
|
||||
{% elsif T < Int %}
|
||||
{% 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
|
||||
{% elsif T == Time %}
|
||||
@value = parse_time(argv.first.value)
|
||||
argv.first.processed
|
||||
{% 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
|
||||
argv.first.processed
|
||||
{% elsif T.class < CliGen::Parsable %}
|
||||
@@ -104,7 +203,7 @@ module CliGen
|
||||
@value = T.parse_args(argv)
|
||||
post_processed = argv.select(&.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
|
||||
{% else %}
|
||||
{% raise "ERROR : Flag({{T}}#process : Generic Type #{T} is not supported. To add support you must extend with CliGen::Parsable & implement the class method" %}
|
||||
@@ -126,7 +225,7 @@ module CliGen
|
||||
|
||||
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!
|
||||
end
|
||||
|
||||
@@ -151,45 +250,99 @@ module CliGen
|
||||
v = value!
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def check! : Nil
|
||||
raise "ERROR : Flag({{T}}, long: #{@long})#check! : -h is reserved for internal help usage" 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! : -h is reserved for internal help") if @short == "-h"
|
||||
raise CliGen::ReservedFlagError.new("Flag({{T}}, long: #{@long})#check! : --help is reserved for internal help") if @long_key == "--help"
|
||||
end
|
||||
|
||||
private def coerce(raw : String) : T
|
||||
{% if T == Bool %}
|
||||
case raw
|
||||
when "t","true","1"
|
||||
when /^(t|true|y|yes|1)$/i
|
||||
true
|
||||
when "f","false","0"
|
||||
when /^(f|false|n|no|0)$/i
|
||||
false
|
||||
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
|
||||
{% elsif T == Int32 %}
|
||||
raw.to_i
|
||||
{% elsif T < Int %}
|
||||
{% 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 %}
|
||||
parse_time(raw)
|
||||
{% elsif T <= Array %}
|
||||
{% elem = T.type_vars.first %}
|
||||
{% if elem == Int32 %}
|
||||
raw.split(',').map(&.to_i)
|
||||
{% elsif elem == String %}
|
||||
raw.split(',')
|
||||
{% elsif elem.class < CliGen::Coercable %}
|
||||
{% delim = elem.has_constant?("DELIMITER") ? elem.constant("DELIMITER") : ',' %}
|
||||
raw.split({{delim}}).map{|i| {{elem}}.coerce(i)}
|
||||
{% else %}
|
||||
{% 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 %}
|
||||
{% elsif T < Array %}
|
||||
if raw.includes?(@delimiter)
|
||||
{% elem = T.type_vars.first %}
|
||||
raw.split(@delimiter).map do |val|
|
||||
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 %}
|
||||
val
|
||||
{% elsif elem.class < CliGen::Coercable %}
|
||||
{{elem}}.coerce(val)
|
||||
{% else %}
|
||||
{% 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
|
||||
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
|
||||
raw
|
||||
{% elsif T.class < CliGen::Coercable %}
|
||||
@@ -204,7 +357,7 @@ module CliGen
|
||||
CliGen::Regex::INPUT_DATETIME_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])
|
||||
Time.parse_local(raw, CliGen::Format::INPUT_DATETIME_FORMAT)
|
||||
else
|
||||
|
||||
@@ -3,6 +3,15 @@ require "./flag"
|
||||
module CliGen
|
||||
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)
|
||||
{% 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 %}
|
||||
|
||||
+6
-3
@@ -1,9 +1,12 @@
|
||||
module CliGen::Regex
|
||||
FLAG_REGEX=/^(-[a-zA-Z]|--[a-zA-Z-_0-9]+)/
|
||||
FLAG_REGEX=/^(-[a-zA-Z]|--[a-zA-Z-_0-9]+)$/
|
||||
FLAG_WITH_ARG=/^(?<flag>(-[a-zA-Z]|--[a-zA-Z-_]+))="?(?<arg>\S+?)"?$/
|
||||
FLAG_MULTIPLE_SHORT=/^-[a-zA-Z]+$/
|
||||
SHORT_WITH_INLINE_ARG=/^-[a-zA-Z][a-zA-Z0-9]+$/
|
||||
FLAG_MULTIPLE_SHORT=/^-[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_DATE_REGEX = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/
|
||||
|
||||
FLOAT = /^[-+]?[[:digit:]]+(\.[[:digit:]]+)?$/
|
||||
UINT = /^[[:digit:]]+$/
|
||||
INT = /^[-+]?[[:digit:]]+$/
|
||||
end
|
||||
|
||||
@@ -3,21 +3,36 @@ Command: <%= @name %>
|
||||
Description: <%= @description %>
|
||||
<%- end -%>
|
||||
|
||||
<%- unless @flags.empty? -%>
|
||||
<%- len = @flags.map{|f| f.short.nil? ? f.long.size : "#{f.short},#{f.long}".size}.max + 5 -%>
|
||||
|
||||
Flags:
|
||||
---------------------------------------------------------------
|
||||
<%- @flags.each do |flag| -%>
|
||||
<%- unless flag.short.nil? -%>
|
||||
<%= "%-15s %s" % ["#{flag.short.not_nil!.strip},#{flag.long.strip}", flag.description.strip] %>
|
||||
<%- if flag.short.nil? -%>
|
||||
<%- flags = [flag.long.strip] -%>
|
||||
<%- 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 -%>
|
||||
<%- unless @commands.empty? -%>
|
||||
Other Commands
|
||||
---------------------------------------------------------------
|
||||
<%- @commands.each do |command| -%>
|
||||
<%= "%-15s %s" % [ command.name, command.description ] %>
|
||||
<%= "%-#{len}s %s" % [ command.name, command.description ] %>
|
||||
<%- end -%>
|
||||
|
||||
<%- end -%>
|
||||
@@ -25,7 +40,7 @@ Other Commands
|
||||
SubCommands of <%= @name %>:
|
||||
---------------------------------------------------------------
|
||||
<%- subcommands.each do |cmd| -%>
|
||||
<%= "%-15s %s" % [cmd.name, cmd.description] %>
|
||||
<%= "%-#{len}s %s" % [cmd.name, cmd.description] %>
|
||||
<%- end -%>
|
||||
|
||||
<%- cmds = subcommands.select{|c| ! c.examples.nil? } -%>
|
||||
|
||||
Reference in New Issue
Block a user