Skip to content

Commit

Permalink
🕹️ v2.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
m-o-e committed May 26, 2023
1 parent 79ca27e commit 96d958f
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 58 deletions.
3 changes: 3 additions & 0 deletions .ameba.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ Metrics/CyclomaticComplexity:
- src/cliq.cr
Enabled: true
Severity: Convention

Lint/NotNil:
Enabled: false
2 changes: 1 addition & 1 deletion .github/workflows/crystal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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> Name to greet"]
command "greet person"
summary "Greet someone"
description "I greet, therefore I am"
args ["<name> Name to greet"]
# Declare the flags for this command
flags({
yell: Bool?,
count: {
Expand All @@ -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


Expand All @@ -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
Expand Down
46 changes: 18 additions & 28 deletions examples/demo.cr
Original file line number Diff line number Diff line change
@@ -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!"
Expand All @@ -35,19 +22,21 @@ class GreetWorld < Cliq::Command
end

class GreetPerson < Cliq::Command
command "greet person"
summary "Greet someone"
args ["<name> 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> Name to greet"]

def call(args)
raise Cliq::Error.new("Missing argument: <name>") if args.size < 1
greeting = "Hello #{args[0]}!"
Expand All @@ -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"
Expand Down
5 changes: 3 additions & 2 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: cliq
version: 1.0.1
version: 2.0.0

authors:
- moe <moe@busyloop.net>

crystal: 1.2.2
crystal: 1.8.2

description: |
CLI Framework
Expand All @@ -14,6 +14,7 @@ license: MIT
dependencies:
toka:
github: Papierkorb/toka
version: 0.1.2

development_dependencies:
stdio:
Expand Down
58 changes: 41 additions & 17 deletions src/cliq.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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] == '-'
Expand All @@ -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
Expand All @@ -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}"
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down

0 comments on commit 96d958f

Please sign in to comment.