aspera-cli 4.27.2 → 4.27.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +133 -0
- data/CONTRIBUTING.md +5 -2
- data/bin/ascli +3 -1
- data/docs/README.md +1006 -766
- data/lib/aspera/agent/base.rb +7 -2
- data/lib/aspera/agent/connect.rb +6 -8
- data/lib/aspera/agent/desktop.rb +2 -6
- data/lib/aspera/agent/direct.rb +52 -22
- data/lib/aspera/agent/node.rb +9 -8
- data/lib/aspera/agent/transferd.rb +2 -2
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +14 -12
- data/lib/aspera/api/ats.rb +1 -1
- data/lib/aspera/api/cos_node.rb +2 -2
- data/lib/aspera/api/faspex.rb +11 -7
- data/lib/aspera/api/httpgw.rb +38 -35
- data/lib/aspera/api/node.rb +39 -34
- data/lib/aspera/ascmd.rb +3 -1
- data/lib/aspera/ascp/installation.rb +63 -28
- data/lib/aspera/ascp/management.rb +1 -0
- data/lib/aspera/assert.rb +4 -0
- data/lib/aspera/cli/ascp_actions.rb +20 -41
- data/lib/aspera/cli/async_transfer_store.rb +12 -11
- data/lib/aspera/cli/bootstrapper.rb +14 -16
- data/lib/aspera/cli/command_line.rb +252 -0
- data/lib/aspera/cli/command_registry.rb +215 -37
- data/lib/aspera/cli/command_spec.rb +104 -15
- data/lib/aspera/cli/completion/ascli.bash +12 -0
- data/lib/aspera/cli/completion/ascli.fish +16 -0
- data/lib/aspera/cli/completion/ascli.zsh +19 -0
- data/lib/aspera/cli/context.rb +3 -0
- data/lib/aspera/cli/deprecation.rb +37 -0
- data/lib/aspera/cli/extended_value.rb +6 -3
- data/lib/aspera/cli/formatter.rb +94 -80
- data/lib/aspera/cli/gem_checker.rb +1 -1
- data/lib/aspera/cli/hints.rb +7 -6
- data/lib/aspera/cli/http.rb +22 -34
- data/lib/aspera/cli/info.rb +3 -0
- data/lib/aspera/cli/mcp_tool.rb +47 -83
- data/lib/aspera/cli/option_declarator.rb +33 -42
- data/lib/aspera/cli/option_registry.rb +69 -0
- data/lib/aspera/cli/option_types.rb +105 -0
- data/lib/aspera/cli/option_value.rb +278 -0
- data/lib/aspera/cli/options.schema.yaml +124 -15
- data/lib/aspera/cli/parser.rb +333 -862
- data/lib/aspera/cli/plugins/alee.rb +7 -4
- data/lib/aspera/cli/plugins/aoc.rb +545 -518
- data/lib/aspera/cli/plugins/ats.rb +59 -80
- data/lib/aspera/cli/plugins/base.rb +221 -265
- data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
- data/lib/aspera/cli/plugins/config.rb +263 -184
- data/lib/aspera/cli/plugins/console.rb +103 -39
- data/lib/aspera/cli/plugins/cos.rb +6 -23
- data/lib/aspera/cli/plugins/factory.rb +3 -0
- data/lib/aspera/cli/plugins/faspex5.rb +204 -182
- data/lib/aspera/cli/plugins/faspio.rb +6 -11
- data/lib/aspera/cli/plugins/httpgw.rb +8 -11
- data/lib/aspera/cli/plugins/mcp.rb +20 -55
- data/lib/aspera/cli/plugins/node.rb +300 -327
- data/lib/aspera/cli/plugins/orchestrator.rb +152 -110
- data/lib/aspera/cli/plugins/preview.rb +96 -105
- data/lib/aspera/cli/plugins/server.rb +78 -53
- data/lib/aspera/cli/plugins/shares.rb +80 -131
- data/lib/aspera/cli/preset_actions.rb +44 -27
- data/lib/aspera/cli/preset_manager.rb +44 -19
- data/lib/aspera/cli/prompt.rb +36 -0
- data/lib/aspera/cli/result.rb +42 -36
- data/lib/aspera/cli/runner.rb +32 -59
- data/lib/aspera/cli/special_values.rb +5 -0
- data/lib/aspera/cli/sync_actions.rb +51 -46
- data/lib/aspera/cli/terminal_formatter.rb +9 -3
- data/lib/aspera/cli/transfer_actions.rb +14 -9
- data/lib/aspera/cli/transfer_agent.rb +34 -38
- data/lib/aspera/cli/transfer_progress.rb +290 -55
- data/lib/aspera/cli/vault_manager.rb +0 -17
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +5 -3
- data/lib/aspera/coverage.rb +1 -1
- data/lib/aspera/environment.rb +35 -5
- data/lib/aspera/faspex_gw.rb +2 -1
- data/lib/aspera/faspex_postproc.rb +1 -0
- data/lib/aspera/graphql.rb +5 -5
- data/lib/aspera/json_rpc/client.rb +5 -5
- data/lib/aspera/keychain/encrypted_hash.rb +2 -2
- data/lib/aspera/keychain/factory.rb +2 -1
- data/lib/aspera/keychain/one_password_api.rb +1 -1
- data/lib/aspera/link_header.rb +2 -2
- data/lib/aspera/log.rb +47 -27
- data/lib/aspera/markdown.rb +2 -0
- data/lib/aspera/mime.rb +25 -0
- data/lib/aspera/node_emulator.rb +759 -0
- data/lib/aspera/oauth/base.rb +37 -26
- data/lib/aspera/oauth/factory.rb +7 -3
- data/lib/aspera/oauth/generic.rb +1 -1
- data/lib/aspera/oauth/json_credentials.rb +34 -0
- data/lib/aspera/oauth/jwt.rb +4 -5
- data/lib/aspera/oauth/web.rb +9 -8
- data/lib/aspera/oauth.rb +1 -0
- data/lib/aspera/persistency_folder.rb +1 -3
- data/lib/aspera/preview/file_types.rb +4 -4
- data/lib/aspera/preview/generator.rb +11 -1
- data/lib/aspera/preview/options.schema.yaml +119 -0
- data/lib/aspera/preview/terminal.rb +4 -3
- data/lib/aspera/preview/utils.rb +9 -6
- data/lib/aspera/products/connect.rb +1 -1
- data/lib/aspera/rainbow.rb +7 -0
- data/lib/aspera/rest/aspera_errors.rb +72 -0
- data/lib/aspera/rest/call_error.rb +27 -0
- data/lib/aspera/rest/client.rb +523 -0
- data/lib/aspera/rest/error_analyzer.rb +113 -0
- data/lib/aspera/rest/list.rb +149 -0
- data/lib/aspera/rest/parameters.rb +55 -0
- data/lib/aspera/rest/util.rb +176 -0
- data/lib/aspera/rest.rb +7 -621
- data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
- data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
- data/lib/aspera/schema/documentation.rb +13 -3
- data/lib/aspera/schema/reader.rb +12 -18
- data/lib/aspera/schema/registry.rb +23 -1
- data/lib/aspera/schema/validator.rb +92 -0
- data/lib/aspera/secret_hider.rb +36 -25
- data/lib/aspera/string_ext.rb +15 -0
- data/lib/aspera/temp_file_manager.rb +6 -5
- data/lib/aspera/transfer/parameters.rb +2 -0
- data/lib/aspera/transfer/spec.rb +1 -0
- data/lib/aspera/uri_reader.rb +11 -11
- data/lib/aspera/web_auth/index.html +147 -0
- data/lib/aspera/web_auth/server.rb +81 -0
- data.tar.gz.sig +0 -0
- metadata +43 -9
- metadata.gz.sig +0 -0
- data/lib/aspera/colors.rb +0 -79
- data/lib/aspera/node_simulator.rb +0 -344
- data/lib/aspera/preview/options.rb +0 -45
- data/lib/aspera/rest_call_error.rb +0 -25
- data/lib/aspera/rest_error_analyzer.rb +0 -111
- data/lib/aspera/rest_errors_aspera.rb +0 -58
- data/lib/aspera/rest_list.rb +0 -136
- data/lib/aspera/web_auth.rb +0 -211
data/lib/aspera/cli/parser.rb
CHANGED
|
@@ -5,417 +5,41 @@ require 'aspera/cli/extended_value'
|
|
|
5
5
|
require 'aspera/cli/error'
|
|
6
6
|
require 'aspera/cli/special_values'
|
|
7
7
|
require 'aspera/cli/terminal_formatter'
|
|
8
|
-
require 'aspera/
|
|
9
|
-
require 'aspera/
|
|
8
|
+
require 'aspera/cli/option_types'
|
|
9
|
+
require 'aspera/cli/option_registry'
|
|
10
|
+
require 'aspera/cli/command_line'
|
|
11
|
+
require 'aspera/cli/prompt'
|
|
12
|
+
require 'aspera/schema/validator'
|
|
10
13
|
require 'aspera/log'
|
|
11
14
|
require 'aspera/assert'
|
|
12
15
|
require 'aspera/dot_container'
|
|
13
|
-
require 'aspera/schema/registry'
|
|
14
|
-
require 'io/console'
|
|
15
16
|
require 'terminal-table'
|
|
17
|
+
require 'aspera/rainbow'
|
|
18
|
+
using Rainbow
|
|
16
19
|
|
|
17
20
|
module Aspera
|
|
18
21
|
module Cli
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# @param schema_path [String, nil] path to schema file, or `nil` if not available
|
|
27
|
-
def initialize(type, name, schema_path)
|
|
28
|
-
super("#{type}: #{name}")
|
|
29
|
-
@path = schema_path
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
module BoolValue
|
|
34
|
-
# boolean options are set to true/false from the following values
|
|
35
|
-
YES_SYM = :yes
|
|
36
|
-
NO_SYM = :no
|
|
37
|
-
FALSE_VALUES = [NO_SYM, false].freeze
|
|
38
|
-
TRUE_VALUES = [YES_SYM, true].freeze
|
|
39
|
-
private_constant :YES_SYM, :NO_SYM, :FALSE_VALUES, :TRUE_VALUES
|
|
40
|
-
# Boolean values
|
|
41
|
-
# @return [Array<true, false, :yes, :no>]
|
|
42
|
-
ALL = (TRUE_VALUES + FALSE_VALUES).freeze
|
|
43
|
-
# `false` and `true`
|
|
44
|
-
TYPES = [FalseClass, TrueClass].freeze
|
|
45
|
-
SYMBOLS = [NO_SYM, YES_SYM].freeze
|
|
46
|
-
# @return [Boolean] `true` if value is a value for `true` in ALL
|
|
47
|
-
def true?(enum)
|
|
48
|
-
Aspera.assert_values(enum, ALL) { 'boolean' }
|
|
49
|
-
TRUE_VALUES.include?(enum)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# @return [:yes, :no]
|
|
53
|
-
def to_sym(enum) = true?(enum) ? YES_SYM : NO_SYM
|
|
54
|
-
|
|
55
|
-
# @return [Boolean] `true` if value is a value for `true` or `false` in ALL
|
|
56
|
-
def symbol?(sym)
|
|
57
|
-
ALL.include?(sym)
|
|
58
|
-
end
|
|
59
|
-
module_function :true?, :to_sym, :symbol?
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Type specifiers for the `allowed:` parameter of option declarations.
|
|
63
|
-
# Public API: STRING_ARRAY, SYMBOL_ARRAY, INTEGER, BOOLEAN, NONE.
|
|
64
|
-
# Internal (do not pass as `allowed:`):
|
|
65
|
-
# ENUM - derived internally when `allowed:` is an Array<Symbol> (enum list)
|
|
66
|
-
# STRING - the implicit default; equivalent to omitting `allowed:` entirely
|
|
67
|
-
module Type
|
|
68
|
-
# Option value is a String or Array of Strings (cumulative)
|
|
69
|
-
STRING_ARRAY = [Array, String].freeze
|
|
70
|
-
# Option value is a Symbol from a constrained list; use as prefix: SYMBOL_ARRAY + [:val1, :val2]
|
|
71
|
-
SYMBOL_ARRAY = [Array, Symbol].freeze
|
|
72
|
-
# Option value is coerced to Integer
|
|
73
|
-
INTEGER = [Integer].freeze
|
|
74
|
-
# Option value is a Boolean
|
|
75
|
-
BOOLEAN = BoolValue::TYPES
|
|
76
|
-
# Option has no value — it is a flag switch (e.g. `-N`, `--help`)
|
|
77
|
-
NONE = [].freeze
|
|
78
|
-
# Internal: derived when allowed: is an Array<Symbol>; do not pass directly
|
|
79
|
-
ENUM = [Symbol].freeze
|
|
80
|
-
# Internal: implicit default (String); equivalent to omitting allowed: entirely
|
|
81
|
-
STRING = [String].freeze
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Description of option, how to manage
|
|
85
|
-
class OptionValue
|
|
86
|
-
# [Array(Class)] List of allowed types
|
|
87
|
-
attr_reader :types, :sensitive, :schema, :option, :deprecation
|
|
88
|
-
# [Array] List of allowed values (Symbols and specific values)
|
|
89
|
-
attr_accessor :values
|
|
90
|
-
# [String] Help section group name (set by Parser#group)
|
|
91
|
-
attr_accessor :group
|
|
92
|
-
# [Proc, nil] Block to call for flag options (TYPES_NONE)
|
|
93
|
-
attr_accessor :block
|
|
94
|
-
|
|
95
|
-
# @param option [Symbol] Name of option
|
|
96
|
-
# @param description [String, nil] Description for help; if nil, derived from schema
|
|
97
|
-
# @param allowed [nil,Class,Array<Class>,Array<Symbol>] Allowed values
|
|
98
|
-
# @param handler [Hash, nil] Accessor: keys: :o(object) and :m(method); nil for local storage
|
|
99
|
-
# @param deprecation [String] Deprecation message
|
|
100
|
-
# @param schema [String] Declaration of schema
|
|
101
|
-
# `allowed`:
|
|
102
|
-
# - `nil` No validation, so just a string
|
|
103
|
-
# - `Class` The single allowed Class
|
|
104
|
-
# - `Array<Class>` Multiple allowed classes
|
|
105
|
-
# - `Array<Symbol>` List of allowed values
|
|
106
|
-
def initialize(option:, description: nil, allowed: Type::STRING, handler: nil, deprecation: nil, schema: nil)
|
|
107
|
-
Log.log.trace1 { "option: #{option}, allowed: #{allowed}" }
|
|
108
|
-
@option = option
|
|
109
|
-
@description = description
|
|
110
|
-
@group = nil
|
|
111
|
-
@block = nil
|
|
112
|
-
# by default passwords and secrets are sensitive, else specify when declaring the option
|
|
113
|
-
@sensitive = SecretHider.instance.secret?(@option, '')
|
|
114
|
-
@deprecation = deprecation
|
|
115
|
-
@schema = schema
|
|
116
|
-
# Start with local storage; bind_handler wires the delegation if a handler is given.
|
|
117
|
-
@object = nil
|
|
118
|
-
@read_method = nil
|
|
119
|
-
@write_method = nil
|
|
120
|
-
@access = :local
|
|
121
|
-
bind_handler(handler) unless handler.nil?
|
|
122
|
-
@types = nil
|
|
123
|
-
@values = nil
|
|
124
|
-
allowed = infer_allowed_from_schema(schema, allowed) if schema
|
|
125
|
-
apply_allowed(allowed) unless allowed.nil?
|
|
126
|
-
Log.log.trace1 { "declare: #{@option}: #{@access} #{@object.class}.#{@read_method}".green }
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
private
|
|
130
|
-
|
|
131
|
-
# Derive the `allowed:` value from the schema when not explicitly provided.
|
|
132
|
-
# Returns `allowed` unchanged when the schema provides no usable type information.
|
|
133
|
-
# @param schema [String] schema identifier
|
|
134
|
-
# @param allowed [Object] caller-supplied allowed value (may be nil or Type::STRING)
|
|
135
|
-
# @return [Object] resolved allowed (Hash, Array, or the original value)
|
|
136
|
-
def infer_allowed_from_schema(schema, allowed)
|
|
137
|
-
return allowed unless allowed.nil? || allowed.eql?(Type::STRING)
|
|
138
|
-
|
|
139
|
-
schema_reader = Schema::Registry.instance.reader(schema) rescue nil
|
|
140
|
-
schema_node = schema_reader&.current
|
|
141
|
-
return allowed unless schema_node
|
|
142
|
-
|
|
143
|
-
case schema_node['type']
|
|
144
|
-
when 'object' then return Hash
|
|
145
|
-
when 'array' then return Array
|
|
146
|
-
end
|
|
147
|
-
# No top-level type: inspect oneOf/anyOf branches; if all resolve to 'object', infer Hash
|
|
148
|
-
composite_key = (%w[oneOf anyOf] & schema_node.keys).first
|
|
149
|
-
return allowed unless composite_key
|
|
150
|
-
|
|
151
|
-
branch_types = schema_node[composite_key].map do |branch|
|
|
152
|
-
resolved = branch['$ref'] ? schema_reader.resolve_ref(branch['$ref']).current : branch
|
|
153
|
-
resolved['type']
|
|
154
|
-
end
|
|
155
|
-
if branch_types.all?('object')
|
|
156
|
-
Hash
|
|
157
|
-
else
|
|
158
|
-
Aspera.assert(
|
|
159
|
-
!allowed.nil? && !allowed.eql?(Type::STRING),
|
|
160
|
-
"option :#{@option}: schema '#{schema}' has mixed-type oneOf branches #{branch_types.uniq}: specify allowed: explicitly"
|
|
161
|
-
)
|
|
162
|
-
allowed
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Initialise @types and @values from the resolved `allowed` specifier.
|
|
167
|
-
# @param allowed [Class, Array<Class>, Array<Symbol>] resolved allowed value (never nil)
|
|
168
|
-
def apply_allowed(allowed)
|
|
169
|
-
allowed = [allowed] if allowed.is_a?(Class)
|
|
170
|
-
Aspera.assert_type(allowed, Array)
|
|
171
|
-
if allowed.take(Type::SYMBOL_ARRAY.length) == Type::SYMBOL_ARRAY
|
|
172
|
-
# Special case: array of defined symbol values
|
|
173
|
-
@types = Type::SYMBOL_ARRAY
|
|
174
|
-
@values = allowed[Type::SYMBOL_ARRAY.length..]
|
|
175
|
-
assign_value([], where: 'array default', warn_deprecation: false) if value(log: false).nil?
|
|
176
|
-
elsif allowed.all?(Class)
|
|
177
|
-
@types = allowed
|
|
178
|
-
@values = BoolValue::ALL if allowed.eql?(Type::BOOLEAN)
|
|
179
|
-
if @types.first.eql?(Array) && !@types.include?(NilClass) && value(log: false).nil?
|
|
180
|
-
assign_value([], where: 'array default', warn_deprecation: false)
|
|
181
|
-
elsif @types.first.eql?(Hash) && !@types.include?(NilClass) && value(log: false).nil?
|
|
182
|
-
assign_value({}, where: 'hash default', warn_deprecation: false)
|
|
183
|
-
end
|
|
184
|
-
elsif allowed.all?(Symbol)
|
|
185
|
-
@types = Type::ENUM
|
|
186
|
-
@values = allowed
|
|
187
|
-
else
|
|
188
|
-
Aspera.error_unexpected_value(allowed)
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
public
|
|
193
|
-
|
|
194
|
-
# Wire (or re-wire) the getter/setter delegation for this option.
|
|
195
|
-
# Safe to call after construction - used by Parser#set_handler to bind a composed
|
|
196
|
-
# instance variable that did not exist at class-load time (Category C handlers).
|
|
197
|
-
# @param handler [Hash] Accessor hash with keys :o (object) and :m (method symbol)
|
|
198
|
-
# @return [nil]
|
|
199
|
-
def bind_handler(handler)
|
|
200
|
-
Aspera.assert_type(handler, Hash) { 'handler' }
|
|
201
|
-
# Capture any value already stored locally before switching to delegated storage.
|
|
202
|
-
# This transfers defaults (and any preset values already applied) to the new target.
|
|
203
|
-
pending_value = @access.eql?(:local) ? @object : nil
|
|
204
|
-
@object = handler[:o]
|
|
205
|
-
@read_method = handler[:m]
|
|
206
|
-
@write_method = "#{@read_method}=".to_sym
|
|
207
|
-
@access = if @object.respond_to?(@write_method)
|
|
208
|
-
:attr # delegation via attr_accessor-style m / m=
|
|
209
|
-
else
|
|
210
|
-
:custom # delegation via generic 3-arg method m(sym, :get/:set, val)
|
|
211
|
-
end
|
|
212
|
-
Aspera.assert(@object.respond_to?(@read_method)) { "#{@object} does not respond to #{@read_method}" }
|
|
213
|
-
Log.log.trace1 { "bind_handler: #{@option}: #{@access} #{@object.class}.#{@read_method}".green }
|
|
214
|
-
# Push the pending local value to the new target if one was stored
|
|
215
|
-
assign_value(pending_value, where: 'bind_handler', warn_deprecation: false) unless pending_value.nil?
|
|
216
|
-
nil
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
# @return [String] description of the option: explicit one, or first line of schema description
|
|
220
|
-
def description
|
|
221
|
-
return @description unless @description.nil?
|
|
222
|
-
return if @schema.nil?
|
|
223
|
-
schema_node = Schema::Registry.instance.reader(@schema).current
|
|
224
|
-
first_line = (schema_node['title'] || schema_node['description'].to_s).lines.first.to_s.strip
|
|
225
|
-
first_line.end_with?('.') ? first_line[0..-2] : first_line
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# Reset stored value to nil
|
|
229
|
-
# @return [nil]
|
|
230
|
-
def clear
|
|
231
|
-
@object = nil
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
# Get current option value
|
|
235
|
-
# @param log [Boolean] whether to log the value retrieval
|
|
236
|
-
# @return [Object] current value
|
|
237
|
-
def value(log: true)
|
|
238
|
-
current_value =
|
|
239
|
-
case @access
|
|
240
|
-
when :local then @object
|
|
241
|
-
when :attr then @object.send(@read_method)
|
|
242
|
-
when :custom then @object.send(@read_method, @option, :get)
|
|
243
|
-
end
|
|
244
|
-
Log.log.trace1 { "#{@option} -> (#{current_value.class})#{current_value}" } if log
|
|
245
|
-
current_value
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# Assign value to option.
|
|
249
|
-
# Value can be a `String`, then evaluated with `ExtendedValue`, or directly a value.
|
|
250
|
-
# @param value [String, Object] Value to assign to option
|
|
251
|
-
# @param where [String] Where the value is assigned from
|
|
252
|
-
# @param warn_deprecation [Boolean] Emit deprecation warning (false for internal transfers)
|
|
253
|
-
# @return [nil]
|
|
254
|
-
def assign_value(value, where:, warn_deprecation: true)
|
|
255
|
-
Aspera.assert(!@deprecation, type: :warn) { "Option #{@option} is deprecated: #{@deprecation}" } if warn_deprecation
|
|
256
|
-
new_value = ExtendedValue.instance.evaluate(value, context: "option: #{@option}", allowed: @types)
|
|
257
|
-
Log.log.trace1 { "#{where}: #{@option} <- (#{new_value.class})#{new_value}" }
|
|
258
|
-
# Per-type coercion: String input from CLI/env/preset is normalized to the expected type.
|
|
259
|
-
# Centralized here so all sources (CLI dispatch, preset, env) go through the same path.
|
|
260
|
-
case @types
|
|
261
|
-
when Type::ENUM
|
|
262
|
-
new_value = Parser.get_from_list(new_value, @option, @values) if new_value.is_a?(String)
|
|
263
|
-
when Type::BOOLEAN
|
|
264
|
-
new_value = Parser.get_from_list(new_value, @option, BoolValue::ALL) if new_value.is_a?(String)
|
|
265
|
-
new_value = BoolValue.true?(new_value)
|
|
266
|
-
when Type::INTEGER
|
|
267
|
-
new_value = Integer(new_value)
|
|
268
|
-
when Type::STRING_ARRAY
|
|
269
|
-
new_value = [new_value] if new_value.is_a?(String)
|
|
270
|
-
when Type::SYMBOL_ARRAY
|
|
271
|
-
new_value = [new_value] if new_value.is_a?(String)
|
|
272
|
-
Aspera.assert_array_all(new_value, String, type: BadArgument)
|
|
273
|
-
new_value = new_value.map { |v| Parser.get_from_list(v, @option, @values) }
|
|
274
|
-
else
|
|
275
|
-
# nil (setting nil on a Hash/Array option resets to empty container)
|
|
276
|
-
new_value = {} if new_value.nil? && @types&.first.eql?(Hash)
|
|
277
|
-
new_value = [] if new_value.nil? && @types&.first.eql?(Array)
|
|
278
|
-
end
|
|
279
|
-
# Skip type validation for the special 'help' value on Hash options: store it as-is
|
|
280
|
-
# so that get_option(schema:) can raise SchemaRequest with the contextual schema later.
|
|
281
|
-
# Note: set_option already raises SchemaRequest when @schema is set, so this path is
|
|
282
|
-
# only reached when @schema is nil (e.g. --query=help before schema is known).
|
|
283
|
-
if new_value.eql?(Parser::HELP) && @types&.include?(Hash)
|
|
284
|
-
store(new_value)
|
|
285
|
-
return
|
|
286
|
-
end
|
|
287
|
-
Aspera.assert_type(new_value, *@types, type: BadArgument) { "Option #{@option}" } if @types
|
|
288
|
-
if new_value.is_a?(Hash) || new_value.is_a?(Array)
|
|
289
|
-
current_value = value(log: false)
|
|
290
|
-
new_value = current_value.deep_merge(new_value) if new_value.is_a?(Hash) && current_value.is_a?(Hash) && !current_value.empty?
|
|
291
|
-
new_value = current_value + new_value if new_value.is_a?(Array) && current_value.is_a?(Array) && !current_value.empty?
|
|
292
|
-
end
|
|
293
|
-
store(new_value)
|
|
294
|
-
Log.log.trace1 { v = value(log: false); "#{@option} <- (#{v.class})#{v}" } # rubocop:disable Style/Semicolon
|
|
295
|
-
nil
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
private
|
|
299
|
-
|
|
300
|
-
# Store value according to access mode
|
|
301
|
-
# @param new_value [Object] value to store
|
|
302
|
-
# @return [Object] stored value
|
|
303
|
-
def store(new_value)
|
|
304
|
-
case @access
|
|
305
|
-
when :local then @object = new_value
|
|
306
|
-
when :attr then @object.send(@write_method, new_value)
|
|
307
|
-
when :custom then @object.send(@read_method, @option, :set, new_value)
|
|
308
|
-
end
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
# Represents a positional (non-option) CLI argument token.
|
|
313
|
-
class Argument
|
|
314
|
-
# @return [String] the raw argument value
|
|
315
|
-
attr_reader :value
|
|
316
|
-
|
|
317
|
-
def initialize(value)
|
|
318
|
-
@value = value
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def to_s = @value
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
# Represents a parsed CLI option token (long or short form).
|
|
325
|
-
# Pre-computed at argv-scan time; resolution against @declared_options happens later in parse_options!
|
|
326
|
-
class Option
|
|
327
|
-
# @return [String] raw token as it appeared in argv (e.g. "--log-level=debug", "-Pval")
|
|
328
|
-
attr_reader :raw
|
|
329
|
-
# @return [String, nil] option name with underscores (e.g. "log_level", "custom"); nil for short options
|
|
330
|
-
attr_reader :name
|
|
331
|
-
# @return [String, nil] single-char short option letter (e.g. "P"), nil for long options
|
|
332
|
-
attr_reader :short_char
|
|
333
|
-
# @return [Array<String>, nil] sub-keys for dot-path notation (e.g. ["field"] for --custom.field),
|
|
334
|
-
# nil when name is the full option name (no dot)
|
|
335
|
-
attr_reader :dot_path
|
|
336
|
-
# @return [String, nil] inline value string, or nil if no value was provided inline
|
|
337
|
-
attr_reader :value
|
|
338
|
-
# @return [Boolean] true if `=` (long) or glued value (short) was present in the raw token;
|
|
339
|
-
# false means no inline separator — value may come from the next argv token
|
|
340
|
-
attr_reader :has_value
|
|
341
|
-
|
|
342
|
-
# @param raw [String] full raw token
|
|
343
|
-
# @param name [String, nil] option name (underscored, no prefix)
|
|
344
|
-
# @param short_char [String, nil] single-char short letter
|
|
345
|
-
# @param dot_path [Array<String>, nil] dot-path sub-keys, or nil
|
|
346
|
-
# @param value [String, nil] inline value
|
|
347
|
-
# @param has_value [Boolean] whether an inline value separator was present
|
|
348
|
-
def initialize(raw:, name:, short_char:, dot_path:, value:, has_value:)
|
|
349
|
-
@raw = raw
|
|
350
|
-
@name = name
|
|
351
|
-
@short_char = short_char
|
|
352
|
-
@dot_path = dot_path
|
|
353
|
-
@value = value
|
|
354
|
-
@has_value = has_value
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
def to_s = @raw
|
|
358
|
-
|
|
359
|
-
class << self
|
|
360
|
-
# Build an Option from a raw long-option token (starts with `--`)
|
|
361
|
-
# @param raw [String] e.g. "--log-level=debug" or "--custom.field" or "--log-level"
|
|
362
|
-
def from_long(raw)
|
|
363
|
-
without_prefix = raw.delete_prefix(PREFIX)
|
|
364
|
-
eq_idx = without_prefix.index(VALUE_SEP)
|
|
365
|
-
if eq_idx
|
|
366
|
-
name_raw = without_prefix[0, eq_idx]
|
|
367
|
-
value = without_prefix[eq_idx + 1..]
|
|
368
|
-
has_value = true
|
|
369
|
-
else
|
|
370
|
-
name_raw = without_prefix
|
|
371
|
-
value = nil
|
|
372
|
-
has_value = false
|
|
373
|
-
end
|
|
374
|
-
parts = name_raw.split(DotContainer::SEPARATOR)
|
|
375
|
-
root = parts.shift.gsub(NAME_SEP_LINE, NAME_SEP_SYMBOL)
|
|
376
|
-
dot_path = parts.empty? ? nil : parts
|
|
377
|
-
new(raw: raw, name: root, short_char: nil, dot_path: dot_path, value: value, has_value: has_value)
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
# Build an Option from a raw short-option token (starts with `-` but not `--`)
|
|
381
|
-
# @param raw [String] e.g. "-P", "-Pval", "-h"
|
|
382
|
-
def from_short(raw)
|
|
383
|
-
short_char = raw[1]
|
|
384
|
-
if raw.length > 2
|
|
385
|
-
new(raw: raw, name: nil, short_char: short_char, dot_path: nil, value: raw[2..], has_value: true)
|
|
386
|
-
else
|
|
387
|
-
new(raw: raw, name: nil, short_char: short_char, dot_path: nil, value: nil, has_value: false)
|
|
388
|
-
end
|
|
389
|
-
end
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
# Option name separator on command line (e.g. `--option-name`, the `-` between words)
|
|
393
|
-
NAME_SEP_LINE = '-'
|
|
394
|
-
# Option name separator in code/symbol (e.g. `:option_name`, the `_` between words)
|
|
395
|
-
NAME_SEP_SYMBOL = '_'
|
|
396
|
-
# Separator between option name and its inline value (e.g. `--opt=val`, the `=`)
|
|
397
|
-
VALUE_SEP = '='
|
|
398
|
-
# Long-option prefix (e.g. `--opt`)
|
|
399
|
-
PREFIX = '--'
|
|
400
|
-
# (public: shared with Parser)
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
# parse command line options
|
|
404
|
-
# arguments options start with '-', others are commands
|
|
405
|
-
# resolves on extended value syntax
|
|
22
|
+
# Parse command line options and positional arguments.
|
|
23
|
+
#
|
|
24
|
+
# Options are declared incrementally (global, then plugin options).
|
|
25
|
+
# Command line options are applied when their option is declared: explicitly with `parse_options!`,
|
|
26
|
+
# or automatically on next read of an option or argument. Unknown options are kept for later.
|
|
27
|
+
#
|
|
28
|
+
# Option values come from several sources, see `OptionSource`: highest priority wins, whatever the order.
|
|
406
29
|
class Parser
|
|
30
|
+
include Prompt
|
|
31
|
+
|
|
407
32
|
class << self
|
|
408
33
|
# Find shortened string value in allowed symbol list
|
|
409
|
-
# @param short_value
|
|
410
|
-
# @param descr
|
|
411
|
-
# @param allowed_values [Array]
|
|
34
|
+
# @param short_value [String] Value or prefix to find
|
|
35
|
+
# @param descr [String] Description for error messages
|
|
36
|
+
# @param allowed_values [Array] List of allowed values
|
|
412
37
|
# @return [Symbol, Boolean] matched symbol or boolean value
|
|
413
38
|
def get_from_list(short_value, descr, allowed_values)
|
|
414
39
|
Aspera.assert_type(short_value, String)
|
|
415
40
|
# we accept shortcuts
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
matching = allowed_values.select { |i| i.to_s.start_with?(short_value) }
|
|
41
|
+
matching = allowed_values.select { |i| i.to_s.eql?(short_value) }
|
|
42
|
+
matching = allowed_values.select { |i| i.to_s.start_with?(short_value) } unless matching.length.eql?(1)
|
|
419
43
|
raise BadArgument, "Identifier '#{short_value}' used where a #{descr} is expected: place the identifier after the command" if matching.empty? && short_value.match?(REGEX_LOOKUP_ID_BY_FIELD)
|
|
420
44
|
Aspera.assert(!matching.empty?, multi_choice_assert_msg("unknown value for #{descr}: #{short_value}", allowed_values), type: BadArgument)
|
|
421
45
|
Aspera.assert(matching.length.eql?(1), multi_choice_assert_msg("ambiguous shortcut for #{descr}: #{short_value}", matching), type: BadArgument)
|
|
@@ -423,14 +47,6 @@ module Aspera
|
|
|
423
47
|
matching.first
|
|
424
48
|
end
|
|
425
49
|
|
|
426
|
-
# Find a key in a list by exact match or unique prefix match
|
|
427
|
-
# @return [Object, nil] the matching key, or nil if none or ambiguous
|
|
428
|
-
def match_prefix(short_value, allowed_values)
|
|
429
|
-
return short_value if allowed_values.include?(short_value)
|
|
430
|
-
matches = allowed_values.select { |k| k.to_s.start_with?(short_value.to_s) }
|
|
431
|
-
matches.length == 1 ? matches.first : nil
|
|
432
|
-
end
|
|
433
|
-
|
|
434
50
|
# Generates error message with list of allowed values
|
|
435
51
|
# @param error_msg [String] Error message
|
|
436
52
|
# @param accept_list [Array<Symbol>] List of allowed values
|
|
@@ -473,90 +89,47 @@ module Aspera
|
|
|
473
89
|
end
|
|
474
90
|
|
|
475
91
|
# Using dotted hash notation, convert value to bool, int, float or extended value
|
|
92
|
+
# `true` and `yes` are converted to `true`, `false` and `no` to `false`
|
|
476
93
|
# @param value [String] The value to convert to appropriate type
|
|
477
94
|
# @return [Boolean, Integer, Float, String, Array, Hash] the converted value
|
|
478
95
|
def smart_convert(value)
|
|
479
96
|
case value
|
|
480
|
-
when 'true'
|
|
481
|
-
when 'false' then false
|
|
97
|
+
when 'true', BoolValue::YES_SYM.to_s then true
|
|
98
|
+
when 'false', BoolValue::NO_SYM.to_s then false
|
|
482
99
|
else
|
|
483
|
-
Integer(value, exception: false) ||
|
|
100
|
+
Integer(value, 10, exception: false) ||
|
|
484
101
|
Float(value, exception: false) ||
|
|
485
102
|
ExtendedValue.instance.evaluate(value, context: 'dotted expression')
|
|
486
103
|
end
|
|
487
104
|
end
|
|
488
105
|
end
|
|
489
106
|
|
|
490
|
-
attr_accessor :ask_missing_mandatory, :ask_missing_optional
|
|
107
|
+
attr_accessor :ask_missing_mandatory, :ask_missing_optional
|
|
491
108
|
attr_writer :fail_on_missing_mandatory
|
|
492
109
|
|
|
493
110
|
# @param program_name [String] Name of the program
|
|
494
111
|
# @param argv [Array<String>, nil] Command line arguments to parse
|
|
495
112
|
def initialize(program_name, argv = nil)
|
|
496
|
-
|
|
497
|
-
# @type [Hash{Symbol => OptionValue}]
|
|
498
|
-
@declared_options = {}
|
|
113
|
+
@registry = OptionRegistry.new
|
|
499
114
|
# do we ask missing options and arguments to user ?
|
|
500
115
|
@ask_missing_mandatory = false # STDIN.isatty
|
|
501
116
|
# ask optional options if not provided and in interactive
|
|
502
117
|
@ask_missing_optional = false
|
|
503
118
|
# get_option fails if a mandatory parameter is asked
|
|
504
119
|
@fail_on_missing_mandatory = true
|
|
505
|
-
#
|
|
506
|
-
@
|
|
507
|
-
#
|
|
508
|
-
@
|
|
120
|
+
# Values from presets and env vars, for options not declared yet: option symbol -> [{value:, source:}]
|
|
121
|
+
@pending_values = {}
|
|
122
|
+
# `true` when options were declared or presets added since last parse
|
|
123
|
+
@parse_needed = true
|
|
509
124
|
# options can also be provided by env vars : --param-name -> ASCLI_PARAM_NAME
|
|
510
|
-
@option_pairs_batch = {}
|
|
511
|
-
@option_pairs_env = {}
|
|
512
|
-
# Short option char -> option symbol, e.g. {'h' => :help, 'v' => :version}
|
|
513
|
-
@short_options = {}
|
|
514
|
-
# Current help section group name, set by #group
|
|
515
|
-
@current_group = 'global'
|
|
516
125
|
env_prefix = program_name.upcase + Option::NAME_SEP_SYMBOL
|
|
517
126
|
ENV.each do |k, v|
|
|
518
|
-
|
|
519
|
-
end
|
|
520
|
-
Log.dump(:env, @option_pairs_env)
|
|
521
|
-
# Ordered list of all CLI tokens after `--` splitting.
|
|
522
|
-
# Single source of truth for CLI parsing — pending_arguments and pending_options are derived views.
|
|
523
|
-
# @type [Array<Option, Argument>]
|
|
524
|
-
@argv_tokens = []
|
|
525
|
-
# Frozen snapshot of @argv_tokens used by unprocessed_options_with_value.
|
|
526
|
-
@initial_argv_tokens = [].freeze
|
|
527
|
-
# Number of original positional args before the option currently being parsed (nil = positional context).
|
|
528
|
-
@current_option_args_offset = nil
|
|
529
|
-
# Current index in @argv_tokens during parse_options! loop (used by shift_next_argument_token).
|
|
530
|
-
@current_parse_idx = nil
|
|
531
|
-
return if argv.nil?
|
|
532
|
-
# true until `--` is found (stop options)
|
|
533
|
-
process_options = true
|
|
534
|
-
argv.each do |value|
|
|
535
|
-
if process_options && value.start_with?('-')
|
|
536
|
-
Log.log.trace1 { "opt: #{value}" }
|
|
537
|
-
if value.eql?(OPTIONS_STOP)
|
|
538
|
-
process_options = false
|
|
539
|
-
else
|
|
540
|
-
@argv_tokens.push(value.start_with?(Option::PREFIX) ? Option.from_long(value) : Option.from_short(value))
|
|
541
|
-
end
|
|
542
|
-
else
|
|
543
|
-
Log.log.trace1 { "arg: #{value}" }
|
|
544
|
-
@argv_tokens.push(Argument.new(value))
|
|
545
|
-
end
|
|
127
|
+
add_pending_value(k.delete_prefix(env_prefix).downcase.to_sym, v, :env, replace: true) if k.start_with?(env_prefix)
|
|
546
128
|
end
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
declare(:interactive, description: 'Use interactive input of missing params', allowed: Type::BOOLEAN,
|
|
550
|
-
declare(:ask_options, description: 'Ask even optional options', allowed: Type::BOOLEAN,
|
|
551
|
-
# do not parse options yet, let's wait for option `-h` to be overridden
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
# Add a type to the message if not special types
|
|
555
|
-
# @param types [Array<Class>] types to add
|
|
556
|
-
# @return [String] Types if relevant
|
|
557
|
-
def add_types_info(types)
|
|
558
|
-
return '' if !types || types.empty? || types.eql?(Type::ENUM) || types.eql?(Type::BOOLEAN) || types.eql?(Type::STRING)
|
|
559
|
-
" (#{types.map(&:name).join(', ')})"
|
|
129
|
+
Log.dump(:env, @pending_values)
|
|
130
|
+
@command_line = CommandLine.new(argv || [])
|
|
131
|
+
declare(:interactive, description: 'Use interactive input of missing params', allowed: Type::BOOLEAN, default: false, on_set: method(:ask_missing_mandatory=))
|
|
132
|
+
declare(:ask_options, description: 'Ask even optional options', allowed: Type::BOOLEAN, default: false, on_set: method(:ask_missing_optional=))
|
|
560
133
|
end
|
|
561
134
|
|
|
562
135
|
# Declare an option
|
|
@@ -570,70 +143,71 @@ module Aspera
|
|
|
570
143
|
# - Use `allowed: [Hash, String]` when the option additionally accepts a plain String shorthand;
|
|
571
144
|
# the schema then documents the Hash form and `=help` still shows it.
|
|
572
145
|
# @param default [Object] default value
|
|
573
|
-
# @param
|
|
574
|
-
#
|
|
146
|
+
# @param on_set [#call] Called with the new value each time the value is set (e.g. a `Method`, or a lambda).
|
|
147
|
+
# For a flag (`Type::NONE`): called without argument when the flag is found
|
|
148
|
+
# @param shorthand [String] For a `Hash` option: a `String` value is stored as `{shorthand => value}`
|
|
149
|
+
# @param deprecation [Hash, nil] deprecation: `{last:, message:}`, see `Deprecation`
|
|
575
150
|
# @param schema [String] schema path documenting the Hash form of this option
|
|
576
151
|
# @param block [Proc] Block to execute when option is found
|
|
577
|
-
def declare(option_symbol, description: nil, short: nil, allowed: nil, default: nil,
|
|
152
|
+
def declare(option_symbol, description: nil, short: nil, allowed: nil, default: nil, on_set: nil, shorthand: nil, deprecation: nil, schema: nil, &block)
|
|
578
153
|
Aspera.assert_type(option_symbol, Symbol)
|
|
579
|
-
Aspera.assert(!@
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
154
|
+
Aspera.assert(!@registry.declared?(option_symbol)) { "#{option_symbol} already declared" }
|
|
155
|
+
if on_set && allowed.eql?(Type::NONE)
|
|
156
|
+
Aspera.assert(block.nil?) { "#{option_symbol}: flag with both on_set and block" }
|
|
157
|
+
block = on_set
|
|
158
|
+
on_set = nil
|
|
159
|
+
end
|
|
160
|
+
# An abbreviation already used on command line must stay unambiguous
|
|
161
|
+
@command_line.abbreviated_option_tokens.each do |tok|
|
|
162
|
+
Aspera.assert(!option_symbol.to_s.start_with?(tok.name), type: BadArgument) do
|
|
163
|
+
"Ambiguous option #{tok.raw}: used as #{self.class.option_name_to_line(tok.abbreviation_of)}, but also matches #{self.class.option_name_to_line(option_symbol)}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
option = @registry.add(
|
|
167
|
+
OptionValue.new(
|
|
168
|
+
option: option_symbol,
|
|
169
|
+
description: description,
|
|
170
|
+
allowed: allowed,
|
|
171
|
+
on_set: on_set,
|
|
172
|
+
shorthand: shorthand,
|
|
173
|
+
deprecation: deprecation,
|
|
174
|
+
schema: schema
|
|
175
|
+
),
|
|
176
|
+
short: short
|
|
589
177
|
)
|
|
590
|
-
|
|
591
|
-
description = option_attrs.description
|
|
178
|
+
description = option.description
|
|
592
179
|
Aspera.assert(!description.nil?) { "#{option_symbol}: no description and no schema to derive one from" }
|
|
593
180
|
Aspera.assert(description[-1] != '.') { "#{option_symbol} ends with dot" }
|
|
594
181
|
Aspera.assert(description[0] == description[0].upcase) { "#{option_symbol} description does not start with an uppercase" }
|
|
595
182
|
Aspera.assert(!['hash', 'extended value'].any? { |s| description.downcase.include?(s) }) { "#{option_symbol} shall use :allowed instead of hash/extended value in option description" }
|
|
596
|
-
set_option(option_symbol, default,
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
set_option(option_symbol, BoolValue.true?(default), where: 'default', warn_deprecation: false) if option_attrs.values.eql?(BoolValue::ALL) && !default.nil?
|
|
601
|
-
when Type::NONE
|
|
602
|
-
Aspera.assert_type(block, Proc) { "missing execution block for #{option_symbol}" }
|
|
603
|
-
option_attrs.block = block
|
|
183
|
+
set_option(option_symbol, default, source: :default, warn_deprecation: false) unless default.nil?
|
|
184
|
+
if option.flag?
|
|
185
|
+
Aspera.assert(block.respond_to?(:call)) { "missing execution block for #{option_symbol}" }
|
|
186
|
+
option.block = block
|
|
604
187
|
end
|
|
605
|
-
@
|
|
606
|
-
Log.log.trace1 { "declare: #{option_symbol}, group: #{
|
|
188
|
+
@parse_needed = true
|
|
189
|
+
Log.log.trace1 { "declare: #{option_symbol}, group: #{option.group}, short: #{short}" }
|
|
607
190
|
end
|
|
608
191
|
|
|
609
192
|
# Set the current help section group name for subsequent declarations
|
|
610
193
|
# @param name [String] group name, shown as section header in help text
|
|
611
194
|
def group(name)
|
|
612
|
-
@
|
|
195
|
+
@registry.group = name
|
|
613
196
|
end
|
|
614
197
|
|
|
615
|
-
#
|
|
616
|
-
#
|
|
617
|
-
#
|
|
618
|
-
# @param
|
|
619
|
-
def rename_current_group(name)
|
|
620
|
-
@declared_options.each_value { |opt| opt.group = name if opt.group.eql?(@current_group) }
|
|
621
|
-
@current_group = name
|
|
622
|
-
end
|
|
623
|
-
|
|
624
|
-
# Low-level positional argument reader. Prefer +Base#resolve_argument+ from action methods.
|
|
625
|
-
# Direct calls from outside +Parser+ are legacy exceptions documented in ST12/ST13
|
|
626
|
-
# (mixins without DSL: sync_actions, ascp_actions; setup callbacks: aoc.rb).
|
|
627
|
-
# @api private
|
|
628
|
-
# @param descr [String] description for help
|
|
198
|
+
# Low-level positional argument reader.
|
|
199
|
+
# Prefer `Base#resolve_argument` from action methods.
|
|
200
|
+
# The only direct call from outside `Cli::Parser` and `Cli::Plugins::Base` is to retrieve the list of files.
|
|
201
|
+
# @param descr [String] Description for help
|
|
629
202
|
# @param mandatory [Boolean] `true`: raise error no more argument
|
|
630
203
|
# @param multiple [Boolean] `true`: return all remaining arguments (Array). String: until marker
|
|
631
|
-
# @param accept_list [Array<Symbol>,
|
|
632
|
-
# @param validation [Class, Array,
|
|
204
|
+
# @param accept_list [Array<Symbol>, nil] list of allowed values
|
|
205
|
+
# @param validation [Class, Array, nil] Accepted value type(s) or list of Symbols
|
|
633
206
|
# @param aliases [Hash] map of aliases: key = alias, value = real value
|
|
634
207
|
# @param default [Object] default value
|
|
635
208
|
# @return [Object, Array, nil] one value, list or nil (if optional and no default)
|
|
636
209
|
def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: Type::STRING, aliases: nil, default: nil, schema: nil)
|
|
210
|
+
ensure_parsed
|
|
637
211
|
Aspera.assert_array_all(accept_list, Symbol) unless accept_list.nil?
|
|
638
212
|
Aspera.assert_hash_all(aliases, Symbol, Symbol) unless aliases.nil?
|
|
639
213
|
validation = Symbol unless accept_list.nil?
|
|
@@ -641,47 +215,28 @@ module Aspera
|
|
|
641
215
|
Aspera.assert_array_all(validation, Class) { 'validation' } unless validation.nil?
|
|
642
216
|
descr = "#{descr}#{add_types_info(validation)}"
|
|
643
217
|
result =
|
|
644
|
-
if
|
|
645
|
-
values = extract_argument_tokens(multiple)
|
|
646
|
-
values = values.map { |v| ExtendedValue.instance.evaluate(v, context: "argument: #{descr}", allowed: validation) }
|
|
647
|
-
# If expecting list and only one arg of type array : it is the list
|
|
648
|
-
values = values.first if multiple && values.length.eql?(1) && values.first.is_a?(Array)
|
|
649
|
-
if accept_list
|
|
650
|
-
allowed_values = [].concat(accept_list)
|
|
651
|
-
allowed_values.concat(aliases.keys) unless aliases.nil?
|
|
652
|
-
values = values.map { |v| self.class.get_from_list(v, descr, allowed_values) }
|
|
653
|
-
end
|
|
654
|
-
multiple ? values : values.first
|
|
218
|
+
if !@command_line.pending_arguments.empty? then read_arguments(descr, multiple: multiple, validation: validation, accept_list: accept_list, aliases: aliases)
|
|
655
219
|
elsif !default.nil? then default
|
|
656
220
|
elsif mandatory then get_interactive(descr, multiple: multiple, accept_list: accept_list, aliases: aliases, schema: schema)
|
|
657
221
|
end
|
|
658
222
|
Log.log.trace1 { "#{descr}=#{result}" }
|
|
659
223
|
result = aliases[result] if aliases&.key?(result)
|
|
660
|
-
|
|
661
|
-
result
|
|
662
|
-
# Integer coercion: a String argument for an INTEGER option must be parseable
|
|
663
|
-
if result.is_a?(String) && validation&.eql?(Type::INTEGER)
|
|
664
|
-
int_result = Integer(result, exception: false)
|
|
665
|
-
raise Cli::BadArgument, "Invalid integer: #{result}" if int_result.nil?
|
|
666
|
-
result = int_result
|
|
667
|
-
end
|
|
668
|
-
if validation && (mandatory || !result.nil?)
|
|
669
|
-
value_list = multiple ? result : [result]
|
|
670
|
-
value_list.each { |value| validate_argument(value, validation: validation, descr: descr, schema: schema) }
|
|
671
|
-
end
|
|
224
|
+
result = convert_argument(result, validation)
|
|
225
|
+
(multiple ? result : [result]).each { |value| validate_argument(value, validation: validation, descr: descr, schema: schema) } if validation && (mandatory || !result.nil?)
|
|
672
226
|
result
|
|
673
227
|
end
|
|
674
228
|
|
|
675
229
|
# Resource identifier as positional parameter
|
|
676
230
|
#
|
|
677
|
-
# @param description [String]
|
|
678
|
-
# @param
|
|
679
|
-
# @
|
|
231
|
+
# @param description [String] description of the identifier
|
|
232
|
+
# @param multiple [Boolean] `true`: read a list of IDs (bulk operation)
|
|
233
|
+
# @param block [Proc] block to search for identifier based on attribute value
|
|
234
|
+
# @return [String, Array<String>] identifier or list of IDs (if `multiple`)
|
|
680
235
|
# @yieldparam field [String] The field name from percent selector
|
|
681
236
|
# @yieldparam value [String] The value from percent selector
|
|
682
237
|
# @yieldreturn [String] Resolved identifier
|
|
683
|
-
def instance_identifier(description: 'identifier', &block)
|
|
684
|
-
res_id = get_next_argument(description, multiple:
|
|
238
|
+
def instance_identifier(description: 'identifier', multiple: false, &block)
|
|
239
|
+
res_id = get_next_argument(description, multiple: multiple)
|
|
685
240
|
# Can be an Array
|
|
686
241
|
if res_id.is_a?(String) && (m = Parser.percent_selector(res_id))
|
|
687
242
|
Aspera.assert(block_given?, type: Cli::BadArgument) { "Percent syntax for #{description} not supported in this context" }
|
|
@@ -700,23 +255,21 @@ module Aspera
|
|
|
700
255
|
# @param option_symbol [Symbol] name of the option
|
|
701
256
|
# @return [Boolean]
|
|
702
257
|
def option_declared?(option_symbol)
|
|
703
|
-
@
|
|
258
|
+
@registry.declared?(option_symbol)
|
|
704
259
|
end
|
|
705
260
|
|
|
706
261
|
# @return [Hash{Symbol => OptionValue}] all declared options (read-only view)
|
|
707
|
-
|
|
262
|
+
def declared_options = @registry.options
|
|
708
263
|
|
|
709
264
|
# Get an option definition by name
|
|
710
265
|
# @param option_symbol [Symbol] name of the option
|
|
711
266
|
# @return [OptionValue] Option definition
|
|
712
267
|
# @raise [Cli::BadArgument] if option not found
|
|
713
268
|
def option_def(option_symbol)
|
|
714
|
-
|
|
715
|
-
@declared_options[option_symbol]
|
|
269
|
+
@registry.fetch(option_symbol)
|
|
716
270
|
end
|
|
717
271
|
|
|
718
|
-
# Get an option value by name
|
|
719
|
-
# either return value or calls handler, can return nil
|
|
272
|
+
# Get an option value by name, can return nil
|
|
720
273
|
# ask interactively if requested/required
|
|
721
274
|
# @param option_symbol [Symbol] name of the option to retrieve
|
|
722
275
|
# @param mandatory [Boolean] if true, raise error if option not set
|
|
@@ -724,37 +277,32 @@ module Aspera
|
|
|
724
277
|
# if the option value is 'help' (used for --query whose schema depends on the current command)
|
|
725
278
|
def get_option(option_symbol, mandatory: false, schema: nil)
|
|
726
279
|
Aspera.assert_type(option_symbol, Symbol)
|
|
727
|
-
|
|
728
|
-
|
|
280
|
+
ensure_parsed
|
|
281
|
+
option = option_def(option_symbol)
|
|
282
|
+
result = option.value
|
|
729
283
|
# Contextual schema: raise SchemaRequest when value is 'help'
|
|
730
|
-
raise SchemaRequest.new(:option, option_symbol.to_s, schema) if schema && result.eql?(
|
|
284
|
+
raise SchemaRequest.new(:option, option_symbol.to_s, schema) if schema && result.eql?(SchemaRequest::KEYWORD)
|
|
731
285
|
# Do not fail for manual generation if option mandatory but not set
|
|
732
286
|
return :skip_missing_mandatory if result.nil? && mandatory && !@fail_on_missing_mandatory
|
|
733
287
|
if result.nil?
|
|
734
288
|
if !@ask_missing_mandatory
|
|
735
289
|
Aspera.assert(!mandatory, type: Cli::BadArgument) { "Missing mandatory option: #{option_symbol}" }
|
|
736
290
|
elsif @ask_missing_optional || mandatory
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
set_option(option_symbol, result, where: 'interactive')
|
|
291
|
+
result = get_interactive(option_symbol.to_s, accept_list: option.values, schema: option.schema)
|
|
292
|
+
set_option(option_symbol, result, source: :interactive)
|
|
740
293
|
end
|
|
741
294
|
end
|
|
742
295
|
result
|
|
743
296
|
end
|
|
744
297
|
|
|
745
|
-
# Set an option value by name
|
|
298
|
+
# Set an option value by name: store value and call the `on_set` callback
|
|
746
299
|
# String is given to extended value
|
|
747
300
|
# @param option_symbol [Symbol] option name
|
|
748
|
-
# @param value
|
|
749
|
-
# @param
|
|
750
|
-
def set_option(option_symbol, value,
|
|
301
|
+
# @param value [String] Value to set
|
|
302
|
+
# @param source [Symbol] `OptionSource` of value
|
|
303
|
+
def set_option(option_symbol, value, source: :code, warn_deprecation: true)
|
|
751
304
|
Aspera.assert_type(option_symbol, Symbol)
|
|
752
|
-
|
|
753
|
-
# Raise immediately only when the option has a static schema: the schema is known at parse time.
|
|
754
|
-
# When schema is nil (e.g. --query), 'help' is stored as-is and SchemaRequest is raised later
|
|
755
|
-
# in get_option() with the contextual schema provided by the calling command.
|
|
756
|
-
raise SchemaRequest.new(:option, option.option, option.schema) if option.types&.include?(Hash) && value.eql?(HELP) && option.schema
|
|
757
|
-
option.assign_value(value, where: where, warn_deprecation: warn_deprecation)
|
|
305
|
+
option_def(option_symbol).assign_value(value, source: source, warn_deprecation: warn_deprecation)
|
|
758
306
|
end
|
|
759
307
|
|
|
760
308
|
# Set option to `nil`
|
|
@@ -765,72 +313,71 @@ module Aspera
|
|
|
765
313
|
option_def(option_symbol).clear
|
|
766
314
|
end
|
|
767
315
|
|
|
768
|
-
#
|
|
769
|
-
#
|
|
770
|
-
# (e.g. @gen_options) is created after class-load time.
|
|
771
|
-
# @param option_symbol [Symbol] name of the already-declared option
|
|
772
|
-
# @param object [Object] the target object for get/set delegation
|
|
773
|
-
# @param method [Symbol] accessor method name on object
|
|
774
|
-
# @return [nil]
|
|
775
|
-
def set_handler(option_symbol, object:, method:)
|
|
776
|
-
Aspera.assert_type(option_symbol, Symbol)
|
|
777
|
-
option_def(option_symbol).bind_handler(o: object, m: method)
|
|
778
|
-
end
|
|
779
|
-
|
|
780
|
-
# Adds each of the keys of specified hash as an option
|
|
316
|
+
# Adds each of the keys of specified hash as an option.
|
|
317
|
+
# Values are applied by the next parse, and never override a value from env or command line.
|
|
781
318
|
# @param preset_hash [Hash] Options to add
|
|
782
|
-
# @param where [String] Where the value comes from
|
|
783
|
-
# @param override [Boolean]
|
|
319
|
+
# @param where [String] Where the value comes from (for logs)
|
|
320
|
+
# @param override [Boolean] `false` for plugin default presets: lower priority than other presets
|
|
784
321
|
def add_option_preset(preset_hash, where, override: true)
|
|
785
322
|
Aspera.assert_type(preset_hash, Hash)
|
|
786
323
|
Log.log.debug { "add_option_preset: #{preset_hash}, #{where}, #{override}" }
|
|
324
|
+
source = override ? :preset : :plugin_preset
|
|
787
325
|
preset_hash.each do |k, v|
|
|
788
326
|
# Ignore comment/meta keys (e.g. _comment, _description)
|
|
789
327
|
next if k.to_s.start_with?(PresetManager::Key::META_PREFIX)
|
|
790
|
-
|
|
791
|
-
# Never restore an option that was explicitly cleared from the CLI (e.g. --opt=@none:)
|
|
792
|
-
next if @explicitly_cleared.key?(option_symbol)
|
|
793
|
-
@option_pairs_batch[option_symbol] = v if override || !@option_pairs_batch.key?(option_symbol)
|
|
328
|
+
add_pending_value(k.to_sym, v, source, replace: override)
|
|
794
329
|
end
|
|
330
|
+
@parse_needed = true
|
|
795
331
|
end
|
|
796
332
|
|
|
797
333
|
# Allows a plugin to add an argument as next argument to process
|
|
798
334
|
# @param argument [String] argument value to prepend
|
|
799
|
-
# @return [
|
|
335
|
+
# @return [nil]
|
|
800
336
|
def unshift_next_argument(argument)
|
|
801
|
-
@
|
|
337
|
+
@command_line.unshift_argument(argument)
|
|
338
|
+
nil
|
|
802
339
|
end
|
|
803
340
|
|
|
804
341
|
# Check if there are no pending positional arguments
|
|
805
342
|
# @return [Boolean] true if no pending positional arguments
|
|
806
343
|
def command_or_arg_empty?
|
|
807
|
-
|
|
344
|
+
ensure_parsed
|
|
345
|
+
@command_line.pending_arguments.empty?
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Check the next pending positional argument, without consuming it
|
|
349
|
+
# @param values [Array<String>] values to compare with
|
|
350
|
+
# @return [Boolean] true if the next pending positional argument is one of values
|
|
351
|
+
def next_argument_in?(values)
|
|
352
|
+
ensure_parsed
|
|
353
|
+
values.include?(@command_line.pending_arguments.first)
|
|
808
354
|
end
|
|
809
355
|
|
|
810
356
|
# Check for unprocessed options or arguments error messages
|
|
811
357
|
# @return [Array<String>] list of error messages for unprocessed tokens
|
|
812
358
|
def final_errors
|
|
359
|
+
begin
|
|
360
|
+
ensure_parsed
|
|
361
|
+
rescue StandardError => e
|
|
362
|
+
# already in error processing: report only unprocessed tokens
|
|
363
|
+
Log.log.debug { "final parse: #{e}" }
|
|
364
|
+
end
|
|
813
365
|
result = []
|
|
814
|
-
result.push("unprocessed options: #{pending_options}") unless pending_options.empty?
|
|
815
|
-
result.push("unprocessed values: #{pending_arguments}") unless pending_arguments.empty?
|
|
366
|
+
result.push("unprocessed options: #{@command_line.pending_options}") unless @command_line.pending_options.empty?
|
|
367
|
+
result.push("unprocessed values: #{@command_line.pending_arguments}") unless @command_line.pending_arguments.empty?
|
|
816
368
|
result
|
|
817
369
|
end
|
|
818
370
|
|
|
819
|
-
# Get all
|
|
820
|
-
#
|
|
371
|
+
# Get all long options with a value from command line, used to generate a config in config file.
|
|
372
|
+
# They are marked as processed.
|
|
373
|
+
# @return [Hash] options with value, dotted notation expanded
|
|
821
374
|
def unprocessed_options_with_value
|
|
375
|
+
ensure_parsed
|
|
822
376
|
result = {}
|
|
823
|
-
@
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
# ignore options without value
|
|
828
|
-
next if value.nil?
|
|
829
|
-
name = tok.dot_path ? [tok.name, *tok.dot_path].join(DotContainer::SEPARATOR) : tok.name
|
|
830
|
-
Log.log.debug { "option #{name}=#{value}" }
|
|
831
|
-
path = [tok.name, *(tok.dot_path || [])]
|
|
832
|
-
DotContainer.dotted_to_container(path, Parser.smart_convert(value), result)
|
|
833
|
-
@argv_tokens.reject! { |t| t.is_a?(Option) && t.raw.eql?(tok.raw) }
|
|
377
|
+
@command_line.each_long_option_with_value do |tok|
|
|
378
|
+
path = [tok.name, *tok.dot_path]
|
|
379
|
+
Log.log.debug { "option #{path.join(DotContainer::SEPARATOR)}=#{tok.value}" }
|
|
380
|
+
DotContainer.dotted_to_container(path, Parser.smart_convert(tok.value), result)
|
|
834
381
|
end
|
|
835
382
|
result
|
|
836
383
|
end
|
|
@@ -839,7 +386,7 @@ module Aspera
|
|
|
839
386
|
# @return [Hash] options as taken from config file and command line just before command execution
|
|
840
387
|
def known_options(only_defined: false)
|
|
841
388
|
result = {}
|
|
842
|
-
@
|
|
389
|
+
@registry.options.each_key do |option_symbol|
|
|
843
390
|
v = get_option(option_symbol)
|
|
844
391
|
result[option_symbol] = v unless only_defined && v.nil?
|
|
845
392
|
rescue => e
|
|
@@ -848,186 +395,55 @@ module Aspera
|
|
|
848
395
|
result
|
|
849
396
|
end
|
|
850
397
|
|
|
851
|
-
#
|
|
398
|
+
# Apply values of options declared so far: from presets, env vars and command line.
|
|
399
|
+
# Can be called any number of times: tokens of options not declared yet are kept for a later call.
|
|
400
|
+
# Called automatically on read of option or argument, but must be called explicitly
|
|
401
|
+
# when values set by `on_set` callbacks are used.
|
|
852
402
|
def parse_options!
|
|
853
403
|
Log.log.trace1('parse_options!'.red)
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
@
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
@argv_tokens.delete_at(@current_parse_idx)
|
|
877
|
-
# @current_option_args_offset = number of positional Argument tokens that appear BEFORE
|
|
878
|
-
# this option in @argv_tokens (i.e. at indices 0...@current_parse_idx after the delete).
|
|
879
|
-
# Used by args_as_extended to skip those leading args when collecting @: values.
|
|
880
|
-
@current_option_args_offset = @argv_tokens[0...@current_parse_idx].count { |t| t.is_a?(Argument) }
|
|
881
|
-
if tok.short_char
|
|
882
|
-
# Short option: -X or -Xvalue
|
|
883
|
-
option_sym = @short_options[tok.short_char]
|
|
884
|
-
if option_sym
|
|
885
|
-
raw_value = tok.value
|
|
886
|
-
# No inline value and option expects a value: consume the next :argument token
|
|
887
|
-
raw_value = shift_next_argument_token \
|
|
888
|
-
if !tok.has_value && !@declared_options[option_sym].types.eql?(Type::NONE)
|
|
889
|
-
dispatch_option(option_sym, raw_value)
|
|
890
|
-
else
|
|
891
|
-
deferred_tokens.push(tok)
|
|
892
|
-
end
|
|
893
|
-
elsif tok.dot_path
|
|
894
|
-
# Dotted notation: --a.b.c=val or --a.b.c val (always takes priority over plain option lookup)
|
|
895
|
-
Log.log.trace1 { "Dotted option: #{tok.raw}".red }
|
|
896
|
-
if tok.has_value
|
|
897
|
-
raw_value = tok.value
|
|
898
|
-
value_from_next_token = false
|
|
899
|
-
else
|
|
900
|
-
raw_value = shift_next_argument_token
|
|
901
|
-
value_from_next_token = true
|
|
902
|
-
end
|
|
903
|
-
if @declared_options.key?(tok.name.to_sym)
|
|
904
|
-
set_option(tok.name.to_sym, DotContainer.dotted_to_container(tok.dot_path, Parser.smart_convert(raw_value), get_option(tok.name.to_sym)), where: 'dotted')
|
|
905
|
-
else
|
|
906
|
-
# Only re-inject if value was space-separated (consumed from next token); inline values stay in tok.value
|
|
907
|
-
@argv_tokens.unshift(Argument.new(raw_value)) if raw_value && value_from_next_token
|
|
908
|
-
deferred_tokens.push(tok)
|
|
909
|
-
end
|
|
910
|
-
elsif (resolved_sym = self.class.match_prefix(tok.name.to_sym, @declared_options.keys))
|
|
911
|
-
# Known long option (plain, no dot-path)
|
|
912
|
-
raw_value = tok.value
|
|
913
|
-
# No inline `=` and option expects a value: consume the next :argument token
|
|
914
|
-
raw_value = shift_next_argument_token \
|
|
915
|
-
if !tok.has_value && !@declared_options[resolved_sym].types.eql?(Type::NONE)
|
|
916
|
-
dispatch_option(resolved_sym, raw_value)
|
|
917
|
-
else
|
|
918
|
-
Log.log.trace1 { "Unknown long option: #{tok.raw}".red }
|
|
919
|
-
deferred_tokens.push(tok)
|
|
920
|
-
end
|
|
921
|
-
end
|
|
922
|
-
@current_option_args_offset = nil
|
|
923
|
-
@current_parse_idx = nil
|
|
924
|
-
Log.log.trace1('After parse')
|
|
925
|
-
Log.log.trace1 { "deferred: #{deferred_tokens}" }
|
|
926
|
-
# @argv_tokens now contains only: remaining Arguments + deferred Options (already in correct order).
|
|
927
|
-
# Re-insert deferred Options at their original positions by rebuilding from @initial_argv_tokens.
|
|
928
|
-
deferred_ids = deferred_tokens.map(&:object_id).to_set
|
|
929
|
-
remaining_arg_ids = @argv_tokens.grep(Argument).map(&:object_id).to_set
|
|
930
|
-
injected = @argv_tokens.grep(Argument).reject { |t| @initial_argv_tokens.include?(t) }
|
|
931
|
-
@argv_tokens = @initial_argv_tokens.select do |t|
|
|
932
|
-
(t.is_a?(Option) && deferred_ids.include?(t.object_id)) ||
|
|
933
|
-
(t.is_a?(Argument) && remaining_arg_ids.include?(t.object_id))
|
|
934
|
-
end
|
|
935
|
-
# Prepend arguments injected at runtime (e.g. via unshift_next_argument) not in @initial_argv_tokens.
|
|
936
|
-
@argv_tokens.unshift(*injected)
|
|
937
|
-
end
|
|
938
|
-
|
|
939
|
-
# Extract raw string tokens from @argv_tokens (Argument objects) according to `multiple`.
|
|
940
|
-
# Consumed Argument tokens are removed from @argv_tokens.
|
|
941
|
-
# @param multiple [false, true, String] consumption mode:
|
|
942
|
-
# false — consume exactly one token
|
|
943
|
-
# true — consume all remaining tokens
|
|
944
|
-
# String — consume up to (but not including) the marker token, or all if absent
|
|
945
|
-
# @return [Array<String>] consumed raw token strings
|
|
946
|
-
def extract_argument_tokens(multiple)
|
|
947
|
-
arg_tokens = @argv_tokens.grep(Argument)
|
|
948
|
-
case multiple
|
|
949
|
-
when false
|
|
950
|
-
tok = arg_tokens.first
|
|
951
|
-
@argv_tokens.delete(tok)
|
|
952
|
-
[tok.value]
|
|
953
|
-
when true
|
|
954
|
-
arg_tokens.each { |t| @argv_tokens.delete(t) }
|
|
955
|
-
arg_tokens.map(&:value)
|
|
956
|
-
when String
|
|
957
|
-
idx = arg_tokens.index { |t| t.value.eql?(multiple) }
|
|
958
|
-
consumed = idx ? arg_tokens[0, idx] : arg_tokens
|
|
959
|
-
marker = idx ? arg_tokens[idx] : nil
|
|
960
|
-
(consumed + [marker].compact).each { |t| @argv_tokens.delete(t) }
|
|
961
|
-
consumed.map(&:value)
|
|
962
|
-
else Aspera.error_unexpected_value(multiple) { 'multiple' }
|
|
963
|
-
end
|
|
964
|
-
end
|
|
965
|
-
|
|
966
|
-
# Validate and coerce a single argument value.
|
|
967
|
-
# Raises SchemaRequest when the value is 'help' and validation includes Hash.
|
|
968
|
-
# Raises BadArgument when the value is an Integer on a STRING validation (coerced upstream).
|
|
969
|
-
# Raises BadArgument when the value's type is not in the validation list.
|
|
970
|
-
# @param value [Object] the value to validate
|
|
971
|
-
# @param validation [Array<Class>] accepted types
|
|
972
|
-
# @param descr [String] argument description (for error messages)
|
|
973
|
-
# @param schema [String, nil] schema path for SchemaRequest
|
|
974
|
-
def validate_argument(value, validation:, descr:, schema:)
|
|
975
|
-
raise SchemaRequest.new(:argument, descr, schema) if validation.include?(Hash) && value.eql?(HELP)
|
|
976
|
-
raise Cli::BadArgument,
|
|
977
|
-
"Argument #{descr} is a #{value.class} but must be #{'one of: ' if validation.length > 1}#{validation.map(&:name).join(', ')}" \
|
|
978
|
-
unless validation.any? { |t| value.is_a?(t) }
|
|
979
|
-
end
|
|
980
|
-
|
|
981
|
-
# Prompt user for console input
|
|
982
|
-
# @param prompt [String] prompt string to display
|
|
983
|
-
# @param sensitive [Boolean] whether to hide typed input
|
|
984
|
-
# @return [String] user input stripped of trailing newline
|
|
985
|
-
def prompt_user_input(prompt, sensitive: false)
|
|
986
|
-
return $stdin.getpass("#{prompt}> ") if sensitive
|
|
987
|
-
print("#{prompt}> ")
|
|
988
|
-
line = $stdin.gets
|
|
989
|
-
Aspera.assert_type(line, String) { 'Unexpected end of standard input' }
|
|
990
|
-
line.chomp
|
|
991
|
-
end
|
|
992
|
-
|
|
993
|
-
# prompt user for input in a list of symbols
|
|
994
|
-
# @param prompt [String] prompt to display
|
|
995
|
-
# @param sym_list [Array] list of symbols to select from
|
|
996
|
-
# @return [Symbol] selected symbol
|
|
997
|
-
def prompt_user_input_in_list(prompt, sym_list)
|
|
998
|
-
loop do
|
|
999
|
-
input = prompt_user_input(prompt).to_sym
|
|
1000
|
-
if sym_list.any? { |a| a.eql?(input) }
|
|
1001
|
-
return input
|
|
1002
|
-
else
|
|
1003
|
-
$stderr.puts("No such #{prompt}: #{input}, select one of: #{sym_list.join(', ')}") # rubocop:disable Style/StderrPuts
|
|
1004
|
-
end
|
|
1005
|
-
end
|
|
1006
|
-
end
|
|
1007
|
-
|
|
1008
|
-
# Prompt user for input in a list of symbols
|
|
1009
|
-
# @param descr [String] description for help
|
|
1010
|
-
# @param check_option [Boolean] Check attributes of option with name=descr
|
|
404
|
+
@parse_needed = false
|
|
405
|
+
apply_pending_values
|
|
406
|
+
@command_line.pending_option_tokens.each { |tok| apply_option_token(tok) }
|
|
407
|
+
# Presets loaded by a command line option (e.g. `-P`)
|
|
408
|
+
apply_pending_values
|
|
409
|
+
Log.log.trace1 { "unprocessed options: #{@command_line.pending_options}" }
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Execute the block with interactive input of missing mandatory options and arguments, as with `--interactive=yes`.
|
|
413
|
+
# The previous state is restored afterwards, so that interactive input does not leak to the rest of the command.
|
|
414
|
+
# @param enabled [Boolean] `false`: execute the block with current state
|
|
415
|
+
# @return [Object] result of the block
|
|
416
|
+
def with_interactive(enabled: true)
|
|
417
|
+
previous = @ask_missing_mandatory
|
|
418
|
+
@ask_missing_mandatory = true if enabled
|
|
419
|
+
yield
|
|
420
|
+
ensure
|
|
421
|
+
@ask_missing_mandatory = previous
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Prompt user for missing option or argument, or raise if not interactive
|
|
425
|
+
# @param descr [String] option name, or argument description
|
|
1011
426
|
# @param multiple [Boolean, String] `true` if multiple values expected
|
|
1012
|
-
# @param accept_list [Array<Symbol>,
|
|
427
|
+
# @param accept_list [Array<Symbol>, nil] List of expected values
|
|
428
|
+
# @param aliases [Hash, nil] aliases of values, for error message
|
|
429
|
+
# @param schema [String, nil] schema of value, for error message
|
|
1013
430
|
# @return [String] user input
|
|
1014
|
-
def get_interactive(descr,
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
default_prompt = "#{what}: #{descr}"
|
|
431
|
+
def get_interactive(descr, multiple: false, accept_list: nil, aliases: nil, schema: nil)
|
|
432
|
+
option = @registry.options[descr.to_sym]
|
|
433
|
+
default_prompt = "#{option ? 'option' : 'argument'}: #{descr}"
|
|
1018
434
|
if !@ask_missing_mandatory
|
|
1019
435
|
message = "Missing #{default_prompt}"
|
|
1020
436
|
message = self.class.multi_choice_assert_msg(message, accept_list, aliases: aliases) if accept_list
|
|
1021
|
-
message += "\n#{TerminalFormatter
|
|
437
|
+
message += "\n#{TerminalFormatter.hint}Give `#{SchemaRequest::KEYWORD}` as argument to retrieve the schema of the missing argument." if schema
|
|
1022
438
|
raise Cli::MissingArgument, message
|
|
1023
439
|
end
|
|
1024
|
-
#
|
|
440
|
+
# Ask interactively
|
|
1025
441
|
result = []
|
|
1026
|
-
puts(' (one per line, end with empty line)') if multiple
|
|
442
|
+
$stderr.puts(' (one per line, end with empty line)') if multiple # rubocop:disable Style/StderrPuts
|
|
1027
443
|
loop do
|
|
1028
444
|
prompt = default_prompt
|
|
1029
445
|
prompt = "#{accept_list.join(' ')}\n#{default_prompt}" if accept_list
|
|
1030
|
-
entry = prompt_user_input(prompt, sensitive:
|
|
446
|
+
entry = prompt_user_input(prompt, sensitive: option&.sensitive)
|
|
1031
447
|
break if entry.empty? && multiple
|
|
1032
448
|
entry = ExtendedValue.instance.evaluate(entry, context: 'interactive input')
|
|
1033
449
|
entry = self.class.get_from_list(entry, descr, accept_list) if accept_list
|
|
@@ -1038,30 +454,19 @@ module Aspera
|
|
|
1038
454
|
end
|
|
1039
455
|
|
|
1040
456
|
# Read remaining args and build an `Array` or `Hash`
|
|
1041
|
-
#
|
|
457
|
+
# When used in an option value, only positional arguments after the option are used.
|
|
458
|
+
# @param end_marker [String] Argument to `@:` extended value
|
|
1042
459
|
# @return [Hash, Array] Object representing dot-path values
|
|
1043
460
|
def args_as_extended(end_marker)
|
|
1044
|
-
# This extended value does not take args (`@:`)
|
|
1045
|
-
# ExtendedValue.assert_no_value(end_marker, :p)
|
|
1046
461
|
end_marker = SpecialValues::EOA if end_marker.empty?
|
|
1047
|
-
# When called from an option value, skip positional args that appear before the option in argv.
|
|
1048
|
-
# @current_option_args_offset is set by parse_options! to the number of args in
|
|
1049
|
-
# @unprocessed_cmd_line_arguments that preceded this option; nil when called from a positional context.
|
|
1050
|
-
skip_count = @current_option_args_offset || 0
|
|
1051
|
-
# Skip leading positional args that precede this option in original argv order.
|
|
1052
|
-
# Grab the first `skip_count` Argument tokens and temporarily remove them.
|
|
1053
|
-
arg_tokens = @argv_tokens.grep(Argument)
|
|
1054
|
-
skipped_tokens = skip_count.positive? ? arg_tokens.first(skip_count) : []
|
|
1055
|
-
skipped_tokens.each { |t| @argv_tokens.delete(t) }
|
|
1056
|
-
Log.log.trace1 { "args_as_extended: skipping #{skipped_tokens.length} args before option: #{skipped_tokens.map(&:value)}" } unless skipped_tokens.empty?
|
|
1057
462
|
result = nil
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
463
|
+
@command_line.with_arguments_after_current_option do
|
|
464
|
+
get_next_argument('args', multiple: end_marker).each do |argument|
|
|
465
|
+
Aspera.assert(argument.include?(Option::VALUE_SEP)) { "Positional argument: #{argument} does not include #{Option::VALUE_SEP}" }
|
|
466
|
+
path, value = argument.split(Option::VALUE_SEP, 2)
|
|
467
|
+
result = DotContainer.dotted_to_container(path.split(DotContainer::SEPARATOR), Parser.smart_convert(value), result)
|
|
468
|
+
end
|
|
1062
469
|
end
|
|
1063
|
-
# Restore skipped tokens so they remain available for command dispatching
|
|
1064
|
-
skipped_tokens.reverse_each { |t| @argv_tokens.unshift(t) }
|
|
1065
470
|
result
|
|
1066
471
|
end
|
|
1067
472
|
|
|
@@ -1071,15 +476,14 @@ module Aspera
|
|
|
1071
476
|
def help_text(banner: nil)
|
|
1072
477
|
rows = []
|
|
1073
478
|
current_group = nil
|
|
1074
|
-
@
|
|
479
|
+
@registry.options.each do |sym, opt|
|
|
1075
480
|
if opt.group != current_group
|
|
1076
481
|
current_group = opt.group
|
|
1077
482
|
rows << [{value: "OPTIONS: #{current_group}", colspan: 2}]
|
|
1078
483
|
end
|
|
1079
|
-
|
|
1080
|
-
short_part = short_char ? "-#{short_char}, " : ' '
|
|
484
|
+
short_part = opt.short ? "-#{opt.short}, " : ' '
|
|
1081
485
|
flag = "#{short_part}#{symbol_to_option(sym, option_display_value(opt))}"
|
|
1082
|
-
desc = opt.deprecation ? "#{opt.description} (
|
|
486
|
+
desc = opt.deprecation ? "#{opt.description} (#{opt.deprecation})" : opt.description
|
|
1083
487
|
rows << [flag, desc]
|
|
1084
488
|
end
|
|
1085
489
|
table = Terminal::Table.new(rows: rows, style: {border: HELP_BORDER, padding_left: 0, padding_right: 2})
|
|
@@ -1099,14 +503,70 @@ module Aspera
|
|
|
1099
503
|
b.remove_horizontals
|
|
1100
504
|
end.freeze
|
|
1101
505
|
|
|
506
|
+
# Parse if options were declared or presets added since last parse
|
|
507
|
+
def ensure_parsed
|
|
508
|
+
parse_options! if @parse_needed
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Add a type to the message if not special types
|
|
512
|
+
# @param types [Array<Class>] types to add
|
|
513
|
+
# @return [String] Types if relevant
|
|
514
|
+
def add_types_info(types)
|
|
515
|
+
return '' if !types || types.empty? || types.eql?(Type::ENUM) || types.eql?(Type::BOOLEAN) || types.eql?(Type::STRING)
|
|
516
|
+
" (#{types.map(&:name).join(', ')})"
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Consume and evaluate positional arguments
|
|
520
|
+
# @return [Object, Array] one value, or list if `multiple`
|
|
521
|
+
def read_arguments(descr, multiple:, validation:, accept_list:, aliases:)
|
|
522
|
+
values = @command_line.shift_arguments(multiple)
|
|
523
|
+
values = values.map { |v| ExtendedValue.instance.evaluate(v, context: "argument: #{descr}", allowed: validation) }
|
|
524
|
+
# If expecting list and only one arg of type array : it is the list
|
|
525
|
+
values = values.first if multiple && values.length.eql?(1) && values.first.is_a?(Array)
|
|
526
|
+
if accept_list
|
|
527
|
+
allowed_values = accept_list + (aliases&.keys || [])
|
|
528
|
+
values = values.map { |v| self.class.get_from_list(v, descr, allowed_values) }
|
|
529
|
+
end
|
|
530
|
+
multiple ? values : values.first
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Convert argument to expected type, when unambiguous
|
|
534
|
+
# @param value [Object] argument value
|
|
535
|
+
# @param validation [Array<Class>, nil] accepted types
|
|
536
|
+
# @return [Object] converted value
|
|
537
|
+
def convert_argument(value, validation)
|
|
538
|
+
# if value comes from JSON/YAML, it may come as Integer
|
|
539
|
+
return value.to_s if value.is_a?(Integer) && validation.eql?(Type::STRING)
|
|
540
|
+
return value unless value.is_a?(String) && validation.eql?(Type::INTEGER)
|
|
541
|
+
Integer(value, 10, exception: false).tap { |i| raise Cli::BadArgument, "Invalid integer: #{value}" if i.nil? }
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Validate a single argument value.
|
|
545
|
+
# @param value [Object] the value to validate
|
|
546
|
+
# @param validation [Array<Class>] accepted types
|
|
547
|
+
# @param descr [String] argument description (for error messages)
|
|
548
|
+
# @param schema [String, nil] schema path for SchemaRequest and validation
|
|
549
|
+
# @raise [SchemaRequest] when the value is 'help' and validation includes Hash.
|
|
550
|
+
# @raise [BadArgument] when the value's type is not in the validation list.
|
|
551
|
+
# @raise [BadArgument] when the value does not match its schema.
|
|
552
|
+
def validate_argument(value, validation:, descr:, schema:)
|
|
553
|
+
raise SchemaRequest.new(:argument, descr, schema) if validation.include?(Hash) && value.eql?(SchemaRequest::KEYWORD)
|
|
554
|
+
raise BadArgument,
|
|
555
|
+
"Argument #{descr} is a #{value.class} but must be #{'one of: ' if validation.length > 1}#{validation.map(&:name).join(', ')}" \
|
|
556
|
+
unless validation.any? { |t| value.is_a?(t) }
|
|
557
|
+
errors = Schema::Validator.instance.errors(value, schema) if schema && (value.is_a?(Hash) || value.is_a?(Array))
|
|
558
|
+
raise BadArgument, "Argument #{descr}: #{errors.join('; ')} (give `#{SchemaRequest::KEYWORD}` as argument for schema)" unless errors.nil? || errors.empty?
|
|
559
|
+
end
|
|
560
|
+
|
|
1102
561
|
# @param opt [OptionValue] option descriptor
|
|
1103
|
-
# @return [String, nil] placeholder shown in flag column
|
|
562
|
+
# @return [String, nil] placeholder shown in flag column, or nil for flags
|
|
1104
563
|
def option_display_value(opt)
|
|
1105
|
-
case opt.
|
|
1106
|
-
when
|
|
1107
|
-
when
|
|
1108
|
-
when
|
|
1109
|
-
when
|
|
564
|
+
case opt.kind
|
|
565
|
+
when :flag then nil
|
|
566
|
+
when :boolean then 'yes|no'
|
|
567
|
+
when :integer then 'INT'
|
|
568
|
+
when :float then 'FLOAT'
|
|
569
|
+
when :enum
|
|
1110
570
|
opt.values&.any? && opt.values.length <= 4 ? opt.values.join('|') : 'ENUM'
|
|
1111
571
|
else
|
|
1112
572
|
if opt.types&.include?(Hash) || !opt.schema.nil?
|
|
@@ -1119,22 +579,69 @@ module Aspera
|
|
|
1119
579
|
end
|
|
1120
580
|
end
|
|
1121
581
|
|
|
1122
|
-
#
|
|
1123
|
-
# @param
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
if
|
|
1128
|
-
|
|
582
|
+
# Apply a command line option token if its option is declared, else keep it for later.
|
|
583
|
+
# @param tok [Option] option token
|
|
584
|
+
def apply_option_token(tok)
|
|
585
|
+
return if tok.consumed
|
|
586
|
+
option = tok.short_char ? @registry.by_short(tok.short_char) : resolve_long_name(tok)
|
|
587
|
+
return if option.nil?
|
|
588
|
+
if option.flag?
|
|
589
|
+
flags = flag_options(tok, option)
|
|
590
|
+
return if flags.nil?
|
|
591
|
+
@command_line.consume(tok, takes_value: false)
|
|
592
|
+
flags.each { |f| f.block.call }
|
|
1129
593
|
else
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
594
|
+
value = @command_line.consume(tok, takes_value: true)
|
|
595
|
+
@command_line.with_current_option(tok) do
|
|
596
|
+
if tok.dot_path.nil?
|
|
597
|
+
option.assign_value(value, source: :cmdline)
|
|
598
|
+
else
|
|
599
|
+
# Fill a copy of the current value: the current value is not modified in place (`on_set` callback already received it),
|
|
600
|
+
# and the result is complete (e.g. `--opt.0=a --opt.1=b`): not merged again
|
|
601
|
+
current = copy_containers(option.value(log: false))
|
|
602
|
+
value = DotContainer.dotted_to_container(tok.dot_path, Parser.smart_convert(value), current)
|
|
603
|
+
option.assign_value(value, source: :cmdline, merge: false)
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# Copy nested `Hash` and `Array` containers, keep other values as-is (e.g. a `Proc` cannot be marshalled)
|
|
610
|
+
# @param value [Object] value to copy
|
|
611
|
+
# @return [Object] copy
|
|
612
|
+
def copy_containers(value)
|
|
613
|
+
case value
|
|
614
|
+
when Hash then value.transform_values { |v| copy_containers(v) }
|
|
615
|
+
when Array then value.map { |v| copy_containers(v) }
|
|
616
|
+
else value
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# Flags to execute for a flag token: `--help`, `-h`, or combined `-hN`
|
|
621
|
+
# @param tok [Option] flag token
|
|
622
|
+
# @param option [OptionValue] declared flag option
|
|
623
|
+
# @return [Array<OptionValue>, nil] flags, or `nil` if combined flags are not all declared yet
|
|
624
|
+
# @raise [BadArgument] if a value is given
|
|
625
|
+
def flag_options(tok, option)
|
|
626
|
+
if tok.short_char.nil?
|
|
627
|
+
Aspera.assert(!tok.inline? && tok.dot_path.nil?, type: BadArgument) { "Option #{self.class.option_name_to_line(option.option)} does not take a value" }
|
|
628
|
+
return [option]
|
|
1137
629
|
end
|
|
630
|
+
flags = [option, *tok.inline_value.to_s.chars.map { |c| @registry.by_short(c) }]
|
|
631
|
+
return if flags.any?(&:nil?)
|
|
632
|
+
Aspera.assert(flags.all?(&:flag?), type: BadArgument) { "Option -#{tok.short_char} does not take a value: #{tok.raw}" }
|
|
633
|
+
flags
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Resolve long option name: exact match, or unique abbreviation (recorded on token).
|
|
637
|
+
# @param tok [Option] long option token
|
|
638
|
+
# @return [OptionValue, nil] declared option, or `nil` if not declared yet
|
|
639
|
+
# @raise [BadArgument] if abbreviation is ambiguous
|
|
640
|
+
def resolve_long_name(tok)
|
|
641
|
+
# No abbreviation with dotted notation
|
|
642
|
+
option = @registry.by_long(tok.name, allow_abbreviation: tok.dot_path.nil?)
|
|
643
|
+
tok.abbreviation_of = option.option if !option.nil? && !option.option.to_s.eql?(tok.name)
|
|
644
|
+
option
|
|
1138
645
|
end
|
|
1139
646
|
|
|
1140
647
|
# Generate command line option string from option symbol
|
|
@@ -1146,72 +653,36 @@ module Aspera
|
|
|
1146
653
|
opt_val.nil? ? result : "#{result}#{Option::VALUE_SEP}#{opt_val}"
|
|
1147
654
|
end
|
|
1148
655
|
|
|
1149
|
-
#
|
|
1150
|
-
#
|
|
1151
|
-
# @param
|
|
1152
|
-
# @param
|
|
1153
|
-
# @
|
|
1154
|
-
def
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
end
|
|
1162
|
-
end
|
|
1163
|
-
|
|
1164
|
-
#
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
# @return [Hash{Symbol => Object}] pairs whose keys were not yet declared (deferred to next round)
|
|
1172
|
-
def consume_option_pairs(option_pairs, where)
|
|
1173
|
-
Log.log.trace1 { "consume_option_pairs: #{where}" }
|
|
1174
|
-
remaining = {}
|
|
1175
|
-
option_pairs.each do |k, v|
|
|
1176
|
-
if @declared_options.key?(k)
|
|
1177
|
-
set_option(k, v, where: where)
|
|
1178
|
-
else
|
|
1179
|
-
Log.log.trace1 { "unprocessed: #{k}: #{v}" }
|
|
1180
|
-
remaining[k] = v
|
|
656
|
+
# Keep value from preset or env until option is declared
|
|
657
|
+
# @param option_symbol [Symbol] option name
|
|
658
|
+
# @param value [Object] value
|
|
659
|
+
# @param source [Symbol] `OptionSource`
|
|
660
|
+
# @param replace [Boolean] replace a pending value from same source
|
|
661
|
+
def add_pending_value(option_symbol, value, source, replace:)
|
|
662
|
+
entries = (@pending_values[option_symbol] ||= [])
|
|
663
|
+
index = entries.index { |e| e[:source].eql?(source) }
|
|
664
|
+
if index.nil?
|
|
665
|
+
entries.push({value: value, source: source})
|
|
666
|
+
elsif replace
|
|
667
|
+
entries[index] = {value: value, source: source}
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# Apply pending values (presets, env) of declared options, lowest priority first
|
|
672
|
+
def apply_pending_values
|
|
673
|
+
ready = @pending_values.select { |k, _| @registry.declared?(k) }
|
|
674
|
+
ready.each_key { |k| @pending_values.delete(k) }
|
|
675
|
+
ready.each do |option_symbol, entries|
|
|
676
|
+
entries.sort_by { |e| OptionSource.priority(e[:source]) }.each do |e|
|
|
677
|
+
set_option(option_symbol, e[:value], source: e[:source])
|
|
1181
678
|
end
|
|
1182
679
|
end
|
|
1183
|
-
remaining
|
|
1184
|
-
end
|
|
1185
|
-
|
|
1186
|
-
# Consume the Argument token immediately following the current option in @argv_tokens.
|
|
1187
|
-
# After delete_at(@current_parse_idx), the next token is at @argv_tokens[@current_parse_idx].
|
|
1188
|
-
# @return [String, nil] the consumed argument value, or nil if the next token is not an Argument
|
|
1189
|
-
def shift_next_argument_token
|
|
1190
|
-
pos = @current_parse_idx || 0
|
|
1191
|
-
return unless @argv_tokens[pos]&.is_a?(Argument)
|
|
1192
|
-
|
|
1193
|
-
@argv_tokens.delete_at(pos).value
|
|
1194
|
-
end
|
|
1195
|
-
|
|
1196
|
-
# @return [Array<String>] values of all pending positional Argument tokens
|
|
1197
|
-
def pending_arguments
|
|
1198
|
-
@argv_tokens.grep(Argument).map(&:value)
|
|
1199
|
-
end
|
|
1200
|
-
|
|
1201
|
-
# @return [Array<String>] raw strings of all pending Option tokens
|
|
1202
|
-
def pending_options
|
|
1203
|
-
@argv_tokens.grep(Option).map(&:raw)
|
|
1204
680
|
end
|
|
1205
681
|
|
|
1206
|
-
# when this is alone, this stops option processing (same characters as Option::PREFIX but distinct semantics)
|
|
1207
|
-
OPTIONS_STOP = '--'
|
|
1208
|
-
SOURCE_USER = 'cmdline' # cspell:disable-line
|
|
1209
682
|
# Percent selector: select by this field for this value
|
|
1210
683
|
REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
|
|
1211
|
-
# Ask for schema of Extended value
|
|
1212
|
-
HELP = 'help'
|
|
1213
684
|
|
|
1214
|
-
private_constant :
|
|
685
|
+
private_constant :REGEX_LOOKUP_ID_BY_FIELD, :HELP_BORDER
|
|
1215
686
|
end
|
|
1216
687
|
end
|
|
1217
688
|
end
|