dry-cli 0.6.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
- require 'concurrent/hash'
3
+ require "set"
5
4
 
6
5
  module Dry
7
6
  class CLI
@@ -13,52 +12,65 @@ module Dry
13
12
  # @since 0.1.0
14
13
  # @api private
15
14
  def initialize
15
+ @_mutex = Mutex.new
16
16
  @root = Node.new
17
17
  end
18
18
 
19
19
  # @since 0.1.0
20
20
  # @api private
21
21
  def set(name, command, aliases)
22
- node = @root
23
- name.split(/[[:space:]]/).each do |token|
24
- node = node.put(node, token)
25
- end
22
+ @_mutex.synchronize do
23
+ node = @root
24
+ name.split(/[[:space:]]/).each do |token|
25
+ node = node.put(node, token)
26
+ end
26
27
 
27
- node.aliases!(aliases)
28
- node.leaf!(command) if command
28
+ node.aliases!(aliases)
29
+ if command
30
+ node.leaf!(command)
31
+ node.subcommands!(command)
32
+ end
29
33
 
30
- nil
34
+ nil
35
+ end
31
36
  end
32
37
 
33
38
  # @since 0.1.0
34
39
  # @api private
35
40
  #
36
41
  def get(arguments)
37
- node = @root
38
- args = []
39
- names = []
40
- result = LookupResult.new(node, args, names, node.leaf?)
41
-
42
- arguments.each_with_index do |token, i|
43
- tmp = node.lookup(token)
44
-
45
- if tmp.nil?
46
- result = LookupResult.new(node, args, names, false)
47
- break
48
- elsif tmp.leaf?
49
- args = arguments[i + 1..-1]
50
- names = arguments[0..i]
51
- node = tmp
52
- result = LookupResult.new(node, args, names, true)
53
- break
54
- else
55
- names = arguments[0..i]
56
- node = tmp
57
- result = LookupResult.new(node, args, names, node.leaf?)
42
+ @_mutex.synchronize do
43
+ node = @root
44
+ args = []
45
+ names = []
46
+ valid_leaf = nil
47
+ result = LookupResult.new(node, args, names, node.leaf?)
48
+
49
+ arguments.each_with_index do |token, i|
50
+ tmp = node.lookup(token)
51
+
52
+ if tmp.nil? && valid_leaf
53
+ result = valid_leaf
54
+ break
55
+ elsif tmp.nil?
56
+ result = LookupResult.new(node, args, names, false)
57
+ break
58
+ elsif tmp.leaf?
59
+ args = arguments[i + 1..-1]
60
+ names = arguments[0..i]
61
+ node = tmp
62
+ result = LookupResult.new(node, args, names, true)
63
+ valid_leaf = result
64
+ break unless tmp.children?
65
+ else
66
+ names = arguments[0..i]
67
+ node = tmp
68
+ result = LookupResult.new(node, args, names, node.leaf?)
69
+ end
58
70
  end
59
- end
60
71
 
61
- result
72
+ result
73
+ end
62
74
  end
63
75
 
64
76
  # Node of the registry
@@ -94,8 +106,8 @@ module Dry
94
106
  # @api private
95
107
  def initialize(parent = nil)
96
108
  @parent = parent
97
- @children = Concurrent::Hash.new
98
- @aliases = Concurrent::Hash.new
109
+ @children = {}
110
+ @aliases = {}
99
111
  @command = nil
100
112
 
101
113
  @before_callbacks = Chain.new
@@ -120,6 +132,13 @@ module Dry
120
132
  @command = command
121
133
  end
122
134
 
135
+ # @since 0.7.0
136
+ # @api private
137
+ def subcommands!(command)
138
+ command_class = command.is_a?(Class) ? command : command.class
139
+ command_class.subcommands = children
140
+ end
141
+
123
142
  # @since 0.1.0
124
143
  # @api private
125
144
  def alias!(key, child)
@@ -139,6 +158,12 @@ module Dry
139
158
  def leaf?
140
159
  !command.nil?
141
160
  end
161
+
162
+ # @since 0.7.0
163
+ # @api private
164
+ def children?
165
+ children.any?
166
+ end
142
167
  end
143
168
 
144
169
  # Result of a registry lookup
@@ -10,7 +10,7 @@ module Dry
10
10
  def self.dasherize(input)
11
11
  return nil unless input
12
12
 
13
- input.to_s.downcase.gsub(/[[[:space:]]_]/, '-')
13
+ input.to_s.downcase.gsub(/[[[:space:]]_]/, "-")
14
14
  end
15
15
  end
16
16
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'backports/2.5.0/module/define_method' if RUBY_VERSION < '2.5'
3
+ require "backports/2.5.0/module/define_method" if RUBY_VERSION < "2.5"
4
4
 
5
5
  module Dry
6
6
  class CLI
7
- require 'dry/cli'
7
+ require "dry/cli"
8
8
  # Inline Syntax (aka DSL) to implement one-file applications
9
9
  #
10
10
  # `dry/cli/inline` is not required by default
@@ -62,8 +62,8 @@ module Dry
62
62
  # @since 0.6.0
63
63
  def run(arguments: ARGV, out: $stdout)
64
64
  command = AnonymousCommand
65
- command.define_method(:call) do |*args|
66
- yield(*args)
65
+ command.define_method(:call) do |**args|
66
+ yield(**args)
67
67
  end
68
68
 
69
69
  Dry.CLI(command).call(arguments: arguments, out: out)
@@ -32,7 +32,7 @@ module Dry
32
32
  # @api private
33
33
  def desc
34
34
  desc = options[:desc]
35
- values ? "#{desc}: (#{values.join('/')})" : desc
35
+ values ? "#{desc}: (#{values.join("/")})" : desc
36
36
  end
37
37
 
38
38
  # @since 0.1.0
@@ -108,7 +108,7 @@ module Dry
108
108
  # @api private
109
109
  def alias_names
110
110
  aliases
111
- .map { |name| name.gsub(/^-{1,2}/, '') }
111
+ .map { |name| name.gsub(/^-{1,2}/, "") }
112
112
  .compact
113
113
  .uniq
114
114
  .map { |name| name.size == 1 ? "-#{name}" : "--#{name}" }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'optparse'
4
- require 'dry/cli/program_name'
3
+ require "optparse"
4
+ require "dry/cli/program_name"
5
5
 
6
6
  module Dry
7
7
  class CLI
@@ -13,7 +13,7 @@ module Dry
13
13
  # @since 0.1.0
14
14
  # @api private
15
15
  #
16
- def self.call(command, arguments, names)
16
+ def self.call(command, arguments, prog_name)
17
17
  original_arguments = arguments.dup
18
18
  parsed_options = {}
19
19
 
@@ -24,28 +24,22 @@ module Dry
24
24
  end
25
25
  end
26
26
 
27
- opts.on_tail('-h', '--help') do
27
+ opts.on_tail("-h", "--help") do
28
28
  return Result.help
29
29
  end
30
30
  end.parse!(arguments)
31
31
 
32
32
  parsed_options = command.default_params.merge(parsed_options)
33
- parse_required_params(command, arguments, names, parsed_options)
33
+ parse_required_params(command, arguments, prog_name, parsed_options)
34
34
  rescue ::OptionParser::ParseError
35
- Result.failure("Error: \"#{names.last}\" was called with arguments \"#{original_arguments.join(' ')}\"") # rubocop:disable Metrics/LineLength
36
- end
37
-
38
- # @since 0.1.0
39
- # @api private
40
- def self.full_command_name(names)
41
- ProgramName.call(names)
35
+ Result.failure("ERROR: \"#{prog_name}\" was called with arguments \"#{original_arguments.join(" ")}\"") # rubocop:disable Metrics/LineLength
42
36
  end
43
37
 
44
38
  # @since 0.1.0
45
39
  # @api private
46
40
  #
47
41
  # rubocop:disable Metrics/AbcSize
48
- def self.parse_required_params(command, arguments, names, parsed_options)
42
+ def self.parse_required_params(command, arguments, prog_name, parsed_options)
49
43
  parsed_params = match_arguments(command.arguments, arguments)
50
44
  parsed_required_params = match_arguments(command.required_arguments, arguments)
51
45
  all_required_params_satisfied = command.required_arguments.all? { |param| !parsed_required_params[param.name].nil? } # rubocop:disable Metrics/LineLength
@@ -55,12 +49,16 @@ module Dry
55
49
  unless all_required_params_satisfied
56
50
  parsed_required_params_values = parsed_required_params.values.compact
57
51
 
58
- usage = "\nUsage: \"#{full_command_name(names)} #{command.required_arguments.map(&:description_name).join(' ')}\"" # rubocop:disable Metrics/LineLength
52
+ usage = "\nUsage: \"#{prog_name} #{command.required_arguments.map(&:description_name).join(" ")}" # rubocop:disable Metrics/LineLength
53
+
54
+ usage += " | #{prog_name} SUBCOMMAND" if command.subcommands.any?
55
+
56
+ usage += '"'
59
57
 
60
- if parsed_required_params_values.empty? # rubocop:disable Style/GuardClause
61
- return Result.failure("ERROR: \"#{full_command_name(names)}\" was called with no arguments#{usage}") # rubocop:disable Metrics/LineLength
58
+ if parsed_required_params_values.empty?
59
+ return Result.failure("ERROR: \"#{prog_name}\" was called with no arguments#{usage}")
62
60
  else
63
- return Result.failure("ERROR: \"#{full_command_name(names)}\" was called with arguments #{parsed_required_params_values}#{usage}") # rubocop:disable Metrics/LineLength
61
+ return Result.failure("ERROR: \"#{prog_name}\" was called with arguments #{parsed_required_params_values}#{usage}") # rubocop:disable Metrics/LineLength
64
62
  end
65
63
  end
66
64
 
@@ -103,7 +101,7 @@ module Dry
103
101
 
104
102
  # @since 0.1.0
105
103
  # @api private
106
- def self.failure(error = 'Error: Invalid param provided')
104
+ def self.failure(error = "Error: Invalid param provided")
107
105
  new(error: error)
108
106
  end
109
107
 
@@ -9,7 +9,7 @@ module Dry
9
9
  module ProgramName
10
10
  # @since 0.1.0
11
11
  # @api private
12
- SEPARATOR = ' '
12
+ SEPARATOR = " "
13
13
 
14
14
  # @since 0.1.0
15
15
  # @api private
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/cli/command_registry'
3
+ require "dry/cli/command_registry"
4
4
 
5
5
  module Dry
6
6
  class CLI
@@ -12,6 +12,7 @@ module Dry
12
12
  # @api private
13
13
  def self.extended(base)
14
14
  base.class_eval do
15
+ @_mutex = Mutex.new
15
16
  @commands = CommandRegistry.new
16
17
  end
17
18
  end
@@ -75,6 +76,8 @@ module Dry
75
76
  # end
76
77
  # end
77
78
  def register(name, command = nil, aliases: [], &block)
79
+ @commands.set(name, command, aliases)
80
+
78
81
  if block_given?
79
82
  prefix = Prefix.new(@commands, name, aliases)
80
83
  if block.arity.zero?
@@ -82,8 +85,6 @@ module Dry
82
85
  else
83
86
  yield(prefix)
84
87
  end
85
- else
86
- @commands.set(name, command, aliases)
87
88
  end
88
89
  end
89
90
 
@@ -170,7 +171,9 @@ module Dry
170
171
  # end
171
172
  # end
172
173
  def before(command_name, callback = nil, &blk)
173
- command(command_name).before_callbacks.append(&_callback(callback, blk))
174
+ @_mutex.synchronize do
175
+ command(command_name).before_callbacks.append(&_callback(callback, blk))
176
+ end
174
177
  end
175
178
 
176
179
  # Register an after callback.
@@ -256,7 +259,9 @@ module Dry
256
259
  # end
257
260
  # end
258
261
  def after(command_name, callback = nil, &blk)
259
- command(command_name).after_callbacks.append(&_callback(callback, blk))
262
+ @_mutex.synchronize do
263
+ command(command_name).after_callbacks.append(&_callback(callback, blk))
264
+ end
260
265
  end
261
266
 
262
267
  # @since 0.1.0
@@ -267,7 +272,7 @@ module Dry
267
272
 
268
273
  private
269
274
 
270
- COMMAND_NAME_SEPARATOR = ' '
275
+ COMMAND_NAME_SEPARATOR = " "
271
276
 
272
277
  # @since 0.2.0
273
278
  # @api private
data/lib/dry/cli/usage.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/cli/program_name'
3
+ require "dry/cli/program_name"
4
4
 
5
5
  module Dry
6
6
  class CLI
@@ -11,12 +11,13 @@ module Dry
11
11
  module Usage
12
12
  # @since 0.1.0
13
13
  # @api private
14
- SUBCOMMAND_BANNER = ' [SUBCOMMAND]'
14
+ SUBCOMMAND_BANNER = " [SUBCOMMAND]"
15
+ ROOT_COMMAND_WITH_SUBCOMMANDS_BANNER = " [ARGUMENT|SUBCOMMAND]"
15
16
 
16
17
  # @since 0.1.0
17
18
  # @api private
18
19
  def self.call(result)
19
- header = 'Commands:'
20
+ header = "Commands:"
20
21
  max_length, commands = commands_and_arguments(result)
21
22
 
22
23
  commands.map do |banner, node|
@@ -30,7 +31,9 @@ module Dry
30
31
  def self.commands_and_arguments(result)
31
32
  max_length = 0
32
33
  ret = commands(result).each_with_object({}) do |(name, node), memo|
33
- args = if node.leaf?
34
+ args = if node.command && node.leaf? && node.children?
35
+ ROOT_COMMAND_WITH_SUBCOMMANDS_BANNER
36
+ elsif node.leaf?
34
37
  arguments(node.command)
35
38
  else
36
39
  SUBCOMMAND_BANNER
@@ -52,11 +55,11 @@ module Dry
52
55
  required_arguments = command.required_arguments
53
56
  optional_arguments = command.optional_arguments
54
57
 
55
- required = required_arguments.map { |arg| arg.name.upcase }.join(' ') if required_arguments.any? # rubocop:disable Metrics/LineLength
56
- optional = optional_arguments.map { |arg| "[#{arg.name.upcase}]" }.join(' ') if optional_arguments.any? # rubocop:disable Metrics/LineLength
58
+ required = required_arguments.map { |arg| arg.name.upcase }.join(" ") if required_arguments.any? # rubocop:disable Metrics/LineLength
59
+ optional = optional_arguments.map { |arg| "[#{arg.name.upcase}]" }.join(" ") if optional_arguments.any? # rubocop:disable Metrics/LineLength
57
60
  result = [required, optional].compact
58
61
 
59
- " #{result.join(' ')}" unless result.empty?
62
+ " #{result.join(" ")}" unless result.empty?
60
63
  end
61
64
 
62
65
  # @since 0.1.0
@@ -70,7 +73,7 @@ module Dry
70
73
  # @since 0.1.0
71
74
  # @api private
72
75
  def self.justify(string, padding, usage)
73
- return string.chomp(' ') if usage.nil?
76
+ return string.chomp(" ") if usage.nil?
74
77
 
75
78
  string.ljust(padding + padding / 2)
76
79
  end
@@ -3,6 +3,6 @@
3
3
  module Dry
4
4
  class CLI
5
5
  # @since 0.1.0
6
- VERSION = '0.6.0'
6
+ VERSION = "1.0.0"
7
7
  end
8
8
  end
data/lib/dry/cli.rb CHANGED
@@ -8,14 +8,14 @@ module Dry
8
8
  #
9
9
  # @since 0.1.0
10
10
  class CLI
11
- require 'dry/cli/version'
12
- require 'dry/cli/errors'
13
- require 'dry/cli/command'
14
- require 'dry/cli/registry'
15
- require 'dry/cli/parser'
16
- require 'dry/cli/usage'
17
- require 'dry/cli/banner'
18
- require 'dry/cli/inflector'
11
+ require "dry/cli/version"
12
+ require "dry/cli/errors"
13
+ require "dry/cli/command"
14
+ require "dry/cli/registry"
15
+ require "dry/cli/parser"
16
+ require "dry/cli/usage"
17
+ require "dry/cli/banner"
18
+ require "dry/cli/inflector"
19
19
 
20
20
  # Check if command
21
21
  #
@@ -62,9 +62,11 @@ module Dry
62
62
  # @since 0.1.0
63
63
  def call(arguments: ARGV, out: $stdout, err: $stderr)
64
64
  @out, @err = out, err
65
- return perform_command(arguments) if kommand
66
-
67
- perform_registry(arguments)
65
+ kommand ? perform_command(arguments) : perform_registry(arguments)
66
+ rescue SignalException => e
67
+ signal_exception(e)
68
+ rescue Errno::EPIPE
69
+ # no op
68
70
  end
69
71
 
70
72
  private
@@ -106,16 +108,13 @@ module Dry
106
108
  # @api private
107
109
  def perform_registry(arguments)
108
110
  result = registry.get(arguments)
111
+ return usage(result) unless result.found?
109
112
 
110
- if result.found?
111
- command, args = parse(result.command, result.arguments, result.names)
113
+ command, args = parse(result.command, result.arguments, result.names)
112
114
 
113
- result.before_callbacks.run(command, args)
114
- command.call(**args)
115
- result.after_callbacks.run(command, args)
116
- else
117
- usage(result)
118
- end
115
+ result.before_callbacks.run(command, args)
116
+ command.call(**args)
117
+ result.after_callbacks.run(command, args)
119
118
  end
120
119
 
121
120
  # Parse arguments for a command.
@@ -131,26 +130,37 @@ module Dry
131
130
  # @since 0.6.0
132
131
  # @api private
133
132
  def parse(command, arguments, names)
134
- result = Parser.call(command, arguments, names)
133
+ prog_name = ProgramName.call(names)
135
134
 
136
- if result.help?
137
- out.puts Banner.call(command, names)
138
- exit(0)
139
- end
135
+ result = Parser.call(command, arguments, prog_name)
140
136
 
141
- if result.error?
142
- err.puts(result.error)
143
- exit(1)
144
- end
137
+ return help(command, prog_name) if result.help?
138
+
139
+ return error(result) if result.error?
145
140
 
146
- [command.new, result.arguments]
141
+ [build_command(command), result.arguments]
142
+ end
143
+
144
+ # @since 0.6.0
145
+ # @api private
146
+ def build_command(command)
147
+ command.is_a?(Class) ? command.new : command
148
+ end
149
+
150
+ # @since 0.6.0
151
+ # @api private
152
+ def help(command, prog_name)
153
+ out.puts Banner.call(command, prog_name)
154
+ exit(0) # Successful exit
155
+ end
156
+
157
+ # @since 0.6.0
158
+ # @api private
159
+ def error(result)
160
+ err.puts(result.error)
161
+ exit(1)
147
162
  end
148
163
 
149
- # Prints the command usage and exit.
150
- #
151
- # @param result [Dry::CLI::CommandRegistry::LookupResult]
152
- # @param out [IO] sta output
153
- #
154
164
  # @since 0.1.0
155
165
  # @api private
156
166
  def usage(result)
@@ -158,6 +168,15 @@ module Dry
158
168
  exit(1)
159
169
  end
160
170
 
171
+ # Handles Exit codes for signals
172
+ # Fatal error signal "n". Say 130 = 128 + 2 (SIGINT) or 137 = 128 + 9 (SIGKILL)
173
+ #
174
+ # @since 0.7.0
175
+ # @api private
176
+ def signal_exception(exception)
177
+ exit(128 + exception.signo)
178
+ end
179
+
161
180
  # Check if command
162
181
  #
163
182
  # @param command [Object] the command to check
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luca Guidi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-06 00:00:00.000000000 Z
11
+ date: 2022-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: concurrent-ruby
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: bundler
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -72,6 +58,20 @@ dependencies:
72
58
  - - "~>"
73
59
  - !ruby/object:Gem::Version
74
60
  version: '3.7'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rubocop
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.82'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.82'
75
75
  - !ruby/object:Gem::Dependency
76
76
  name: simplecov
77
77
  requirement: !ruby/object:Gem::Requirement
@@ -109,14 +109,13 @@ files:
109
109
  - lib/dry/cli/program_name.rb
110
110
  - lib/dry/cli/registry.rb
111
111
  - lib/dry/cli/usage.rb
112
- - lib/dry/cli/utils/files.rb
113
112
  - lib/dry/cli/version.rb
114
113
  homepage: https://dry-rb.org/gems/dry-cli
115
114
  licenses:
116
115
  - MIT
117
116
  metadata:
118
117
  allowed_push_host: https://rubygems.org
119
- changelog_uri: https://github.com/dry-rb/dry-cli/blob/master/CHANGELOG.md
118
+ changelog_uri: https://github.com/dry-rb/dry-cli/blob/main/CHANGELOG.md
120
119
  source_code_uri: https://github.com/dry-rb/dry-cli
121
120
  bug_tracker_uri: https://github.com/dry-rb/dry-cli/issues
122
121
  post_install_message:
@@ -127,14 +126,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
127
126
  requirements:
128
127
  - - ">="
129
128
  - !ruby/object:Gem::Version
130
- version: 2.3.0
129
+ version: 2.7.0
131
130
  required_rubygems_version: !ruby/object:Gem::Requirement
132
131
  requirements:
133
132
  - - ">="
134
133
  - !ruby/object:Gem::Version
135
134
  version: '0'
136
135
  requirements: []
137
- rubygems_version: 3.0.3
136
+ rubygems_version: 3.1.6
138
137
  signing_key:
139
138
  specification_version: 4
140
139
  summary: Common framework to build command line interfaces with Ruby