Initial commit

This commit is contained in:
Tristan Ancelet
2026-02-23 10:30:03 -06:00
commit b91ecad7e5
10 changed files with 339 additions and 0 deletions

70
src/cli_generator.cr Normal file
View File

@@ -0,0 +1,70 @@
require "option_parser"
module CliGenerator
VERSION = "0.1.0"
annotation DefaultFlag
end
macro define_section(section_name, parser)
{{parser}}.separator ""
{{parser}}.separator "{{section_name.id}}".colorize(:green)
{{parser}}.separator "-------------------------------------------------------------------------------".colorize(:blue)
end
macro define_default_flags(parser)
define_section("Misc Flags", {{parser}})
{{parser}}.on("-h", "--help", "Print out this help output"){ abort {{parser}} }
{{parser}}.invalid_option{|opt|
abort "#{PROGRAM_NAME} : invalid_option : Inavlid option provided #{opt}"
}
{{parser}}.invalid_option{|opt|
case opt
when /^-+[a-z-]+/
Logger.debug "#{PROGRAM_NAME} : ERROR : {{parser}} : invalid_option : Inavlid option provided #{opt}"
else
abort "#{PROGRAM_NAME} : ERROR : {{parser}} : invalid_option : Inavlid option provided #{opt}"
end
}
{{parser}}.missing_option{|opt|
abort "ERROR : {{parser}} : missing_option : Argument was not provided to #{opt}"
}
{{parser}}.unknown_args{|opt|
unless opt.empty?
case opt.first
when /^-+[a-z-]+$/
Logger.debug "ERROR : {{parser}} : unknown_args : #{opt} not a configured argument"
else
abort "ERROR : {{parser}} : unknown_args : #{opt} not a configured argument"
end
end
}
end
macro define_root_parser
ROOT_COMMAND = OptionParser.new do |parser|
parser.banner = "#{PROGRAM_NAME} [command] [flags]"
{% commands = ::CliGenerator::Command.subclasses %}
{% raise "ERROR : No commands defined (no subclasses of ::CliGenerator::Command were found)" if commands.empty? %}
{% for command in commands %}
{{command.name}}.make_parser(parser)
{% end %}
define_default_flags(parser)
abort parser if ARGV.empty?
end
end
macro finished
define_root_parser
end
def self.parse
ROOT_COMMAND.parse
end
end
require "./command"

157
src/command.cr Normal file
View File

@@ -0,0 +1,157 @@
require "colorize"
require "time"
require "./command/parser"
module CliGenerator
annotation CommandPreRun
end
annotation CommandInfo
end
annotation CommandArgument
end
annotation SubCommand
end
class Command
extend CliGenerator::Parser
@@action : String = ""
macro inherited
macro finished
define_actions
define_header
define_runner
define_parser
end
end
macro define_actions
ACTIONS : Array(String) = {{@type.class.methods.select(&.annotation(::CliGenerator::SubCommand)).map(&.name.stringify)}}
end
macro define_header
{% name = @type.name.split("::").last.downcase.id %}
{% info_annos = @type.class.methods.select(&.annotation(::CliGenerator::SubCommand)).map(&.annotation(::CliGenerator::SubCommand)) %}
HEADER = [
"#{PROGRAM_NAME} {{name}} [[flags]]",
"",
{% unless info_annos.empty? %}
"Examples:".colorize(:green),
"-------------------------------------------------------------------------------".colorize(:blue),
{% for anno in info_annos %}
{% examples = anno[:examples].resolve %}
{% for example in examples %}
{{example}},
{% end %}
{% end %}
{% end %}
].join("\n")
end
macro define_argument(arg_name, variable = nil, type = String, short = "", long = "", description = nil, subtype = Nil, format = nil, check = nil, def_getter = false, default = nil, logger = nil)
{% raise "define_argument : You must provide a format or set it to \"auto\" to use builtin formats when defining a Time argument" if type.id.stringify == "Time" && format == nil %}
{% raise "define_argument : You MUST provide a description" unless description %}
{% variable = arg_name unless variable %}
{% _type = type.id.stringify %}
{% _subtype = subtype.id.stringify %}
{% if _type == "String" %}
@@{{variable}} : {{type}} = ""
{% elsif _type == "Bool" %}
@@{{variable}} : {{type}} = false
{% elsif _type == "Int32" %}
@@{{variable}} : {{type}} = 0
{% elsif _type == "Array" %}
{% raise "define_argument : subtype can only be Int32 or String" unless %w[ Int32 String ].includes?(_subtype) %}
@@{{variable}} : {{type}}({{subtype}}) = [] of {{subtype}}
{% elsif _type == "Time" %}
@@{{variable}} : {{type}} = Time.unix(seconds: 0)
{% end %}
{% if def_getter %}
def self.get_{{arg_name}} : {{type}}
@@{{variable}}
end
{% end %}
@[CommandArgument(type: {{type}}, short: {{short}}, long: {{long}}, description: {{description}})]
def self.{{arg_name}}(var : String) : Nil
{% if check %}
## If the check exists go ahead and call it
{{check}}(var)
{% end %}
{% if format %}
abort "ERROR : Action : {{arg_name}} : Incorrect format for provided data #{var}" unless var =~ {{format}}
{% end %}
## If a logger method has been provided by the user
{% if logger %}
{{logger.id}} "Command : subclass : argument_setter : {{arg_name}} : Entered with #{var}"
{% end %}
{% if _type == "Time" %}
formats = [
CliGenerator::Regex::INPUT_DATETIME_REGEX,
CliGenerator::Regex::INPUT_DATE_REGEX
]
abort "ERROR : Command : {{arg_name}} : Incorrect format for provided data #{var}" unless formats.any?{|f| var.match(f)}
if var =~ formats[0]
@@{{variable}} = Time.parse_local(var, CliGenerator::Format::INPUT_DATETIME_FORMAT)
elsif var =~ formats[1]
@@{{variable}} = Time.parse_local(var, CliGenerator::Format::INPUT_DATE_FORMAT)
end
{% elsif _type == "Array" %}
{% if _subtype == "Int32" %}
var : Int32 = var.to_i
{% elsif _subtype == "String" %}
{% else %}
var = {{subtype}}.new(var)
{% end %}
@@{{variable}} << var
{% elsif _type == "Bool" %}
@@{{variable}} = true
{% elsif _type == "Int32" %}
@@{{variable}} = var.to_i
{% elsif _type == "String" %}
@@{{variable}} = var
{% end %}
end
end
macro define_runner
def self.run
{% pre_run_commands = @type.class.methods.select(&.annotation(::CliGenerator::CommandPreRun)) %}
{% unless pre_run_commands.empty? %}
## Ensuring that all runs that are tagged with the the CommandPreRun annotation are run
{% for method in pre_run_commands %}
{{method.name}}
{% end %}
{% end %}
{% methods = @type.class.methods.select(&.annotation(::CliGenerator::SubCommand)) %}
{% raise "ERROR : No commands defined for #{@type.name}" if methods.empty? %}
{% begin %}
case @@action
{% for method in methods %}
when {{method.name.stringify}}
{{method.name}}
{% end %}
else
abort "ERROR : No action provided to command #{@type.name}"
end
{% end %}
end
end
def self.action=(action : String)
abort "ERROR : Action(#{action}) is not valid. Only #{ACTIONS.join(", ")} are acceptable" unless ACTIONS.includes?(action)
@@action = action
end
end
end

51
src/command/parser.cr Normal file
View File

@@ -0,0 +1,51 @@
module CliGenerator::Parser
macro extended
macro define_parser
def self.make_parser(parent_parser : OptionParser) : OptionParser
{% puts "#{@type.name} OptionParser is being generated" %}
{% name = @type.name.split("::").last %}
{% var = name.downcase %}
{% info = @type.annotation(::CliGenerator::CommandInfo) %}
subparser = OptionParser.new do |parser|
parser.banner = {{@type.name}}::HEADER
{% subcommands = @type.class.methods.select(&.annotation(::CliGenerator::SubCommand)) %}
{% if subcommands.size > 0 %}
define_section("Subcommands", parser)
{% for subcommand in subcommands %}
{% subcommand_anno = subcommand.annotation(::CliGenerator::SubCommand) %}
parser.on({{subcommand.name.stringify}}, {{subcommand_anno[:description]}}){
{{@type.name}}.action= {{subcommand.name.stringify}}
}
{% end %}
{% end %}
{% arguments = @type.class.methods.select(&.annotation(::CliGenerator::CommandArgument)) %}
{% if arguments.size > 0 %}
define_section("Provide Arguments", parser)
{% for argument in arguments %}
{% argument_anno = argument.annotation(::CliGenerator::CommandArgument) %}
{% if argument_anno[:short] == "" && argument_anno[:long] != "" %}
parser.on({{argument_anno[:long]}}, {{argument_anno[:description]}}){|val|
{% elsif argument_anno[:short] != "" && argument_anno[:long] == "" %}
parser.on({{argument_anno[:short]}}, {{argument_anno[:description]}}){|val|
{% else %}
parser.on({{argument_anno[:short]}}, {{argument_anno[:long]}}, {{argument_anno[:description]}}){|val|
{% end %}
{{@type.name}}.{{argument.name}}(val)
}
{% end %}
{% end %}
define_default_flags(parser)
end
parent_parser.on({{var}}, {{info[:description]}}){
ARGV.delete({{var}})
abort {{var.id}} if ARGV.empty?
subparser.parse
{{@type.name}}.run
}
end
end
end
end

4
src/format.cr Normal file
View File

@@ -0,0 +1,4 @@
module CliGenerator::Format
INPUT_DATETIME_FORMAT = "%Y-%m-%d"
INPUT_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
end

4
src/regex.cr Normal file
View File

@@ -0,0 +1,4 @@
module CliGenerator::Regex
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}$/
end