Made several modifications:

- Added more complex date/time parsing for Time objects. This includes
  the ability to provide times in epoch format and several others with
  the ability to provide inline timezone offsets without external
  codebase works to convert them. Alongside this I added the ability to
  do relative time (ex: [+-]2 hours|minutes|days) (along with the ability
  to specify timezone if needed)
- Littered the entire codebase with Log object calls to show state of
  everything as the framework does it's stuff. Just needs Log to be
  configured in the codebase consuming my shard
- removed flag collection for use in displaying all flags in the app
  help output. So it will only show global flags & subcommands
- Changed CommandNode#get(short:) & get(long:) & flag?(arg : String)
  methods to check @flags -> @commands.@flags -> CliGen::GLOBAL_FLAGS
  when finding flags. Making it possible to not PRINT out the flag
  output but still be able to set values to a child flag recursively.
This commit is contained in:
2026-08-29 18:51:08 -05:00
parent fd4c0eb171
commit fe6112795a
9 changed files with 259 additions and 41 deletions
+7
View File
@@ -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
-3
View File
@@ -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,
@@ -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 %}
+58 -16
View File
@@ -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 <flag>=<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 <flag>=<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
+145 -16
View File
@@ -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
Time.parse_local(raw, CliGen::Format::INPUT_DATE_FORMAT)
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
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
+4 -2
View File
@@ -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
+1 -1
View File
@@ -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}},
+11 -3
View File
@@ -1,10 +1,18 @@
module CliGen::Regex
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-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 = /(?<timezone>[-+][0-9]{4})/
INPUT_TIME = /(?<time>(?<hour>[0-9]{2}):(?<minute>[0-9]{2}):(?<second>[0-9]{2}))/
INPUT_DATE_SIMPLE = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/
INPUT_DATE_SIMPLE_WITH_TIMEZONE = /^#{INPUT_DATE_SIMPLE}\s+#{INPUT_TIMEZONE}$/
INPUT_DATE_FULL = /^(?<date>#{INPUT_DATE_SIMPLE})\s+#{INPUT_TIME}\s+#{INPUT_TIMEZONE}$/
INPUT_DATE_PARTIAL = /^(?<date>#{INPUT_DATE_SIMPLE})\s+#{INPUT_TIME}$/
INPUT_DATE_RELATIVE = /(?<sign>[+-])?(?<quantity>[0-9]+)\s+(?<unit>seconds?|days?|hours?|minutes?|years?)/
INPUT_DATE_RELATIVE_WITH_TIMEZONE = /#{INPUT_DATE_RELATIVE}\s+#{INPUT_TIMEZONE}/
INPUT_DATE_EPOCH = /@(?<epoch>[0-9]+)/
INPUT_DATE_EPOCH_WITH_TIMEZONE = /#{INPUT_DATE_EPOCH}\s+#{INPUT_TIMEZONE}/
FLOAT = /^[-+]?[[:digit:]]+(\.[[:digit:]]+)?$/
UINT = /^[[:digit:]]+$/
+26
View File
@@ -27,6 +27,32 @@ Flags:
<%- end -%>
<%- end -%>
<%- end -%>
<%- unless CliGen::GLOBAL_FLAGS.empty? -%>
<%- len = CliGen::GLOBAL_FLAGS.map{|f| f.short.nil? ? f.long.size : "#{f.short},#{f.long}".size}.max + 5 -%>
Global Flags
---------------------------------------------------------------
<%- CliGen::GLOBAL_FLAGS.each do |flag| -%>
<%- if flag.short.nil? -%>
<%- flags = [flag.long.strip] -%>
<%- else -%>
<%- 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