toys-core 0.3.2 → 0.3.3

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.
@@ -48,18 +48,21 @@ module Toys
48
48
  # @return [String] Converted string
49
49
  #
50
50
  def to_path_name(str)
51
- str.to_s.gsub(/([a-zA-Z])([A-Z])/) { |_m| "#{$1}_#{$2.downcase}" }.downcase
51
+ str = str.to_s.sub(/^_/, "").sub(/_$/, "").gsub(/_+/, "_")
52
+ while str.sub!(/([^_])([A-Z])/, "\\1_\\2") do end
53
+ str.downcase
52
54
  end
53
55
 
54
56
  ##
55
57
  # Convert the given string to a module name. Specifically, converts
56
- # to `UpperCamelCase`.
58
+ # to `UpperCamelCase`, and then to a symbol.
57
59
  #
58
60
  # @param [String,Symbol] str String to convert.
59
- # @return [String] Converted string
61
+ # @return [Symbol] Converted name
60
62
  #
61
63
  def to_module_name(str)
62
- str.to_s.gsub(/(^|_)([a-zA-Z0-9])/) { |_m| $2.upcase }
64
+ str = str.to_s.sub(/^_/, "").sub(/_$/, "").gsub(/_+/, "_")
65
+ str.to_s.gsub(/(^|_)([a-zA-Z])/) { |_m| $2.upcase }.to_sym
63
66
  end
64
67
 
65
68
  ##
@@ -77,7 +80,16 @@ module Toys
77
80
  #
78
81
  def lookup!(collection, name)
79
82
  require "toys/#{to_path_name(collection)}/#{to_path_name(name)}"
80
- ::Toys.const_get(to_module_name(collection)).const_get(to_module_name(name))
83
+ collection_sym = to_module_name(collection)
84
+ unless ::Toys.constants.include?(collection_sym)
85
+ raise ::NameError, "Module does not exist: Toys::#{collection_sym}"
86
+ end
87
+ collection_mod = ::Toys.const_get(collection_sym)
88
+ name_sym = to_module_name(name)
89
+ unless collection_mod.constants.include?(name_sym)
90
+ raise ::NameError, "Module does not exist: Toys::#{collection_sym}::#{name_sym}"
91
+ end
92
+ collection_mod.const_get(name_sym)
81
93
  end
82
94
 
83
95
  ##
@@ -37,6 +37,18 @@ module Toys
37
37
  # and related options.
38
38
  #
39
39
  class Usage
40
+ ##
41
+ # Default width of first column
42
+ # @return [Integer]
43
+ #
44
+ DEFAULT_LEFT_COLUMN_WIDTH = 32
45
+
46
+ ##
47
+ # Default indent
48
+ # @return [Integer]
49
+ #
50
+ DEFAULT_INDENT = 4
51
+
40
52
  ##
41
53
  # Create a usage helper given an execution context.
42
54
  #
@@ -44,22 +56,22 @@ module Toys
44
56
  # @return [Toys::Utils::Usage]
45
57
  #
46
58
  def self.from_context(context)
47
- new(context[Context::TOOL], context[Context::BINARY_NAME], context[Context::LOADER])
59
+ new(context[Context::TOOL], context[Context::LOADER], context[Context::BINARY_NAME])
48
60
  end
49
61
 
50
62
  ##
51
63
  # Create a usage helper.
52
64
  #
53
65
  # @param [Toys::Tool] tool The tool for which to generate documentation.
54
- # @param [String] binary_name The name of the binary. e.g. `"toys"`.
55
66
  # @param [Toys::Loader] loader A loader that can provide subcommands.
67
+ # @param [String] binary_name The name of the binary. e.g. `"toys"`.
56
68
  #
57
69
  # @return [Toys::Utils::Usage]
58
70
  #
59
- def initialize(tool, binary_name, loader)
71
+ def initialize(tool, loader, binary_name)
60
72
  @tool = tool
61
- @binary_name = binary_name
62
73
  @loader = loader
74
+ @binary_name = binary_name
63
75
  end
64
76
 
65
77
  ##
@@ -71,27 +83,29 @@ module Toys
71
83
  # listing subcommands. Defaults to `nil` which finds all subcommands.
72
84
  # @param [Boolean] show_path If true, shows the path to the config file
73
85
  # containing the tool definition (if set). Defaults to false.
86
+ # @param [Integer] left_column_width Width of the first column. Default
87
+ # is {DEFAULT_LEFT_COLUMN_WIDTH}.
88
+ # @param [Integer] indent Indent width. Default is {DEFAULT_INDENT}.
74
89
  #
75
90
  # @return [String] A usage string.
76
91
  #
77
- def string(recursive: false, search: nil, show_path: false)
78
- optparse = ::OptionParser.new
79
- optparse.banner = @tool.includes_executor? ? tool_banner : group_banner
80
- unless @tool.effective_long_desc.empty?
81
- optparse.separator("")
82
- optparse.separator(@tool.effective_long_desc)
83
- end
84
- if show_path && @tool.definition_path
85
- optparse.separator("")
86
- optparse.separator("Defined in #{@tool.definition_path}")
87
- end
88
- add_switches(optparse)
92
+ def string(recursive: false, search: nil, show_path: false,
93
+ left_column_width: nil, indent: nil,
94
+ wrap_width: nil, right_column_wrap_width: nil)
95
+ left_column_width ||= DEFAULT_LEFT_COLUMN_WIDTH
96
+ indent ||= DEFAULT_INDENT
97
+ right_column_wrap_width ||= wrap_width - left_column_width - indent - 1 if wrap_width
98
+ lines = []
99
+ lines << (@tool.includes_executor? ? tool_banner : group_banner)
100
+ add_description(lines, wrap_width, show_path)
101
+ add_switches(lines, indent, left_column_width, right_column_wrap_width)
89
102
  if @tool.includes_executor?
90
- add_positional_arguments(optparse)
103
+ add_positional_arguments(lines, indent, left_column_width, right_column_wrap_width)
91
104
  else
92
- add_command_list(optparse, recursive, search)
105
+ add_command_list(lines, recursive, search, indent,
106
+ left_column_width, right_column_wrap_width)
93
107
  end
94
- optparse.to_s
108
+ lines.join("\n") + "\n"
95
109
  end
96
110
 
97
111
  private
@@ -118,40 +132,62 @@ module Toys
118
132
  # Returns the banner string for a group
119
133
  #
120
134
  def group_banner
121
- list = ["Usage:", @binary_name] +
122
- @tool.full_name +
123
- ["<command>", "<command-arguments...>"]
124
- list.join(" ")
135
+ banner = ["Usage:", @binary_name] +
136
+ @tool.full_name +
137
+ ["<command>", "<command-arguments...>"]
138
+ banner.join(" ")
139
+ end
140
+
141
+ def add_description(lines, wrap_width, show_path)
142
+ long_desc = @tool.effective_long_desc(wrap_width: wrap_width)
143
+ unless long_desc.empty?
144
+ lines << ""
145
+ lines.concat(long_desc)
146
+ end
147
+ if show_path && @tool.definition_path
148
+ lines << ""
149
+ lines << "Defined in #{@tool.definition_path}"
150
+ end
125
151
  end
126
152
 
127
153
  #
128
154
  # Add switches from the tool to the given optionparser. Causes the
129
155
  # optparser to generate documentation for those switches.
130
156
  #
131
- def add_switches(optparse)
157
+ def add_switches(lines, indent, left_column_width, right_column_wrap_width)
132
158
  return if @tool.switch_definitions.empty?
133
- optparse.separator("")
134
- optparse.separator("Options:")
159
+ lines << ""
160
+ lines << "Options:"
135
161
  @tool.switch_definitions.each do |switch|
136
- optparse.on(*switch.optparse_info)
162
+ add_switch(lines, switch, indent, left_column_width, right_column_wrap_width)
137
163
  end
138
164
  end
139
165
 
166
+ #
167
+ # Add a single switch
168
+ #
169
+ def add_switch(lines, switch, indent, left_column_width, right_column_wrap_width)
170
+ switches_str = (switch.single_switch_syntax.map(&:str_without_value) +
171
+ switch.double_switch_syntax.map(&:str_without_value)).join(", ")
172
+ switches_str << switch.value_delim << switch.value_label if switch.value_label
173
+ switches_str = " #{switches_str}" if switch.single_switch_syntax.empty?
174
+ add_doc(lines, switches_str, switch.wrapped_docs(right_column_wrap_width),
175
+ indent, left_column_width)
176
+ end
177
+
140
178
  #
141
179
  # Add documentation for the tool's positional arguments, to the given
142
180
  # option parser.
143
181
  #
144
- def add_positional_arguments(optparse)
182
+ def add_positional_arguments(lines, indent, left_column_width, right_column_wrap_width)
145
183
  args_to_display = @tool.required_arg_definitions + @tool.optional_arg_definitions
146
184
  args_to_display << @tool.remaining_args_definition if @tool.remaining_args_definition
147
185
  return if args_to_display.empty?
148
- optparse.separator("")
149
- optparse.separator("Positional arguments:")
186
+ lines << ""
187
+ lines << "Positional arguments:"
150
188
  args_to_display.each do |arg_info|
151
- optparse.separator(" #{arg_info.canonical_name.ljust(31)} #{arg_info.doc.first}")
152
- (arg_info.doc[1..-1] || []).each do |d|
153
- optparse.separator(" #{d}")
154
- end
189
+ add_doc(lines, arg_info.canonical_name, arg_info.wrapped_docs(right_column_wrap_width),
190
+ indent, left_column_width)
155
191
  end
156
192
  end
157
193
 
@@ -159,23 +195,40 @@ module Toys
159
195
  # Add documentation for the tool's subcommands, to the given option
160
196
  # parser.
161
197
  #
162
- def add_command_list(optparse, recursive, search)
198
+ def add_command_list(lines, recursive, search, indent,
199
+ left_column_width, right_column_wrap_width)
163
200
  name_len = @tool.full_name.length
164
201
  subtools = find_commands(recursive, search)
165
202
  return if subtools.empty?
166
- optparse.separator("")
167
- if search
168
- optparse.separator("Commands with search term #{search.inspect}:")
169
- else
170
- optparse.separator("Commands:")
171
- end
203
+ lines << ""
204
+ lines << (search ? "Commands with search term #{search.inspect}:" : "Commands:")
172
205
  subtools.each do |subtool|
173
- tool_name = subtool.full_name.slice(name_len..-1).join(" ").ljust(31)
174
- if subtool.is_a?(Alias)
175
- optparse.separator(" #{tool_name} (Alias of #{subtool.display_target})")
206
+ tool_name = subtool.full_name.slice(name_len..-1).join(" ")
207
+ doc =
208
+ if subtool.is_a?(Alias)
209
+ ["(Alias of #{subtool.display_target})"]
210
+ else
211
+ subtool.effective_desc(wrap_width: right_column_wrap_width)
212
+ end
213
+ add_doc(lines, tool_name, doc, indent, left_column_width)
214
+ end
215
+ end
216
+
217
+ #
218
+ # Add a line with possible documentation strings.
219
+ #
220
+ def add_doc(lines, initial, doc, indent, left_column_width)
221
+ initial = "#{' ' * indent}#{initial.ljust(left_column_width)}"
222
+ remaining_doc =
223
+ if initial.size <= indent + left_column_width
224
+ lines << "#{initial} #{doc.first}"
225
+ doc[1..-1] || []
176
226
  else
177
- optparse.separator(" #{tool_name} #{subtool.effective_desc}")
227
+ lines << initial
228
+ doc
178
229
  end
230
+ remaining_doc.each do |d|
231
+ lines << "#{' ' * (indent + left_column_width)} #{d}"
179
232
  end
180
233
  end
181
234
 
@@ -187,8 +240,9 @@ module Toys
187
240
  return subtools if search.nil? || search.empty?
188
241
  regex = Regexp.new("(^|\\s)#{Regexp.escape(search)}(\\s|$)", Regexp::IGNORECASE)
189
242
  subtools.find_all do |tool|
190
- regex =~ tool.display_name || regex =~ tool.effective_desc ||
191
- regex =~ tool.effective_long_desc
243
+ regex =~ tool.display_name ||
244
+ tool.effective_desc.find { |d| regex =~ d } ||
245
+ tool.effective_long_desc.find { |d| regex =~ d }
192
246
  end
193
247
  end
194
248
  end
@@ -0,0 +1,92 @@
1
+ # Copyright 2018 Daniel Azuma
2
+ #
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # * Neither the name of the copyright holder, nor the names of any other
14
+ # contributors to this software, may be used to endorse or promote products
15
+ # derived from this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ # POSSIBILITY OF SUCH DAMAGE.
28
+ ;
29
+
30
+ module Toys
31
+ module Utils
32
+ ##
33
+ # A string intended to be wrapped.
34
+ #
35
+ class WrappableString
36
+ ##
37
+ # Create a wrapped string.
38
+ # @param [String] string The string.
39
+ #
40
+ def initialize(string = "")
41
+ @string = string
42
+ end
43
+
44
+ ##
45
+ # Returns the string.
46
+ # @return [String]
47
+ #
48
+ attr_reader :string
49
+
50
+ ##
51
+ # Returns the string.
52
+ # @return [String]
53
+ #
54
+ def to_s
55
+ string
56
+ end
57
+
58
+ ## @private
59
+ def ==(other)
60
+ other.is_a?(WrappableString) ? other.string == string : false
61
+ end
62
+ alias eql? ==
63
+
64
+ ## @private
65
+ def hash
66
+ string.hash
67
+ end
68
+
69
+ ##
70
+ # Wraps the string to the given width.
71
+ #
72
+ # @param [Integer] width Width in characters.
73
+ # @return [Array<String>] Wrapped lines
74
+ #
75
+ def wrap(width)
76
+ lines = []
77
+ str = string.gsub(/\s/, " ").sub(/^\s+/, "")
78
+ until str.empty?
79
+ i = str.index(/\S(\s|$)/) + 1
80
+ loop do
81
+ next_i = str.index(/\S(\s|$)/, i)
82
+ break if next_i.nil? || next_i >= width
83
+ i = next_i + 1
84
+ end
85
+ lines << str[0, i]
86
+ str = str[i..-1].sub(/^\s+/, "")
87
+ end
88
+ lines
89
+ end
90
+ end
91
+ end
92
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toys-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Azuma
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-07 00:00:00.000000000 Z
11
+ date: 2018-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: highline
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: minitest
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -100,7 +114,9 @@ files:
100
114
  - lib/toys/errors.rb
101
115
  - lib/toys/helpers.rb
102
116
  - lib/toys/helpers/exec.rb
103
- - lib/toys/helpers/file_utils.rb
117
+ - lib/toys/helpers/fileutils.rb
118
+ - lib/toys/helpers/highline.rb
119
+ - lib/toys/helpers/spinner.rb
104
120
  - lib/toys/loader.rb
105
121
  - lib/toys/middleware.rb
106
122
  - lib/toys/middleware/add_verbosity_switches.rb
@@ -119,6 +135,7 @@ files:
119
135
  - lib/toys/tool.rb
120
136
  - lib/toys/utils/module_lookup.rb
121
137
  - lib/toys/utils/usage.rb
138
+ - lib/toys/utils/wrappable_string.rb
122
139
  homepage: https://github.com/dazuma/toys
123
140
  licenses:
124
141
  - BSD-3-Clause