toys-core 0.3.3 → 0.3.4

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