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 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