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.
@@ -36,7 +36,7 @@ module Toys
36
36
  ##
37
37
  # The base middleware does not affect tool configuration.
38
38
  #
39
- def config(_tool)
39
+ def config(_tool, _loader)
40
40
  yield
41
41
  end
42
42
 
@@ -30,15 +30,16 @@
30
30
  require "highline"
31
31
 
32
32
  require "toys/middleware/base"
33
- require "toys/utils/usage"
33
+ require "toys/utils/help_text"
34
+ require "toys/utils/line_output"
34
35
 
35
36
  module Toys
36
37
  module Middleware
37
38
  ##
38
39
  # This middleware handles the case of a usage error. If a usage error, such
39
- # as an unrecognized switch or an unfulfilled required argument, is
40
- # detected, this middleware intercepts execution and displays the error
41
- # along with the usage string, and terminates execution with an error code.
40
+ # as an unrecognized flag or an unfulfilled required argument, is detected,
41
+ # this middleware intercepts execution and displays the error along with
42
+ # the short help string, and terminates execution with an error code.
42
43
  #
43
44
  class HandleUsageErrors < Base
44
45
  ##
@@ -46,9 +47,14 @@ module Toys
46
47
  #
47
48
  # @param [Intgeer] exit_code The exit code to return if a usage error
48
49
  # occurs. Default is -1.
50
+ # @param [IO] stream Output stream to write to. Default is stderr.
51
+ # @param [Boolean,nil] styled_output Cause the tool to display help text
52
+ # with ansi styles. If `nil`, display styles if the output stream is
53
+ # a tty. Default is `nil`.
49
54
  #
50
- def initialize(exit_code: -1)
55
+ def initialize(exit_code: -1, stream: $stderr, styled_output: nil)
51
56
  @exit_code = exit_code
57
+ @output = Utils::LineOutput.new(stream, styled: styled_output)
52
58
  end
53
59
 
54
60
  ##
@@ -57,10 +63,10 @@ module Toys
57
63
  def execute(context)
58
64
  if context[Context::USAGE_ERROR]
59
65
  width = ::HighLine.new.output_cols
60
- usage = Utils::Usage.from_context(context)
61
- puts(context[Context::USAGE_ERROR])
62
- puts("")
63
- puts(usage.string(show_path: true, wrap_width: width))
66
+ help_text = Utils::HelpText.from_context(context)
67
+ @output.puts(context[Context::USAGE_ERROR], :bright_red, :bold)
68
+ @output.puts("")
69
+ @output.puts(help_text.usage_string(wrap_width: width))
64
70
  context.exit(@exit_code)
65
71
  else
66
72
  yield
@@ -41,89 +41,166 @@ module Toys
41
41
  # The default description for tools.
42
42
  # @return [String]
43
43
  #
44
- DEFAULT_TOOL_DESC = "(No description available)".freeze
44
+ DEFAULT_TOOL_DESC = "(No tool description available)".freeze
45
45
 
46
46
  ##
47
47
  # The default description for groups.
48
48
  # @return [String]
49
49
  #
50
- DEFAULT_GROUP_DESC = "(A group of commands)".freeze
50
+ DEFAULT_GROUP_DESC = "(A group of tools)".freeze
51
51
 
52
52
  ##
53
53
  # The default description for the root tool.
54
54
  # @return [String]
55
55
  #
56
- DEFAULT_ROOT_DESC =
57
- "This command line tool was built using the toys-core gem." \
58
- " See https://www.rubydoc.info/gems/toys-core for more info." \
59
- " To replace this message, configure the SetDefaultDescriptions" \
60
- " middleware.".freeze
56
+ DEFAULT_ROOT_DESC = "Command line tool built using the toys-core gem.".freeze
57
+
58
+ ##
59
+ # The default long description for the root tool.
60
+ # @return [String]
61
+ #
62
+ DEFAULT_ROOT_LONG_DESC =
63
+ "This command line tool was built using the toys-core gem. See" \
64
+ " https://www.rubydoc.info/gems/toys-core for more info. To replace this message," \
65
+ " configure the SetDefaultDescriptions middleware.".freeze
66
+
67
+ ##
68
+ # The default description for flags.
69
+ # @return [String]
70
+ #
71
+ DEFAULT_FLAG_DESC = "(No flag description available)".freeze
72
+
73
+ ##
74
+ # The default description for required args.
75
+ # @return [String]
76
+ #
77
+ DEFAULT_REQUIRED_ARG_DESC = "(Required argument - no description available)".freeze
78
+
79
+ ##
80
+ # The default description for optional args.
81
+ # @return [String]
82
+ #
83
+ DEFAULT_OPTIONAL_ARG_DESC = "(Optional argument - no description available)".freeze
84
+
85
+ ##
86
+ # The default description for remaining args.
87
+ # @return [String]
88
+ #
89
+ DEFAULT_REMAINING_ARG_DESC = "(Remaining arguments - no description available)".freeze
61
90
 
62
91
  ##
63
92
  # Create a SetDefaultDescriptions middleware given default descriptions.
64
93
  #
65
- # @param [String] default_tool_desc The default short description for
66
- # tools with an eecutor. Defaults to {DEFAULT_TOOL_DESC}.
94
+ # @param [String,nil] default_tool_desc The default short description for
95
+ # tools with an executor, or `nil` not to set one. Defaults to
96
+ # {DEFAULT_TOOL_DESC}.
67
97
  # @param [String,nil] default_tool_long_desc The default long description
68
- # for tools with an eecutor. If `nil` (the default), falls back to
69
- # the value of the `default_tool_desc` parameter.
70
- # @param [String] default_group_desc The default short description for
71
- # tools with no eecutor. Defaults to {DEFAULT_GROUP_DESC}.
98
+ # for tools with an executor, or `nil` not to set one. Defaults to
99
+ # `nil`.
100
+ # @param [String,nil] default_group_desc The default short description
101
+ # for tools with no executor, or `nil` not to set one. Defaults to
102
+ # {DEFAULT_TOOL_DESC}.
72
103
  # @param [String,nil] default_group_long_desc The default long
73
- # description for tools with no eecutor. If `nil` (the default),
74
- # falls back to the value of the `default_group_desc` parameter.
75
- # @param [String] default_root_desc The default long description for the
76
- # root tool. Defaults to {DEFAULT_ROOT_DESC}.
104
+ # description for tools with no executor, or `nil` not to set one.
105
+ # Defaults to `nil`.
106
+ # @param [String,nil] default_root_desc The default short description for
107
+ # the root tool, or `nil` not to set one. Defaults to
108
+ # {DEFAULT_ROOT_DESC}.
109
+ # @param [String,nil] default_root_long_desc The default long description
110
+ # for the root tool, or `nil` not to set one. Defaults to
111
+ # {DEFAULT_ROOT_LONG_DESC}.
112
+ # @param [String,nil] default_flag_desc The default short description for
113
+ # flags, or `nil` not to set one. Defaults to {DEFAULT_FLAG_DESC}.
114
+ # @param [String,nil] default_flag_long_desc The default long description
115
+ # for flags, or `nil` not to set one. Defaults to `nil`.
116
+ # @param [String,nil] default_required_arg_desc The default short
117
+ # description for required args, or `nil` not to set one. Defaults to
118
+ # {DEFAULT_REQUIRED_ARG_DESC}.
119
+ # @param [String,nil] default_required_arg_long_desc The default long
120
+ # description for required args, or `nil` not to set one. Defaults to
121
+ # `nil`.
122
+ # @param [String,nil] default_optional_arg_desc The default short
123
+ # description for optional args, or `nil` not to set one. Defaults to
124
+ # {DEFAULT_OPTIONAL_ARG_DESC}.
125
+ # @param [String,nil] default_optional_arg_long_desc The default long
126
+ # description for optional args, or `nil` not to set one. Defaults to
127
+ # `nil`.
128
+ # @param [String,nil] default_remaining_arg_desc The default short
129
+ # description for remaining args, or `nil` not to set one. Defaults
130
+ # to {DEFAULT_REMAINING_ARG_DESC}.
131
+ # @param [String,nil] default_remaining_arg_long_desc The default long
132
+ # description for remaining args, or `nil` not to set one. Defaults
133
+ # to `nil`.
77
134
  #
78
135
  def initialize(default_tool_desc: DEFAULT_TOOL_DESC,
79
136
  default_tool_long_desc: nil,
80
137
  default_group_desc: DEFAULT_GROUP_DESC,
81
138
  default_group_long_desc: nil,
82
- default_root_desc: DEFAULT_ROOT_DESC)
139
+ default_root_desc: DEFAULT_ROOT_DESC,
140
+ default_root_long_desc: DEFAULT_ROOT_LONG_DESC,
141
+ default_flag_desc: DEFAULT_FLAG_DESC,
142
+ default_flag_long_desc: nil,
143
+ default_required_arg_desc: DEFAULT_REQUIRED_ARG_DESC,
144
+ default_required_arg_long_desc: nil,
145
+ default_optional_arg_desc: DEFAULT_OPTIONAL_ARG_DESC,
146
+ default_optional_arg_long_desc: nil,
147
+ default_remaining_arg_desc: DEFAULT_REMAINING_ARG_DESC,
148
+ default_remaining_arg_long_desc: nil)
83
149
  @default_tool_desc = default_tool_desc
84
150
  @default_tool_long_desc = default_tool_long_desc
85
151
  @default_group_desc = default_group_desc
86
152
  @default_group_long_desc = default_group_long_desc
87
153
  @default_root_desc = default_root_desc
154
+ @default_root_long_desc = default_root_long_desc
155
+ @default_flag_desc = default_flag_desc
156
+ @default_flag_long_desc = default_flag_long_desc
157
+ @default_required_arg_desc = default_required_arg_desc
158
+ @default_required_arg_long_desc = default_required_arg_long_desc
159
+ @default_optional_arg_desc = default_optional_arg_desc
160
+ @default_optional_arg_long_desc = default_optional_arg_long_desc
161
+ @default_remaining_arg_desc = default_remaining_arg_desc
162
+ @default_remaining_arg_long_desc = default_remaining_arg_long_desc
88
163
  end
89
164
 
90
165
  ##
91
166
  # Add default description text to tools.
92
167
  #
93
- def config(tool)
168
+ def config(tool, _loader)
94
169
  if tool.root?
95
- config_root_desc(tool)
170
+ config_descs(tool, @default_root_desc, @default_root_long_desc)
96
171
  elsif tool.includes_executor?
97
- config_tool_desc(tool)
172
+ config_descs(tool, @default_tool_desc, @default_tool_long_desc)
98
173
  else
99
- config_group_desc(tool)
174
+ config_descs(tool, @default_group_desc, @default_group_long_desc)
175
+ end
176
+ tool.flag_definitions.each do |flag|
177
+ config_descs(flag, @default_flag_desc, @default_flag_long_desc)
100
178
  end
179
+ config_args(tool)
101
180
  yield
102
181
  end
103
182
 
104
183
  private
105
184
 
106
- def config_root_desc(tool)
107
- if @default_root_desc && tool.effective_long_desc.empty?
108
- tool.long_desc = @default_root_desc
185
+ def config_args(tool)
186
+ tool.required_arg_definitions.each do |arg|
187
+ config_descs(arg, @default_required_arg_desc, @default_required_arg_long_desc)
109
188
  end
110
- end
111
-
112
- def config_tool_desc(tool)
113
- if @default_tool_long_desc && tool.effective_long_desc.empty?
114
- tool.long_desc = @default_tool_long_desc
189
+ tool.optional_arg_definitions.each do |arg|
190
+ config_descs(arg, @default_optional_arg_desc, @default_optional_arg_long_desc)
115
191
  end
116
- if @default_tool_desc && tool.effective_desc.empty?
117
- tool.desc = @default_tool_desc
192
+ if tool.remaining_args_definition
193
+ config_descs(tool.remaining_args_definition,
194
+ @default_remaining_arg_desc, @default_remaining_arg_long_desc)
118
195
  end
119
196
  end
120
197
 
121
- def config_group_desc(tool)
122
- if @default_group_long_desc && tool.effective_long_desc.empty?
123
- tool.long_desc = @default_group_long_desc
198
+ def config_descs(object, default_desc, default_long_desc)
199
+ if default_desc && object.desc.empty?
200
+ object.desc = default_desc
124
201
  end
125
- if @default_group_desc && tool.effective_desc.empty?
126
- tool.desc = @default_group_desc
202
+ if default_long_desc && object.long_desc.empty?
203
+ object.long_desc = default_long_desc
127
204
  end
128
205
  end
129
206
  end
@@ -0,0 +1,245 @@
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
+ require "toys/middleware/base"
33
+ require "toys/utils/help_text"
34
+ require "toys/utils/line_output"
35
+
36
+ module Toys
37
+ module Middleware
38
+ ##
39
+ # A middleware that shows help text for the tool when a flag (typically
40
+ # `--help`) is provided. It can also be configured to show help by
41
+ # default if the tool is a group with no executor.
42
+ #
43
+ # If a tool has no executor, this middleware can also add a
44
+ # `--[no-]recursive` flag, which, when set to `true` (the default), shows
45
+ # all subtools recursively rather than only immediate subtools. This
46
+ # middleware can also search for keywords in its subtools.
47
+ #
48
+ class ShowHelp < Base
49
+ ##
50
+ # Default help flags
51
+ # @return [Array<String>]
52
+ #
53
+ DEFAULT_HELP_FLAGS = ["-?", "--help"].freeze
54
+
55
+ ##
56
+ # Default usage flags
57
+ # @return [Array<String>]
58
+ #
59
+ DEFAULT_USAGE_FLAGS = ["--usage"].freeze
60
+
61
+ ##
62
+ # Default recursive flags
63
+ # @return [Array<String>]
64
+ #
65
+ DEFAULT_RECURSIVE_FLAGS = ["-r", "--[no-]recursive"].freeze
66
+
67
+ ##
68
+ # Default search flags
69
+ # @return [Array<String>]
70
+ #
71
+ DEFAULT_SEARCH_FLAGS = ["-s WORD", "--search=WORD"].freeze
72
+
73
+ ##
74
+ # Create a ShowHelp middleware.
75
+ #
76
+ # @param [Boolean,Array<String>,Proc] help_flags Specify flags to
77
+ # display help. The value may be any of the following:
78
+ # * An array of flags.
79
+ # * The `true` value to use {DEFAULT_HELP_FLAGS}.
80
+ # * The `false` value for no flags. (Default)
81
+ # * A proc that takes a tool and returns any of the above.
82
+ # @param [Boolean,Array<String>,Proc] usage_flags Specify flags to
83
+ # display usage. The value may be any of the following:
84
+ # * An array of flags.
85
+ # * The `true` value to use {DEFAULT_USAGE_FLAGS}.
86
+ # * The `false` value for no flags. (Default)
87
+ # * A proc that takes a tool and returns any of the above.
88
+ # @param [Boolean,Array<String>,Proc] recursive_flags Specify flags
89
+ # to control recursive subtool search. The value may be any of the
90
+ # following:
91
+ # * An array of flags.
92
+ # * The `true` value to use {DEFAULT_RECURSIVE_FLAGS}.
93
+ # * The `false` value for no flags. (Default)
94
+ # * A proc that takes a tool and returns any of the above.
95
+ # @param [Boolean,Array<String>,Proc] search_flags Specify flags
96
+ # to search subtools for a search term. The value may be any of
97
+ # the following:
98
+ # * An array of flags.
99
+ # * The `true` value to use {DEFAULT_SEARCH_FLAGS}.
100
+ # * The `false` value for no flags. (Default)
101
+ # * A proc that takes a tool and returns any of the above.
102
+ # @param [Boolean] default_recursive Whether to search recursively for
103
+ # subtools by default. Default is `false`.
104
+ # @param [Boolean] fallback_execution Cause the tool to display its own
105
+ # help text if it does not otherwise have an executor. This is
106
+ # mostly useful for groups, which have children but no executor.
107
+ # Default is `false`.
108
+ # @param [Boolean] allow_root_args If the root tool includes flags for
109
+ # help or usage, and doesn't otherwise use positional arguments,
110
+ # then a tool name can be passed as arguments to display help for
111
+ # that tool.
112
+ # @param [IO] stream Output stream to write to. Default is stdout.
113
+ # @param [Boolean,nil] styled_output Cause the tool to display help text
114
+ # with ansi styles. If `nil`, display styles if the output stream is
115
+ # a tty. Default is `nil`.
116
+ #
117
+ def initialize(help_flags: false,
118
+ usage_flags: false,
119
+ recursive_flags: false,
120
+ search_flags: false,
121
+ default_recursive: false,
122
+ fallback_execution: false,
123
+ allow_root_args: false,
124
+ stream: $stdout,
125
+ styled_output: nil)
126
+ @help_flags = help_flags
127
+ @usage_flags = usage_flags
128
+ @recursive_flags = recursive_flags
129
+ @search_flags = search_flags
130
+ @default_recursive = default_recursive ? true : false
131
+ @fallback_execution = fallback_execution
132
+ @allow_root_args = allow_root_args
133
+ @stream = stream
134
+ @styled_output = styled_output
135
+ end
136
+
137
+ ##
138
+ # Configure flags and default data.
139
+ #
140
+ def config(tool, loader)
141
+ help_flags = add_help_flags(tool)
142
+ usage_flags = add_usage_flags(tool)
143
+ if @allow_root_args && (!help_flags.empty? || !usage_flags.empty?)
144
+ if tool.root? && tool.arg_definitions.empty?
145
+ tool.set_remaining_args(:_tool_name, display_name: "TOOL_NAME",
146
+ desc: "The tool for which to display help")
147
+ end
148
+ end
149
+ if (!help_flags.empty? || @fallback_execution) && loader.has_subtools?(tool.full_name)
150
+ add_recursive_flags(tool)
151
+ add_search_flags(tool)
152
+ end
153
+ yield
154
+ end
155
+
156
+ ##
157
+ # Display help text if requested.
158
+ #
159
+ def execute(context)
160
+ if context[:_show_usage]
161
+ help_text = get_help_text(context)
162
+ str = help_text.usage_string(wrap_width: output_cols)
163
+ output.puts(str)
164
+ elsif @fallback_execution && !context[Context::TOOL].includes_executor? ||
165
+ context[:_show_help]
166
+ help_text = get_help_text(context)
167
+ str = help_text.help_string(recursive: context[:_recursive_subtools],
168
+ search: context[:_search_subtools],
169
+ wrap_width: output_cols)
170
+ output.puts(str)
171
+ else
172
+ yield
173
+ end
174
+ end
175
+
176
+ private
177
+
178
+ def output_cols
179
+ @output_cols ||= ::HighLine.new(nil, @stream).output_cols
180
+ end
181
+
182
+ def output
183
+ @output ||= Utils::LineOutput.new(@stream, styled: @styled_output)
184
+ end
185
+
186
+ def get_help_text(context)
187
+ tool_name = context[:_tool_name]
188
+ return Utils::HelpText.from_context(context) if tool_name.nil? || tool_name.empty?
189
+ loader = context[Context::LOADER]
190
+ tool, rest = loader.lookup(tool_name)
191
+ help_text = Utils::HelpText.new(tool, loader, context[Context::BINARY_NAME])
192
+ report_usage_error(context, tool_name, help_text) unless rest.empty?
193
+ help_text
194
+ end
195
+
196
+ def report_usage_error(context, tool_name, help_text)
197
+ output.puts("Tool not found: #{tool_name.join(' ')}", :bright_red, :bold)
198
+ output.puts
199
+ output.puts help_text.usage_string(wrap_width: output_cols)
200
+ context.exit(1)
201
+ end
202
+
203
+ def add_help_flags(tool)
204
+ help_flags = Middleware.resolve_flags_spec(@help_flags, tool,
205
+ DEFAULT_HELP_FLAGS)
206
+ unless help_flags.empty?
207
+ tool.add_flag(:_show_help, *help_flags,
208
+ desc: "Display help for this tool", only_unique: true)
209
+ end
210
+ help_flags
211
+ end
212
+
213
+ def add_usage_flags(tool)
214
+ usage_flags = Middleware.resolve_flags_spec(@usage_flags, tool,
215
+ DEFAULT_USAGE_FLAGS)
216
+ unless usage_flags.empty?
217
+ tool.add_flag(:_show_usage, *usage_flags,
218
+ desc: "Display a brief usage string for this tool", only_unique: true)
219
+ end
220
+ usage_flags
221
+ end
222
+
223
+ def add_recursive_flags(tool)
224
+ recursive_flags = Middleware.resolve_flags_spec(@recursive_flags, tool,
225
+ DEFAULT_RECURSIVE_FLAGS)
226
+ unless recursive_flags.empty?
227
+ tool.add_flag(:_recursive_subtools, *recursive_flags,
228
+ default: @default_recursive,
229
+ desc: "Show all subtools recursively (default is #{@default_recursive})",
230
+ only_unique: true)
231
+ end
232
+ end
233
+
234
+ def add_search_flags(tool)
235
+ search_flags = Middleware.resolve_flags_spec(@search_flags, tool,
236
+ DEFAULT_SEARCH_FLAGS)
237
+ unless search_flags.empty?
238
+ tool.add_flag(:_search_subtools, *search_flags,
239
+ desc: "Search subtools for the given regular expression",
240
+ only_unique: true)
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end