dry-cli 0.6.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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