ergane 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -1
- data/LICENSE +18 -17
- data/README.md +259 -10
- data/lib/ergane/argument_definition.rb +15 -0
- data/lib/ergane/command.rb +85 -0
- data/lib/ergane/concerns/inheritance.rb +44 -0
- data/lib/ergane/concerns/option_handling.rb +43 -0
- data/lib/ergane/core_ext/array.rb +13 -0
- data/lib/ergane/core_ext/hash.rb +10 -0
- data/lib/ergane/core_ext/object.rb +43 -0
- data/lib/{core_ext → ergane/core_ext}/option_parser.rb +3 -2
- data/lib/ergane/core_ext/string.rb +22 -0
- data/lib/ergane/dsl/block_dsl.rb +29 -0
- data/lib/ergane/dsl/command_dsl.rb +50 -0
- data/lib/ergane/errors.rb +51 -0
- data/lib/ergane/formatter.rb +29 -0
- data/lib/ergane/help_formatter.rb +101 -0
- data/lib/ergane/option_definition.rb +49 -0
- data/lib/ergane/path_registry.rb +47 -0
- data/lib/ergane/runner.rb +60 -0
- data/lib/ergane/tool.rb +64 -95
- data/lib/ergane/util/debug.rb +24 -0
- data/lib/ergane/util/formatting.rb +31 -0
- data/lib/ergane/version.rb +3 -1
- data/lib/ergane.rb +32 -77
- metadata +69 -72
- data/app/commands/ergane/console.rb +0 -22
- data/bin/ergane +0 -24
- data/lib/ergane/command_definition.rb +0 -176
- data/lib/ergane/debug.rb +0 -75
- data/lib/ergane/helpers/hashall.rb +0 -8
- data/lib/ergane/switch_definition.rb +0 -49
- data/lib/ergane/util.rb +0 -77
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ergane
|
|
4
|
+
module DSL
|
|
5
|
+
module CommandDSL
|
|
6
|
+
def description(text = nil)
|
|
7
|
+
text ? (@description = text) : (@description || "")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def aliases(*names)
|
|
11
|
+
names.any? ? (@aliases = names.flatten.map(&:to_sym)) : (@aliases || [])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def option(name, type = nil, short: nil, description: nil, default: nil, required: false, optional: false)
|
|
15
|
+
option_definitions[name.to_sym] = OptionDefinition.new(
|
|
16
|
+
name, type, short: short, description: description,
|
|
17
|
+
default: default, required: required, optional: optional
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def flag(name, short: nil, description: nil)
|
|
22
|
+
option(name, nil, short: short, description: description, default: false)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def argument(name, type = String, description: nil, required: true, default: nil)
|
|
26
|
+
argument_definitions << ArgumentDefinition.new(
|
|
27
|
+
name, type, description: description, required: required, default: default
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def command(name, aliases: [], &block)
|
|
32
|
+
klass = Class.new(command_base_for(name))
|
|
33
|
+
klass.command_name = name.to_sym
|
|
34
|
+
klass.aliases(*aliases) if aliases.any?
|
|
35
|
+
|
|
36
|
+
const_name = name.to_s.split("_").map(&:capitalize).join
|
|
37
|
+
const_set(const_name, klass) if const_name.match?(/\A[A-Z]/)
|
|
38
|
+
|
|
39
|
+
BlockDSL.new(klass).instance_eval(&block) if block
|
|
40
|
+
klass
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def command_base_for(_name)
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ergane
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class CommandNotFound < Error
|
|
7
|
+
attr_reader :token, :available
|
|
8
|
+
|
|
9
|
+
def initialize(token, available: [])
|
|
10
|
+
@token = token
|
|
11
|
+
@available = available
|
|
12
|
+
suggestion = find_suggestion
|
|
13
|
+
msg = "Unknown command: '#{token}'"
|
|
14
|
+
msg += ". Did you mean '#{suggestion}'?" if suggestion
|
|
15
|
+
msg += "\nAvailable commands: #{available.join(', ')}" if available.any?
|
|
16
|
+
super(msg)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def find_suggestion
|
|
22
|
+
return nil if available.empty?
|
|
23
|
+
best = available.min_by { |cmd| levenshtein(token.to_s, cmd.to_s) }
|
|
24
|
+
dist = levenshtein(token.to_s, best.to_s)
|
|
25
|
+
dist <= [token.to_s.length / 2, 3].max ? best : nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def levenshtein(a, b)
|
|
29
|
+
m, n = a.length, b.length
|
|
30
|
+
return n if m.zero?
|
|
31
|
+
return m if n.zero?
|
|
32
|
+
|
|
33
|
+
d = Array.new(m + 1) { |i| i }
|
|
34
|
+
(1..n).each do |j|
|
|
35
|
+
prev = d[0]
|
|
36
|
+
d[0] = j
|
|
37
|
+
(1..m).each do |i|
|
|
38
|
+
cost = a[i - 1] == b[j - 1] ? 0 : 1
|
|
39
|
+
temp = d[i]
|
|
40
|
+
d[i] = [d[i] + 1, d[i - 1] + 1, prev + cost].min
|
|
41
|
+
prev = temp
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
d[m]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class MissingArgument < Error; end
|
|
49
|
+
class InvalidOption < Error; end
|
|
50
|
+
class AbstractCommand < Error; end
|
|
51
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ergane
|
|
4
|
+
# Small, generic output helpers for interactive CLIs.
|
|
5
|
+
module Formatter
|
|
6
|
+
class << self
|
|
7
|
+
# Prompt on stderr and return true only for an affirmative answer.
|
|
8
|
+
# Returns false when stdin isn't a TTY (non-interactive / piped input).
|
|
9
|
+
def confirm?(prompt)
|
|
10
|
+
$stderr.print "#{prompt} [y/N] "
|
|
11
|
+
return false unless $stdin.tty?
|
|
12
|
+
|
|
13
|
+
answer = $stdin.gets&.strip
|
|
14
|
+
answer&.match?(/\Ay(es)?\z/i)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Humanize the distance from +time+ to now: "just now", "5m ago",
|
|
18
|
+
# "3h ago", "2d ago".
|
|
19
|
+
def time_ago(time)
|
|
20
|
+
seconds = (Time.now - time).to_i
|
|
21
|
+
return 'just now' if seconds < 60
|
|
22
|
+
return "#{seconds / 60}m ago" if seconds < 3600
|
|
23
|
+
return "#{seconds / 3600}h ago" if seconds < 86400
|
|
24
|
+
|
|
25
|
+
"#{seconds / 86400}d ago"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ergane
|
|
4
|
+
class HelpFormatter
|
|
5
|
+
attr_reader :command_class, :command_path
|
|
6
|
+
|
|
7
|
+
def initialize(command_class, command_path: [])
|
|
8
|
+
@command_class = command_class
|
|
9
|
+
@command_path = command_path
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def format
|
|
13
|
+
[
|
|
14
|
+
description_section,
|
|
15
|
+
version_section,
|
|
16
|
+
usage_section,
|
|
17
|
+
subcommands_section,
|
|
18
|
+
options_section,
|
|
19
|
+
arguments_section
|
|
20
|
+
].compact.join("\n\n") + "\n"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def description_section
|
|
26
|
+
desc = command_class.description
|
|
27
|
+
desc if desc.present?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def version_section
|
|
31
|
+
ver = command_class.respond_to?(:version) && command_class.version
|
|
32
|
+
"Version: #{ver.to_s.light_blue}" if ver
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def usage_section
|
|
36
|
+
path = command_path.any? ? command_path.join(" ") : command_class.command_name.to_s
|
|
37
|
+
usage = path.light_red
|
|
38
|
+
usage += " [options]".light_cyan if command_class.option_definitions.any?
|
|
39
|
+
usage += " [subcommand]".light_black.underline if command_class.subcommands.any?
|
|
40
|
+
command_class.argument_definitions.each do |arg|
|
|
41
|
+
label = arg.required ? "<#{arg.name}>" : "[#{arg.name}]"
|
|
42
|
+
usage += " #{label}".light_yellow
|
|
43
|
+
end
|
|
44
|
+
"Usage:".light_cyan + " " + usage
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def subcommands_section
|
|
48
|
+
subs = command_class.subcommands
|
|
49
|
+
return if subs.empty?
|
|
50
|
+
|
|
51
|
+
max_width = subs.keys.map { |k| k.to_s.length }.max
|
|
52
|
+
Util::Formatting.reset_colors!
|
|
53
|
+
|
|
54
|
+
lines = []
|
|
55
|
+
lines << "Subcommands:".light_cyan
|
|
56
|
+
header_len = lines.last.uncolorize.length
|
|
57
|
+
lines << (" \u250C" + ("\u2500" * (header_len - 2)) + "\u2518").light_black
|
|
58
|
+
|
|
59
|
+
subs.each do |name, sub_class|
|
|
60
|
+
label = name.to_s.ljust(max_width + 2)
|
|
61
|
+
desc = sub_class.description.present? ? sub_class.description.light_black : ""
|
|
62
|
+
lines << " \u251C\u2500\u2510".light_black + " #{label.send(Util::Formatting.next_color)} #{desc}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
lines << (" \u2514" + "\u2500" * 40).light_black
|
|
66
|
+
lines.join("\n")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def options_section
|
|
70
|
+
opts = command_class.option_definitions
|
|
71
|
+
return if opts.empty?
|
|
72
|
+
|
|
73
|
+
max_width = opts.values.map { |o| o.signature.length }.max
|
|
74
|
+
|
|
75
|
+
lines = ["Options:".light_cyan]
|
|
76
|
+
opts.each_value do |opt|
|
|
77
|
+
sig = opt.signature.ljust(max_width + 2)
|
|
78
|
+
desc = opt.description || ""
|
|
79
|
+
default_note = opt.default_value ? " (default: #{opt.default_value})".light_black : ""
|
|
80
|
+
lines << " #{sig.light_green} #{desc}#{default_note}"
|
|
81
|
+
end
|
|
82
|
+
lines.join("\n")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def arguments_section
|
|
86
|
+
args = command_class.argument_definitions
|
|
87
|
+
return if args.empty?
|
|
88
|
+
|
|
89
|
+
max_width = args.map { |a| a.name.to_s.length }.max
|
|
90
|
+
|
|
91
|
+
lines = ["Arguments:".light_cyan]
|
|
92
|
+
args.each do |arg|
|
|
93
|
+
label = arg.name.to_s.ljust(max_width + 2)
|
|
94
|
+
desc = arg.description || ""
|
|
95
|
+
req = arg.required ? " (required)".light_red : " (optional)".light_black
|
|
96
|
+
lines << " #{label.light_yellow} #{desc}#{req}"
|
|
97
|
+
end
|
|
98
|
+
lines.join("\n")
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ergane
|
|
4
|
+
class OptionDefinition
|
|
5
|
+
attr_reader :name, :type, :short, :description, :default, :required, :optional
|
|
6
|
+
|
|
7
|
+
def initialize(name, type = nil, short: nil, description: nil, default: nil, required: false, optional: false)
|
|
8
|
+
@name = name.to_sym
|
|
9
|
+
@type = type
|
|
10
|
+
@short = short&.to_s
|
|
11
|
+
@description = description
|
|
12
|
+
@default = default
|
|
13
|
+
@required = required
|
|
14
|
+
@optional = optional
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def boolean?
|
|
18
|
+
type.nil? || type == TrueClass || type == FalseClass
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def default_value
|
|
22
|
+
boolean? ? (default.nil? ? false : default) : default
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def long_flag
|
|
26
|
+
flag = "--#{name.to_s.tr('_', '-')}"
|
|
27
|
+
unless boolean?
|
|
28
|
+
flag += optional ? "=[VALUE]" : "=VALUE"
|
|
29
|
+
end
|
|
30
|
+
flag
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def short_flag
|
|
34
|
+
"-#{short}" if short
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def signature
|
|
38
|
+
[("#{short_flag}," if short), long_flag].compact.join(" ")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def attach(parser, store)
|
|
42
|
+
args = [short_flag, long_flag].compact
|
|
43
|
+
args << type if type && !boolean?
|
|
44
|
+
args << description if description
|
|
45
|
+
|
|
46
|
+
parser.on(*args) { |value| store[name] = value }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ergane
|
|
4
|
+
# An ordered registry of path-prefix → label substitutions, used by
|
|
5
|
+
# Command#abbreviate_path. Ships with $HOME → "~"; consumers register
|
|
6
|
+
# their own, e.g.:
|
|
7
|
+
#
|
|
8
|
+
# Ergane.paths.register("~/Workspace", "@ws")
|
|
9
|
+
#
|
|
10
|
+
# When abbreviating, the longest matching prefix wins, and a prefix only
|
|
11
|
+
# matches at a path boundary so "/home/user" never clips "/home/username".
|
|
12
|
+
class PathRegistry
|
|
13
|
+
Substitution = Struct.new(:prefix, :label)
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@substitutions = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Register a +prefix+ to collapse to +label+. The prefix is expanded
|
|
20
|
+
# (so "~" and relative paths resolve), and re-registering a prefix
|
|
21
|
+
# replaces its previous label. Returns self for chaining.
|
|
22
|
+
def register(prefix, label)
|
|
23
|
+
expanded = File.expand_path(prefix.to_s)
|
|
24
|
+
@substitutions.reject! { |sub| sub.prefix == expanded }
|
|
25
|
+
@substitutions << Substitution.new(expanded, label.to_s)
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Remove every registered substitution. Returns self for chaining.
|
|
30
|
+
def clear
|
|
31
|
+
@substitutions.clear
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Collapse the longest matching prefix in +path+ to its label,
|
|
36
|
+
# returning the path unchanged when nothing matches.
|
|
37
|
+
def abbreviate(path)
|
|
38
|
+
str = path.to_s
|
|
39
|
+
best = @substitutions
|
|
40
|
+
.select { |sub| str == sub.prefix || str.start_with?("#{sub.prefix}/") }
|
|
41
|
+
.max_by { |sub| sub.prefix.length }
|
|
42
|
+
return str unless best
|
|
43
|
+
|
|
44
|
+
"#{best.label}#{str[best.prefix.length..]}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ergane
|
|
4
|
+
class Runner
|
|
5
|
+
attr_reader :root, :argv
|
|
6
|
+
|
|
7
|
+
def initialize(root, argv)
|
|
8
|
+
@root = root
|
|
9
|
+
@argv = argv.dup
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def execute
|
|
13
|
+
command_class, remaining, path = resolve(root, argv)
|
|
14
|
+
|
|
15
|
+
if help_requested?(remaining)
|
|
16
|
+
$stdout.puts HelpFormatter.new(command_class, command_path: path).format
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if version_requested?(remaining) && command_class.respond_to?(:version) && command_class.version
|
|
21
|
+
$stdout.puts "#{command_class.command_name} #{command_class.version}"
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
instance = command_class.new(remaining)
|
|
26
|
+
instance.run(*instance.args)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def help_requested?(args)
|
|
32
|
+
args.include?("--help") || args.include?("-h")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def version_requested?(args)
|
|
36
|
+
args.include?("--version") || args.include?("-V")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def resolve(command_class, args, path = [])
|
|
40
|
+
path << (command_class.command_name || command_class.name || "command").to_s
|
|
41
|
+
return [command_class, args, path] if args.empty?
|
|
42
|
+
|
|
43
|
+
token = args.first
|
|
44
|
+
return [command_class, args, path] if token.start_with?("-")
|
|
45
|
+
|
|
46
|
+
sub = find_subcommand(command_class, token.to_sym)
|
|
47
|
+
if sub
|
|
48
|
+
args.shift
|
|
49
|
+
resolve(sub, args, path)
|
|
50
|
+
else
|
|
51
|
+
[command_class, args, path]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def find_subcommand(command_class, token)
|
|
56
|
+
command_class.subcommands[token] ||
|
|
57
|
+
command_class.subcommands.each_value.find { |cmd| cmd.terms.include?(token) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/ergane/tool.rb
CHANGED
|
@@ -1,116 +1,85 @@
|
|
|
1
|
-
|
|
2
|
-
class Tool < CommandDefinition
|
|
3
|
-
attr_reader :title #capitalized :label
|
|
4
|
-
attr_reader :version
|
|
1
|
+
# frozen_string_literal: true
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
module Ergane
|
|
4
|
+
class Tool < Command
|
|
5
|
+
self.abstract_class = true
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def command_class(klass = nil)
|
|
9
|
+
if klass
|
|
10
|
+
@command_class = klass
|
|
11
|
+
wire_command_class(klass)
|
|
12
|
+
else
|
|
13
|
+
@command_class
|
|
14
|
+
end
|
|
15
|
+
end
|
|
8
16
|
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
def tool_name(name = nil)
|
|
18
|
+
name ? (self.command_name = name) : command_name
|
|
19
|
+
end
|
|
11
20
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
switches do
|
|
16
|
-
switch :help, default: false, kind: TrueClass, description: "Display this help block" do
|
|
17
|
-
raise Help
|
|
18
|
-
end
|
|
19
|
-
switch :version, default: false, kind: TrueClass, description: "Display the version" do
|
|
20
|
-
# TODO: Push ARGV into a variable at the command level that can be manipulated by flags/switches
|
|
21
|
-
# NOTE: This would allow a --version to morph into a version command and we could push all this logic to that.
|
|
22
|
-
# Additional logic for --version would be -1 or --short etc.
|
|
23
|
-
puts "#{@title} Version: #{Ergane::VERSION}"
|
|
24
|
-
exit
|
|
25
|
-
end
|
|
26
|
-
# switch verbose: FalseClass, short: :v, "Turn on verbose logging"
|
|
27
|
-
end
|
|
21
|
+
def version(ver = nil)
|
|
22
|
+
ver ? (@version = ver) : @version
|
|
23
|
+
end
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
def start(argv = ARGV)
|
|
26
|
+
Runner.new(self, argv.dup).execute
|
|
27
|
+
rescue Interrupt
|
|
28
|
+
$stderr.puts "\nAborted."
|
|
29
|
+
exit 130
|
|
30
|
+
rescue Ergane::Error => e
|
|
31
|
+
$stderr.puts e.message
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
puts "\nOkay. Aborting."
|
|
39
|
-
rescue RuntimeError
|
|
40
|
-
puts "RuntimeError"
|
|
41
|
-
binding.pry
|
|
42
|
-
rescue Help
|
|
43
|
-
puts help(command, args)
|
|
44
|
-
ensure
|
|
45
|
-
system "printf '\033]0;\007'"
|
|
46
|
-
end
|
|
35
|
+
def load_commands(*patterns)
|
|
36
|
+
patterns.flatten.each do |pattern|
|
|
37
|
+
Dir[pattern].sort.each { |file| require file }
|
|
47
38
|
end
|
|
48
39
|
end
|
|
49
40
|
|
|
50
|
-
|
|
41
|
+
def inherited(subclass)
|
|
42
|
+
super
|
|
43
|
+
create_command_base(subclass) if self == Tool
|
|
44
|
+
end
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
end
|
|
46
|
+
private
|
|
54
47
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
b.blink if is_missing
|
|
59
|
-
end
|
|
60
|
-
end.join(' ')
|
|
61
|
-
|
|
62
|
-
command.switch_parser.banner = [].tap do |text|
|
|
63
|
-
text << "About: #{description}"
|
|
64
|
-
text << "Version: #{version.to_s.light_blue}"
|
|
65
|
-
text << "Usage: #{([label] + chain).join(' ').light_red} #{'[options]'.light_cyan} "
|
|
66
|
-
if commands.any?
|
|
67
|
-
text.last << "[subcommand]".light_black.underline
|
|
68
|
-
text << (" ┌" + ("─" * (text.last.uncolorize.length - 12)) + "┘").light_black
|
|
69
|
-
commands.each do |key, command|
|
|
70
|
-
# text << " ├─┐".light_black + " #{(klass.terms.join(', ')).ljust(24, ' ')} ".send(Athena::Util.next_color) + klass.description.light_black
|
|
71
|
-
text << " ├─┐".light_black + " #{key.to_s.ljust(24, ' ')} " + command.description.light_black
|
|
72
|
-
end
|
|
73
|
-
text << (" └" + "─" * 64).light_black
|
|
74
|
-
else
|
|
75
|
-
# text.last << command.arguments(missing_args.keys)
|
|
76
|
-
end
|
|
77
|
-
# text << list_examples if examples.any?
|
|
78
|
-
text << "Options:".light_cyan
|
|
79
|
-
end.join("\n")
|
|
80
|
-
switch_parser
|
|
81
|
-
end
|
|
48
|
+
def command_base_for(_name)
|
|
49
|
+
command_class || self
|
|
50
|
+
end
|
|
82
51
|
|
|
83
|
-
|
|
84
|
-
|
|
52
|
+
def create_command_base(tool_subclass)
|
|
53
|
+
return if tool_subclass.command_class
|
|
85
54
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
55
|
+
base = Class.new(Ergane::Command)
|
|
56
|
+
base.abstract_class = true
|
|
57
|
+
tool_subclass.const_set(:Command, base)
|
|
58
|
+
tool_subclass.command_class(base)
|
|
90
59
|
end
|
|
91
60
|
|
|
92
|
-
|
|
93
|
-
|
|
61
|
+
def wire_command_class(klass)
|
|
62
|
+
tool = self
|
|
63
|
+
klass.define_singleton_method(:tool) { tool }
|
|
94
64
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
file = path.split('/').last
|
|
102
|
-
Ergane.logger.debug " - loading #{path.split('/').last(4).join('/')}"
|
|
103
|
-
instance_eval(File.read(path), file)
|
|
65
|
+
klass.define_singleton_method(:inherited) do |subclass|
|
|
66
|
+
super(subclass)
|
|
67
|
+
cmd_name = subclass.command_name
|
|
68
|
+
if cmd_name && !subclass.abstract_class? && subclass.superclass.abstract_class?
|
|
69
|
+
subclass.instance_variable_set(:@_derived_name, cmd_name)
|
|
70
|
+
tool.subcommands[cmd_name] = subclass
|
|
104
71
|
end
|
|
105
72
|
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
73
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
74
|
+
klass.define_singleton_method(:inherited_command_name_set) do |subclass|
|
|
75
|
+
cmd_name = subclass.command_name
|
|
76
|
+
if cmd_name && !subclass.abstract_class? && subclass.superclass.abstract_class?
|
|
77
|
+
derived = subclass.instance_variable_get(:@_derived_name)
|
|
78
|
+
tool.subcommands.delete(derived) if derived && derived != cmd_name
|
|
79
|
+
tool.subcommands[cmd_name] = subclass
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
113
83
|
end
|
|
114
|
-
|
|
115
84
|
end
|
|
116
85
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ergane
|
|
4
|
+
module Util
|
|
5
|
+
module Debug
|
|
6
|
+
def self.enabled?
|
|
7
|
+
!!$ergane_debug
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.enable!
|
|
11
|
+
$ergane_debug = true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.disable!
|
|
15
|
+
$ergane_debug = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.log(message)
|
|
19
|
+
return unless enabled?
|
|
20
|
+
$stderr.puts "[Ergane DEBUG] #{message}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ergane
|
|
4
|
+
module Util
|
|
5
|
+
module Formatting
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
COLORS = %i[light_red light_yellow light_green light_blue light_cyan light_magenta].freeze
|
|
9
|
+
|
|
10
|
+
def color_cycle
|
|
11
|
+
@color_cycle ||= COLORS.cycle
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def next_color
|
|
15
|
+
color_cycle.next
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def reset_colors!
|
|
19
|
+
@color_cycle = COLORS.cycle
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def rainbow(string, delimiter = " ")
|
|
23
|
+
string.split(delimiter).map { |word| word.to_s.send(next_color) }.join(delimiter)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def colorize_list(items)
|
|
27
|
+
items.map { |item| item.to_s.send(next_color) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/ergane/version.rb
CHANGED