Initial commit
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Tristan Ancelet <tristan.ancelet@acumera.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
12
README.md
Normal file
12
README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# CliGenerator
|
||||
|
||||
This is a crystal project to manage setting up OptionParser objects based around "Command" objects and arguments you define inside them.
|
||||
|
||||
## Usage
|
||||
```crystal
|
||||
require "cli_generator"
|
||||
```
|
||||
|
||||
## Contributors
|
||||
|
||||
- [Tristan Ancelet](https://github.com/your-github-user) - creator and maintainer
|
||||
9
shard.yml
Normal file
9
shard.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: cli_generator
|
||||
version: 0.1.0
|
||||
|
||||
authors:
|
||||
- Tristan Ancelet <tristan.ancelet@acumera.com>
|
||||
|
||||
crystal: '>= 1.19.1'
|
||||
|
||||
license: MIT
|
||||
9
spec/cli_generator_spec.cr
Normal file
9
spec/cli_generator_spec.cr
Normal file
@@ -0,0 +1,9 @@
|
||||
require "./spec_helper"
|
||||
|
||||
describe CliGenerator do
|
||||
# TODO: Write tests
|
||||
|
||||
it "works" do
|
||||
false.should eq(true)
|
||||
end
|
||||
end
|
||||
2
spec/spec_helper.cr
Normal file
2
spec/spec_helper.cr
Normal file
@@ -0,0 +1,2 @@
|
||||
require "spec"
|
||||
require "../src/cli_generator"
|
||||
70
src/cli_generator.cr
Normal file
70
src/cli_generator.cr
Normal 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
157
src/command.cr
Normal 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
51
src/command/parser.cr
Normal 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
4
src/format.cr
Normal 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
4
src/regex.cr
Normal 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
|
||||
Reference in New Issue
Block a user