diff --git a/.ameba.yml b/.ameba.yml index 7e650ef..e0cb3d4 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -5,3 +5,6 @@ Metrics/CyclomaticComplexity: - src/cliq.cr Enabled: true Severity: Convention + +Lint/NotNil: + Enabled: false diff --git a/.github/workflows/crystal.yml b/.github/workflows/crystal.yml index 43f193b..1f03fa2 100644 --- a/.github/workflows/crystal.yml +++ b/.github/workflows/crystal.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest container: - image: crystallang/crystal:1.2.2 + image: crystallang/crystal:1.8.2 steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 1cbf6e5..2c91fbb 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,11 @@ The quick way to create a user-friendly **Command Line Interface** in Crystal. require "cliq" class GreetPerson < Cliq::Command - # Declare the command name, description and help-text(s) for positional arguments - command "greet person", "Greet someone", [" Name to greet"] + command "greet person" + summary "Greet someone" + description "I greet, therefore I am" + args [" Name to greet"] - # Declare the flags for this command flags({ yell: Bool?, count: { @@ -62,15 +63,18 @@ Cliq.invoke(ARGV) ## How it works -* You can have any number of `Cliq::Command` subclasses in your program. +* You can have any number of `Cliq::Command` subclasses in your program. Cliq merges them together to form the final CLI. * Each must have a method `#call(args : Array(String))`. -* Use the `command`-macro to declare the _command name_, _description_ and _description of positional arguments_ - * The latter two are optional. - * Spaces are allowed in the _command name_. +* Use `command` to declare the _command name_ + * Spaces are allowed. If you want a sub-command `foo bar batz` then just put exactly that in there. -* Use the `flags`-macro to declare the flags that your command accepts - +* Use `summary` to declare a short text to be displayed in the command list (top level help screen) +* Use `description` to declare a longer text to be displayed in the command help screen (`./foo bar --help`) +* Both `summary` and `description` can be multi-line strings and will be auto-indented as needed +* Use `args` to describe positional arguments + * Takes an Array of String's in format `"[foo] Description"` +* Use `flags` to declare the flags that your command accepts * See [examples/demo.cr](./examples/demo.cr) for a demo with multiple sub-commands @@ -86,7 +90,7 @@ flags({ }) ``` -This allows `--verbose` or `-v` (optional) +This allows `--verbose` or `-v` and requires `--count N` or `-c N` (where N must be an integer). ### Long-hand syntax diff --git a/examples/demo.cr b/examples/demo.cr index d26aec7..9c20d3a 100644 --- a/examples/demo.cr +++ b/examples/demo.cr @@ -1,32 +1,19 @@ require "../src/cliq" class GreetWorld < Cliq::Command - # `flags` declares the flags that - # this command will recognize. - # - # We'll keep it simple for this first example - # and only have a mandatory Int32 option - # called "--count". + command "greet world" + summary "Greet the world" # Shown on top-level help-screen + description "World greeter" # Shown on command help-screen + flags({ - count: Int32 + count: Int32, }) - # `command` registers this class with Cliq. - # It takes up to 3 arguments: - # - # 1. Command name (required, may contain spaces) - # 2. Description (optional, shown on help screen) - # 3. Array of positional arguments (optional, shown on help screen) - # - # We use only the first two here because our command - # doesn't take any positional arguments. - command "greet world", "Greet the world" - # `#call` gets called when your command is invoked. # # It's the only mandatory method that your Cliq::Command # subclass must have. Here you can see how to access the - # option values (`@count`) and positional args (`args`). + # flag values (`@count`) and positional args (`args`). def call(args) @count.times do puts "Hello world!" @@ -35,19 +22,21 @@ class GreetWorld < Cliq::Command end class GreetPerson < Cliq::Command + command "greet person" + summary "Greet someone" + args [" Name to greet"] + # See https://github.com/Papierkorb/toka#advanced-usage flags({ - yell: Bool?, + yell: Bool?, count: { - type: Int32, - default: 1, - value_name: "TIMES", - description: "Print the greeting this many times (default: 1)" - } + type: Int32, + default: 1, + value_name: "TIMES", + description: "Print the greeting this many times (default: 1)", + }, }) - command "greet person", "Greet someone", [" Name to greet"] - def call(args) raise Cliq::Error.new("Missing argument: ") if args.size < 1 greeting = "Hello #{args[0]}!" @@ -59,7 +48,8 @@ class GreetPerson < Cliq::Command end class Ping < Cliq::Command - command "ping", "Minimum viable example" + command "ping" + summary "Minimum viable example" def call(args) puts "pong" diff --git a/shard.yml b/shard.yml index a69a3c3..8e6adc3 100644 --- a/shard.yml +++ b/shard.yml @@ -1,10 +1,10 @@ name: cliq -version: 1.0.1 +version: 2.0.0 authors: - moe -crystal: 1.2.2 +crystal: 1.8.2 description: | CLI Framework @@ -14,6 +14,7 @@ license: MIT dependencies: toka: github: Papierkorb/toka + version: 0.1.2 development_dependencies: stdio: diff --git a/src/cliq.cr b/src/cliq.cr index eddd161..9289179 100644 --- a/src/cliq.cr +++ b/src/cliq.cr @@ -10,13 +10,13 @@ class Cliq EXE = File.basename(Process.executable_path.not_nil!) - @@command_map = {} of String => { Cliq::Command.class, String, Array(String) }? + @@command_map = {} of String => Cliq::Command.class - def self.command(cmd_path : String, cmd_class, desc = "", pos_arg_spec = [] of String) - @@command_map[cmd_path] = { cmd_class, desc, pos_arg_spec } + def self.command(cmd_path : String, cmd_class) + @@command_map[cmd_path] = cmd_class end - def self.invoke(argv=ARGV) + def self.invoke(argv = ARGV) args = [] of String argv.each do |arg| next if arg[0] == '-' @@ -25,7 +25,7 @@ class Cliq next end - cmd : { Cliq::Command.class, String, Array(String) }? = nil + cmd : Cliq::Command.class | Nil = nil guess : Array(String)? = nil n = args.size - 1 @@ -42,22 +42,30 @@ class Cliq if cmd begin - raise Cliq::Error.new(nil) if argv.includes?("-h") || argv.includes?("--help") || argv.includes?("-----ArrrRRrgh!!1!") + raise Cliq::Error.new(nil) if argv.includes?("-h") || argv.includes?("--help") - 🍺 = cmd[0].new(argv) + 🍺 = cmd.new(argv) pos_opts = 🍺.positional_options - (0..n+1).each do + (0..n + 1).each do pos_opts.shift end 🍺.call(pos_opts) rescue ex : Toka::MissingOptionError | Toka::ConversionError | Toka::UnknownOptionError | Cliq::Error - puts "\nUsage: #{EXE} #{part} #{cmd[2].map { |e| e.split(" ")[0] }.join(" ")}" + puts "\nUsage: #{EXE} #{part} #{cmd.args.map { |e| e.split(" ")[0] }.join(" ")}" - unless cmd[2].empty? - indent = cmd[2].max_of { |e| e.includes?(" ") ? e.split(" ")[0].size : 0 } + if desc = cmd.description + puts + desc.split("\n").each do |line| + print " " + puts line + end + end + + unless cmd.args.empty? + indent = cmd.args.max_of { |e| e.includes?(" ") ? e.split(" ")[0].size : 0 } if 0 < indent puts - cmd[2].each do |pos_arg_spec| + cmd.args.each do |pos_arg_spec| if pos_arg_spec.includes? " " spec, desc = pos_arg_spec.split(" ", 2) print " #{spec}" @@ -72,7 +80,7 @@ class Cliq end end - puts Toka::HelpPageRenderer.new(cmd[0]) + puts Toka::HelpPageRenderer.new(cmd) puts "\e[31;1m" + ex.message.not_nil! + "\n\n" unless ex.message.nil? end return @@ -90,10 +98,10 @@ class Cliq print "\e[33;1m" if guess.try &.includes? cmd_name print " #{cmd_name}" print "\e[0m" - if cmd[1] == "" + if cmd.summary == "" puts else - lines = cmd[1].split("\n") + lines = cmd.summary.split("\n") print " " * (max_width - cmd_name.size + 3) lines.each_with_index do |line, i| puts line @@ -108,8 +116,24 @@ end abstract class Cliq::Command abstract def call(args : Array(String)) - def self.command(cmd_path : String, desc = "", pos_args = [] of String) - Cliq.command(cmd_path, self, desc, pos_args) + class_getter summary : String = "" + class_getter description : String? = nil + class_getter args : Array(String) = [] of String + + def self.command(name : String) + Cliq.command(name, self) + end + + def self.summary(text : String) + @@summary = text + end + + def self.description(text : String) + @@description = text + end + + def self.args(args : Array(String)) + @@args = args end macro flags(*args)