diff --git a/src/cligen.cr b/src/cligen.cr index 398414a..b4cfc98 100644 --- a/src/cligen.cr +++ b/src/cligen.cr @@ -1,3 +1,4 @@ +require "./cligen/exceptions" require "./cligen/coercable" require "./cligen/parsable" require "./cligen/annotations" diff --git a/src/cligen/app.cr b/src/cligen/app.cr index 8a52bec..554b753 100644 --- a/src/cligen/app.cr +++ b/src/cligen/app.cr @@ -18,10 +18,25 @@ module CliGen super end + def self.handle_command_raises(&block) : Nil + begin + block.call + 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 diff --git a/src/cligen/arg.cr b/src/cligen/arg.cr index 70f2e42..e5f52c7 100644 --- a/src/cligen/arg.cr +++ b/src/cligen/arg.cr @@ -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 diff --git a/src/cligen/command/argument.cr b/src/cligen/command/argument.cr index c374712..71b4500 100644 --- a/src/cligen/command/argument.cr +++ b/src/cligen/command/argument.cr @@ -70,7 +70,7 @@ module CliGen {% 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 diff --git a/src/cligen/command/define_command_initializer.cr b/src/cligen/command/define_command_initializer.cr index 87f7d43..6dd86d6 100644 --- a/src/cligen/command/define_command_initializer.cr +++ b/src/cligen/command/define_command_initializer.cr @@ -11,7 +11,7 @@ module CliGen flg.validate! @{{var.id}} = flg.as(CliGen::Flag({{var.type}})).value! else - raise "ERROR : {{@type.name}}\#{{@def.name}} : No flag found for \"{{var.name}}\"?" + 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 %} diff --git a/src/cligen/command_node.cr b/src/cligen/command_node.cr index 4bcd8fd..d752dcc 100644 --- a/src/cligen/command_node.cr +++ b/src/cligen/command_node.cr @@ -63,7 +63,7 @@ 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 @@ -75,6 +75,14 @@ module CliGen @flags.find{|f| f.short == short} end + def handle_flag_raises(&work) + begin + work.call + rescue e : CliGen::RuntimeError + abort e.message + end + end + def find_match(arg : String) if subcommand?(arg) return CliGen::MatchType::SubCommand @@ -172,7 +180,7 @@ module CliGen check_for_duplicates!(@flags) @commands.each(&.check!) {% unless T == Nil %} - raise "ERROR : CommandNode({{T}})#check! : {{T}} has no subcommands and no #main defined" \ + raise CliGen::MissingDispatchError.new("CommandNode({{T}})#check! : {{T}} has no subcommands and no #main defined") \ if subcommands.empty? && !{{T.has_method?(:main)}} {% end %} end @@ -199,35 +207,41 @@ module CliGen 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::FlagMultipleShort @@ -236,43 +250,34 @@ module CliGen # 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 do |flag| + 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 - if flag == "-#{chars.last}" - if match.requires_arg? - match.process(args.reject(&.processed?)) + 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 - else - abort "#{CliGen::APPNAME}: cannot bundle flag that requires an argument: #{flag}" if match.requires_arg? - match.process end when MatchType::NoMatch - abort "ERROR : CommandNode(#{@name}).process : No flag match for '#{flag}'" + 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 - flg = "-#{val[0]}" - argument = Arg.new(value: val[1..], index: 0) - case match = find_match(flg) - when BaseFlag - if match.requires_arg? - match.process([argument]) - else - abort "ERROR : CommandNode(#{@name}).process : Unknown characters proceeding a boolean flag(#{flg}) : #{val[1..]}" - end - when MatchType::NoMatch - abort "ERROR : CommandNode(#{@name}).process : No flag match for '#{arg.value}'" - end + raise CliGen::FlagArgumentError.new("#{CliGen::APPNAME}: inline flag arguments are not supported — did you mean '-#{val[0]} #{val[1..]}'?") end when MatchType::NoMatch - puts "ERROR : CommandNode(#{@name}).process : No command, subcommand or flag match for '#{arg.value}'" - abort help + raise CliGen::HelpRequestedError.new("#{CliGen::APPNAME}: unknown token '#{arg.value}'\n\n#{help}") end end diff --git a/src/cligen/exceptions.cr b/src/cligen/exceptions.cr new file mode 100644 index 0000000..7927c3d --- /dev/null +++ b/src/cligen/exceptions.cr @@ -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 diff --git a/src/cligen/flag.cr b/src/cligen/flag.cr index 909a3d8..2dc0f9f 100644 --- a/src/cligen/flag.cr +++ b/src/cligen/flag.cr @@ -67,10 +67,10 @@ module CliGen @on_match : Proc(Nil)? = nil, @format : ::Regex? = nil ) - {% unless T.has_method? :to_s %} + {% 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 %} + {% 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 %} @@ -99,14 +99,14 @@ 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| @@ -119,27 +119,54 @@ module CliGen end end end - {% if elem == Int32 %} - if arg.value.includes?(@delimiter) - @value = (@value || [] of Int32) + arg.value.split(@delimiter).map(&.to_i32) + {% 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 - abort "ERROR : Flag({{T}}, long: #{@long_key}) : Provided arguemnt(#{arg.value}) was not an integer" unless arg.int? - (@value ||= [] of Int32) << arg.value.to_i + 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| + if arg.value.includes?(@delimiter) + @value = (@value || [] of String) + arg.value.split(@delimiter).map { |v| unless @format.nil? - abort "ERROR : Flag({{T}}, long: #{@long_key} ) : Provided arguemnt(#{v}) did not match a valid fromat \"#{@format.not_nil!.source}\"" unless v =~ @format + 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 - (@value ||= [] of String) << arg.value + 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)} + 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 @@ -148,16 +175,25 @@ module CliGen {% end %} arg.processed end - {% elsif T == Int32 %} - abort "ERROR : Flag(#{T}, long: #{@long_key}) : #{argv.first.value} is not an int" unless argv.first.int? - @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 - unless @format.nil? - abort "ERROR : Flag(#{T}, long: #{@long_key}) : #{argv.first.value} does not match a correct format #{@format.not_nil!.source}" unless argv.first.value =~ @format + 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 @@ -167,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" %} @@ -189,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 @@ -216,48 +252,66 @@ module CliGen if opts = @options {% if T < Array %} v.each do |v2| - abort "#{CliGen::APPNAME}: '#{v2}' is not a valid value for #{@long_key} (valid: #{opts.join(", ")})" unless opts.first.includes?(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 %} - abort "#{CliGen::APPNAME}: '#{v}' is not a valid value for #{@long_key} (valid: #{opts.join(", ")})" unless opts.includes?(v) + 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} when testing #{v}" 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 - abort "ERROR : Flag({{T}}, long: #{@long_key}) : #{raw} is not a valid boolean identifier" + 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 %} + {% elsif T < Array %} if raw.includes?(@delimiter) {% elem = T.type_vars.first %} raw.split(@delimiter).map do |val| unless @format.nil? - abort "ERROR : Flag({{T}}, long: #{@long_key}) : #{val} does not match a valid format" unless val =~ @format + 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 == Int32 %} - abort "ERROR : Flag({{T}}, long: #{@long_key}) : #{val} is not an integer" unless val =~ /^[[:digit:]]+$/ - val.to_i + {% 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 %} @@ -267,12 +321,20 @@ module CliGen {% end %} end else - unless @format.nil? - abort "ERROR : Flag({{T}}, long: #{@long_key}) : #{raw} does not match a valid format" unless raw =~ @format + 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 == Int32 %} - abort "ERROR : Flag({{T}}, long: #{@long_key}) : #{raw} is not an integer" unless raw =~ /^[[:digit:]]+$/ - [ raw.to_i ] + {% 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 %} @@ -295,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 diff --git a/src/cligen/regex.cr b/src/cligen/regex.cr index 783370d..f3c7587 100644 --- a/src/cligen/regex.cr +++ b/src/cligen/regex.cr @@ -5,4 +5,8 @@ module CliGen::Regex 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