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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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:]]+$/
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user