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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 194c6fb318a717adb92458539db84f4e82c0c7d8
4
- data.tar.gz: ed39204760603166cb42990fef86db6f69f1e44a
3
+ metadata.gz: d38479bdf5b6e03725e44c04e6b44c306bad6a59
4
+ data.tar.gz: 4268a52a77172588b586e1e5e21cee635e7ed99a
5
5
  SHA512:
6
- metadata.gz: 325c4f7a072a9004275079d04b9925ef791a4d450ac63d757f9bf294234551a4b8f946e817d4287b146849d6823d46ae7a22a732b616bb1a2ceb1cbcd24f3fe2
7
- data.tar.gz: 0507ec4b45eddf96c736c4b552a45bea2528cdff30666006c86d5796d71503b1e25ec9dea415d1b1220b86a055a29a12535e94ed671dec5719301a2e55ee7670
6
+ metadata.gz: ddd5316dc74a06d96a35ad8953b198de161b1700822f15ff62e9987e46957fc493a7c6af1e605d435235567805a2bc230d4d287677a372619d3653e12c31e66e
7
+ data.tar.gz: e61a9cac88db2728fd81c5d782288788a8b6a95579e00b3c27b0c86129e0afcee3fbdde18104e3c8beb997fc9d9ee9190f7de6e4f22570679b93f8ed494ad1d7
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dev-kit (0.1.0)
5
- dev-ui (>= 0.1.0)
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
- dev-ui (0.1.0)
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
- dev-kit!
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.
@@ -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 :System, 'cli/kit/system'
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
@@ -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
@@ -0,0 +1,6 @@
1
+ module Enumerable
2
+ def min_by(n=nil, &block)
3
+ return sort_by(&block).first unless n
4
+ sort_by(&block).first(n)
5
+ end if instance_method(:min_by).arity == 0
6
+ end
@@ -1,5 +1,5 @@
1
1
  module CLI
2
2
  module Kit
3
- VERSION = "1.0.0"
3
+ VERSION = "3.0.0.pre"
4
4
  end
5
5
  end
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: 1.0.0
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: 2017-12-15 00:00:00.000000000 Z
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: '0'
118
+ version: 1.3.1
110
119
  requirements: []
111
120
  rubyforge_project:
112
121
  rubygems_version: 2.6.14