cli-kit 1.0.0 → 3.0.0.pre
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/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
|