Got Array(T) working in flag.cr. Hacky stuff it is to convince the compiler that a value isn't going to be nil if I set it

This commit is contained in:
Tristan Ancelet
2026-08-10 12:57:31 -05:00
parent ac5711c0df
commit 29327a3e83
17 changed files with 177 additions and 64 deletions
+9
View File
@@ -0,0 +1,9 @@
root = true
[*.cr]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
+5 -7
View File
@@ -44,22 +44,20 @@ module MyModule
"myutil mycommand do_thing --myvar 5"
]
subcommand do_thing : Int32,
description: "Do the THING",
examples: MyModule::MyCommand::DO_THING_EXAMPLES \
do
subcommand do_thing, description: "Do the THING", examples: MyModule::MyCommand::DO_THING_EXAMPLES do
output = 0
@myvar.times do |i|
puts "thing done %i times" % [ i + 1 ]
output += i
end
output
puts "Total : %i" % output
end
end
CliGen::App.process(ARGV)
CliGen::App.process
end
----
+6
View File
@@ -1,3 +1,4 @@
require "./cligen/coercable"
require "./cligen/parsable"
require "./cligen/annotations"
require "./cligen/format"
@@ -10,6 +11,11 @@ require "./cligen/app"
module CliGen
VERSION = "0.1.0"
macro override_help_template(filepath)
{% raise "ERROR : CliGen.override_help_template : File(#{filepath}) doesn't exist" unless file_exists?(filepath) %}
CliGen::HELP_OVERRIDE_TEMPLATE = {{`readlink -f #{filepath}`.strip.stringify}}
end
APPNAME = File.basename(PROGRAM_NAME)
record AdditionalDefaultFlag,
+2 -2
View File
@@ -30,7 +30,7 @@ module CliGen
validate: {% if anno[:validate] %} {{anno[:validate]}} {% else %} nil {% end %},
on_match: {% if anno[:on_match] %} {{anno[:on_match]}} {% else %} nil {% end %}
)
{% debug %}
{% debug if env("DEBUG") %}
{% end %}
# Keep a copy of every flag on the root for global matching
@@ -44,7 +44,7 @@ module CliGen
post_run_commands: cmd_post_run_cmds,
description: {{cmd_info[:description]}}
)
{% debug %}
{% debug if env("DEBUG") %}
{% end %}
{% end %}
+24
View File
@@ -24,6 +24,30 @@ module CliGen
def initialize(@value, @index)
end
def flag? : Bool
if @value =~ CliGen::Regex::FLAG_REGEX
true
else
false
end
end
def int? : Bool
if @value =~ /^[[:digit:]]+$/
true
else
false
end
end
def float? : Bool
if @value =~ /^[[:digit:]]+(\.[[:digit:]]+)?$/
true
else
false
end
end
# This serves as a trigger that tells the object that it has been processed
#
# This will raise an exception if it is re-called after already having been
+1 -1
View File
@@ -1,3 +1,3 @@
module CliGen::Coercable
abstract def coerce(arg : String) : self
abstract def coerce(arg : String)
end
+1 -1
View File
@@ -4,7 +4,7 @@ require "time"
require "./annotations"
require "./command/argument"
require "./command/selection"
require "./command/trigger"
require "./command/help_template"
require "./command/subcommand"
module CliGen
+15 -7
View File
@@ -1,28 +1,34 @@
module CliGen
class Command
macro argument(variable, long, description, short = nil, validation = nil, on_match = nil)
macro argument(variable, description, long = nil, short = nil, validation = nil, on_match = nil, def_setter = false)
{% raise "ERROR : CliGen::Command.argument : def_setter must be a Bool" unless def_setter.is_a? BoolLiteral %}
{% raise "ERROR : CliGen::Command.argument : First argument (#{variable}) must be a TypeDeclaration (ex: '<var> : <type> [= val]')" unless variable.is_a? TypeDeclaration %}
{% name = variable.var %}
{% type = variable.type %}
{% if short %}
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided short must be a string" unless short.is_a? StringLiteral %}
{% end %}
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided long must be a string" unless long.is_a? StringLiteral || long == nil %}
{% raise "ERROR : CliGen::Command.argument(#{name}) : You must provide a short or long" unless long || short %}
{% if long %}
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided long must be a string" unless long.is_a? StringLiteral %}
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided long must match --[a-zA-Z0-9-_]+" unless long =~ /^--[a-zA-Z0-9-_]+/ %}
{% else %}
{% long = "--#{name.downcase}" %}
{% end %}
{% raise "ERROR : CliGen::Command.argument(#{name}) : You must provide a description" unless description %}
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided description must be a String" unless description.is_a? StringLiteral %}
{% unless on_match.nil? %}
{% puts on_match %}
{% puts "DEBUG : #{@type.name}.argument(#{name}) : OnMatch:\n\tid: #{on_match}\n\treturn_type: #{on_match.return_type}\n\tinput_vars: #{on_match.args}" if env("DEBUG")%}
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided on_match must be a Proc" unless on_match.is_a? ProcLiteral %}
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided on_match return type must be a Bool" unless on_match.return_type == Nil %}
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided on_match return type must be Nil" unless on_match.return_type.resolve == Nil %}
{% end %}
{% unless validation.nil? %}
{% puts "DEBUG : #{@type.name}.argument(#{name}) : Validation:\n\tid: #{validation}\n\treturn_type: #{validation.return_type}\n\tinput_vars: #{validation.args}" if env("DEBUG")%}
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation must be a Proc" unless validation.is_a? ProcLiteral %}
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation return type must be a Bool" unless validation.return_type == Bool %}
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation return type must be a Bool" unless validation.return_type.resolve == Bool %}
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation provided validation must have an input variable" if validation.args.empty? %}
{% arg = validation.args.first %}
{% unless arg.restriction == type %}
{% example = "->(#{arg.name} : #{type}) : #{type} { #{validation.body} }" %}
{% example = "->(#{arg.name} : #{type}) : Bool { #{validation.body} }" %}
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation input value must be #{type}. EX: #{example}" %}
{% end %}
{% end %}
@@ -30,12 +36,14 @@ module CliGen
@[CliGen::Argument(short: {{short}}, long: {{long}}, description: {{description}}, validation: {{validation}}, on_match: {{on_match}})]
@{{variable}}
{% if def_setter %}
def {{variable.var}}= (value : {{type}})
{% unless validation.nil? %}
raise "ERROR : #{@type.name}##{@def.name} : Provided value #{value} is not passing validation" unless {{validation}}.call(value)
{% end %}
@{{name}} = value
end
{% end %}
end
end
end
+10
View File
@@ -0,0 +1,10 @@
module CliGen
class Command
macro help_template(filepath)
{% raise "ERROR : CliGen::Command.help_template : #{filepath} does not exist" unless file_exists? filepath %}
HELP_TEMPLATE = {{`readlink -f #{filepath}`.strip.stringify}}
{% puts "DEBUG : #{@type.name} : Set HELP_TEMPLATE to #{filepath}" if env("DEBUG") %}
{% debug if env("DEBUG") %}
end
end
end
-21
View File
@@ -1,21 +0,0 @@
module CliGen
class Command
macro trigger(short, long, argument = nil, &on_match)
{% raise "ERROR : CliGen::Command.trigger : Provided short must be a string" unless short.is_a? StringLiteral %}
{% raise "ERROR : CliGen::Command.trigger : Provided long must be a string" unless long.is_a? StringLiteral %}
{% raise "ERROR : CliGen::Command.trigger : Must provide a block for on_match trigger" unless on_match %}
{% name = long.gsub(/--/, "") %}
@[CliGen::Trigger(short: {{short}}, long: {{long}}, argument: {{argument}})]
{% if argument %}
def self.__cligen_trigger__{{name}}__({{name}} : {{argument}}) : Nil
{{on_match.body}}
end
{% else %}
def self.__cligen_trigger__{{name}}__ : Nil
{{on_match.body}}
end
{% end %}
end
end
end
+22 -5
View File
@@ -33,10 +33,6 @@ module CliGen
@flags = (flags + CliGen::GLOBAL_FLAGS + @commands.flat_map(&.flags)).uniq
end
def help : String
ECR.render("src/cligen/template/cmd_help.ecr")
end
def check_for_duplicates!(flags : Array(BaseFlag)) : Nil
shorts = flags.compact_map(&.short)
short_duplicates = [] of String
@@ -79,6 +75,7 @@ module CliGen
if subcommand?(arg)
return CliGen::MatchType::SubCommand
end
case arg
when "-h", "--help"
CliGen::MatchType::Help
@@ -111,6 +108,10 @@ module CliGen
subcommands.any?{|f| f.name == arg}
end
def flag?(arg : String) : BaseFlag
@flags.any?(&.matches?(arg))
end
# Converts String array to Arg array and hands off to the typed process method
def process(args : Array(String)) : Nil
new_args = args.each_with_index.map { |arg, i| CliGen::Arg.new(value: arg, index: i) }.to_a
@@ -143,6 +144,22 @@ module CliGen
{% end %}
end
def help : String
{% begin %}
{% if T.has_constant? "HELP_TEMPLATE" %}
{% puts "#{T} was found to have HELP_TEMPLATE defined using this instead" if env("DEBUG") %}
ECR.render({{T.constant("HELP_TEMPLATE")}})
{% elsif CliGen.has_constant? "HELP_OVERRIDE_TEMPLATE" %} # If we have a global override use it
{% puts "Global override found. Using" if env("DEBUG") %}
ECR.render({{CliGen::HELP_OVERRIDE_TEMPLATE}})
{% else %}
{% puts "No type overrided help output. Using default" if env("DEBUG") %}
ECR.render("src/cligen/template/cmd_help.ecr")
{% end %} # otherwise
{% debug if env("DEBUG") %}
{% end %}
end
def check! : Nil
@flags.each(&.check!)
check_for_duplicates!(@flags)
@@ -238,7 +255,7 @@ module CliGen
{% if T.has_method?(:main) %}
cls.main
{% else %}
raise "ERROR : CommandNode({{T}})#{{@def.name}} : No subcommand matched and no #main defined"
raise "ERROR : CommandNode({{T}})\#{{@def.name}} : No subcommand matched and no #main defined"
{% end %}
end
{% end %}
+23 -16
View File
@@ -17,8 +17,8 @@ module CliGen
@description : String
)
# if the user provides just a "--long" I want the @long_key to match it
if @long.includes?(" ")
@long_key = @long.split(" ").first
if @long =~ /\s|=/
@long_key = @long.split(/\s|=/).first
else
@long_key = @long
end
@@ -51,7 +51,7 @@ module CliGen
@options : Array(T)? = nil,
@validate : (T -> Bool)? = nil,
@on_match : Proc(Nil)? = nil
)
)
super(var, short, long, env_var, description)
end
@@ -64,25 +64,31 @@ module CliGen
raise "ERROR : Flag({{T}}) : Array requires an argument but provided array is empty" if argv.empty?
end
if argv.first.flag?
abort "ERROR : Flag({{T}}) : Provided argument was a flag (#{argv.first.value})"
end
{% if T == Bool %}
@value = true
{% elsif T <= Array %}
{% raise "ERROR : Flag(#{T}) : You cannot define multiple types of array entries" if T.type_vars.size > 1 %}
{% elem = T.type_vars.first %}
argv.each do |arg|
break if arg.value.starts_with('-')
break if arg.flag?
{% 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
{% elsif elem == String %}
(@value ||= [] of String) << arg.value
{% elsif elem < CliGen::Coercable %}
if arg.value.includes?(",")
(@value ||= [] of {{elem}}) += arg.value.split(",").map{|i| {{elem}}.coerce(i)}
{% elsif elem.class < CliGen::Coercable %}
{% delim = elem.has_constant?("DELIMITER") ? elem.constant("DELIMITER") : ',' %}
if arg.value.includes?({{delim}})
@value = (@value || [] of {{elem}}) + arg.value.split({{delim}}).map{|i| {{elem}}.coerce(i)}
else
(@value ||= [] of {{elem}}) << {{elem}}.coerce(arg.value)
@value = (@value || [] of {{elem}}) + [({{elem}}.coerce(arg.value))]
end
{% else %}
{% raise "ERROR : Flag(#{T}) : #{elem} is not a coercable type. If you wish to coerce it from a bare string include CliGen::Coercable & implement the class method" %}
{% raise "ERROR : Flag(#{T}) : #{elem} is not a coercable type. If you wish to coerce it from a bare string extend with CliGen::Coercable & implement the class method" %}
{% end %}
arg.processed
end
@@ -95,7 +101,7 @@ module CliGen
{% elsif T == String %} # String
@value = argv.first.value
argv.first.processed
{% elsif T < CliGen::Parsable %}
{% elsif T.class < CliGen::Parseable %}
processed = argv.select(&.processed?)
@value = T.parse_args(argv)
post_processed = argv.select(&.processed?)
@@ -103,7 +109,7 @@ module CliGen
raise "ERROR : Flag({{T}}, long: #{@long_key})#process : Your {{T}}#process_args did not mark processed args as processed. Please check your code"
end
{% else %}
{% raise "ERROR : Flag({{T}}, long: #{@long_key})#process : Generic Type #{T} is not supported. To add support you must include CliGen::Parsable & implement the class method" %}
{% raise "ERROR : Flag({{T}}#process : Generic Type #{T} is not supported. To add support you must include CliGen::Parsable & implement the class method" %}
{% end %}
validate!
@@ -180,17 +186,18 @@ module CliGen
raw.split(',').map(&.to_i)
{% elsif elem == String %}
raw.split(',')
{% elsif elem < CliGen::Coercable %}
raw.split(',').map{|i| {{elem}}.coerce(i)}
{% elsif elem.class < CliGen::Coercable %}
{% delim = elem.has_constant?("DELIMITER") ? elem.constant("DELIMITER") : ',' %}
raw.split({{delim}}).map{|i| {{elem}}.coerce(i)}
{% else %}
{% raise "ERROR : Flag(#{T}) : #{elem} is not a coercable type. If you wish to coerce it from a bare string include CliGen::Coercable & implement the class method" %}
{% raise "ERROR : Flag(#{T}) : #{elem} is not a coercable type. If you wish to coerce it from a bare string extend CliGen::Coercable & implement the class method" %}
{% end %}
{% elsif T == String %} # String
raw
{% elsif T < CliGen::Coercable %}
{% elsif T.class < CliGen::Coercable %}
T.coerce(raw)
{% else %}
{% raise "ERROR : Flag(#{T}) : #{T} is not a coercable type. If you wish to coerce it from a bare string include CliGen::Coercable & implement the class method" %}
{% raise "ERROR : Flag(#{T}) : #{T} is not a coercable type. If you wish to coerce it from a bare string extend CliGen::Coercable & implement the class method" %}
{% end %}
end
+1 -1
View File
@@ -1,3 +1,3 @@
module CliGen::Parseable
abstract def parse_args(args : Array(CliGen::Arg)) : self
abstract def parse_args(args : Array(CliGen::Arg))
end
+1 -1
View File
@@ -1,5 +1,5 @@
module CliGen::Regex
FLAG_REGEX=/^(-[a-zA-Z]|--[a-zA-Z-_]+)$/
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-Z]+$/
SHORT_WITH_INLINE_ARG=/^-[a-zA-Z][a-zA-Z0-9]+$/
+2 -2
View File
@@ -17,7 +17,7 @@ Flags:
Other Commands
---------------------------------------------------------------
<%- @commands.each do |command| -%>
<%= "%-10s %s" % [ command.name, command.description ] %>
<%= "%-15s %s" % [ command.name, command.description ] %>
<%- end -%>
<%- end -%>
@@ -25,7 +25,7 @@ Other Commands
SubCommands of <%= @name %>:
---------------------------------------------------------------
<%- subcommands.each do |cmd| -%>
<%= "%-10s %s" % [cmd.name, cmd.description] %>
<%= "%-15s %s" % [cmd.name, cmd.description] %>
<%- end -%>
<%- cmds = subcommands.select{|c| ! c.examples.nil? } -%>
+52
View File
@@ -0,0 +1,52 @@
require "./src/cligen"
#CliGen.override_help_template "test.ecr"
module MyModule
class MyData
extend CliGen::Coercable
extend CliGen::Parseable
DELIMITER = '.'
getter val : Int32
def initialize(@val : Int32)
end
def self.parse_args(args : Array(CliGen::Arg)) : MyData
arg = args.first
arg.processed
new(val: arg.value.to_i32)
end
def self.coerce(arg : String) : MyData
if arg =~ /^[[:digit:]]+$/
new(val: arg.to_i32)
else
raise "ERROR parsing #{arg}"
end
end
end
@[CliGen::CommandInfo(description: "Test")]
class MyClass < CliGen::Command
argument(myvar3 : Array(MyModule::MyData), description: "Custom data")
argument(myvar2 : MyModule::MyData, description: "Custom data", validation: ->(v : MyModule::MyData) : Bool { v.val != 2 })
argument(myvar : Int32 = 123, description: "Set myvar", validation: ->(v : Int32) : Bool { (0..123).includes?(v) }, on_match: ->() : Nil { puts "Was called" })
subcommand dothing : Nil, description: "DOING ALL THE THINGS" do
puts "DO THE THING : myvar : #{@myvar}"
puts @myvar2.val
@myvar3.each do |var|
puts var.val
end
end
end
CliGen::App.process
end
+3
View File
@@ -0,0 +1,3 @@
Command: <%= @name %>
This was a test