diff --git a/src/cligen/app.cr b/src/cligen/app.cr index 19fec0f..714e502 100644 --- a/src/cligen/app.cr +++ b/src/cligen/app.cr @@ -19,13 +19,20 @@ module CliGen end def self.handle_command_raises(&) : Nil + # doing Fiber.yield to allow Log to print log output before shoving data to screen begin + # Had to use yeild because doing .call on a provided block had the compiler + # freaking out about not being able to determine a type of a variable in the + # context it was being called from. yield rescue e : CliGen::RuntimeError + Fiber.yield abort e.message rescue e : CliGen::ConfigurationError + Fiber.yield abort e.message rescue e : CliGen::HelpRequestedError + Fiber.yield puts e.message exit 0 end diff --git a/src/cligen/app/generate.cr b/src/cligen/app/generate.cr index df1691b..24a9959 100644 --- a/src/cligen/app/generate.cr +++ b/src/cligen/app/generate.cr @@ -36,9 +36,6 @@ module CliGen {% debug if env("DEBUG") %} {% end %} - # Keep a copy of every flag on the root for global matching - app_flags += cmd_flags - app_commands << CommandNode({{cmd}}).new( name: {{cmd_name}}, flags: cmd_flags, diff --git a/src/cligen/command/define_command_initializer.cr b/src/cligen/command/define_command_initializer.cr index bc3de6e..b7e5100 100644 --- a/src/cligen/command/define_command_initializer.cr +++ b/src/cligen/command/define_command_initializer.cr @@ -3,23 +3,30 @@ module CliGen macro define_command_initializer def initialize(*, handler : CliGen::BaseCommandNode) {% verbatim do %} + Log.debug { "#{self.class.name}#initialize : Initializing class" } {% for var in @type.instance_vars %} + Log.debug { "{{@type.name}}#initialize : Checking {{var.name}}" } {% 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? %} + Log.debug { "{{@type.name}}#initialize : {{var.name}} is a CliGen managed ivar. Will attempt to gather from associated CliGen::Flag" } if flg = handler.flags.find{|f| f.var == {{var.name.stringify}} && f.long == {{anno[:long]}}} + Log.debug { "{{@type.name}}#initialize : {{var.name}} : Found Flag(long: #{flg.long}). Calling validate! to make sure data provided (in whatever format) is valid" } flg.validate! + Log.debug { "{{@type.name}}#initialize : {{var.name}} : Found Flag(long: #{flg.long}). Data was valid seems like (or at least a default was set)" } @{{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 %} + Log.debug { "{{@type.name}}#initialize : {{var.name}} is not a CliGen managed ivar. Will initialize to default defined in class" } {% raise "ERROR : #{@type.name}#{@def.name} : Instance Variable(#{var.name}) is not handled by CliGen and does not have a default value" if var.default_value.nil? %} @{{var.id}} = {{var.default_value}} {% end %} {% end %} {% if @type.has_method? :after_initialize %} + Log.debug { "{{@type.name}}#initialize : Developer defined 'after_initialize' so going to call it" } after_initialize {% end %} {% end %} diff --git a/src/cligen/command_node.cr b/src/cligen/command_node.cr index e250c14..f6317a3 100644 --- a/src/cligen/command_node.cr +++ b/src/cligen/command_node.cr @@ -2,6 +2,7 @@ require "./global_flag" require "./match_type" require "./flag" require "./arg" +require "log" require "ecr" module CliGen @@ -22,18 +23,20 @@ module CliGen @pre_run_commands : Array(RunCommand) @post_run_commands : Array(RunCommand) + Log = ::Log.for(CliGen::CommandInfo) + def initialize( @name : String, - flags : Array(BaseFlag), + @flags : Array(BaseFlag), @commands : Array(BaseCommandNode), @pre_run_commands : Array(RunCommand), @post_run_commands : Array(RunCommand), @description : String? = nil ) - @flags = (flags + CliGen::GLOBAL_FLAGS + @commands.flat_map(&.flags)).uniq end def check_for_duplicates!(flags : Array(BaseFlag)) : Nil + Log.trace { "CommandNode(#{@name})#check_for_duplicates! : entered with #{flags.map(&.long_key)}" } shorts = flags.compact_map(&.short) short_duplicates = [] of String longs = flags.compact_map { |f| f.long_key unless f.long_key.empty? } @@ -68,11 +71,13 @@ module CliGen end def get(*, long : String) : BaseFlag? - @flags.find{|f| f.long_key == long} + Log.trace { "CommandNode(#{@name})#get(long: #{long}) : entered" } + @flags.find{|f| f.long_key == long} || @commands.find(&.flag?(long)).try(&.get(long: long)) || CliGen::GLOBAL_FLAGS.find(&.long_key.==(long)) end def get(*, short : String) : BaseFlag? - @flags.find{|f| f.short == short} + Log.trace { "CommandNode(#{@name})#get(short: #{short}) : entered" } + @flags.find{|f| f.short == short} || @commands.find(&.flag?(short)).try(&.get(short: short)) || CliGen::GLOBAL_FLAGS.find(&.short.==(short)) end def handle_flag_raises(&) : Nil @@ -84,27 +89,38 @@ module CliGen end def find_match(arg : String) + Log.trace { "CommandNode(#{@name})#find_match(#{arg}) : Entered" } if subcommand?(arg) + Log.debug { "CommandNode(#{@name})#find_match(#{arg}) : arg found to be subcommand" } return CliGen::MatchType::SubCommand end case arg when "-h", "--help" + Log.debug { "CommandNode(#{@name})#find_match(#{arg}) : was found to be a help flag" } CliGen::MatchType::Help when CliGen::Regex::FLAG_REGEX - if flg = @flags.find(&.matches?(arg)) + Log.debug { "CommandNode(#{@name})#find_match(#{arg}) : arg found to match the format of a flag" } + if flg = flag?(arg) + Log.debug { "CommandNode(#{@name})#find_match(#{arg}) : arg found to be a Flag(long: #{flg.long_key})" } flg else + Log.debug { "CommandNode(#{@name})#find_match(#{arg}) : arg found not to have a flag associated with it" } CliGen::MatchType::NoMatch end when CliGen::Regex::FLAG_WITH_ARG + Log.debug { "CommandNode(#{@name})#find_match(#{arg}) : arg found to match the format of a flag with an arg =" } CliGen::MatchType::FlagWithArg when CliGen::Regex::FLAG_MULTIPLE_SHORT + Log.debug { "CommandNode(#{@name})#find_match(#{arg}) : arg found to match the clumped flag format" } CliGen::MatchType::FlagMultipleShort else - if cmd = @commands.find { |c| c.name == arg } + Log.debug { "CommandNode(#{@name})#find_match(#{arg}) : Found no obvious match format wise. Checking if arg is a command" } + if cmd = @commands.find(&.name.== arg) + Log.debug { "CommandNode(#{@name})#find_match(#{arg}) : Looks like the arg matched a defined command" } cmd else + Log.debug { "CommandNode(#{@name})#find_match(#{arg}) : No match found for arg" } CliGen::MatchType::NoMatch end end @@ -115,15 +131,18 @@ module CliGen end def subcommand?(arg : String) : Bool - subcommands.any?{|f| f.name == arg} + Log.trace { "CommandNode(#{@name})#subcommand?(#{arg}) : Entered" } + subcommands.any?(&.name.== arg) end - def flag?(arg : String) : Bool - @flags.any?(&.matches?(arg)) + def flag?(arg : String) : BaseFlag? + Log.trace { "CommandNode(#{@name})#flag?(#{arg}) : Entered" } + get(short: arg) || get(long: arg) end # Converts String array to Arg array and hands off to the typed process method def process(args : Array(String)) : Nil + Log.trace { "CommandNode(#{@name})#process(#{args}) : Entered" } new_args = args.each_with_index.map { |arg, i| CliGen::Arg.new(value: arg, index: i) }.to_a process(new_args) end @@ -134,6 +153,7 @@ module CliGen end class CommandNode(T) < BaseCommandNode + def subcommands : Array(SubCommandInfo) {% begin %} {% subcmds = T.methods.select(&.annotation(CliGen::SubCommand)) %} @@ -177,15 +197,16 @@ module CliGen def check! : Nil @flags.each(&.check!) - check_for_duplicates!(@flags) + check_for_duplicates!(@flags + CliGen::GLOBAL_FLAGS) @commands.each(&.check!) {% unless T == Nil %} - raise CliGen::MissingDispatchError.new("CommandNode({{T}})#check! : {{T}} has no subcommands and no #main defined") \ + raise CliGen::MissingDispatchError.new("CommandNode(#{@name})#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 + Log.trace { "CommandNode(#{@name})#process(#{args.map(&.value)}) : Entered" } check! passed_execution = false matched_subcommand : String? = nil @@ -193,11 +214,20 @@ module CliGen @pre_run_commands.each(&.call) args.each do |arg| - next if arg.processed? + Log.trace { "CommandNode(#{@name})#process : Iterating with arg Arg(index: #{arg.index}, value: #{arg.value})" } + if arg.processed? + Log.debug { "CommandNode(#{@name})#process : Arg(#{arg.value}) was already processed. Skipping" } + next + end + Log.trace { "CommandNode(#{@name})#process : Arg(#{arg.value}) wasn't processed yet. Continuing and marking arg as processed" } arg.processed case match = find_match(arg.value) when BaseCommandNode + Log.trace { + "CommandNode(#{@name})#process : Arg(#{arg.value}) was found to be a child command. " \ + "Handing off rest of execution & parsing to it" + } # Hand off remainder to the child; we're done at this level {% begin %} case match @@ -207,54 +237,66 @@ module CliGen exit 0 {% end %} else - raise CliGen::UnknownCommandNodeError.new("CommandNode({{T}})#process : matched a BaseCommandNode that isn't a known CommandNode(T)") + raise CliGen::UnknownCommandNodeError.new("CommandNode(#{@name})#process : matched a BaseCommandNode that isn't a known CommandNode(T)") end {% end %} passed_execution = true when BaseFlag + Log.trace { "CommandNode(#{@name})#process : Arg(#{arg.value}) was found to be a flag" } # Ensuring we catch any exceptions to have them abort with the # error message if caught handle_flag_raises do if match.requires_arg? + Log.trace { "CommandNode(#{@name})#process : Arg(#{arg.value}) flag requires values so handing it the data without a match or that matches it's valid options" } match.process(args.reject(&.processed?).take_while{|v| find_match(v.value) == CliGen::MatchType::NoMatch || !!match.meta.options.try(&.includes?(v.value)) }) else + Log.trace { "CommandNode(#{@name})#process : Arg(#{arg.value}) flag does not require an argument so just calling process" } match.process end end when MatchType::SubCommand - raise CliGen::InternalError.new("CommandNode({{T}})#process : subcommand '#{matched_subcommand}' was already matched — duplicate subcommand token") if matched_subcommand + Log.trace { "CommandNode(#{@name})#process : Arg(#{arg.value}) was found to be a subcomand. Marking it as the matched sub-command" } + raise CliGen::InternalError.new("CommandNode(#{@name})#process : subcommand '#{matched_subcommand}' was already matched — duplicate subcommand token") if matched_subcommand matched_subcommand = arg.value when MatchType::Help + Log.trace { "CommandNode(#{@name})#process : Arg(#{arg.value}) is a help option. Raising to have App print out help output" } raise CliGen::HelpRequestedError.new(help) when MatchType::FlagWithArg + Log.debug { "CommandNode(#{@name})#process : Arg(#{arg.value}) was found to be a flag with an arg =" } if regex_match = CliGen::Regex::FLAG_WITH_ARG.match(arg.value) + Log.trace { "CommandNode(#{@name})#process : Arg(#{arg.value}) flag: #{regex_match["flag"]} & arg: #{regex_match["arg"]}" } case flag_match = find_match(regex_match["flag"]) when BaseFlag + Log.debug { "CommandNode(#{@name})#process : Arg(#{arg.value}) flag: #{regex_match["flag"]} is actually a flag" } handle_flag_raises do flag_match.process([CliGen::Arg.new(value: regex_match["arg"], index: arg.index)]) end else + Log.debug { "CommandNode(#{@name})#process : Arg(#{arg.value}) flag: #{regex_match["flag"]} had no flag matches" } 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") + 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 + Log.debug { "CommandNode(#{@name})#process : Arg(#{arg.value}) was found to be an combined short 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| + Log.trace { "CommandNode(#{@name})#process : Arg(#{arg.value}) : char(flag: #{flag}, index: #{index}) being processed" } case match = find_match(flag) when BaseFlag + Log.trace { "CommandNode(#{@name})#process : Arg(#{arg.value}) : char(flag: #{flag}, index: #{index}) was actually found to be a flag" } # 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 @@ -304,7 +346,7 @@ module CliGen {% if T.has_method?(:main) %} cls.main {% else %} - puts "ERROR : CommandNode({{T}})\#{{@def.name}} : No subcommand matched and no #main defined" + puts "ERROR : CommandNode(#{@name})#process : No subcommand matched and no #main defined" puts help {% end %} exit 0 diff --git a/src/cligen/flag.cr b/src/cligen/flag.cr index 2dc0f9f..d206161 100644 --- a/src/cligen/flag.cr +++ b/src/cligen/flag.cr @@ -19,6 +19,8 @@ module CliGen getter delimiter : String getter meta : FlagMeta + Log = ::Log.for(CliGen::Flag) + def initialize( @var : String, @short : String?, @@ -28,6 +30,15 @@ module CliGen @delimiter : String, @meta : FlagMeta ) + Log.trace { + "Flag was initialized:\n" \ + "\t@var : #{@var}\n" \ + "\t@short : #{@short}\n" \ + "\t@long : #{@long}\n" \ + "\t@env_var : #{@env_var}\n" \ + "\t@description : #{@description}\n" \ + "\t@delimiter : #{@delimiter}\n" + } # if the user provides just a "--long" I want the @long_key to match it if @long =~ /\s|=/ @long_key = @long.split(/\s|=/).first @@ -37,6 +48,7 @@ module CliGen end def matches?(token : String) : Bool + Log.trace { "Flag(#{@long})#matches?(#{token}) : entered" } token == @short || (!@long_key.empty? && token == @long_key) end @@ -51,7 +63,7 @@ module CliGen @default : T? @options : Array(T)? @validate : (T -> Bool)? - @on_match : Proc(Nil)? + @on_match : Proc(T, Nil)? @format : ::Regex? def initialize( @@ -64,7 +76,7 @@ module CliGen @default : T? = nil, @options : Array(T)? = nil, @validate : (T -> Bool)? = nil, - @on_match : Proc(Nil)? = nil, + @on_match : Proc(T, Nil)? = nil, @format : ::Regex? = nil ) {% unless T.class.has_method? :to_s %} @@ -98,6 +110,7 @@ module CliGen end def process(argv : Array(Arg) = [] of Arg) : Nil + Log.trace { "Flag(#{@long}, type: #{@meta.type})#process : entered with args #{argv.map(&.value)}" } if requires_arg? 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? @@ -109,12 +122,18 @@ module CliGen {% 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 %} + Log.debug { "Flag(long: #{@long}, type: #{@meta.type})#process : Beginning iteration of arguments" } argv.each do |arg| - break if arg.flag? + Log.trace { "Flag(long: #{@long}, type: #{@meta.type})#process : Iterating with Arg(index: #{arg.index}, value: #{arg.value})" } + if arg.flag? + Log.debug { "Flag(long: #{@long}, type: #{@meta.type})#process : Arg(index: #{arg.index}, value: #{arg.value}) was a flag. Breaking loop" } + break + end unless @format.nil? + Log.debug { "Flag(long: #{@long}, type: #{@meta.type}) : Arg(index: #{arg.index}, value: #{arg.value}) format regex was provided. Going to check argument value against it" } 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"]? + Log.debug { "Flag(long: #{@long}, type: #{@meta.type}) : Arg(index: #{arg.index}, value: #{arg.value}) was not found to be matching the defined filter #{@format}. So breaking from parse loop" } break end end @@ -122,7 +141,9 @@ module CliGen {% if elem < Int %} {% int_case = elem.stringify =~ /^UInt/ ? "uint?".id : "int?".id %} if arg.value.includes?(@delimiter) + Log.debug { "Flag(long: #{@long}, type: #{@meta.type})#process : Arg(index: #{arg.index}, value: #{arg.value}) argument provided is delimited with provided delimiter . Splitting and parsing individual values" } @value = (@value || T.new) + arg.value.split(@delimiter).map do |val| + Log.trace { "Flag(long: #{@long}, type: #{@meta.type})#process : Arg(index: #{arg.index}, value: #{arg.value}) : Indexing with Value(#{val})" } val = val.strip unless arg.{{int_case}}(val) raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{val}' is not a valid {{elem}}") @@ -152,8 +173,11 @@ module CliGen end {% elsif elem == String %} if arg.value.includes?(@delimiter) + Log.debug { "Flag(long: #{@long}, type: #{@meta.type})#process : Arg(index: #{arg.index}, value: #{arg.value}) argument provided is delimited with provided delimiter . Splitting and parsing individual values" } @value = (@value || [] of String) + arg.value.split(@delimiter).map { |v| + Log.trace { "Flag(long: #{@long}, type: #{@meta.type})#process : Arg(index: #{arg.index}, value: #{arg.value}) : Indexing with Value(#{v})" } unless @format.nil? + Log.debug { "Flag(long: #{@long}, type: #{@meta.type})#process : Arg(index: #{arg.index}, value: #{arg.value}) format regex (#{@format.not_nil!.source}) was provided. Checking value against it" } raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : '#{v}' does not match required format /#{@format.not_nil!.source}/") unless v =~ @format end v @@ -199,10 +223,14 @@ module CliGen @value = argv.first.value argv.first.processed {% elsif T.class < CliGen::Parsable %} - processed = argv.select(&.processed?) + unprocessed = argv.reject(&.processed?) + Log.trace { "Flag(long: #{@long}, type: #{@meta.type})#process : unprocessed before : #{unprocessed.map(&.value)}" } @value = T.parse_args(argv) - post_processed = argv.select(&.processed?) - if processed == post_processed + Log.trace { "Flag(long: #{@long}, type: #{@meta.type})#process : unprocessed after : #{unprocessed.reject(&.processed?).map(&.value)}" } + # Essentially if the unprocessed array stays the same (aka if it shows the same number of unprocessed + # arguments it will complain and raise. Only possible because the array holds references to the objects + # in case the user (for some reason) shifts/pops options out when getting/parsing data from argv) + if unprocessed.size == unprocessed.reject(&.processed?).size raise CliGen::ParseableInvariantError.new("Flag({{T}}, long: #{@long_key})#process : {{T}}#parse_args did not mark any args as processed") end {% else %} @@ -210,10 +238,12 @@ module CliGen {% end %} validate! - @on_match.try(&.call) + @on_match.try(&.call(value!)) end def value! : T + Log.trace { "Flag(long: #{@long}, type: #{@meta.type})#value! : called" } + # Essentially if the unprocessed array stays the same (aka if it shows the same number of unprocessed v = @value # Priority: provided arg → env var → default → abort @@ -230,6 +260,7 @@ module CliGen end def raw_value : String? + Log.trace { "Flag(long: #{@long}, type: #{@meta.type})#raw_value : called" } {% if T == Bool %} @value.try(&.to_s) {% elsif T <= Array %} @@ -240,6 +271,7 @@ module CliGen end def satisfied? : Bool + Log.trace { "Flag(long: #{@long}, type: #{@meta.type})#satisfied? : called" } return true if !@value.nil? return true if @env_var && ENV[@env_var]? return true if !@default.nil? @@ -247,6 +279,7 @@ module CliGen end def validate! : Nil + Log.trace { "Flag(long: #{@long}, type: #{@meta.type})#validate! : called" } v = value! if opts = @options @@ -265,11 +298,13 @@ module CliGen end def check! : Nil + Log.trace { "Flag(long: #{@long}, type: #{@meta.type})#check! : called" } 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 + Log.trace { "Flag(long: #{@long}, type: #{@meta.type})#coerce : entered with \"#{raw}\"" } {% if T == Bool %} case raw when /^(t|true|y|yes|1)$/i @@ -353,15 +388,109 @@ module CliGen end private def parse_time(raw : String) : Time - formats = [ - CliGen::Regex::INPUT_DATETIME_REGEX, - CliGen::Regex::INPUT_DATE_REGEX - ] - 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) + get_location = -> (tz : String) { + sign = tz.starts_with?("-") ? -1 : 1 + hour = tz[1..2].to_i + min = tz[3..4].to_i + Time::Location.fixed(tz, sign * ((hour * 3600) + (min * 60))) + } + + get_time = ->(sign : String, quantity : Int32, unit : String) { + time = Time.local + diff = case unit + when /year/ + quantity.year + when /month/ + quantity.month + when /day/ + quantity.day + when /hour/ + quantity.hour + when /minute/ + quantity.minute + when /second/ + quantity.second + else + raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : #{unit} is not a valid unit (valid: year, month, day, hour, minute, second)") + end + + case sign + when "+" + time + diff + when "-" + time - diff + else + raise CliGen::InvalidFlagValueError.new("Flag({{T}}, long: #{@long_key}) : #{sign} is not a valid modifier (valid: - or +)") + end + } + + case raw + when CliGen::Regex::INPUT_DATE_EPOCH_WITH_TIMEZONE + epoch = $1 + tz = $2 + ::Time.unix(epoch.to_i).in(get_location.call(tz)) + when CliGen::Regex::INPUT_DATE_EPOCH + ::Time.unix($1.to_i) + when CliGen::Regex::INPUT_DATE_FULL + ::Time.parse!(raw, CliGen::Format::INPUT_DATE_FULL) + when CliGen::Regex::INPUT_DATE_PARTIAL + ::Time.parse_local(raw, CliGen::Format::INPUT_DATE_PARTIAL) + when CliGen::Regex::INPUT_DATE_SIMPLE_WITH_TIMEZONE + ::Time.parse!(raw, CliGen::Format::INPUT_DATE_SIMPLE_WITH_TIMEZONE) + when CliGen::Regex::INPUT_DATE_SIMPLE + ::Time.parse_local(raw, CliGen::Format::INPUT_DATE_SIMPLE) + when CliGen::Regex::INPUT_DATE_RELATIVE_WITH_TIMEZONE + if match = raw.match(CliGen::Regex::INPUT_DATE_RELATIVE_WITH_TIMEZONE) + sign = match["sign"] + quantity = match["quantity"].to_i32 + unit = match["unit"] + tz = match["timezone"] + get_time.call(sign, quantity, unit).in(get_location.call(tz)) + else + raise CliGen::InvalidFlagValueError.new("#{CliGen::APPNAME}: How did regex break?") + end + when CliGen::Regex::INPUT_DATE_RELATIVE + if match = raw.match(CliGen::Regex::INPUT_DATE_RELATIVE) + sign = match["sign"] + quantity = match["quantity"].to_i32 + unit = match["unit"] + get_time.call(sign, quantity, unit) + else + raise CliGen::InvalidFlagValueError.new("#{CliGen::APPNAME}: How did regex break?") + end else - Time.parse_local(raw, CliGen::Format::INPUT_DATE_FORMAT) + raise CliGen::InvalidFlagValueError.new(<<-EOF + #{CliGen::APPNAME}: Flag(type: #{@meta.type}, long: #{@long_key}) : invalid date/time format "#{raw}". + + Valid are: + 1) %Y-%m-%d %H:%M:%S %z + 2) %Y-%m-%d %H:%M:%S + 3) %Y-%m-%d %z + 4) %Y-%m-%d + 5) %s %z + 6) %s + 7) [+-][0-9]+ [years|months|days|hours|minutes|seconds] %z + 8) [+-][0-9]+ [years|months|days|hours|minutes|seconds] + + Note on format: + # Timezone Offset (ex: -0500 == CST) + %z == [-+][0-9]{4} + # Year (ex: 2026) + %Y == [0-9]{4} + # month + %m == [0-9]{2} + # day + %d == [0-9]{2} + # hour + %H == [0-9]{2} + # minute + %S == [0-9]{2} + # epoch time + %s == @[0-9]+ + + + EOF + ) end end end diff --git a/src/cligen/format.cr b/src/cligen/format.cr index 9b66647..f76b82c 100644 --- a/src/cligen/format.cr +++ b/src/cligen/format.cr @@ -1,4 +1,6 @@ module CliGen::Format - INPUT_DATE_FORMAT = "%Y-%m-%d" - INPUT_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" + INPUT_DATE_PARTIAL = "%Y-%m-%d %H:%M:%S" + INPUT_DATE_FULL = "%Y-%m-%d %H:%M:%S %z" + INPUT_DATE_SIMPLE_WITH_TIMEZONE = "%Y-%m-%d %z" + INPUT_DATE_SIMPLE = "%Y-%m-%d" end diff --git a/src/cligen/global_flag.cr b/src/cligen/global_flag.cr index 9242ff4..90ae559 100644 --- a/src/cligen/global_flag.cr +++ b/src/cligen/global_flag.cr @@ -33,7 +33,7 @@ module CliGen {% else %} {% env_var = long.gsub(/--/, "").upcase %} {% end %} - CliGen::GLOBAL_FLAGS << CliGen::Flag({{type}}).new( + ::CliGen::GLOBAL_FLAGS << CliGen::Flag({{type}}).new( var: "", short: {{short}}, long: {{long}}, diff --git a/src/cligen/regex.cr b/src/cligen/regex.cr index f3c7587..d713df6 100644 --- a/src/cligen/regex.cr +++ b/src/cligen/regex.cr @@ -1,10 +1,18 @@ module CliGen::Regex FLAG_REGEX=/^(-[a-zA-Z]|--[a-zA-Z-_0-9]+)$/ FLAG_WITH_ARG=/^(?(-[a-zA-Z]|--[a-zA-Z-_]+))="?(?\S+?)"?$/ - FLAG_MULTIPLE_SHORT=/^-[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}$/ + INPUT_TIMEZONE = /(?[-+][0-9]{4})/ + INPUT_TIME = /(?