rake-toolkit_program 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/CONTRIBUTING.md +24 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/README.md +211 -0
- data/Rakefile +6 -0
- data/bin/console +18 -0
- data/bin/setup +8 -0
- data/lib/program_exit_from_error.rb +56 -0
- data/lib/rake/toolkit_program.rb +231 -0
- data/lib/rake/toolkit_program/command_option_parser.rb +256 -0
- data/lib/rake/toolkit_program/completion.rb +158 -0
- data/lib/rake/toolkit_program/errors.rb +77 -0
- data/lib/rake/toolkit_program/help.rb +105 -0
- data/lib/rake/toolkit_program/help_styling.rb +68 -0
- data/lib/rake/toolkit_program/task_ext.rb +83 -0
- data/lib/rake/toolkit_program/utils.rb +48 -0
- data/lib/rake/toolkit_program/version.rb +10 -0
- data/rake-toolkit_program.gemspec +51 -0
- metadata +271 -0
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler"
|
|
4
|
+
Bundler.setup(:default, :development)
|
|
5
|
+
require "rake/toolkit_program"
|
|
6
|
+
|
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
+
|
|
10
|
+
# Pry (if it's in the Gemfile)
|
|
11
|
+
begin
|
|
12
|
+
require "pry"
|
|
13
|
+
rescue LoadError
|
|
14
|
+
require "irb"
|
|
15
|
+
IRB.start
|
|
16
|
+
else
|
|
17
|
+
Pry.start
|
|
18
|
+
end
|
data/bin/setup
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Copyright 2019 PayTrace, Inc.
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
# See the License for the specific language governing permissions and
|
|
16
|
+
# limitations under the License.
|
|
17
|
+
#
|
|
18
|
+
# This file defines a way to handle errors when running a CLI.
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Idiomatic support for dealing with errors during CLI use
|
|
22
|
+
#
|
|
23
|
+
# Rather than allowing an error to propagate to the top level of the
|
|
24
|
+
# interpreter, this module provides the #exit_program! method, which
|
|
25
|
+
# prints a message to STDERR and exits with an exit code of 1 (unless
|
|
26
|
+
# otherwise specified with .exit_with_code).
|
|
27
|
+
#
|
|
28
|
+
# For debugging support, this module also exposes the #exit_code it
|
|
29
|
+
# would use to exit the program were #exit_program! called.
|
|
30
|
+
#
|
|
31
|
+
module ProgramExitFromError
|
|
32
|
+
def self.included(klass)
|
|
33
|
+
klass.extend(ClassMethods)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module ClassMethods
|
|
37
|
+
def exit_with_code(n)
|
|
38
|
+
@exit_code = n
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
attr_reader :exit_code
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def exit_code
|
|
45
|
+
self.class.instance_variable_get(:@exit_code) || 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def exit_program!
|
|
49
|
+
print_error_message
|
|
50
|
+
Kernel.exit(exit_code)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def print_error_message
|
|
54
|
+
$stderr.puts("! ERROR: #{self}".bold.red)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Copyright 2019 PayTrace, Inc.
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
# See the License for the specific language governing permissions and
|
|
16
|
+
# limitations under the License.
|
|
17
|
+
#
|
|
18
|
+
# This is the main file of the rake-toolkit_program library.
|
|
19
|
+
|
|
20
|
+
require "rake/toolkit_program/version"
|
|
21
|
+
require 'dedent'
|
|
22
|
+
require 'optparse'
|
|
23
|
+
require 'pathname'
|
|
24
|
+
require 'rake'
|
|
25
|
+
require 'shellwords'
|
|
26
|
+
require 'program_exit_from_error'
|
|
27
|
+
|
|
28
|
+
%w[
|
|
29
|
+
command_option_parser
|
|
30
|
+
errors
|
|
31
|
+
help_styling
|
|
32
|
+
task_ext
|
|
33
|
+
utils
|
|
34
|
+
].each do |support_file|
|
|
35
|
+
require "rake/toolkit_program/#{support_file}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# From https://stackoverflow.com/a/8781522/160072
|
|
39
|
+
Rake::TaskManager.record_task_metadata = true
|
|
40
|
+
|
|
41
|
+
module Rake
|
|
42
|
+
##
|
|
43
|
+
# Tools to easily build CLI programs for UNIX-like systems
|
|
44
|
+
#
|
|
45
|
+
# In addition to any commands defined, this module provides:
|
|
46
|
+
#
|
|
47
|
+
# * a <tt>--install-completions</tt> command
|
|
48
|
+
# * a <tt>help</tt> command
|
|
49
|
+
# * <tt>-h</tt> and <tt>--help</tt> flags (until <tt>--</tt> is encountered)
|
|
50
|
+
#
|
|
51
|
+
# Use .command_tasks to define commands and .run to execute the CLI.
|
|
52
|
+
#
|
|
53
|
+
module ToolkitProgram
|
|
54
|
+
extend Rake::DSL
|
|
55
|
+
NAMESPACE="cli_cmd"
|
|
56
|
+
|
|
57
|
+
def self.title=(s)
|
|
58
|
+
@title = s
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.title
|
|
62
|
+
@title || "#{script_name.capitalize} Toolkit Program"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
@help_styling = HelpStyling.new
|
|
66
|
+
##
|
|
67
|
+
# Access the HelpStyling object used for styling generated help
|
|
68
|
+
#
|
|
69
|
+
def self.help_styling
|
|
70
|
+
if block_given?
|
|
71
|
+
yield @help_styling
|
|
72
|
+
end
|
|
73
|
+
@help_styling
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.task_name(name)
|
|
77
|
+
"#{NAMESPACE}:#{name}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.is_task_name?(name)
|
|
81
|
+
name.to_s.start_with?(NAMESPACE + ':')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.known_command?(name)
|
|
85
|
+
!name.nil? && Rake::Task.task_defined?(task_name(name))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.available_commands(include: :all)
|
|
89
|
+
Rake::Task.tasks.select {|t| is_task_name?(t.name)}.reject do |t|
|
|
90
|
+
case include
|
|
91
|
+
when :all then false
|
|
92
|
+
when :listable then t.comment.to_s.empty?
|
|
93
|
+
else raise ArgumentError, "#{include.inspect} not valid as include:"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.find(name, raise_if_missing: false)
|
|
99
|
+
case
|
|
100
|
+
when known_command?(name)
|
|
101
|
+
Rake::Task[task_name(name)]
|
|
102
|
+
when raise_if_missing
|
|
103
|
+
raise UnknownName.new(name)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
# Run a CLI
|
|
109
|
+
#
|
|
110
|
+
# Idiomatically, this is usually invoked as:
|
|
111
|
+
#
|
|
112
|
+
# if __FILE__ == $0
|
|
113
|
+
# Rake::ToolkitProgram.run(on_error: :exit_program!)
|
|
114
|
+
# end
|
|
115
|
+
#
|
|
116
|
+
# The first element of +args+ (which defaults to ARGV) names the command to
|
|
117
|
+
# execute. Additional arguments are available via .args.
|
|
118
|
+
#
|
|
119
|
+
# +on_error+ may be anything supporting #to_proc (including a Proc or
|
|
120
|
+
# lambda) and accepts one parameter, which is an error object that is
|
|
121
|
+
# guaranteed to have an #exit_program! method. Since Symbol#to_proc
|
|
122
|
+
# creates a Proc that sends the target Symbol to the single argument of the
|
|
123
|
+
# created Proc, passing <tt>:exit_program!</tt> (as in the idiomatic
|
|
124
|
+
# invocation) results in program exit according to the error being handled.
|
|
125
|
+
#
|
|
126
|
+
# If the error to be handled by +on_error+ does not #respond_to?
|
|
127
|
+
# <tt>:exit_program!</tt>, it will be extended with ProgramExitFromError,
|
|
128
|
+
# giving it default #exit_program! behavior of printing the error message
|
|
129
|
+
# on STDERR and exiting with code 1.
|
|
130
|
+
#
|
|
131
|
+
# When +on_error+ is nil, any errors are allowed to propagate out of #run.
|
|
132
|
+
#
|
|
133
|
+
def self.run(argv=ARGV, on_error: nil)
|
|
134
|
+
name, *@args = argv
|
|
135
|
+
raise NoCommand if name.nil?
|
|
136
|
+
if_help_request {name, args[0] = 'help', name}
|
|
137
|
+
specified_task = find(name, raise_if_missing: true)
|
|
138
|
+
if specified_task.kind_of?(ArgParsingTask)
|
|
139
|
+
new_args = specified_task.parsed_arguments
|
|
140
|
+
specified_task.argument_parser.parse(
|
|
141
|
+
*case new_args
|
|
142
|
+
when Hash then [args, {into: new_args}]
|
|
143
|
+
else [args]
|
|
144
|
+
end
|
|
145
|
+
)
|
|
146
|
+
@args = new_args
|
|
147
|
+
end
|
|
148
|
+
specified_task.invoke
|
|
149
|
+
rescue StandardError => error
|
|
150
|
+
error.extend(ProgramExitFromError) unless error.respond_to?(:exit_program!)
|
|
151
|
+
|
|
152
|
+
case
|
|
153
|
+
when on_error then on_error.to_proc
|
|
154
|
+
else method(:raise)
|
|
155
|
+
end.call(error)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.if_help_request
|
|
159
|
+
if args[0] == 'help'
|
|
160
|
+
yield
|
|
161
|
+
else
|
|
162
|
+
args.each do |a|
|
|
163
|
+
case a
|
|
164
|
+
when '--'
|
|
165
|
+
break
|
|
166
|
+
when '-h', '--help'
|
|
167
|
+
yield
|
|
168
|
+
break
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def self.args
|
|
175
|
+
@args
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
##
|
|
179
|
+
# Specify a standard type for parsed argument accumulation
|
|
180
|
+
#
|
|
181
|
+
# If this is called, the block is used to construct the argument
|
|
182
|
+
# accumulator if no accumulator object is explicitly specified when calling
|
|
183
|
+
# Rake::Task(Rake::ToolkitProgram::TaskExt)#parse_args. This helps
|
|
184
|
+
# Rake::ToolkitProgram.args be more consistent throughout the client
|
|
185
|
+
# program.
|
|
186
|
+
#
|
|
187
|
+
def self.default_parsed_args(&blk)
|
|
188
|
+
@default_parsed_args_creator = blk
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
##
|
|
192
|
+
# Construct a parsed argument accumulator
|
|
193
|
+
#
|
|
194
|
+
# The constructor can be defined via the block of .default_parsed_args
|
|
195
|
+
#
|
|
196
|
+
def self.new_default_parsed_args(&blk)
|
|
197
|
+
(@default_parsed_args_creator || blk).call
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def self.script_name(placeholder_ok: true)
|
|
201
|
+
case
|
|
202
|
+
when $0 != '-' then $0
|
|
203
|
+
when ENV['THIS_SCRIPT'] then ENV['THIS_SCRIPT']
|
|
204
|
+
when placeholder_ok then '<script-name>'
|
|
205
|
+
else raise "Script name unknown"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
##
|
|
210
|
+
# Access the Rake namespace for CLI tasks/commands
|
|
211
|
+
#
|
|
212
|
+
# Defining Rake tasks inside the block of this method (with #task) defines
|
|
213
|
+
# the tasks such that they are recognized as invocable from the command
|
|
214
|
+
# line (via the first command line argument, or the first element of +args+
|
|
215
|
+
# passed to .run).
|
|
216
|
+
#
|
|
217
|
+
# To make commands/tasks defined in this block visible in the help and
|
|
218
|
+
# shell completion, use #desc to describe the task.
|
|
219
|
+
#
|
|
220
|
+
def self.command_tasks
|
|
221
|
+
namespace(NAMESPACE) {yield}
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Require this at the end because it defines tasks, using some methods defined
|
|
227
|
+
# above
|
|
228
|
+
%w[
|
|
229
|
+
completion
|
|
230
|
+
help
|
|
231
|
+
].each {|task_file| require "rake/toolkit_program/#{task_file}"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Copyright 2019 PayTrace, Inc.
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
# See the License for the specific language governing permissions and
|
|
16
|
+
# limitations under the License.
|
|
17
|
+
#
|
|
18
|
+
# This file creates the Rake::ToolkitProgram::CommandOptionParser subclass
|
|
19
|
+
# of the standard library's OptionParser with supporting operations for a
|
|
20
|
+
# toolkit program, particularly around dealing with positional arguments.
|
|
21
|
+
|
|
22
|
+
require 'optparse'
|
|
23
|
+
|
|
24
|
+
module Rake
|
|
25
|
+
module ToolkitProgram
|
|
26
|
+
class CommandOptionParser < OptionParser
|
|
27
|
+
IDENTITY = ->(v) {v}
|
|
28
|
+
RUBY_GTE_2_4 = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.4')
|
|
29
|
+
|
|
30
|
+
def initialize(arg_dest)
|
|
31
|
+
super()
|
|
32
|
+
@argument_destination = arg_dest
|
|
33
|
+
@positional_mapper = IDENTITY
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :argument_destination
|
|
37
|
+
|
|
38
|
+
# Method override, see OptionParser#order!
|
|
39
|
+
if RUBY_GTE_2_4
|
|
40
|
+
def order!(argv = default_argv, into: nil, &nonopt)
|
|
41
|
+
super(argv, into: into) do |arg|
|
|
42
|
+
nonopt.call(@positional_mapper.call(arg))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
def order!(argv = default_args, into: nil, &nonopt)
|
|
47
|
+
raise ArgumentError, "Ruby #{RUBY_VERSION} cannot accept 'into' for OptionParser#order!" unless into.nil?
|
|
48
|
+
super(argv) do |arg|
|
|
49
|
+
nonopt.call(@positional_mapper.call(arg))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Method override, see OptionParser#parse! -- though we don't do POSIXLY_COMPLIANT
|
|
55
|
+
def parse!(argv = default_argv, into: nil)
|
|
56
|
+
positionals = []
|
|
57
|
+
do_positional_capture(positionals) if @precapture_positionals_array
|
|
58
|
+
order!(argv, into: into, &positionals.method(:<<)).tap do
|
|
59
|
+
argv[0, 0] = positionals
|
|
60
|
+
do_positional_capture(argv) if !@precapture_positionals_array
|
|
61
|
+
|
|
62
|
+
unless positional_cardinality_ok?(positionals.length)
|
|
63
|
+
raise WrongArgumentCount.new(
|
|
64
|
+
@positional_cardinality_test,
|
|
65
|
+
positionals.length
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
return argv
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# Query whether a given number of positional arguments is acceptable
|
|
74
|
+
#
|
|
75
|
+
# The result is based on the test established by
|
|
76
|
+
# #expect_positional_cardinality, and returns true if no such test
|
|
77
|
+
# has been established.
|
|
78
|
+
#
|
|
79
|
+
def positional_cardinality_ok?(n)
|
|
80
|
+
pc_test = @positional_cardinality_test
|
|
81
|
+
!pc_test || pc_test === n
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
##
|
|
85
|
+
# True unless positional arguments have been prohibited
|
|
86
|
+
#
|
|
87
|
+
# Technically, this test can only check that the established cardinality
|
|
88
|
+
# test is for 0, given as an Integer. If the test established by
|
|
89
|
+
# #expect_positional_cardinality is a Proc that only returns true for
|
|
90
|
+
# 0 or the Range <tt>0..0</tt>, this method will report incorrect
|
|
91
|
+
# results.
|
|
92
|
+
#
|
|
93
|
+
def positional_arguments_allowed?
|
|
94
|
+
@positional_cardinality_test != 0
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
##
|
|
98
|
+
# Return the established test for positional cardinality (or nil)
|
|
99
|
+
#
|
|
100
|
+
# If a test has been established by #expect_positional_cardinality,
|
|
101
|
+
# this method returns that test. Otherwise, it returns nil.
|
|
102
|
+
#
|
|
103
|
+
def positional_cardinality
|
|
104
|
+
@positional_cardinality_test
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
# String explanation of the positional cardinality, for help
|
|
109
|
+
#
|
|
110
|
+
def positional_cardinality_explanation
|
|
111
|
+
@positional_cardinality_explanation.tap do |explicit|
|
|
112
|
+
return explicit if explicit
|
|
113
|
+
end
|
|
114
|
+
obscure_answer = "A rule exists about the number of positional arguments."
|
|
115
|
+
|
|
116
|
+
case (pc_test = @positional_cardinality_test)
|
|
117
|
+
when nil, 0 then nil
|
|
118
|
+
when 1 then "Requires 1 positional argument."
|
|
119
|
+
when Integer then "Requires #{pc_test} positional arguments."
|
|
120
|
+
when Range then "Requires #{pc_test.to_inclusive} (inclusive) positional arguments."
|
|
121
|
+
when Proc then begin
|
|
122
|
+
[pc_test.call(:explain)].map do |exp|
|
|
123
|
+
case exp
|
|
124
|
+
when String then exp
|
|
125
|
+
else obscure_answer
|
|
126
|
+
end
|
|
127
|
+
end[0]
|
|
128
|
+
rescue StandardError
|
|
129
|
+
obscure_answer
|
|
130
|
+
end
|
|
131
|
+
else obscure_answer
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
##
|
|
136
|
+
# Explicitly define capture of positional (non-option) arguments
|
|
137
|
+
#
|
|
138
|
+
# When parsing into a Hash, the default is to store the Array of
|
|
139
|
+
# remaining positional arguments in the +nil+ key. This method
|
|
140
|
+
# overrides that behavior by either specifying a specific key to
|
|
141
|
+
# use or by specifying a block to call with the positional arguments,
|
|
142
|
+
# which is much more useful when accumulating arguments to a non-Hash
|
|
143
|
+
# object. Passing +key+ or a block are mutually exclusive.
|
|
144
|
+
#
|
|
145
|
+
# +precapture_dest_array+ can be set to +true+ to cause the capture
|
|
146
|
+
# to take place before the positional arguments are accumulated. In this
|
|
147
|
+
# case, the Array object yielded to the block (if this method is called
|
|
148
|
+
# with a block) _must_ be stored, as it will be the recipient of all
|
|
149
|
+
# positional arguments. In any case, when this option is passed to this
|
|
150
|
+
# method, capture behavior for the (empty) Array into which the
|
|
151
|
+
# positional arguments will be stored is carried out _before_ option
|
|
152
|
+
# parsing, and values are (after any transformation dictated by
|
|
153
|
+
# #map_positional_args) stored in the positional arguments Array; there
|
|
154
|
+
# is no option to store positionals for consumption by the command task
|
|
155
|
+
# code in anything other than an Array. Note that the Array into which
|
|
156
|
+
# arguments are captured <i>is not</i> the same array either passed to
|
|
157
|
+
# or returned from the #parse! (or #parse, for that matter) method.
|
|
158
|
+
#
|
|
159
|
+
# If multiple calls to this method are made, the last one is the one
|
|
160
|
+
# that defines the behavior.
|
|
161
|
+
#
|
|
162
|
+
# +key+:: Key under which positionals shall be accumulated in a Hash
|
|
163
|
+
# +precapture_dest_array+:: Capture before argument accumulation
|
|
164
|
+
#
|
|
165
|
+
def capture_positionals(key=nil, precapture_dest_array: false, &blk)
|
|
166
|
+
if blk && !key.nil?
|
|
167
|
+
raise ArgumentError, "either specify key or block"
|
|
168
|
+
end
|
|
169
|
+
@positionals_key = key
|
|
170
|
+
@positionals_capture = blk
|
|
171
|
+
@precapture_positionals_array = precapture_dest_array
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
##
|
|
175
|
+
# Constrain the number of positional arguments
|
|
176
|
+
#
|
|
177
|
+
# This is a declarative way of expressing how many positional (i.e.
|
|
178
|
+
# non-option) arguments should be accepted by the command. The
|
|
179
|
+
# #=== (i.e. "case match") method of +cardinality_test+ is used to test
|
|
180
|
+
# the length of the positional argument Array, raising
|
|
181
|
+
# Rake::ToolkitProgram::WrongArgumentCount if #=== returns false.
|
|
182
|
+
#
|
|
183
|
+
# A special case exists when +cardinality_test+ is a Symbol: because
|
|
184
|
+
# a Symbol could never match an Integer, Symbol#to_proc is called to
|
|
185
|
+
# create a useful test.
|
|
186
|
+
#
|
|
187
|
+
# *NOTE* It is worth attention that Proc#=== is an alias for Proc#call,
|
|
188
|
+
# so the operator argument is passed to the Proc. This enables arbitrary
|
|
189
|
+
# computation for the validity of the positional argument count,
|
|
190
|
+
# syntactically aided by the "stabby-lambda" notation.
|
|
191
|
+
#
|
|
192
|
+
# While this gem will do its best to explain the argument cardinality,
|
|
193
|
+
# +explanation+ provides an opportunity to explicitly provide a
|
|
194
|
+
# sentence to be included in the help about the allowed cardinality
|
|
195
|
+
# (i.e. count) of positional arguments.
|
|
196
|
+
#
|
|
197
|
+
def expect_positional_cardinality(cardinality_test, explanation=nil)
|
|
198
|
+
@positional_cardinality_explanation = explanation
|
|
199
|
+
if cardinality_test.kind_of?(Symbol)
|
|
200
|
+
cardinality_test.to_s.tap do |test_name|
|
|
201
|
+
if explanation.nil? && test_name.end_with?('?')
|
|
202
|
+
@positional_cardinality_explanation = (
|
|
203
|
+
"Positional argument count must be #{test_name[0..-2].gsub('_', ' ')}."
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
cardinality_test = cardinality_test.to_proc
|
|
208
|
+
end
|
|
209
|
+
@positional_cardinality_test = cardinality_test
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
##
|
|
213
|
+
# Disallow positional arguments
|
|
214
|
+
#
|
|
215
|
+
# The command will raise Rake::ToolkitProgram::WrongArgumentCount if
|
|
216
|
+
# any positional arguments are given.
|
|
217
|
+
#
|
|
218
|
+
def no_positional_args!
|
|
219
|
+
expect_positional_cardinality(0)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
##
|
|
223
|
+
# Convenience method for raising Rake::ToolkitProgram::InvalidCommandLine
|
|
224
|
+
#
|
|
225
|
+
# The error raised is the standard error for an invalid command line
|
|
226
|
+
# when using Rake::ToolkitProgram.
|
|
227
|
+
#
|
|
228
|
+
def invalid_args!(message)
|
|
229
|
+
raise InvalidCommandLine, message
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
##
|
|
233
|
+
# Define a mapping function for positional arguments during accumulation
|
|
234
|
+
#
|
|
235
|
+
# The block given to this method will be called with each positional
|
|
236
|
+
# argument in turn; the return value of the block will be acculumated
|
|
237
|
+
# as the positional argument. The block's computation may be purely
|
|
238
|
+
# functional or it may refer to outside factors in its binding such as
|
|
239
|
+
# accumulated values for options or preceding positional arguments.
|
|
240
|
+
#
|
|
241
|
+
def map_positional_args(&blk)
|
|
242
|
+
raise ArgumentError, "block required" if blk.nil?
|
|
243
|
+
@positional_mapper = blk
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
private
|
|
247
|
+
def do_positional_capture(positionals)
|
|
248
|
+
if @positionals_capture
|
|
249
|
+
@positionals_capture.call(positionals)
|
|
250
|
+
elsif argument_destination.kind_of?(Hash)
|
|
251
|
+
argument_destination[@positionals_key] = positionals
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|