cli-kit 1.0.0 → 3.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -4
- data/README.md +16 -0
- data/lib/cli/kit.rb +50 -1
- data/lib/cli/kit/base_command.rb +43 -0
- data/lib/cli/kit/command_registry.rb +93 -0
- data/lib/cli/kit/config.rb +100 -0
- data/lib/cli/kit/error_handler.rb +95 -0
- data/lib/cli/kit/executor.rb +87 -0
- data/lib/cli/kit/ini.rb +93 -0
- data/lib/cli/kit/levenshtein.rb +82 -0
- data/lib/cli/kit/resolver.rb +24 -0
- data/lib/cli/kit/ruby_backports/enumerable.rb +6 -0
- data/lib/cli/kit/version.rb +1 -1
- metadata +13 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d38479bdf5b6e03725e44c04e6b44c306bad6a59
|
4
|
+
data.tar.gz: 4268a52a77172588b586e1e5e21cee635e7ed99a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ddd5316dc74a06d96a35ad8953b198de161b1700822f15ff62e9987e46957fc493a7c6af1e605d435235567805a2bc230d4d287677a372619d3653e12c31e66e
|
7
|
+
data.tar.gz: e61a9cac88db2728fd81c5d782288788a8b6a95579e00b3c27b0c86129e0afcee3fbdde18104e3c8beb997fc9d9ee9190f7de6e4f22570679b93f8ed494ad1d7
|
data/Gemfile.lock
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
|
5
|
-
|
4
|
+
cli-kit (2.0.0)
|
5
|
+
cli-ui (>= 1.0.0)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
@@ -11,7 +11,7 @@ GEM
|
|
11
11
|
ast (2.3.0)
|
12
12
|
builder (3.2.3)
|
13
13
|
byebug (9.0.6)
|
14
|
-
|
14
|
+
cli-ui (1.0.0)
|
15
15
|
metaclass (0.0.4)
|
16
16
|
method_source (0.8.2)
|
17
17
|
minitest (5.10.2)
|
@@ -45,7 +45,7 @@ PLATFORMS
|
|
45
45
|
DEPENDENCIES
|
46
46
|
bundler (~> 1.15)
|
47
47
|
byebug
|
48
|
-
|
48
|
+
cli-kit!
|
49
49
|
method_source
|
50
50
|
minitest (>= 5.0.0)
|
51
51
|
minitest-reporters
|
data/README.md
CHANGED
@@ -1,2 +1,18 @@
|
|
1
1
|
# cli-kit
|
2
2
|
|
3
|
+
`cli-kit` is a ruby Command-Line application framework. Its primary design goals are:
|
4
|
+
|
5
|
+
1. Modularity: The framework tries not to own your application, but rather to live on its edges.
|
6
|
+
2. Startup Time: `cli-kit` encourages heavy use of autoloading (and uses it extensively internally)
|
7
|
+
to reduce the amount of code loaded and evaluated whilst booting your application. We are able to
|
8
|
+
achieve a 130ms runtime in a project with 21kLoC and ~50 commands.
|
9
|
+
|
10
|
+
`cli-kit` is developed and heavily used by the Developer Infrastructure team at Shopify. We use it
|
11
|
+
to build a number of internal developer tools, along with
|
12
|
+
[cli-ui](https://github.com/shopify/cli-ui).
|
13
|
+
|
14
|
+
## Example Usage
|
15
|
+
|
16
|
+
You can see example usage [here](https://github.com/Shopify/cli-kit-example). We may one day build
|
17
|
+
an application generator, as this framework paradigm requires a small amount of boilerplate. For
|
18
|
+
now, the best way to get going is to use this example application as a starting point.
|
data/lib/cli/kit.rb
CHANGED
@@ -1,7 +1,56 @@
|
|
1
1
|
require 'cli/ui'
|
2
|
+
require 'cli/kit/ruby_backports/enumerable'
|
2
3
|
|
3
4
|
module CLI
|
4
5
|
module Kit
|
5
|
-
autoload :
|
6
|
+
autoload :BaseCommand, 'cli/kit/base_command'
|
7
|
+
autoload :CommandRegistry, 'cli/kit/command_registry'
|
8
|
+
autoload :Config, 'cli/kit/config'
|
9
|
+
autoload :ErrorHandler, 'cli/kit/error_handler'
|
10
|
+
autoload :Executor, 'cli/kit/executor'
|
11
|
+
autoload :Ini, 'cli/kit/ini'
|
12
|
+
autoload :Levenshtein, 'cli/kit/levenshtein'
|
13
|
+
autoload :Resolver, 'cli/kit/resolver'
|
14
|
+
autoload :System, 'cli/kit/system'
|
15
|
+
|
16
|
+
EXIT_FAILURE_BUT_NOT_BUG = 30
|
17
|
+
EXIT_BUG = 1
|
18
|
+
EXIT_SUCCESS = 0
|
19
|
+
|
20
|
+
# Abort, Bug, AbortSilent, and BugSilent are four ways of immediately bailing
|
21
|
+
# on command-line execution when an unrecoverable error occurs.
|
22
|
+
#
|
23
|
+
# Note that these don't inherit from StandardError, and so are not caught by
|
24
|
+
# a bare `rescue => e`.
|
25
|
+
#
|
26
|
+
# * Abort prints its message in red and exits 1;
|
27
|
+
# * Bug additionally submits the exception to Bugsnag;
|
28
|
+
# * AbortSilent and BugSilent do the same as above, but do not print
|
29
|
+
# messages before exiting.
|
30
|
+
#
|
31
|
+
# Treat these like panic() in Go:
|
32
|
+
# * Don't rescue them. Use a different Exception class if you plan to recover;
|
33
|
+
# * Provide a useful message, since it will be presented in brief to the
|
34
|
+
# user, and will be useful for debugging.
|
35
|
+
# * Avoid using it if it does actually make sense to recover from an error.
|
36
|
+
#
|
37
|
+
# Additionally:
|
38
|
+
# * Do not subclass these.
|
39
|
+
# * Only use AbortSilent or BugSilent if you prefer to print a more
|
40
|
+
# contextualized error than Abort or Bug would present to the user.
|
41
|
+
# * In general, don't attach a message to AbortSilent or BugSilent.
|
42
|
+
# * Never raise GenericAbort directly.
|
43
|
+
# * Think carefully about whether Abort or Bug is more appropriate. Is this
|
44
|
+
# a bug in the tool? Or is it just user error, transient network
|
45
|
+
# failure, etc.?
|
46
|
+
# * One case where it's ok to rescue (cli-kit internals or tests aside):
|
47
|
+
# 1. rescue Abort or Bug
|
48
|
+
# 2. Print a contextualized error message
|
49
|
+
# 3. Re-raise AbortSilent or BugSilent respectively.
|
50
|
+
GenericAbort = Class.new(Exception)
|
51
|
+
Abort = Class.new(GenericAbort)
|
52
|
+
Bug = Class.new(GenericAbort)
|
53
|
+
BugSilent = Class.new(GenericAbort)
|
54
|
+
AbortSilent = Class.new(GenericAbort)
|
6
55
|
end
|
7
56
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'cli/kit'
|
2
|
+
|
3
|
+
module CLI
|
4
|
+
module Kit
|
5
|
+
class BaseCommand
|
6
|
+
def self.defined?
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.statsd_increment(metric, **kwargs)
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.statsd_time(metric, **kwargs)
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.call(args, command_name)
|
19
|
+
cmd = new
|
20
|
+
stats_tags = ["task:#{cmd.class}"]
|
21
|
+
stats_tags << "subcommand:#{args.first}" if args && args.first && cmd.has_subcommands?
|
22
|
+
begin
|
23
|
+
statsd_increment("cli.command.invoked", tags: stats_tags)
|
24
|
+
statsd_time("cli.command.time", tags: stats_tags) do
|
25
|
+
cmd.call(args, command_name)
|
26
|
+
end
|
27
|
+
statsd_increment("cli.command.success", tags: stats_tags)
|
28
|
+
rescue => e
|
29
|
+
statsd_increment("cli.command.exception", tags: stats_tags + ["exception:#{e.class}"])
|
30
|
+
raise e
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def call(args, command_name)
|
35
|
+
raise NotImplementedError
|
36
|
+
end
|
37
|
+
|
38
|
+
def has_subcommands?
|
39
|
+
false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'cli/kit'
|
2
|
+
|
3
|
+
module CLI
|
4
|
+
module Kit
|
5
|
+
class CommandRegistry
|
6
|
+
attr_reader :commands, :aliases
|
7
|
+
|
8
|
+
module NullContextualResolver
|
9
|
+
def self.command_names
|
10
|
+
[]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.aliases
|
14
|
+
{}
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.command_class(_name)
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(default:, contextual_resolver: NullContextualResolver)
|
23
|
+
@commands = {}
|
24
|
+
@aliases = {}
|
25
|
+
@default = default
|
26
|
+
@contextual_resolver = contextual_resolver
|
27
|
+
end
|
28
|
+
|
29
|
+
def resolved_commands
|
30
|
+
@commands.each_with_object({}) do |(k, v), a|
|
31
|
+
a[k] = resolve_class(v)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def add(const, name)
|
36
|
+
commands[name] = const
|
37
|
+
end
|
38
|
+
|
39
|
+
def lookup_command(name)
|
40
|
+
name = @default if name.to_s.empty?
|
41
|
+
resolve_command(name)
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_alias(from, to)
|
45
|
+
aliases[from] = to unless aliases[from]
|
46
|
+
end
|
47
|
+
|
48
|
+
def resolve_command(name)
|
49
|
+
resolve_global_command(name) || \
|
50
|
+
resolve_contextual_command(name) || \
|
51
|
+
[nil, resolve_alias(name)]
|
52
|
+
end
|
53
|
+
|
54
|
+
def resolve_alias(name)
|
55
|
+
aliases[name] || @contextual_resolver.aliases.fetch(name, name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def resolve_global_command(name)
|
59
|
+
name = resolve_alias(name)
|
60
|
+
klass = resolve_class(commands.fetch(name, ""))
|
61
|
+
return nil unless klass.defined? # (BaseCommand)
|
62
|
+
[klass, name]
|
63
|
+
rescue NameError
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def resolve_contextual_command(name)
|
68
|
+
name = resolve_alias(name)
|
69
|
+
found = @contextual_resolver.command_names.include?(name)
|
70
|
+
return nil unless found
|
71
|
+
[@contextual_resolver.command_class(name), name]
|
72
|
+
end
|
73
|
+
|
74
|
+
def command_names
|
75
|
+
@contextual_resolver.command_names + commands.keys
|
76
|
+
end
|
77
|
+
|
78
|
+
def exist?(name)
|
79
|
+
!resolve_command(name).first.nil?
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def resolve_class(class_or_proc)
|
85
|
+
if class_or_proc.is_a?(Class)
|
86
|
+
class_or_proc
|
87
|
+
else
|
88
|
+
class_or_proc.call
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'cli/kit'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module CLI
|
5
|
+
module Kit
|
6
|
+
class Config
|
7
|
+
XDG_CONFIG_HOME = 'XDG_CONFIG_HOME'
|
8
|
+
|
9
|
+
def initialize(tool_name:)
|
10
|
+
@tool_name = tool_name
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns the config corresponding to `name` from the config file
|
14
|
+
# `false` is returned if it doesn't exist
|
15
|
+
#
|
16
|
+
# #### Parameters
|
17
|
+
# `section` : the section of the config value you are looking for
|
18
|
+
# `name` : the name of the config value you are looking for
|
19
|
+
#
|
20
|
+
# #### Returns
|
21
|
+
# `value` : the value of the config variable (false if none)
|
22
|
+
#
|
23
|
+
# #### Example Usage
|
24
|
+
# `config.get('name.of.config')`
|
25
|
+
#
|
26
|
+
def get(section, name)
|
27
|
+
all_configs.dig("[#{section}]", name) || false
|
28
|
+
end
|
29
|
+
|
30
|
+
# Sets the config value in the config file
|
31
|
+
#
|
32
|
+
# #### Parameters
|
33
|
+
# `section` : the section of the config you are setting
|
34
|
+
# `name` : the name of the config you are setting
|
35
|
+
# `value` : the value of the config you are setting
|
36
|
+
#
|
37
|
+
# #### Example Usage
|
38
|
+
# `config.set('section', 'name.of.config', 'value')`
|
39
|
+
#
|
40
|
+
def set(section, name, value)
|
41
|
+
all_configs["[#{section}]"] ||= {}
|
42
|
+
all_configs["[#{section}]"][name] = value.nil? ? nil : value.to_s
|
43
|
+
write_config
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_section(section)
|
47
|
+
(all_configs["[#{section}]"] || {}).dup
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns a path from config in expanded form
|
51
|
+
# e.g. shopify corresponds to ~/src/shopify, but is expanded to /Users/name/src/shopify
|
52
|
+
#
|
53
|
+
# #### Example Usage
|
54
|
+
# `config.get_path('srcpath', 'shopify')`
|
55
|
+
#
|
56
|
+
# #### Returns
|
57
|
+
# `path` : the expanded path to the corrsponding value
|
58
|
+
#
|
59
|
+
def get_path(section, name = nil)
|
60
|
+
v = get(section, name)
|
61
|
+
false == v ? v : File.expand_path(v)
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_s
|
65
|
+
ini.to_s
|
66
|
+
end
|
67
|
+
|
68
|
+
# The path on disk at which the configuration is stored:
|
69
|
+
# `$XDG_CONFIG_HOME/<toolname>/config`
|
70
|
+
# if ENV['XDG_CONFIG_HOME'] is not set, we default to ~/.config, e.g.:
|
71
|
+
# ~/.config/tool/config
|
72
|
+
#
|
73
|
+
def file
|
74
|
+
config_home = ENV.fetch(XDG_CONFIG_HOME, '~/.config')
|
75
|
+
File.expand_path(File.join(@tool_name, 'config'), config_home)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def all_configs
|
81
|
+
ini.ini
|
82
|
+
end
|
83
|
+
|
84
|
+
def ini
|
85
|
+
@ini ||= CLI::Kit::Ini
|
86
|
+
.new(file, default_section: "[global]", convert_types: false)
|
87
|
+
.tap(&:parse)
|
88
|
+
end
|
89
|
+
|
90
|
+
def write_config
|
91
|
+
all_configs.each do |section, sub_config|
|
92
|
+
all_configs[section] = sub_config.reject { |_, value| value.nil? }
|
93
|
+
all_configs.delete(section) if all_configs[section].empty?
|
94
|
+
end
|
95
|
+
FileUtils.mkdir_p(File.dirname(file))
|
96
|
+
File.write(file, to_s)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'cli/kit'
|
2
|
+
|
3
|
+
module CLI
|
4
|
+
module Kit
|
5
|
+
class ErrorHandler
|
6
|
+
def initialize(log_file: nil, exception_reporter: NullExceptionReporter)
|
7
|
+
@log_file = log_file
|
8
|
+
@exception_reporter_or_proc = exception_reporter
|
9
|
+
end
|
10
|
+
|
11
|
+
module NullExceptionReporter
|
12
|
+
def self.report(exception, logs)
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def install!
|
18
|
+
at_exit { handle_final_exception(@exception || $ERROR_INFO) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def handle_abort
|
22
|
+
yield
|
23
|
+
rescue CLI::Kit::GenericAbort => e
|
24
|
+
is_bug = e.is_a?(CLI::Kit::Bug) || e.is_a?(CLI::Kit::BugSilent)
|
25
|
+
is_silent = e.is_a?(CLI::Kit::AbortSilent) || e.is_a?(CLI::Kit::BugSilent)
|
26
|
+
|
27
|
+
print_error_message(e) unless is_silent
|
28
|
+
(@exception = e) if is_bug
|
29
|
+
|
30
|
+
CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
|
31
|
+
rescue Interrupt
|
32
|
+
STDERR.puts(format_error_message("Interrupt"))
|
33
|
+
return CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def handle_final_exception(error)
|
39
|
+
notify_with = nil
|
40
|
+
|
41
|
+
case error
|
42
|
+
when nil # normal, non-error termination
|
43
|
+
when Interrupt # ctrl-c
|
44
|
+
when CLI::Kit::Abort, CLI::Kit::AbortSilent # Not a bug
|
45
|
+
when SignalException
|
46
|
+
skip = %w(SIGTERM SIGHUP SIGINT)
|
47
|
+
unless skip.include?(error.message)
|
48
|
+
notify_with = error
|
49
|
+
end
|
50
|
+
when SystemExit # "exit N" called
|
51
|
+
case error.status
|
52
|
+
when CLI::Kit::EXIT_SUCCESS # submit nothing if it was `exit 0`
|
53
|
+
when CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
|
54
|
+
# if it was `exit 30`, translate the exit code to 1, and submit nothing
|
55
|
+
# 30 is used to signal normal failures that are not indicative of bugs.
|
56
|
+
# But users should see it presented as 1.
|
57
|
+
exit 1
|
58
|
+
else
|
59
|
+
# A weird termination status happened. `error.exception "message"` will maintain backtrace
|
60
|
+
# but allow us to set a message
|
61
|
+
notify_with = error.exception "abnormal termination status: #{error.status}"
|
62
|
+
end
|
63
|
+
else
|
64
|
+
notify_with = error
|
65
|
+
end
|
66
|
+
|
67
|
+
if notify_with
|
68
|
+
logs = begin
|
69
|
+
File.read(@log_file)
|
70
|
+
rescue => e
|
71
|
+
"(#{e.class}: #{e.message})"
|
72
|
+
end
|
73
|
+
exceptiono_reporter.report(notify_with, logs)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def exception_reporter
|
78
|
+
if @exception_reporter_or_proc.respond_to?(:report)
|
79
|
+
@exception_reporter_or_proc
|
80
|
+
else
|
81
|
+
@exception_reporter_or_proc.call
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def format_error_message(msg)
|
86
|
+
CLI::UI.fmt("{{red:#{msg}}}")
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def print_error_message(e)
|
91
|
+
STDERR.puts(format_error_message(e.message))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'cli/kit'
|
2
|
+
require 'English'
|
3
|
+
|
4
|
+
module CLI
|
5
|
+
module Kit
|
6
|
+
class Executor
|
7
|
+
def initialize(
|
8
|
+
tool_name:, command_registry:, error_handler:, log_file: nil
|
9
|
+
)
|
10
|
+
@tool_name = tool_name
|
11
|
+
@command_registry = command_registry
|
12
|
+
@error_handler = error_handler
|
13
|
+
@log_file = log_file
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_logging(&block)
|
17
|
+
return yield unless @log_file
|
18
|
+
CLI::UI.log_output_to(@log_file, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def commands_and_aliases
|
22
|
+
@command_registry.command_names + @command_registry.aliases.keys
|
23
|
+
end
|
24
|
+
|
25
|
+
def trap_signals
|
26
|
+
trap('QUIT') do
|
27
|
+
z = caller
|
28
|
+
CLI::UI.raw do
|
29
|
+
STDERR.puts('SIGQUIT: quit')
|
30
|
+
STDERR.puts(z)
|
31
|
+
end
|
32
|
+
exit 1
|
33
|
+
end
|
34
|
+
trap('INFO') do
|
35
|
+
z = caller
|
36
|
+
CLI::UI.raw do
|
37
|
+
STDERR.puts('SIGINFO:')
|
38
|
+
STDERR.puts(z)
|
39
|
+
# Thread.list.map { |t| t.backtrace }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def call(command, command_name, args)
|
45
|
+
trap_signals
|
46
|
+
with_logging do
|
47
|
+
@error_handler.handle_abort do
|
48
|
+
if command.nil?
|
49
|
+
command_not_found(command_name)
|
50
|
+
raise CLI::Kit::AbortSilent # Already output message
|
51
|
+
end
|
52
|
+
command.call(args, command_name)
|
53
|
+
CLI::Kit::EXIT_SUCCESS # unless an exception was raised
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def command_not_found(name)
|
59
|
+
CLI::UI::Frame.open("Command not found", color: :red, timing: false) do
|
60
|
+
STDERR.puts(CLI::UI.fmt("{{command:#{@tool_name} #{name}}} was not found"))
|
61
|
+
end
|
62
|
+
|
63
|
+
cmds = commands_and_aliases
|
64
|
+
if cmds.all? { |cmd| cmd.is_a?(String) }
|
65
|
+
possible_matches = cmds.min_by(2) do |cmd|
|
66
|
+
CLI::Kit::Levenshtein.distance(cmd, name)
|
67
|
+
end
|
68
|
+
|
69
|
+
# We don't want to match against any possible command
|
70
|
+
# so reject anything that is too far away
|
71
|
+
possible_matches.reject! do |possible_match|
|
72
|
+
CLI::Kit::Levenshtein.distance(possible_match, name) > 3
|
73
|
+
end
|
74
|
+
|
75
|
+
# If we have any matches left, tell the user
|
76
|
+
if possible_matches.any?
|
77
|
+
CLI::UI::Frame.open("{{bold:Did you mean?}}", timing: false, color: :blue) do
|
78
|
+
possible_matches.each do |possible_match|
|
79
|
+
STDERR.puts CLI::UI.fmt("{{command:#{@tool_name} #{possible_match}}}")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/cli/kit/ini.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
module CLI
|
2
|
+
module Kit
|
3
|
+
# INI is a language similar to JSON or YAML, but simplied
|
4
|
+
# The spec is here: https://en.wikipedia.org/wiki/INI_file
|
5
|
+
# This parser includes supports for 2 very basic uses
|
6
|
+
# - Sections
|
7
|
+
# - Key Value Pairs (within and outside of the sections)
|
8
|
+
#
|
9
|
+
# [global]
|
10
|
+
# key = val
|
11
|
+
#
|
12
|
+
# Nothing else is supported right now
|
13
|
+
# See the ini_test.rb file for more examples
|
14
|
+
#
|
15
|
+
class Ini
|
16
|
+
attr_accessor :ini
|
17
|
+
|
18
|
+
def initialize(path = nil, default_section: nil, convert_types: true)
|
19
|
+
@config = File.readlines(path) if path && File.exist?(path)
|
20
|
+
@ini = {}
|
21
|
+
@current_key = nil
|
22
|
+
@default_section = default_section
|
23
|
+
@convert_types = convert_types
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse
|
27
|
+
return @ini if @config.nil?
|
28
|
+
|
29
|
+
@config.each do |l|
|
30
|
+
l.strip!
|
31
|
+
|
32
|
+
# If section, then set current key, this will nest the setting
|
33
|
+
if section_designator?(l)
|
34
|
+
@current_key = l
|
35
|
+
|
36
|
+
# A new line will reset the current key
|
37
|
+
elsif l.strip.empty?
|
38
|
+
@current_key = nil
|
39
|
+
|
40
|
+
# Otherwise set the values
|
41
|
+
else
|
42
|
+
k, v = l.split('=').map(&:strip)
|
43
|
+
set_val(k, v)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
@ini
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
to_ini(@ini).flatten.join("\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def to_ini(h)
|
56
|
+
str = []
|
57
|
+
h.each do |k, v|
|
58
|
+
if section_designator?(k)
|
59
|
+
str << "" unless str.empty?
|
60
|
+
str << k
|
61
|
+
str << to_ini(v)
|
62
|
+
else
|
63
|
+
str << "#{k} = #{v}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
str
|
67
|
+
end
|
68
|
+
|
69
|
+
def set_val(key, val)
|
70
|
+
return if key.nil? && val.nil?
|
71
|
+
|
72
|
+
current_key = @current_key || @default_section
|
73
|
+
if current_key
|
74
|
+
@ini[current_key] ||= {}
|
75
|
+
@ini[current_key][key] = typed_val(val)
|
76
|
+
else
|
77
|
+
@ini[key] = typed_val(val)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def typed_val(val)
|
82
|
+
return val.to_s unless @convert_types
|
83
|
+
return val.to_i if val =~ /^-?[0-9]+$/
|
84
|
+
return val.to_f if val =~ /^-?[0-9]+\.[0-9]*$/
|
85
|
+
val.to_s
|
86
|
+
end
|
87
|
+
|
88
|
+
def section_designator?(k)
|
89
|
+
k.start_with?('[') && k.end_with?(']')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Copyright (c) 2014-2016 Yuki Nishijima
|
2
|
+
|
3
|
+
# MIT License
|
4
|
+
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
# a copy of this software and associated documentation files (the
|
7
|
+
# "Software"), to deal in the Software without restriction, including
|
8
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
# the following conditions:
|
12
|
+
|
13
|
+
# The above copyright notice and this permission notice shall be
|
14
|
+
# included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
module CLI
|
25
|
+
module Kit
|
26
|
+
module Levenshtein
|
27
|
+
# This code is based directly on the Text gem implementation
|
28
|
+
# Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher.
|
29
|
+
#
|
30
|
+
# Returns a value representing the "cost" of transforming str1 into str2
|
31
|
+
def distance(str1, str2)
|
32
|
+
n = str1.length
|
33
|
+
m = str2.length
|
34
|
+
return m if n.zero?
|
35
|
+
return n if m.zero?
|
36
|
+
|
37
|
+
d = (0..m).to_a
|
38
|
+
x = nil
|
39
|
+
|
40
|
+
# to avoid duplicating an enumerable object, create it outside of the loop
|
41
|
+
str2_codepoints = str2.codepoints
|
42
|
+
|
43
|
+
str1.each_codepoint.with_index(1) do |char1, i|
|
44
|
+
j = 0
|
45
|
+
while j < m
|
46
|
+
cost = char1 == str2_codepoints[j] ? 0 : 1
|
47
|
+
x = min3(
|
48
|
+
d[j + 1] + 1, # insertion
|
49
|
+
i + 1, # deletion
|
50
|
+
d[j] + cost # substitution
|
51
|
+
)
|
52
|
+
d[j] = i
|
53
|
+
i = x
|
54
|
+
|
55
|
+
j += 1
|
56
|
+
end
|
57
|
+
d[m] = x
|
58
|
+
end
|
59
|
+
|
60
|
+
x
|
61
|
+
end
|
62
|
+
module_function :distance
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# detects the minimum value out of three arguments. This method is
|
67
|
+
# faster than `[a, b, c].min` and puts less GC pressure.
|
68
|
+
# See https://github.com/yuki24/did_you_mean/pull/1 for a performance
|
69
|
+
# benchmark.
|
70
|
+
def min3(a, b, c)
|
71
|
+
if a < b && a < c
|
72
|
+
a
|
73
|
+
elsif b < c
|
74
|
+
b
|
75
|
+
else
|
76
|
+
c
|
77
|
+
end
|
78
|
+
end
|
79
|
+
module_function :min3
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'cli/kit'
|
2
|
+
|
3
|
+
module CLI
|
4
|
+
module Kit
|
5
|
+
class Resolver
|
6
|
+
def initialize(command_registry:, error_handler:)
|
7
|
+
@command_registry = command_registry
|
8
|
+
@error_handler = error_handler
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(args)
|
12
|
+
args = args.dup
|
13
|
+
command_name = args.shift
|
14
|
+
|
15
|
+
@error_handler.handle_abort do
|
16
|
+
command, command_name = @command_registry.lookup_command(command_name)
|
17
|
+
return [command, command_name, args]
|
18
|
+
end
|
19
|
+
|
20
|
+
exit CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/cli/kit/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cli-kit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0.pre
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Burke Libbey
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2018-02-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: cli-ui
|
@@ -87,6 +87,15 @@ files:
|
|
87
87
|
- cli-kit.gemspec
|
88
88
|
- dev.yml
|
89
89
|
- lib/cli/kit.rb
|
90
|
+
- lib/cli/kit/base_command.rb
|
91
|
+
- lib/cli/kit/command_registry.rb
|
92
|
+
- lib/cli/kit/config.rb
|
93
|
+
- lib/cli/kit/error_handler.rb
|
94
|
+
- lib/cli/kit/executor.rb
|
95
|
+
- lib/cli/kit/ini.rb
|
96
|
+
- lib/cli/kit/levenshtein.rb
|
97
|
+
- lib/cli/kit/resolver.rb
|
98
|
+
- lib/cli/kit/ruby_backports/enumerable.rb
|
90
99
|
- lib/cli/kit/system.rb
|
91
100
|
- lib/cli/kit/version.rb
|
92
101
|
homepage: https://github.com/shopify/cli-kit
|
@@ -104,9 +113,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
104
113
|
version: '0'
|
105
114
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
115
|
requirements:
|
107
|
-
- - "
|
116
|
+
- - ">"
|
108
117
|
- !ruby/object:Gem::Version
|
109
|
-
version:
|
118
|
+
version: 1.3.1
|
110
119
|
requirements: []
|
111
120
|
rubyforge_project:
|
112
121
|
rubygems_version: 2.6.14
|