diff --git a/src/cligen/app.cr b/src/cligen/app.cr index 35446e1..8a52bec 100644 --- a/src/cligen/app.cr +++ b/src/cligen/app.cr @@ -14,11 +14,8 @@ module CliGen @@instance = self end - def check! : Nil - CliGen::GLOBAL_FLAGS.each(&.check!) - # @flags is the amalgamation of all child flags — checked by the commands themselves - check_for_duplicates!([CliGen::GLOBAL_FLAGS, @flags].flatten) - @commands.each(&.check!) + def check! + super end # Convenience entry point; defaults to ARGV diff --git a/src/cligen/app/generate.cr b/src/cligen/app/generate.cr index 73c42f0..df1691b 100644 --- a/src/cligen/app/generate.cr +++ b/src/cligen/app/generate.cr @@ -30,7 +30,7 @@ module CliGen validate: {% if anno[:validation] %} {{anno[:validation]}} {% else %} nil {% end %}, on_match: {% if anno[:on_match] %} {{anno[:on_match]}} {% else %} nil {% end %}, options: {% if anno[:options] %} {{anno[:options]}} {% else %} nil {% end %}, - delimiter: {% if anno[:delimiter] %} {{anno[:delimiter]}} {% else %} nil {% end %}, + delimiter: {% if anno[:delimiter] %} {{anno[:delimiter]}} {% else %} "," {% end %}, format: {% if anno[:format] %} {{anno[:format]}} {% else %} nil {% end %} ) {% debug if env("DEBUG") %} diff --git a/src/cligen/command.cr b/src/cligen/command.cr index f5fc097..751906b 100644 --- a/src/cligen/command.cr +++ b/src/cligen/command.cr @@ -7,6 +7,7 @@ require "./command/selection" require "./command/help_template" require "./command/subcommand" require "./command/def_init" +require "./command/define_command_initializer" module CliGen class Command @@ -18,28 +19,7 @@ module CliGen {% if anno[:def_init] %} def_init {% end %} - def initialize(*, handler : CliGen::BaseCommandNode) - {% verbatim do %} - {% for var in @type.instance_vars %} - {% anno = (var.annotation(CliGen::Argument) || var.annotation(CliGen::Selection)) %} - {% if anno %} - {% raise "ERROR : #{@type.name}#initialize : Argument '#{var.name}' cannot be a nilable type (#{var.type}) — flags always resolve to a concrete value" if var.type.union? %} - if flg = handler.flags.find{|f| f.var == {{var.name.stringify}} && f.long == {{anno[:long]}}} - @{{var.id}} = flg.as(CliGen::Flag({{var.type}})).value! - else - raise "ERROR : {{@type.name}}\#{{@def.name}} : No flag found for \"{{var.name}}\"?" - end - {% else %} - {% raise "ERROR : #{@type.name}#{@def.name} : Instance Variable(#{var.name}) is not handled by CliGen and does not have a default value" unless var.default_value %} - @{{var.id}} = {{var.default_value}} - {% end %} - {% end %} - - {% if @type.has_method? :after_initialize %} - after_initialize - {% end %} - {% end %} - end + define_command_initializer end {% end %} end diff --git a/src/cligen/command/argument.cr b/src/cligen/command/argument.cr index c3f88eb..43252db 100644 --- a/src/cligen/command/argument.cr +++ b/src/cligen/command/argument.cr @@ -44,11 +44,17 @@ module CliGen {% if type.resolve <= Array && format.nil? %} {% elem = type.type_vars.first %} {% unless elem == Int32 %} - {% raise "ERROR : CliGen::Command.argument(#{name}) : When providing custom data types for Array(T) or using Array(String) you must provide a format for argument filtering so that parsing can be done deterministically" %} + {% if format.nil? && options.nil? %} + {% raise "ERROR : CliGen::Command.argument(#{name}) : When providing custom data types for Array(T) or using Array(String) you must provide a format or options for argument filtering so that parsing can be done deterministically" %} + {% end %} {% end %} {% end %} + {% if type.resolve < Array && ! options.nil? %} + @[CliGen::Argument(short: {{short}}, long: {{long}}, description: {{description}}, validation: {{validation}}, on_match: {{on_match}}, options: [{{options}}], delimiter: {{delimiter}}, format: {{format}})] + {% else %} @[CliGen::Argument(short: {{short}}, long: {{long}}, description: {{description}}, validation: {{validation}}, on_match: {{on_match}}, options: {{options}}, delimiter: {{delimiter}}, format: {{format}})] + {% end %} @{{variable}} {% if def_getter %} diff --git a/src/cligen/command/define_command_initializer.cr b/src/cligen/command/define_command_initializer.cr new file mode 100644 index 0000000..87f7d43 --- /dev/null +++ b/src/cligen/command/define_command_initializer.cr @@ -0,0 +1,29 @@ +module CliGen + class Command + macro define_command_initializer + def initialize(*, handler : CliGen::BaseCommandNode) + {% verbatim do %} + {% for var in @type.instance_vars %} + {% anno = (var.annotation(CliGen::Argument) || var.annotation(CliGen::Selection)) %} + {% if anno %} + {% raise "ERROR : #{@type.name}#initialize : Argument '#{var.name}' cannot be a nilable type (#{var.type}) — flags always resolve to a concrete value" if var.type.union? %} + if flg = handler.flags.find{|f| f.var == {{var.name.stringify}} && f.long == {{anno[:long]}}} + flg.validate! + @{{var.id}} = flg.as(CliGen::Flag({{var.type}})).value! + else + raise "ERROR : {{@type.name}}\#{{@def.name}} : No flag found for \"{{var.name}}\"?" + end + {% else %} + {% raise "ERROR : #{@type.name}#{@def.name} : Instance Variable(#{var.name}) is not handled by CliGen and does not have a default value" unless var.default_value %} + @{{var.id}} = {{var.default_value}} + {% end %} + {% end %} + + {% if @type.has_method? :after_initialize %} + after_initialize + {% end %} + {% end %} + end + end + end +end diff --git a/src/cligen/command_node.cr b/src/cligen/command_node.cr index c247cf0..4bcd8fd 100644 --- a/src/cligen/command_node.cr +++ b/src/cligen/command_node.cr @@ -29,7 +29,7 @@ module CliGen @pre_run_commands : Array(RunCommand), @post_run_commands : Array(RunCommand), @description : String? = nil - ) + ) @flags = (flags + CliGen::GLOBAL_FLAGS + @commands.flat_map(&.flags)).uniq end @@ -67,8 +67,12 @@ module CliGen end end - def get(flag_long : String) : BaseFlag? - @flags.find{|f| f.long_key == flag_long} + def get(*, long : String) : BaseFlag? + @flags.find{|f| f.long_key == long} + end + + def get(*, short : String) : BaseFlag? + @flags.find{|f| f.short == short} end def find_match(arg : String) @@ -142,6 +146,11 @@ module CliGen {% end %} end + def verbose? : Bool + @verbose_flag ||= get(long: "--verbose").not_nil!.as(Flag(Bool)) + @verbose_flag.not_nil!.value! + end + def help : String {% begin %} {% if T.has_constant? "HELP_TEMPLATE" %} @@ -162,8 +171,10 @@ module CliGen @flags.each(&.check!) check_for_duplicates!(@flags) @commands.each(&.check!) + {% unless T == Nil %} raise "ERROR : CommandNode({{T}})#check! : {{T}} has no subcommands and no #main defined" \ if subcommands.empty? && !{{T.has_method?(:main)}} + {% end %} end def process(args : Array(CliGen::Arg)) : Nil diff --git a/src/cligen/flag.cr b/src/cligen/flag.cr index cd3a892..cb8ce08 100644 --- a/src/cligen/flag.cr +++ b/src/cligen/flag.cr @@ -1,6 +1,14 @@ require "./arg" module CliGen + # To be able to store metadata for use in the help output + record FlagMeta, + type : String, + array : Bool, + format : String?, + default : String?, + options : Array(String)? + abstract class BaseFlag getter var : String getter short : String? @@ -9,6 +17,7 @@ module CliGen getter env_var : String getter description : String getter delimiter : String + getter meta : FlagMeta def initialize( @var : String, @@ -16,7 +25,8 @@ module CliGen @long : String, @env_var : String, @description : String, - @delimiter : String + @delimiter : String, + @meta : FlagMeta ) # if the user provides just a "--long" I want the @long_key to match it if @long =~ /\s|=/ @@ -40,7 +50,7 @@ module CliGen @value : T? @default : T? @options : Array(T)? - @validate : (T -> Bool)? + @validate : (T -> Bool)? @on_match : Proc(Nil)? @format : ::Regex? @@ -57,7 +67,21 @@ module CliGen @on_match : Proc(Nil)? = nil, @format : ::Regex? = nil ) - super(var, short, long, env_var, description, delimiter) + {% unless T.has_method? :to_s %} + {% raise "ERROR : Flag({{T}}, long: #{long}) : Error your flag type must have a to_s method" %} + {% end %} + {% if T <= Array %} + {% elem = T.type_vars.first %} + {% raise "ERROR : Flag({{T}}, long: #{long}) : Error your flag subtype must have a to_s method" unless elem.has_method? :to_s %} + {% end %} + meta = FlagMeta.new( + type: {{T.stringify}}, + array: {{ T < Array ? true : false }}, + options: {% if T < Array %} @options.try(&.first.map(&.to_s)) {% else %} @options.try(&.map(&.to_s)) {% end %}, + default: @default.try(&.to_s), + format: @format.try(&.source) + ) + super(var, short, long, env_var, description, delimiter, meta) end def requires_arg? : Bool @@ -79,16 +103,31 @@ module CliGen argv.each do |arg| break if arg.flag? unless @format.nil? - unless arg.value =~ @format - puts "DEBUG : Flag({{T}}, long: #{@long_key}) : #{arg.value} was not found to be matching the defined filter #{@format}. So breaking from parse loop" if ENV["DEBUG"] - break + unless arg.value.includes?(@delimiter) + unless arg.value =~ @format + puts "DEBUG : Flag({{T}}, long: #{@long_key}) : #{arg.value} was not found to be matching the defined filter #{@format}. So breaking from parse loop" if ENV["DEBUG"]? + break + end end end {% if elem == Int32 %} - abort "ERROR : Flag({{T}}) : Provided arguemnt(#{arg.value}) was not an integer" unless arg.int? - (@value ||= [] of Int32) << arg.value.to_i + if arg.value.includes?(@delimiter) + @value = (@value || [] of Int32) + arg.value.split(@delimiter).map(&.to_i32) + 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 + end {% elsif elem == String %} - (@value ||= [] of String) << arg.value + 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 + end + v + } + else + (@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)} @@ -101,12 +140,17 @@ module CliGen 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 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 + end + @value = argv.first.value argv.first.processed {% elsif T.class < CliGen::Parsable %} @@ -161,11 +205,17 @@ module CliGen v = value! if opts = @options - abort "#{CliGen::APPNAME}: '#{v}' is not a valid value for #{@long_key} (valid: #{opts.join(", ")})" unless opts.includes?(v) + {% if T < Array %} + v.each do |v2| + abort "#{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) + {% end %} end if check = @validate - abort "#{CliGen::APPNAME}: validation failed for #{@long_key}" unless check.call(v) + abort "#{CliGen::APPNAME}: validation failed for #{@long_key} when testing #{v}" unless check.call(v) end end diff --git a/src/cligen/global_flag.cr b/src/cligen/global_flag.cr index 4b2ecc3..9242ff4 100644 --- a/src/cligen/global_flag.cr +++ b/src/cligen/global_flag.cr @@ -3,6 +3,15 @@ require "./flag" module CliGen GLOBAL_FLAGS = [] of BaseFlag + GLOBAL_FLAGS << Flag(Bool).new( + var: "", + short: "-v", + long: "--verbose", + env_var: "VERBOSE", + default: false, + description: "Enable verbose output from program & help output" + ) + macro add_global_flag(type, long, description, env_var = nil, short = nil, validation = nil, &on_match) {% raise "ERROR : CliGen.add_global_flag : type must be a TypeNode" unless type.is_a? TypeNode %} {% raise "ERROR : CliGen.add_global_flag : long must begin a StringLiteral" unless long.is_a? StringLiteral %} diff --git a/src/cligen/template/cmd_help.ecr b/src/cligen/template/cmd_help.ecr index 52ef103..9dda106 100644 --- a/src/cligen/template/cmd_help.ecr +++ b/src/cligen/template/cmd_help.ecr @@ -9,10 +9,21 @@ Description: <%= @description %> Flags: --------------------------------------------------------------- <%- @flags.each do |flag| -%> - <%- unless flag.short.nil? -%> - <%= "%-#{len}s %s" % ["#{flag.short.not_nil!.strip},#{flag.long.strip}", flag.description.strip] %> + <%- if flag.short.nil? -%> + <%- flags = [flag.long.strip] -%> <%- else -%> - <%= "%-#{len}s %s" % [flag.long.strip, flag.description.strip] %> + <%- flags = [flag.short,flag.long.strip] -%> + <%- end -%> + <%= "%-#{len}s %s" % [flags.join(","), flag.description.strip] %><%= flag.meta.options.nil? ? "" : " (valid: #{flag.meta.options.not_nil!.join(&.to_s)})" %><%= flag.meta.default.nil? ? "" : " (default: #{flag.meta.default})" %> + <%- 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 -%>