toys-core 0.3.3 → 0.3.4

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.
@@ -0,0 +1,487 @@
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
+ require "highline"
31
+
32
+ module Toys
33
+ module Utils
34
+ ##
35
+ # A helper class that generates usage documentation for a tool.
36
+ #
37
+ # This class generates full usage documentation, including description,
38
+ # flags, and arguments. It is used by middleware that implements help
39
+ # and related options.
40
+ #
41
+ class HelpText
42
+ ##
43
+ # Default width of first column
44
+ # @return [Integer]
45
+ #
46
+ DEFAULT_LEFT_COLUMN_WIDTH = 32
47
+
48
+ ##
49
+ # Default indent
50
+ # @return [Integer]
51
+ #
52
+ DEFAULT_INDENT = 4
53
+
54
+ ##
55
+ # Create a usage helper given an execution context.
56
+ #
57
+ # @param [Toys::Context] context The current execution context.
58
+ # @return [Toys::Utils::HelpText]
59
+ #
60
+ def self.from_context(context)
61
+ new(context[Context::TOOL], context[Context::LOADER], context[Context::BINARY_NAME])
62
+ end
63
+
64
+ ##
65
+ # Create a usage helper.
66
+ #
67
+ # @param [Toys::Tool] tool The tool for which to generate documentation.
68
+ # @param [Toys::Loader] loader A loader that can provide subcommands.
69
+ # @param [String] binary_name The name of the binary. e.g. `"toys"`.
70
+ #
71
+ # @return [Toys::Utils::HelpText]
72
+ #
73
+ def initialize(tool, loader, binary_name)
74
+ @tool = tool
75
+ @loader = loader
76
+ @binary_name = binary_name
77
+ end
78
+
79
+ attr_reader :tool
80
+
81
+ ##
82
+ # Generate a short usage string.
83
+ #
84
+ # @param [Boolean] recursive If true, and the tool is a group tool,
85
+ # display all subcommands recursively. 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}.
89
+ # @param [Integer,nil] wrap_width Overall width to wrap to. Default is
90
+ # `nil` indicating no wrapping.
91
+ #
92
+ # @return [String] A usage string.
93
+ #
94
+ def usage_string(recursive: false, left_column_width: nil, indent: nil, wrap_width: nil)
95
+ left_column_width ||= DEFAULT_LEFT_COLUMN_WIDTH
96
+ indent ||= DEFAULT_INDENT
97
+ subtools = find_subtools(recursive, nil)
98
+ assembler = UsageStringAssembler.new(@tool, @binary_name, subtools,
99
+ indent, left_column_width, wrap_width)
100
+ assembler.result
101
+ end
102
+
103
+ ##
104
+ # Generate a long help string.
105
+ #
106
+ # @param [Boolean] recursive If true, and the tool is a group tool,
107
+ # display all subcommands recursively. Defaults to false.
108
+ # @param [String,nil] search An optional string to search for when
109
+ # listing subcommands. Defaults to `nil` which finds all subcommands.
110
+ # @param [Integer] indent Indent width. Default is {DEFAULT_INDENT}.
111
+ # @param [Integer] indent2 Second indent width. Default is
112
+ # {DEFAULT_INDENT}.
113
+ # @param [Integer,nil] wrap_width Wrap width of the column, or `nil` to
114
+ # disable wrap. Default is `nil`.
115
+ # @param [Boolean] styled Output ansi styles. Default is `true`.
116
+ #
117
+ # @return [String] A usage string.
118
+ #
119
+ def help_string(recursive: false, search: nil,
120
+ indent: nil, indent2: nil, wrap_width: nil, styled: true)
121
+ indent ||= DEFAULT_INDENT
122
+ indent2 ||= DEFAULT_INDENT
123
+ subtools = find_subtools(recursive, search)
124
+ assembler = HelpStringAssembler.new(@tool, @binary_name, subtools, search,
125
+ indent, indent2, wrap_width, styled)
126
+ assembler.result
127
+ end
128
+
129
+ private
130
+
131
+ def find_subtools(recursive, search)
132
+ subtools = @loader.list_subtools(@tool.full_name, recursive: recursive)
133
+ return subtools if search.nil? || search.empty?
134
+ regex = Regexp.new(search, Regexp::IGNORECASE)
135
+ subtools.find_all do |tool|
136
+ regex =~ tool.display_name || regex =~ tool.desc.to_s
137
+ end
138
+ end
139
+
140
+ ## @private
141
+ class UsageStringAssembler
142
+ def initialize(tool, binary_name, subtools,
143
+ indent, left_column_width, wrap_width)
144
+ @tool = tool
145
+ @binary_name = binary_name
146
+ @subtools = subtools
147
+ @indent = indent
148
+ @left_column_width = left_column_width
149
+ @wrap_width = wrap_width
150
+ @right_column_wrap_width = wrap_width ? wrap_width - left_column_width - indent - 1 : nil
151
+ @lines = []
152
+ assemble
153
+ end
154
+
155
+ attr_reader :result
156
+
157
+ private
158
+
159
+ def assemble
160
+ add_synopsis_section
161
+ add_flags_section
162
+ add_positional_arguments_section if @tool.includes_executor?
163
+ add_subtool_list_section
164
+ @result = @lines.join("\n") + "\n"
165
+ end
166
+
167
+ def add_synopsis_section
168
+ synopses = []
169
+ synopses << group_synopsis if !@subtools.empty? && !@tool.includes_executor?
170
+ synopses << tool_synopsis
171
+ synopses << group_synopsis if !@subtools.empty? && @tool.includes_executor?
172
+ first = true
173
+ synopses.each do |synopsis|
174
+ @lines << (first ? "Usage: #{synopsis}" : " #{synopsis}")
175
+ first = false
176
+ end
177
+ end
178
+
179
+ def tool_synopsis
180
+ synopsis = [@binary_name] + @tool.full_name
181
+ synopsis << "[FLAGS...]" unless @tool.flag_definitions.empty?
182
+ @tool.arg_definitions.each do |arg_info|
183
+ synopsis << arg_name(arg_info)
184
+ end
185
+ synopsis.join(" ")
186
+ end
187
+
188
+ def group_synopsis
189
+ ([@binary_name] + @tool.full_name + ["TOOL", "[ARGUMENTS...]"]).join(" ")
190
+ end
191
+
192
+ def add_flags_section
193
+ return if @tool.flag_definitions.empty?
194
+ @lines << ""
195
+ @lines << "Flags:"
196
+ @tool.flag_definitions.each do |flag|
197
+ add_flag(flag)
198
+ end
199
+ end
200
+
201
+ def add_flag(flag)
202
+ flags_str = (flag.single_flag_syntax + flag.double_flag_syntax)
203
+ .map(&:str_without_value).join(", ")
204
+ flags_str << flag.value_delim << flag.value_label if flag.value_label
205
+ flags_str = " #{flags_str}" if flag.single_flag_syntax.empty?
206
+ add_right_column_desc(flags_str, wrap_desc(flag.desc))
207
+ end
208
+
209
+ def add_positional_arguments_section
210
+ args_to_display = @tool.arg_definitions
211
+ return if args_to_display.empty?
212
+ @lines << ""
213
+ @lines << "Positional arguments:"
214
+ args_to_display.each do |arg_info|
215
+ add_right_column_desc(arg_name(arg_info), wrap_desc(arg_info.desc))
216
+ end
217
+ end
218
+
219
+ def add_subtool_list_section
220
+ return if @subtools.empty?
221
+ name_len = @tool.full_name.length
222
+ @lines << ""
223
+ @lines << "Tools:"
224
+ @subtools.each do |subtool|
225
+ tool_name = subtool.full_name.slice(name_len..-1).join(" ")
226
+ desc =
227
+ if subtool.is_a?(Alias)
228
+ ["(Alias of #{subtool.display_target})"]
229
+ else
230
+ wrap_desc(subtool.desc)
231
+ end
232
+ add_right_column_desc(tool_name, desc)
233
+ end
234
+ end
235
+
236
+ def add_right_column_desc(initial, desc)
237
+ initial = indent_str(initial.ljust(@left_column_width))
238
+ remaining_doc = desc
239
+ if initial.size <= @indent + @left_column_width
240
+ @lines << "#{initial} #{desc.first}"
241
+ remaining_doc = desc[1..-1] || []
242
+ else
243
+ @lines << initial
244
+ end
245
+ remaining_doc.each do |d|
246
+ @lines << "#{' ' * (@indent + @left_column_width)} #{d}"
247
+ end
248
+ end
249
+
250
+ def arg_name(arg_info)
251
+ case arg_info.type
252
+ when :required
253
+ arg_info.display_name
254
+ when :optional
255
+ "[#{arg_info.display_name}]"
256
+ when :remaining
257
+ "[#{arg_info.display_name}...]"
258
+ end
259
+ end
260
+
261
+ def wrap_desc(desc)
262
+ Utils::WrappableString.wrap_lines(desc, @right_column_wrap_width)
263
+ end
264
+
265
+ def indent_str(str)
266
+ "#{' ' * @indent}#{str}"
267
+ end
268
+ end
269
+
270
+ ## @private
271
+ class HelpStringAssembler
272
+ def initialize(tool, binary_name, subtools, search_term,
273
+ indent, indent2, wrap_width, styled)
274
+ @tool = tool
275
+ @binary_name = binary_name
276
+ @subtools = subtools
277
+ @search_term = search_term
278
+ @indent = indent
279
+ @indent2 = indent2
280
+ @wrap_width = wrap_width
281
+ @styled = styled
282
+ @lines = []
283
+ assemble
284
+ end
285
+
286
+ attr_reader :result
287
+
288
+ private
289
+
290
+ def assemble
291
+ add_name_section
292
+ add_synopsis_section
293
+ add_description_section
294
+ add_flags_section
295
+ add_positional_arguments_section
296
+ add_subtool_list_section
297
+ add_source_section
298
+ @result = @lines.join("\n") + "\n"
299
+ end
300
+
301
+ def add_name_section
302
+ @lines << bold("NAME")
303
+ name_str = ([@binary_name] + @tool.full_name).join(" ")
304
+ add_prefix_with_desc(name_str, @tool.desc)
305
+ end
306
+
307
+ def add_prefix_with_desc(prefix, desc)
308
+ if desc.empty?
309
+ @lines << indent_str(prefix)
310
+ elsif !desc.is_a?(Utils::WrappableString)
311
+ @lines << indent_str("#{prefix} - #{desc}")
312
+ else
313
+ desc = wrap_indent_indent2(Utils::WrappableString.new(["#{prefix} -"] + desc.fragments))
314
+ @lines << indent_str(desc[0])
315
+ desc[1..-1].each do |line|
316
+ @lines << indent2_str(line)
317
+ end
318
+ end
319
+ end
320
+
321
+ def add_synopsis_section
322
+ @lines << ""
323
+ @lines << bold("SYNOPSIS")
324
+ if !@subtools.empty? && !@tool.includes_executor?
325
+ add_synopsis_clause(group_synopsis)
326
+ end
327
+ add_synopsis_clause(tool_synopsis)
328
+ if !@subtools.empty? && @tool.includes_executor?
329
+ add_synopsis_clause(group_synopsis)
330
+ end
331
+ end
332
+
333
+ def add_synopsis_clause(synopsis)
334
+ first = true
335
+ synopsis.each do |line|
336
+ @lines << (first ? indent_str(line) : indent2_str(line))
337
+ first = false
338
+ end
339
+ end
340
+
341
+ def tool_synopsis
342
+ synopsis = [full_binary_name]
343
+ @tool.flag_definitions.each do |flag_def|
344
+ synopsis << "[#{flag_spec_string(flag_def)}]"
345
+ end
346
+ @tool.arg_definitions.each do |arg_info|
347
+ synopsis << arg_name(arg_info)
348
+ end
349
+ wrap_indent_indent2(Utils::WrappableString.new(synopsis))
350
+ end
351
+
352
+ def group_synopsis
353
+ synopsis = [full_binary_name, underline("TOOL"), "[#{underline('ARGUMENTS')}...]"]
354
+ wrap_indent_indent2(Utils::WrappableString.new(synopsis))
355
+ end
356
+
357
+ def full_binary_name
358
+ bold(([@binary_name] + @tool.full_name).join(" "))
359
+ end
360
+
361
+ def add_source_section
362
+ return unless @tool.definition_path
363
+ @lines << ""
364
+ @lines << bold("SOURCE")
365
+ @lines << indent_str("Defined in #{@tool.definition_path}")
366
+ end
367
+
368
+ def add_description_section
369
+ desc = wrap_indent(@tool.long_desc)
370
+ return if desc.empty?
371
+ @lines << ""
372
+ @lines << bold("DESCRIPTION")
373
+ desc.each do |line|
374
+ @lines << indent_str(line)
375
+ end
376
+ end
377
+
378
+ def add_flags_section
379
+ return if @tool.flag_definitions.empty?
380
+ @lines << ""
381
+ @lines << bold("FLAGS")
382
+ precede_with_blank = false
383
+ @tool.flag_definitions.each do |flag|
384
+ add_indented_section(flag_spec_string(flag), flag, precede_with_blank)
385
+ precede_with_blank = true
386
+ end
387
+ end
388
+
389
+ def flag_spec_string(flag)
390
+ single_flags = flag.single_flag_syntax.map do |fs|
391
+ str = bold(fs.str_without_value)
392
+ flag.value_label ? "#{str} #{underline(flag.value_label)}" : str
393
+ end
394
+ double_flags = flag.double_flag_syntax.map do |fs|
395
+ str = bold(fs.str_without_value)
396
+ flag.value_label ? "#{str}#{flag.value_delim}#{underline(flag.value_label)}" : str
397
+ end
398
+ (single_flags + double_flags).join(", ")
399
+ end
400
+
401
+ def add_positional_arguments_section
402
+ args_to_display = @tool.arg_definitions
403
+ return if args_to_display.empty?
404
+ @lines << ""
405
+ @lines << bold("POSITIONAL ARGUMENTS")
406
+ precede_with_blank = false
407
+ args_to_display.each do |arg_info|
408
+ add_indented_section(arg_name(arg_info), arg_info, precede_with_blank)
409
+ precede_with_blank = true
410
+ end
411
+ end
412
+
413
+ def add_subtool_list_section
414
+ return if @subtools.empty?
415
+ @lines << ""
416
+ @lines << bold("TOOLS")
417
+ if @search_term
418
+ @lines << indent_str("Showing search results for \"#{@search_term}\"")
419
+ @lines << ""
420
+ end
421
+ name_len = @tool.full_name.length
422
+ @subtools.each do |subtool|
423
+ tool_name = subtool.full_name.slice(name_len..-1).join(" ")
424
+ desc = subtool.is_a?(Alias) ? "(Alias of #{subtool.display_target})" : subtool.desc
425
+ add_prefix_with_desc(bold(tool_name), desc)
426
+ end
427
+ end
428
+
429
+ def add_indented_section(header, info, precede_with_blank)
430
+ @lines << "" if precede_with_blank
431
+ @lines << indent_str(header)
432
+ desc = info
433
+ unless desc.is_a?(::Array)
434
+ desc = wrap_indent2(info.long_desc)
435
+ desc = wrap_indent2(info.desc) if desc.empty?
436
+ end
437
+ desc.each do |line|
438
+ @lines << indent2_str(line)
439
+ end
440
+ end
441
+
442
+ def arg_name(arg_info)
443
+ case arg_info.type
444
+ when :required
445
+ underline(arg_info.display_name)
446
+ when :optional
447
+ "[#{underline(arg_info.display_name)}]"
448
+ when :remaining
449
+ "[#{underline(arg_info.display_name)}...]"
450
+ end
451
+ end
452
+
453
+ def wrap_indent(input)
454
+ return Utils::WrappableString.wrap_lines(input, nil) unless @wrap_width
455
+ Utils::WrappableString.wrap_lines(input, @wrap_width - @indent)
456
+ end
457
+
458
+ def wrap_indent2(input)
459
+ return Utils::WrappableString.wrap_lines(input, nil) unless @wrap_width
460
+ Utils::WrappableString.wrap_lines(input, @wrap_width - @indent - @indent2)
461
+ end
462
+
463
+ def wrap_indent_indent2(input)
464
+ return Utils::WrappableString.wrap_lines(input, nil) unless @wrap_width
465
+ Utils::WrappableString.wrap_lines(input, @wrap_width - @indent,
466
+ @wrap_width - @indent - @indent2)
467
+ end
468
+
469
+ def bold(str)
470
+ @styled ? ::HighLine.color(str, :bold) : str
471
+ end
472
+
473
+ def underline(str)
474
+ @styled ? ::HighLine.color(str, :underline) : str
475
+ end
476
+
477
+ def indent_str(str)
478
+ "#{' ' * @indent}#{str}"
479
+ end
480
+
481
+ def indent2_str(str)
482
+ "#{' ' * (@indent + @indent2)}#{str}"
483
+ end
484
+ end
485
+ end
486
+ end
487
+ end