aspera-cli 4.27.0 → 4.27.2
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 +46 -1
- data/CONTRIBUTING.md +1 -7
- data/TODO.md +50 -0
- data/docs/README.md +709 -489
- data/lib/aspera/agent/base.rb +10 -2
- data/lib/aspera/agent/connect.rb +7 -7
- data/lib/aspera/agent/desktop.rb +3 -3
- data/lib/aspera/agent/direct.rb +48 -19
- data/lib/aspera/agent/factory.rb +3 -3
- data/lib/aspera/agent/httpgw.rb +55 -14
- data/lib/aspera/agent/node.rb +2 -2
- data/lib/aspera/agent/transferd.rb +12 -12
- data/lib/aspera/api/aoc.rb +28 -28
- data/lib/aspera/api/cos_node.rb +10 -9
- data/lib/aspera/api/faspex.rb +2 -2
- data/lib/aspera/api/httpgw.rb +20 -20
- data/lib/aspera/api/node.rb +31 -53
- data/lib/aspera/ascmd.rb +13 -13
- data/lib/aspera/ascp/installation.rb +43 -36
- data/lib/aspera/ascp/management.rb +6 -6
- data/lib/aspera/assert.rb +12 -13
- data/lib/aspera/cli/ascp_actions.rb +7 -7
- data/lib/aspera/cli/async_transfer_store.rb +49 -5
- data/lib/aspera/cli/bootstrapper.rb +9 -9
- data/lib/aspera/cli/command_registry.rb +1 -1
- data/lib/aspera/cli/command_spec.rb +6 -3
- data/lib/aspera/cli/context.rb +3 -3
- data/lib/aspera/cli/extended_value.rb +26 -26
- data/lib/aspera/cli/formatter.rb +76 -54
- data/lib/aspera/cli/gem_checker.rb +1 -1
- data/lib/aspera/cli/hints.rb +2 -2
- data/lib/aspera/cli/http.rb +22 -29
- data/lib/aspera/cli/mailer.rb +7 -7
- data/lib/aspera/cli/mcp_tool.rb +31 -4
- data/lib/aspera/cli/option_declarator.rb +77 -0
- data/lib/aspera/cli/options.schema.yaml +273 -3
- data/lib/aspera/cli/parser.rb +539 -280
- data/lib/aspera/cli/plugins/alee.rb +1 -1
- data/lib/aspera/cli/plugins/aoc.rb +303 -314
- data/lib/aspera/cli/plugins/ats.rb +25 -21
- data/lib/aspera/cli/plugins/base.rb +357 -186
- data/lib/aspera/cli/plugins/basic_auth.rb +0 -12
- data/lib/aspera/cli/plugins/config.rb +76 -73
- data/lib/aspera/cli/plugins/console.rb +8 -8
- data/lib/aspera/cli/plugins/cos.rb +4 -3
- data/lib/aspera/cli/plugins/factory.rb +2 -2
- data/lib/aspera/cli/plugins/faspex5.rb +259 -248
- data/lib/aspera/cli/plugins/faspio.rb +7 -2
- data/lib/aspera/cli/plugins/httpgw.rb +1 -1
- data/lib/aspera/cli/plugins/mcp.rb +11 -11
- data/lib/aspera/cli/plugins/node.rb +227 -283
- data/lib/aspera/cli/plugins/orchestrator.rb +23 -23
- data/lib/aspera/cli/plugins/preview.rb +61 -64
- data/lib/aspera/cli/plugins/server.rb +36 -31
- data/lib/aspera/cli/plugins/shares.rb +174 -135
- data/lib/aspera/cli/preset_actions.rb +39 -19
- data/lib/aspera/cli/preset_manager.rb +74 -32
- data/lib/aspera/cli/result.rb +18 -16
- data/lib/aspera/cli/runner.rb +75 -39
- data/lib/aspera/cli/secret_finder.rb +1 -1
- data/lib/aspera/cli/sync_actions.rb +36 -35
- data/lib/aspera/cli/terminal_formatter.rb +2 -2
- data/lib/aspera/cli/transfer_actions.rb +17 -10
- data/lib/aspera/cli/transfer_agent.rb +44 -44
- data/lib/aspera/cli/transfer_progress.rb +7 -7
- data/lib/aspera/cli/vault_manager.rb +34 -12
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +17 -20
- data/lib/aspera/colors.rb +3 -3
- data/lib/aspera/command_line_builder.rb +17 -16
- data/lib/aspera/command_line_converter.rb +1 -1
- data/lib/aspera/coverage.rb +1 -2
- data/lib/aspera/data_repository.rb +1 -1
- data/lib/aspera/dot_container.rb +8 -8
- data/lib/aspera/environment.rb +13 -13
- data/lib/aspera/faspex_gw.rb +4 -4
- data/lib/aspera/faspex_postproc.rb +14 -9
- data/lib/aspera/graphql.rb +4 -2
- data/lib/aspera/hash_ext.rb +2 -2
- data/lib/aspera/json_rpc/client.rb +1 -1
- data/lib/aspera/keychain/base.rb +10 -3
- data/lib/aspera/keychain/encrypted_hash.rb +92 -17
- data/lib/aspera/keychain/factory.rb +17 -5
- data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +23 -25
- data/lib/aspera/keychain/one_password_api.rb +86 -0
- data/lib/aspera/keychain/one_password_base.rb +34 -0
- data/lib/aspera/keychain/one_password_cli.rb +98 -0
- data/lib/aspera/link_header.rb +2 -2
- data/lib/aspera/log.rb +13 -13
- data/lib/aspera/markdown.rb +5 -5
- data/lib/aspera/nagios.rb +8 -8
- data/lib/aspera/node_simulator.rb +33 -27
- data/lib/aspera/oauth/base.rb +4 -5
- data/lib/aspera/oauth/boot.rb +3 -3
- data/lib/aspera/oauth/factory.rb +5 -5
- data/lib/aspera/oauth/jwt.rb +5 -7
- data/lib/aspera/oauth/web.rb +4 -4
- data/lib/aspera/persistency_action_once.rb +4 -4
- data/lib/aspera/persistency_folder.rb +8 -8
- data/lib/aspera/preview/file_types.rb +4 -4
- data/lib/aspera/preview/generator.rb +49 -46
- data/lib/aspera/preview/options.rb +3 -2
- data/lib/aspera/preview/terminal.rb +5 -5
- data/lib/aspera/preview/utils.rb +65 -18
- data/lib/aspera/products/connect.rb +2 -2
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/other.rb +2 -2
- data/lib/aspera/products/transferd.rb +3 -3
- data/lib/aspera/proxy_auto_config.rb +10 -10
- data/lib/aspera/rest.rb +149 -115
- data/lib/aspera/rest_error_analyzer.rb +3 -3
- data/lib/aspera/rest_list.rb +10 -9
- data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +39 -0
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
- data/lib/aspera/schema/documentation.rb +17 -11
- data/lib/aspera/schema/reader.rb +78 -4
- data/lib/aspera/schema/registry.rb +26 -7
- data/lib/aspera/secret_hider.rb +5 -5
- data/lib/aspera/ssh.rb +64 -31
- data/lib/aspera/ssl.rb +3 -3
- data/lib/aspera/sync/database.rb +2 -2
- data/lib/aspera/sync/operations.rb +13 -13
- data/lib/aspera/temp_file_manager.rb +24 -4
- data/lib/aspera/transfer/faux_file.rb +2 -2
- data/lib/aspera/transfer/parameters.rb +15 -15
- data/lib/aspera/transfer/resumer.rb +9 -9
- data/lib/aspera/transfer/spec.rb +1 -1
- data/lib/aspera/transfer/spec.schema.yaml +1 -0
- data/lib/aspera/uri_reader.rb +52 -15
- data/lib/aspera/web_auth.rb +6 -5
- data/lib/aspera/web_server_simple.rb +9 -9
- data/lib/aspera/yaml.rb +4 -4
- data.tar.gz.sig +0 -0
- metadata +10 -4
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/plugins/faspex.rb +0 -576
- data/lib/aspera/transfer/uri.rb +0 -56
data/lib/aspera/cli/parser.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'aspera/cli/preset_manager'
|
|
3
4
|
require 'aspera/cli/extended_value'
|
|
4
5
|
require 'aspera/cli/error'
|
|
6
|
+
require 'aspera/cli/special_values'
|
|
5
7
|
require 'aspera/cli/terminal_formatter'
|
|
6
8
|
require 'aspera/colors'
|
|
7
9
|
require 'aspera/secret_hider'
|
|
@@ -43,15 +45,12 @@ module Aspera
|
|
|
43
45
|
SYMBOLS = [NO_SYM, YES_SYM].freeze
|
|
44
46
|
# @return [Boolean] `true` if value is a value for `true` in ALL
|
|
45
47
|
def true?(enum)
|
|
46
|
-
Aspera.assert_values(enum, ALL){'boolean'}
|
|
48
|
+
Aspera.assert_values(enum, ALL) { 'boolean' }
|
|
47
49
|
TRUE_VALUES.include?(enum)
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
# @return [:yes, :no]
|
|
51
|
-
def to_sym(enum)
|
|
52
|
-
Aspera.assert_values(enum, ALL){'boolean'}
|
|
53
|
-
TRUE_VALUES.include?(enum) ? YES_SYM : NO_SYM
|
|
54
|
-
end
|
|
53
|
+
def to_sym(enum) = true?(enum) ? YES_SYM : NO_SYM
|
|
55
54
|
|
|
56
55
|
# @return [Boolean] `true` if value is a value for `true` or `false` in ALL
|
|
57
56
|
def symbol?(sym)
|
|
@@ -60,25 +59,26 @@ module Aspera
|
|
|
60
59
|
module_function :true?, :to_sym, :symbol?
|
|
61
60
|
end
|
|
62
61
|
|
|
63
|
-
#
|
|
64
|
-
# Public API:
|
|
62
|
+
# Type specifiers for the `allowed:` parameter of option declarations.
|
|
63
|
+
# Public API: STRING_ARRAY, SYMBOL_ARRAY, INTEGER, BOOLEAN, NONE.
|
|
65
64
|
# Internal (do not pass as `allowed:`):
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
module
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
78
|
# Internal: derived when allowed: is an Array<Symbol>; do not pass directly
|
|
79
|
-
|
|
79
|
+
ENUM = [Symbol].freeze
|
|
80
80
|
# Internal: implicit default (String); equivalent to omitting allowed: entirely
|
|
81
|
-
|
|
81
|
+
STRING = [String].freeze
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
# Description of option, how to manage
|
|
@@ -103,8 +103,8 @@ module Aspera
|
|
|
103
103
|
# - `Class` The single allowed Class
|
|
104
104
|
# - `Array<Class>` Multiple allowed classes
|
|
105
105
|
# - `Array<Symbol>` List of allowed values
|
|
106
|
-
def initialize(option:, description: nil, allowed:
|
|
107
|
-
Log.log.trace1{"option: #{option}, allowed: #{allowed}"}
|
|
106
|
+
def initialize(option:, description: nil, allowed: Type::STRING, handler: nil, deprecation: nil, schema: nil)
|
|
107
|
+
Log.log.trace1 { "option: #{option}, allowed: #{allowed}" }
|
|
108
108
|
@option = option
|
|
109
109
|
@description = description
|
|
110
110
|
@group = nil
|
|
@@ -121,51 +121,83 @@ module Aspera
|
|
|
121
121
|
bind_handler(handler) unless handler.nil?
|
|
122
122
|
@types = nil
|
|
123
123
|
@values = nil
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
133
146
|
end
|
|
134
|
-
if
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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)
|
|
157
183
|
end
|
|
184
|
+
elsif allowed.all?(Symbol)
|
|
185
|
+
@types = Type::ENUM
|
|
186
|
+
@values = allowed
|
|
187
|
+
else
|
|
188
|
+
Aspera.error_unexpected_value(allowed)
|
|
158
189
|
end
|
|
159
|
-
Log.log.trace1{"declare: #{@option}: #{@access} #{@object.class}.#{@read_method}".green}
|
|
160
190
|
end
|
|
161
191
|
|
|
192
|
+
public
|
|
193
|
+
|
|
162
194
|
# Wire (or re-wire) the getter/setter delegation for this option.
|
|
163
195
|
# Safe to call after construction - used by Parser#set_handler to bind a composed
|
|
164
196
|
# instance variable that did not exist at class-load time (Category C handlers).
|
|
165
197
|
# @param handler [Hash] Accessor hash with keys :o (object) and :m (method symbol)
|
|
166
198
|
# @return [nil]
|
|
167
199
|
def bind_handler(handler)
|
|
168
|
-
Aspera.assert_type(handler, Hash){'handler'}
|
|
200
|
+
Aspera.assert_type(handler, Hash) { 'handler' }
|
|
169
201
|
# Capture any value already stored locally before switching to delegated storage.
|
|
170
202
|
# This transfers defaults (and any preset values already applied) to the new target.
|
|
171
203
|
pending_value = @access.eql?(:local) ? @object : nil
|
|
@@ -173,12 +205,12 @@ module Aspera
|
|
|
173
205
|
@read_method = handler[:m]
|
|
174
206
|
@write_method = "#{@read_method}=".to_sym
|
|
175
207
|
@access = if @object.respond_to?(@write_method)
|
|
176
|
-
:
|
|
208
|
+
:attr # delegation via attr_accessor-style m / m=
|
|
177
209
|
else
|
|
178
|
-
:
|
|
210
|
+
:custom # delegation via generic 3-arg method m(sym, :get/:set, val)
|
|
179
211
|
end
|
|
180
|
-
Aspera.assert(@object.respond_to?(@read_method)){"#{@object} does not respond to #{@read_method}"}
|
|
181
|
-
Log.log.trace1{"bind_handler: #{@option}: #{@access} #{@object.class}.#{@read_method}".green}
|
|
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 }
|
|
182
214
|
# Push the pending local value to the new target if one was stored
|
|
183
215
|
assign_value(pending_value, where: 'bind_handler', warn_deprecation: false) unless pending_value.nil?
|
|
184
216
|
nil
|
|
@@ -193,18 +225,23 @@ module Aspera
|
|
|
193
225
|
first_line.end_with?('.') ? first_line[0..-2] : first_line
|
|
194
226
|
end
|
|
195
227
|
|
|
228
|
+
# Reset stored value to nil
|
|
229
|
+
# @return [nil]
|
|
196
230
|
def clear
|
|
197
231
|
@object = nil
|
|
198
232
|
end
|
|
199
233
|
|
|
234
|
+
# Get current option value
|
|
235
|
+
# @param log [Boolean] whether to log the value retrieval
|
|
236
|
+
# @return [Object] current value
|
|
200
237
|
def value(log: true)
|
|
201
238
|
current_value =
|
|
202
239
|
case @access
|
|
203
|
-
when :local
|
|
204
|
-
when :
|
|
205
|
-
when :
|
|
240
|
+
when :local then @object
|
|
241
|
+
when :attr then @object.send(@read_method)
|
|
242
|
+
when :custom then @object.send(@read_method, @option, :get)
|
|
206
243
|
end
|
|
207
|
-
Log.log.trace1{"#{@option} -> (#{current_value.class})#{current_value}"} if log
|
|
244
|
+
Log.log.trace1 { "#{@option} -> (#{current_value.class})#{current_value}" } if log
|
|
208
245
|
current_value
|
|
209
246
|
end
|
|
210
247
|
|
|
@@ -215,25 +252,25 @@ module Aspera
|
|
|
215
252
|
# @param warn_deprecation [Boolean] Emit deprecation warning (false for internal transfers)
|
|
216
253
|
# @return [nil]
|
|
217
254
|
def assign_value(value, where:, warn_deprecation: true)
|
|
218
|
-
Aspera.assert(!@deprecation, type: :warn){"Option #{@option} is deprecated: #{@deprecation}"} if warn_deprecation
|
|
255
|
+
Aspera.assert(!@deprecation, type: :warn) { "Option #{@option} is deprecated: #{@deprecation}" } if warn_deprecation
|
|
219
256
|
new_value = ExtendedValue.instance.evaluate(value, context: "option: #{@option}", allowed: @types)
|
|
220
|
-
Log.log.trace1{"#{where}: #{@option} <- (#{new_value.class})#{new_value}"}
|
|
257
|
+
Log.log.trace1 { "#{where}: #{@option} <- (#{new_value.class})#{new_value}" }
|
|
221
258
|
# Per-type coercion: String input from CLI/env/preset is normalized to the expected type.
|
|
222
259
|
# Centralized here so all sources (CLI dispatch, preset, env) go through the same path.
|
|
223
260
|
case @types
|
|
224
|
-
when
|
|
261
|
+
when Type::ENUM
|
|
225
262
|
new_value = Parser.get_from_list(new_value, @option, @values) if new_value.is_a?(String)
|
|
226
|
-
when
|
|
263
|
+
when Type::BOOLEAN
|
|
227
264
|
new_value = Parser.get_from_list(new_value, @option, BoolValue::ALL) if new_value.is_a?(String)
|
|
228
265
|
new_value = BoolValue.true?(new_value)
|
|
229
|
-
when
|
|
266
|
+
when Type::INTEGER
|
|
230
267
|
new_value = Integer(new_value)
|
|
231
|
-
when
|
|
268
|
+
when Type::STRING_ARRAY
|
|
232
269
|
new_value = [new_value] if new_value.is_a?(String)
|
|
233
|
-
when
|
|
270
|
+
when Type::SYMBOL_ARRAY
|
|
234
271
|
new_value = [new_value] if new_value.is_a?(String)
|
|
235
272
|
Aspera.assert_array_all(new_value, String, type: BadArgument)
|
|
236
|
-
new_value = new_value.map{ |v| Parser.get_from_list(v, @option, @values)}
|
|
273
|
+
new_value = new_value.map { |v| Parser.get_from_list(v, @option, @values) }
|
|
237
274
|
else
|
|
238
275
|
# nil (setting nil on a Hash/Array option resets to empty container)
|
|
239
276
|
new_value = {} if new_value.nil? && @types&.first.eql?(Hash)
|
|
@@ -241,42 +278,144 @@ module Aspera
|
|
|
241
278
|
end
|
|
242
279
|
# Skip type validation for the special 'help' value on Hash options: store it as-is
|
|
243
280
|
# so that get_option(schema:) can raise SchemaRequest with the contextual schema later.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
when :setter then @object.send(@read_method, @option, :set, new_value)
|
|
249
|
-
end
|
|
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)
|
|
250
285
|
return
|
|
251
286
|
end
|
|
252
|
-
Aspera.assert_type(new_value, *@types, type: BadArgument){"Option #{@option}"} if @types
|
|
287
|
+
Aspera.assert_type(new_value, *@types, type: BadArgument) { "Option #{@option}" } if @types
|
|
253
288
|
if new_value.is_a?(Hash) || new_value.is_a?(Array)
|
|
254
289
|
current_value = value(log: false)
|
|
255
290
|
new_value = current_value.deep_merge(new_value) if new_value.is_a?(Hash) && current_value.is_a?(Hash) && !current_value.empty?
|
|
256
291
|
new_value = current_value + new_value if new_value.is_a?(Array) && current_value.is_a?(Array) && !current_value.empty?
|
|
257
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)
|
|
258
304
|
case @access
|
|
259
|
-
when :local
|
|
260
|
-
when :
|
|
261
|
-
when :
|
|
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)
|
|
262
308
|
end
|
|
263
|
-
Log.log.trace1{v = value(log: false); "#{@option} <- (#{v.class})#{v}"} # rubocop:disable Style/Semicolon
|
|
264
|
-
nil
|
|
265
309
|
end
|
|
266
310
|
end
|
|
267
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
|
+
|
|
268
403
|
# parse command line options
|
|
269
404
|
# arguments options start with '-', others are commands
|
|
270
405
|
# resolves on extended value syntax
|
|
271
406
|
class Parser
|
|
272
407
|
class << self
|
|
273
408
|
# Find shortened string value in allowed symbol list
|
|
409
|
+
# @param short_value [String] value or prefix to find
|
|
410
|
+
# @param descr [String] description for error messages
|
|
411
|
+
# @param allowed_values [Array] list of allowed values
|
|
412
|
+
# @return [Symbol, Boolean] matched symbol or boolean value
|
|
274
413
|
def get_from_list(short_value, descr, allowed_values)
|
|
275
414
|
Aspera.assert_type(short_value, String)
|
|
276
415
|
# we accept shortcuts
|
|
277
|
-
matching_exact = allowed_values.select{ |i| i.to_s.eql?(short_value)}
|
|
416
|
+
matching_exact = allowed_values.select { |i| i.to_s.eql?(short_value) }
|
|
278
417
|
return matching_exact.first if matching_exact.length == 1
|
|
279
|
-
matching = allowed_values.select{ |i| i.to_s.start_with?(short_value)}
|
|
418
|
+
matching = allowed_values.select { |i| i.to_s.start_with?(short_value) }
|
|
280
419
|
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)
|
|
281
420
|
Aspera.assert(!matching.empty?, multi_choice_assert_msg("unknown value for #{descr}: #{short_value}", allowed_values), type: BadArgument)
|
|
282
421
|
Aspera.assert(matching.length.eql?(1), multi_choice_assert_msg("ambiguous shortcut for #{descr}: #{short_value}", matching), type: BadArgument)
|
|
@@ -288,28 +427,42 @@ module Aspera
|
|
|
288
427
|
# @return [Object, nil] the matching key, or nil if none or ambiguous
|
|
289
428
|
def match_prefix(short_value, allowed_values)
|
|
290
429
|
return short_value if allowed_values.include?(short_value)
|
|
291
|
-
matches = allowed_values.select{ |k| k.to_s.start_with?(short_value.to_s)}
|
|
430
|
+
matches = allowed_values.select { |k| k.to_s.start_with?(short_value.to_s) }
|
|
292
431
|
matches.length == 1 ? matches.first : nil
|
|
293
432
|
end
|
|
294
433
|
|
|
295
434
|
# Generates error message with list of allowed values
|
|
296
|
-
# @param error_msg
|
|
435
|
+
# @param error_msg [String] Error message
|
|
297
436
|
# @param accept_list [Array<Symbol>] List of allowed values
|
|
298
|
-
|
|
299
|
-
|
|
437
|
+
# @param aliases [Hash{Symbol=>Symbol}, nil] alias→id map; used to annotate entries with their aliases
|
|
438
|
+
def multi_choice_assert_msg(error_msg, accept_list, aliases: nil)
|
|
439
|
+
# Build reverse map: id → [alias, ...] for annotation
|
|
440
|
+
reverse = aliases&.each_with_object({}) do |(ali, id), h|
|
|
441
|
+
(h[id] ||= []) << ali
|
|
442
|
+
end
|
|
443
|
+
lines = accept_list.map do |choice|
|
|
444
|
+
suffix = reverse&.key?(choice) ? " (alias: #{reverse[choice].join(', ')})" : ''
|
|
445
|
+
"- #{choice}#{suffix}"
|
|
446
|
+
end
|
|
447
|
+
[error_msg, 'Use:', *lines.sort].join("\n")
|
|
300
448
|
end
|
|
301
449
|
|
|
302
450
|
# Change option name with dash to name with underscore
|
|
303
451
|
# @param name [String] option name with dash separators
|
|
304
452
|
# @return [String] option name with underscore separators
|
|
305
453
|
def option_line_to_name(name)
|
|
306
|
-
name.gsub(
|
|
454
|
+
name.gsub(Option::NAME_SEP_LINE, Option::NAME_SEP_SYMBOL)
|
|
307
455
|
end
|
|
308
456
|
|
|
457
|
+
# Convert option symbol to CLI line flag format
|
|
458
|
+
# @param name [Symbol, String] option name
|
|
459
|
+
# @return [String] option flag (e.g. "--option-name")
|
|
309
460
|
def option_name_to_line(name)
|
|
310
|
-
"#{
|
|
461
|
+
"#{Option::PREFIX}#{name.to_s.gsub(Option::NAME_SEP_SYMBOL, Option::NAME_SEP_LINE)}"
|
|
311
462
|
end
|
|
312
463
|
|
|
464
|
+
# Parse percent-selector string into field name and value (extended value is parsed in value)
|
|
465
|
+
# @param identifier [String] identifier to parse
|
|
313
466
|
# @return [Hash{Symbol => String}, nil] `{field:,value:}` if identifier is a percent selector, else `nil`
|
|
314
467
|
def percent_selector(identifier)
|
|
315
468
|
Aspera.assert_type(identifier, String)
|
|
@@ -318,6 +471,20 @@ module Aspera
|
|
|
318
471
|
end
|
|
319
472
|
nil
|
|
320
473
|
end
|
|
474
|
+
|
|
475
|
+
# Using dotted hash notation, convert value to bool, int, float or extended value
|
|
476
|
+
# @param value [String] The value to convert to appropriate type
|
|
477
|
+
# @return [Boolean, Integer, Float, String, Array, Hash] the converted value
|
|
478
|
+
def smart_convert(value)
|
|
479
|
+
case value
|
|
480
|
+
when 'true' then true
|
|
481
|
+
when 'false' then false
|
|
482
|
+
else
|
|
483
|
+
Integer(value, exception: false) ||
|
|
484
|
+
Float(value, exception: false) ||
|
|
485
|
+
ExtendedValue.instance.evaluate(value, context: 'dotted expression')
|
|
486
|
+
end
|
|
487
|
+
end
|
|
321
488
|
end
|
|
322
489
|
|
|
323
490
|
attr_accessor :ask_missing_mandatory, :ask_missing_optional, :help_requested
|
|
@@ -337,6 +504,8 @@ module Aspera
|
|
|
337
504
|
@fail_on_missing_mandatory = true
|
|
338
505
|
# set to true when --help / -h is parsed
|
|
339
506
|
@help_requested = false
|
|
507
|
+
# options explicitly reset to nil from CLI (e.g. --opt=@none:); preset injection skips these
|
|
508
|
+
@explicitly_cleared = {}
|
|
340
509
|
# options can also be provided by env vars : --param-name -> ASCLI_PARAM_NAME
|
|
341
510
|
@option_pairs_batch = {}
|
|
342
511
|
@option_pairs_env = {}
|
|
@@ -344,48 +513,41 @@ module Aspera
|
|
|
344
513
|
@short_options = {}
|
|
345
514
|
# Current help section group name, set by #group
|
|
346
515
|
@current_group = 'global'
|
|
347
|
-
env_prefix = program_name.upcase +
|
|
516
|
+
env_prefix = program_name.upcase + Option::NAME_SEP_SYMBOL
|
|
348
517
|
ENV.each do |k, v|
|
|
349
518
|
@option_pairs_env[k.delete_prefix(env_prefix).downcase.to_sym] = v if k.start_with?(env_prefix)
|
|
350
519
|
end
|
|
351
|
-
Log.
|
|
352
|
-
#
|
|
353
|
-
|
|
354
|
-
#
|
|
355
|
-
@
|
|
356
|
-
#
|
|
357
|
-
@
|
|
358
|
-
#
|
|
359
|
-
|
|
360
|
-
# @
|
|
361
|
-
@
|
|
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
|
|
362
531
|
return if argv.nil?
|
|
363
532
|
# true until `--` is found (stop options)
|
|
364
533
|
process_options = true
|
|
365
|
-
arg_count = 0
|
|
366
534
|
argv.each do |value|
|
|
367
535
|
if process_options && value.start_with?('-')
|
|
368
|
-
Log.log.trace1{"opt: #{value}"}
|
|
536
|
+
Log.log.trace1 { "opt: #{value}" }
|
|
369
537
|
if value.eql?(OPTIONS_STOP)
|
|
370
538
|
process_options = false
|
|
371
539
|
else
|
|
372
|
-
@
|
|
373
|
-
(@args_before_option[value] ||= []).push(arg_count)
|
|
540
|
+
@argv_tokens.push(value.start_with?(Option::PREFIX) ? Option.from_long(value) : Option.from_short(value))
|
|
374
541
|
end
|
|
375
542
|
else
|
|
376
|
-
Log.log.trace1{"arg: #{value}"}
|
|
377
|
-
@
|
|
378
|
-
arg_count += 1
|
|
543
|
+
Log.log.trace1 { "arg: #{value}" }
|
|
544
|
+
@argv_tokens.push(Argument.new(value))
|
|
379
545
|
end
|
|
380
546
|
end
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
@initial_cli_options = @unprocessed_cmd_line_options.dup.freeze
|
|
386
|
-
Log.log.trace1{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
|
|
387
|
-
declare(:interactive, description: 'Use interactive input of missing params', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :ask_missing_mandatory})
|
|
388
|
-
declare(:ask_options, description: 'Ask even optional options', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :ask_missing_optional})
|
|
547
|
+
@initial_argv_tokens = @argv_tokens.dup.freeze
|
|
548
|
+
Log.log.trace1 { "add_cmd_line_options:arguments=#{pending_arguments},options=#{pending_options}".red }
|
|
549
|
+
declare(:interactive, description: 'Use interactive input of missing params', allowed: Type::BOOLEAN, handler: {o: self, m: :ask_missing_mandatory})
|
|
550
|
+
declare(:ask_options, description: 'Ask even optional options', allowed: Type::BOOLEAN, handler: {o: self, m: :ask_missing_optional})
|
|
389
551
|
# do not parse options yet, let's wait for option `-h` to be overridden
|
|
390
552
|
end
|
|
391
553
|
|
|
@@ -393,7 +555,7 @@ module Aspera
|
|
|
393
555
|
# @param types [Array<Class>] types to add
|
|
394
556
|
# @return [String] Types if relevant
|
|
395
557
|
def add_types_info(types)
|
|
396
|
-
return '' if !types || types.empty? || types.eql?(
|
|
558
|
+
return '' if !types || types.empty? || types.eql?(Type::ENUM) || types.eql?(Type::BOOLEAN) || types.eql?(Type::STRING)
|
|
397
559
|
" (#{types.map(&:name).join(', ')})"
|
|
398
560
|
end
|
|
399
561
|
|
|
@@ -401,15 +563,20 @@ module Aspera
|
|
|
401
563
|
# @param option_symbol [Symbol] option name
|
|
402
564
|
# @param description [String, nil] description for help; if nil, derived from schema
|
|
403
565
|
# @param short [String] short option name
|
|
404
|
-
# @param allowed [Object] Allowed values, see `OptionValue
|
|
566
|
+
# @param allowed [Object] Allowed values, see `OptionValue`.
|
|
567
|
+
# When `schema:` is provided:
|
|
568
|
+
# - Omit `allowed:` when the schema has a single type (`type: object/array`): it is inferred automatically.
|
|
569
|
+
# - Omit `allowed:` when the schema uses `oneOf`/`anyOf` and all branches are `object`: `Hash` is inferred.
|
|
570
|
+
# - Use `allowed: [Hash, String]` when the option additionally accepts a plain String shorthand;
|
|
571
|
+
# the schema then documents the Hash form and `=help` still shows it.
|
|
405
572
|
# @param default [Object] default value
|
|
406
573
|
# @param handler [Hash] handler for option value: keys: :o(object) and :m(method)
|
|
407
574
|
# @param deprecation [String] deprecation
|
|
408
|
-
# @param schema [String]
|
|
575
|
+
# @param schema [String] schema path documenting the Hash form of this option
|
|
409
576
|
# @param block [Proc] Block to execute when option is found
|
|
410
577
|
def declare(option_symbol, description: nil, short: nil, allowed: nil, default: nil, handler: nil, deprecation: nil, schema: nil, &block)
|
|
411
578
|
Aspera.assert_type(option_symbol, Symbol)
|
|
412
|
-
Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
|
|
579
|
+
Aspera.assert(!@declared_options.key?(option_symbol)) { "#{option_symbol} already declared" }
|
|
413
580
|
Aspera.assert_type(handler, Hash) if handler
|
|
414
581
|
Aspera.assert(handler.keys.sort.eql?(%i[m o]), 'handler must have keys :m and :o') if handler
|
|
415
582
|
option_attrs = @declared_options[option_symbol] = OptionValue.new(
|
|
@@ -422,21 +589,21 @@ module Aspera
|
|
|
422
589
|
)
|
|
423
590
|
option_attrs.group = @current_group
|
|
424
591
|
description = option_attrs.description
|
|
425
|
-
Aspera.assert(!description.nil?){"#{option_symbol}: no description and no schema to derive one from"}
|
|
426
|
-
Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
|
|
427
|
-
Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with an uppercase"}
|
|
428
|
-
Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :allowed instead of hash/extended value in option description"}
|
|
429
|
-
set_option(option_symbol, default, where: 'default') unless default.nil?
|
|
592
|
+
Aspera.assert(!description.nil?) { "#{option_symbol}: no description and no schema to derive one from" }
|
|
593
|
+
Aspera.assert(description[-1] != '.') { "#{option_symbol} ends with dot" }
|
|
594
|
+
Aspera.assert(description[0] == description[0].upcase) { "#{option_symbol} description does not start with an uppercase" }
|
|
595
|
+
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, where: 'default', warn_deprecation: false) unless default.nil?
|
|
430
597
|
case option_attrs.types
|
|
431
|
-
when
|
|
598
|
+
when Type::ENUM, Type::BOOLEAN
|
|
432
599
|
# This option value must be a symbol (or array of symbols)
|
|
433
|
-
set_option(option_symbol, BoolValue.true?(default), where: 'default') if option_attrs.values.eql?(BoolValue::ALL) && !default.nil?
|
|
434
|
-
when
|
|
435
|
-
Aspera.assert_type(block, Proc){"missing execution block for #{option_symbol}"}
|
|
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}" }
|
|
436
603
|
option_attrs.block = block
|
|
437
604
|
end
|
|
438
605
|
@short_options[short] = option_symbol unless short.nil?
|
|
439
|
-
Log.log.trace1{"declare: #{option_symbol}, group: #{@current_group}, short: #{short}"}
|
|
606
|
+
Log.log.trace1 { "declare: #{option_symbol}, group: #{@current_group}, short: #{short}" }
|
|
440
607
|
end
|
|
441
608
|
|
|
442
609
|
# Set the current help section group name for subsequent declarations
|
|
@@ -450,7 +617,7 @@ module Aspera
|
|
|
450
617
|
# declares its options before its group name is known (e.g. Plugins::Config).
|
|
451
618
|
# @param name [String] new group name
|
|
452
619
|
def rename_current_group(name)
|
|
453
|
-
@declared_options.each_value{ |opt| opt.group = name if opt.group.eql?(@current_group)}
|
|
620
|
+
@declared_options.each_value { |opt| opt.group = name if opt.group.eql?(@current_group) }
|
|
454
621
|
@current_group = name
|
|
455
622
|
end
|
|
456
623
|
|
|
@@ -466,59 +633,41 @@ module Aspera
|
|
|
466
633
|
# @param aliases [Hash] map of aliases: key = alias, value = real value
|
|
467
634
|
# @param default [Object] default value
|
|
468
635
|
# @return [Object, Array, nil] one value, list or nil (if optional and no default)
|
|
469
|
-
def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation:
|
|
636
|
+
def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: Type::STRING, aliases: nil, default: nil, schema: nil)
|
|
470
637
|
Aspera.assert_array_all(accept_list, Symbol) unless accept_list.nil?
|
|
471
638
|
Aspera.assert_hash_all(aliases, Symbol, Symbol) unless aliases.nil?
|
|
472
639
|
validation = Symbol unless accept_list.nil?
|
|
473
640
|
validation = [validation] unless validation.is_a?(Array) || validation.nil?
|
|
474
|
-
Aspera.assert_array_all(validation, Class){'validation'} unless validation.nil?
|
|
641
|
+
Aspera.assert_array_all(validation, Class) { 'validation' } unless validation.nil?
|
|
475
642
|
descr = "#{descr}#{add_types_info(validation)}"
|
|
476
643
|
result =
|
|
477
|
-
if
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
values = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length)
|
|
481
|
-
when false
|
|
482
|
-
values = [@unprocessed_cmd_line_arguments.shift]
|
|
483
|
-
when String
|
|
484
|
-
index = @unprocessed_cmd_line_arguments.index(multiple)
|
|
485
|
-
if index
|
|
486
|
-
values = @unprocessed_cmd_line_arguments.shift(index)
|
|
487
|
-
@unprocessed_cmd_line_arguments.shift # remove end marker
|
|
488
|
-
else
|
|
489
|
-
values = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length)
|
|
490
|
-
end
|
|
491
|
-
else Aspera.error_unexpected_value(multiple){'multiple'}
|
|
492
|
-
end
|
|
493
|
-
values = values.map{ |v| ExtendedValue.instance.evaluate(v, context: "argument: #{descr}", allowed: validation)}
|
|
644
|
+
if !pending_arguments.empty?
|
|
645
|
+
values = extract_argument_tokens(multiple)
|
|
646
|
+
values = values.map { |v| ExtendedValue.instance.evaluate(v, context: "argument: #{descr}", allowed: validation) }
|
|
494
647
|
# If expecting list and only one arg of type array : it is the list
|
|
495
648
|
values = values.first if multiple && values.length.eql?(1) && values.first.is_a?(Array)
|
|
496
649
|
if accept_list
|
|
497
650
|
allowed_values = [].concat(accept_list)
|
|
498
651
|
allowed_values.concat(aliases.keys) unless aliases.nil?
|
|
499
|
-
values = values.map{ |v| self.class.get_from_list(v, descr, allowed_values)}
|
|
652
|
+
values = values.map { |v| self.class.get_from_list(v, descr, allowed_values) }
|
|
500
653
|
end
|
|
501
654
|
multiple ? values : values.first
|
|
502
655
|
elsif !default.nil? then default
|
|
503
|
-
|
|
504
|
-
elsif mandatory then get_interactive(descr, multiple: multiple, accept_list: accept_list, schema: schema)
|
|
656
|
+
elsif mandatory then get_interactive(descr, multiple: multiple, accept_list: accept_list, aliases: aliases, schema: schema)
|
|
505
657
|
end
|
|
506
|
-
|
|
658
|
+
Log.log.trace1 { "#{descr}=#{result}" }
|
|
659
|
+
result = aliases[result] if aliases&.key?(result)
|
|
660
|
+
# if value comes from JSON/YAML, it may come as Integer
|
|
661
|
+
result = result.to_s if result.is_a?(Integer) && validation&.eql?(Type::STRING)
|
|
662
|
+
# Integer coercion: a String argument for an INTEGER option must be parseable
|
|
663
|
+
if result.is_a?(String) && validation&.eql?(Type::INTEGER)
|
|
507
664
|
int_result = Integer(result, exception: false)
|
|
508
665
|
raise Cli::BadArgument, "Invalid integer: #{result}" if int_result.nil?
|
|
509
666
|
result = int_result
|
|
510
667
|
end
|
|
511
|
-
Log.log.trace1{"#{descr}=#{result}"}
|
|
512
|
-
result = aliases[result] if aliases&.key?(result)
|
|
513
|
-
# if value comes from JSON/YAML, it may come as Integer
|
|
514
|
-
result = result.to_s if result.is_a?(Integer) && validation&.eql?(Allowed::TYPES_STRING)
|
|
515
668
|
if validation && (mandatory || !result.nil?)
|
|
516
669
|
value_list = multiple ? result : [result]
|
|
517
|
-
value_list.each
|
|
518
|
-
raise SchemaRequest.new(:argument, descr, schema) if validation.include?(Hash) && value.eql?(HELP)
|
|
519
|
-
raise Cli::BadArgument,
|
|
520
|
-
"Argument #{descr} is a #{value.class} but must be #{'one of: ' if validation.length > 1}#{validation.map(&:name).join(', ')}" unless validation.any?{ |t| value.is_a?(t)}
|
|
521
|
-
end
|
|
670
|
+
value_list.each { |value| validate_argument(value, validation: validation, descr: descr, schema: schema) }
|
|
522
671
|
end
|
|
523
672
|
result
|
|
524
673
|
end
|
|
@@ -535,12 +684,16 @@ module Aspera
|
|
|
535
684
|
res_id = get_next_argument(description, multiple: get_option(:bulk))
|
|
536
685
|
# Can be an Array
|
|
537
686
|
if res_id.is_a?(String) && (m = Parser.percent_selector(res_id))
|
|
538
|
-
Aspera.assert(block_given?, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
|
|
687
|
+
Aspera.assert(block_given?, type: Cli::BadArgument) { "Percent syntax for #{description} not supported in this context" }
|
|
539
688
|
res_id = yield(m[:field], m[:value])
|
|
540
689
|
end
|
|
541
690
|
res_id
|
|
542
691
|
end
|
|
543
692
|
|
|
693
|
+
# Get next positional command argument from accepted list
|
|
694
|
+
# @param command_list [Array<Symbol>] accepted command names
|
|
695
|
+
# @param aliases [Hash, nil] command aliases
|
|
696
|
+
# @return [Symbol] selected command
|
|
544
697
|
def get_next_command(command_list, aliases: nil); get_next_argument('command', accept_list: command_list, aliases: aliases); end
|
|
545
698
|
|
|
546
699
|
# Check whether an option has already been declared in this manager
|
|
@@ -558,7 +711,7 @@ module Aspera
|
|
|
558
711
|
# @return [OptionValue] Option definition
|
|
559
712
|
# @raise [Cli::BadArgument] if option not found
|
|
560
713
|
def option_def(option_symbol)
|
|
561
|
-
Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument){"Unknown option: #{option_symbol}"}
|
|
714
|
+
Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument) { "Unknown option: #{option_symbol}" }
|
|
562
715
|
@declared_options[option_symbol]
|
|
563
716
|
end
|
|
564
717
|
|
|
@@ -579,7 +732,7 @@ module Aspera
|
|
|
579
732
|
return :skip_missing_mandatory if result.nil? && mandatory && !@fail_on_missing_mandatory
|
|
580
733
|
if result.nil?
|
|
581
734
|
if !@ask_missing_mandatory
|
|
582
|
-
Aspera.assert(!mandatory, type: Cli::BadArgument){"Missing mandatory option: #{option_symbol}"}
|
|
735
|
+
Aspera.assert(!mandatory, type: Cli::BadArgument) { "Missing mandatory option: #{option_symbol}" }
|
|
583
736
|
elsif @ask_missing_optional || mandatory
|
|
584
737
|
# ask_missing_mandatory
|
|
585
738
|
result = get_interactive(option_symbol.to_s, check_option: true, accept_list: option_attrs.values, schema: option_attrs.schema)
|
|
@@ -594,17 +747,19 @@ module Aspera
|
|
|
594
747
|
# @param option_symbol [Symbol] option name
|
|
595
748
|
# @param value [String] Value to set
|
|
596
749
|
# @param where [String] Where the value comes from
|
|
597
|
-
def set_option(option_symbol, value, where: 'code override')
|
|
750
|
+
def set_option(option_symbol, value, where: 'code override', warn_deprecation: true)
|
|
598
751
|
Aspera.assert_type(option_symbol, Symbol)
|
|
599
752
|
option = option_def(option_symbol)
|
|
600
753
|
# Raise immediately only when the option has a static schema: the schema is known at parse time.
|
|
601
754
|
# When schema is nil (e.g. --query), 'help' is stored as-is and SchemaRequest is raised later
|
|
602
755
|
# in get_option() with the contextual schema provided by the calling command.
|
|
603
756
|
raise SchemaRequest.new(:option, option.option, option.schema) if option.types&.include?(Hash) && value.eql?(HELP) && option.schema
|
|
604
|
-
option.assign_value(value, where: where)
|
|
757
|
+
option.assign_value(value, where: where, warn_deprecation: warn_deprecation)
|
|
605
758
|
end
|
|
606
759
|
|
|
607
760
|
# Set option to `nil`
|
|
761
|
+
# @param option_symbol [Symbol] option name
|
|
762
|
+
# @return [nil]
|
|
608
763
|
def clear_option(option_symbol)
|
|
609
764
|
Aspera.assert_type(option_symbol, Symbol)
|
|
610
765
|
option_def(option_symbol).clear
|
|
@@ -628,28 +783,36 @@ module Aspera
|
|
|
628
783
|
# @param override [Boolean] Override if already present
|
|
629
784
|
def add_option_preset(preset_hash, where, override: true)
|
|
630
785
|
Aspera.assert_type(preset_hash, Hash)
|
|
631
|
-
Log.log.debug{"add_option_preset: #{preset_hash}, #{where}, #{override}"}
|
|
786
|
+
Log.log.debug { "add_option_preset: #{preset_hash}, #{where}, #{override}" }
|
|
632
787
|
preset_hash.each do |k, v|
|
|
788
|
+
# Ignore comment/meta keys (e.g. _comment, _description)
|
|
789
|
+
next if k.to_s.start_with?(PresetManager::Key::META_PREFIX)
|
|
633
790
|
option_symbol = k.to_sym
|
|
791
|
+
# Never restore an option that was explicitly cleared from the CLI (e.g. --opt=@none:)
|
|
792
|
+
next if @explicitly_cleared.key?(option_symbol)
|
|
634
793
|
@option_pairs_batch[option_symbol] = v if override || !@option_pairs_batch.key?(option_symbol)
|
|
635
794
|
end
|
|
636
795
|
end
|
|
637
796
|
|
|
638
797
|
# Allows a plugin to add an argument as next argument to process
|
|
798
|
+
# @param argument [String] argument value to prepend
|
|
799
|
+
# @return [Array<Option, Argument>] updated tokens list
|
|
639
800
|
def unshift_next_argument(argument)
|
|
640
|
-
@
|
|
801
|
+
@argv_tokens.unshift(Argument.new(argument))
|
|
641
802
|
end
|
|
642
803
|
|
|
643
|
-
# Check if there
|
|
804
|
+
# Check if there are no pending positional arguments
|
|
805
|
+
# @return [Boolean] true if no pending positional arguments
|
|
644
806
|
def command_or_arg_empty?
|
|
645
|
-
|
|
807
|
+
pending_arguments.empty?
|
|
646
808
|
end
|
|
647
809
|
|
|
648
|
-
#
|
|
810
|
+
# Check for unprocessed options or arguments error messages
|
|
811
|
+
# @return [Array<String>] list of error messages for unprocessed tokens
|
|
649
812
|
def final_errors
|
|
650
813
|
result = []
|
|
651
|
-
result.push("unprocessed options: #{
|
|
652
|
-
result.push("unprocessed values: #{
|
|
814
|
+
result.push("unprocessed options: #{pending_options}") unless pending_options.empty?
|
|
815
|
+
result.push("unprocessed values: #{pending_arguments}") unless pending_arguments.empty?
|
|
653
816
|
result
|
|
654
817
|
end
|
|
655
818
|
|
|
@@ -657,17 +820,17 @@ module Aspera
|
|
|
657
820
|
# @return [Hash] options as taken from config file and command line just before command execution
|
|
658
821
|
def unprocessed_options_with_value
|
|
659
822
|
result = {}
|
|
660
|
-
@
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
823
|
+
@initial_argv_tokens.each_with_index do |tok, idx|
|
|
824
|
+
next unless tok.is_a?(Option) && tok.short_char.nil?
|
|
825
|
+
# For space-separated form: value is the immediately following :argument token (if any)
|
|
826
|
+
value = tok.value || @initial_argv_tokens[idx + 1]&.then { |t| t.value if t.is_a?(Argument) }
|
|
664
827
|
# ignore options without value
|
|
665
828
|
next if value.nil?
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
path
|
|
669
|
-
DotContainer.dotted_to_container(path, smart_convert(value), result)
|
|
670
|
-
@
|
|
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) }
|
|
671
834
|
end
|
|
672
835
|
result
|
|
673
836
|
end
|
|
@@ -692,59 +855,138 @@ module Aspera
|
|
|
692
855
|
@option_pairs_batch = consume_option_pairs(@option_pairs_batch, 'set')
|
|
693
856
|
# Then, env var (to override)
|
|
694
857
|
@option_pairs_env = consume_option_pairs(@option_pairs_env, 'env')
|
|
695
|
-
# Then, command line override
|
|
696
|
-
#
|
|
697
|
-
|
|
858
|
+
# Then, command line override.
|
|
859
|
+
# Iterate @argv_tokens in order so that --opt val and -s val can consume the next argument
|
|
860
|
+
# token directly, without any secondary index.
|
|
861
|
+
# Process one option at a time so that @current_option_args_offset can be set before each
|
|
862
|
+
# option is evaluated (used by `@:` extended value).
|
|
863
|
+
deferred_tokens = []
|
|
698
864
|
Log.log.trace1('Before parse')
|
|
699
|
-
Log.dump(:
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
865
|
+
Log.dump(:argv_tokens, @argv_tokens, level: :trace1)
|
|
866
|
+
# Iterate with an index so that Argument tokens stay in @argv_tokens (not shifted).
|
|
867
|
+
# Only Option tokens are removed; Arguments remain until consumed by extract_argument_tokens.
|
|
868
|
+
@current_parse_idx = 0
|
|
869
|
+
while @current_parse_idx < @argv_tokens.length
|
|
870
|
+
tok = @argv_tokens[@current_parse_idx]
|
|
871
|
+
if tok.is_a?(Argument)
|
|
872
|
+
@current_parse_idx += 1
|
|
873
|
+
next
|
|
874
|
+
end
|
|
875
|
+
# tok is an Option — remove it from @argv_tokens
|
|
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')
|
|
713
905
|
else
|
|
714
|
-
#
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
path = name_raw.split(DotContainer::SEPARATOR)
|
|
718
|
-
root_sym = self.class.option_line_to_name(path.shift).to_sym
|
|
719
|
-
if @declared_options.key?(root_sym)
|
|
720
|
-
set_option(root_sym, DotContainer.dotted_to_container(path, smart_convert(raw_value), get_option(root_sym)), where: 'dotted')
|
|
721
|
-
@args_before_option[opt]&.shift # consumed: advance to next occurrence
|
|
722
|
-
next
|
|
723
|
-
end
|
|
724
|
-
end
|
|
725
|
-
# Unknown option: defer to next parse_options! round, do not consume the recorded offset
|
|
726
|
-
unknown_options.push(opt)
|
|
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)
|
|
727
909
|
end
|
|
728
|
-
elsif
|
|
729
|
-
#
|
|
730
|
-
|
|
731
|
-
|
|
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)
|
|
732
917
|
else
|
|
733
|
-
|
|
918
|
+
Log.log.trace1 { "Unknown long option: #{tok.raw}".red }
|
|
919
|
+
deferred_tokens.push(tok)
|
|
734
920
|
end
|
|
735
921
|
end
|
|
736
922
|
@current_option_args_offset = nil
|
|
923
|
+
@current_parse_idx = nil
|
|
737
924
|
Log.log.trace1('After parse')
|
|
738
|
-
Log.log.trace1{"
|
|
739
|
-
#
|
|
740
|
-
|
|
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) }
|
|
741
979
|
end
|
|
742
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
|
|
743
985
|
def prompt_user_input(prompt, sensitive: false)
|
|
744
986
|
return $stdin.getpass("#{prompt}> ") if sensitive
|
|
745
987
|
print("#{prompt}> ")
|
|
746
988
|
line = $stdin.gets
|
|
747
|
-
Aspera.assert_type(line, String){'Unexpected end of standard input'}
|
|
989
|
+
Aspera.assert_type(line, String) { 'Unexpected end of standard input' }
|
|
748
990
|
line.chomp
|
|
749
991
|
end
|
|
750
992
|
|
|
@@ -755,7 +997,7 @@ module Aspera
|
|
|
755
997
|
def prompt_user_input_in_list(prompt, sym_list)
|
|
756
998
|
loop do
|
|
757
999
|
input = prompt_user_input(prompt).to_sym
|
|
758
|
-
if sym_list.any?{ |a| a.eql?(input)}
|
|
1000
|
+
if sym_list.any? { |a| a.eql?(input) }
|
|
759
1001
|
return input
|
|
760
1002
|
else
|
|
761
1003
|
$stderr.puts("No such #{prompt}: #{input}, select one of: #{sym_list.join(', ')}") # rubocop:disable Style/StderrPuts
|
|
@@ -769,13 +1011,13 @@ module Aspera
|
|
|
769
1011
|
# @param multiple [Boolean, String] `true` if multiple values expected
|
|
770
1012
|
# @param accept_list [Array<Symbol>,NilClass] List of expected values
|
|
771
1013
|
# @return [String] user input
|
|
772
|
-
def get_interactive(descr, check_option: false, multiple: false, accept_list: nil, schema: nil)
|
|
1014
|
+
def get_interactive(descr, check_option: false, multiple: false, accept_list: nil, aliases: nil, schema: nil)
|
|
773
1015
|
option_attrs = @declared_options[descr.to_sym]
|
|
774
1016
|
what = option_attrs ? 'option' : 'argument'
|
|
775
1017
|
default_prompt = "#{what}: #{descr}"
|
|
776
1018
|
if !@ask_missing_mandatory
|
|
777
1019
|
message = "Missing #{default_prompt}"
|
|
778
|
-
message = self.class.multi_choice_assert_msg(message, accept_list) if accept_list
|
|
1020
|
+
message = self.class.multi_choice_assert_msg(message, accept_list, aliases: aliases) if accept_list
|
|
779
1021
|
message += "\n#{TerminalFormatter::HINT}Give `#{HELP}` as argument to retrieve the schema of the missing argument." if schema
|
|
780
1022
|
raise Cli::MissingArgument, message
|
|
781
1023
|
end
|
|
@@ -803,23 +1045,23 @@ module Aspera
|
|
|
803
1045
|
# ExtendedValue.assert_no_value(end_marker, :p)
|
|
804
1046
|
end_marker = SpecialValues::EOA if end_marker.empty?
|
|
805
1047
|
# When called from an option value, skip positional args that appear before the option in argv.
|
|
806
|
-
# @current_option_args_offset
|
|
807
|
-
#
|
|
808
|
-
skip_count =
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
Log.log.trace1{"args_as_extended: skipping #{
|
|
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?
|
|
815
1057
|
result = nil
|
|
816
1058
|
get_next_argument('args', multiple: end_marker).each do |argument|
|
|
817
|
-
Aspera.assert(argument.include?(
|
|
818
|
-
path, value = argument.split(
|
|
819
|
-
result = DotContainer.dotted_to_container(path.split(DotContainer::SEPARATOR), smart_convert(value), result)
|
|
1059
|
+
Aspera.assert(argument.include?(Option::VALUE_SEP)) { "Positional argument: #{argument} does not include #{Option::VALUE_SEP}" }
|
|
1060
|
+
path, value = argument.split(Option::VALUE_SEP, 2)
|
|
1061
|
+
result = DotContainer.dotted_to_container(path.split(DotContainer::SEPARATOR), Parser.smart_convert(value), result)
|
|
820
1062
|
end
|
|
821
|
-
# Restore skipped
|
|
822
|
-
@
|
|
1063
|
+
# Restore skipped tokens so they remain available for command dispatching
|
|
1064
|
+
skipped_tokens.reverse_each { |t| @argv_tokens.unshift(t) }
|
|
823
1065
|
result
|
|
824
1066
|
end
|
|
825
1067
|
|
|
@@ -837,7 +1079,8 @@ module Aspera
|
|
|
837
1079
|
short_char = @short_options.key(sym)
|
|
838
1080
|
short_part = short_char ? "-#{short_char}, " : ' '
|
|
839
1081
|
flag = "#{short_part}#{symbol_to_option(sym, option_display_value(opt))}"
|
|
840
|
-
|
|
1082
|
+
desc = opt.deprecation ? "#{opt.description} (deprecated: #{opt.deprecation})" : opt.description
|
|
1083
|
+
rows << [flag, desc]
|
|
841
1084
|
end
|
|
842
1085
|
table = Terminal::Table.new(rows: rows, style: {border: HELP_BORDER, padding_left: 0, padding_right: 2})
|
|
843
1086
|
banner.nil? ? table.to_s : "#{banner}\n#{table}"
|
|
@@ -857,12 +1100,22 @@ module Aspera
|
|
|
857
1100
|
end.freeze
|
|
858
1101
|
|
|
859
1102
|
# @param opt [OptionValue] option descriptor
|
|
860
|
-
# @return [String, nil] placeholder shown in flag column: 'ENUM', 'VALUE', or nil for flag switches
|
|
1103
|
+
# @return [String, nil] placeholder shown in flag column: 'ENUM', 'HASH', 'INT', 'LIST', 'VALUE', or nil for flag switches
|
|
861
1104
|
def option_display_value(opt)
|
|
862
1105
|
case opt.types
|
|
863
|
-
when
|
|
864
|
-
when
|
|
865
|
-
|
|
1106
|
+
when Type::NONE then nil
|
|
1107
|
+
when Type::BOOLEAN then 'yes|no'
|
|
1108
|
+
when Type::INTEGER then 'INT'
|
|
1109
|
+
when Type::ENUM
|
|
1110
|
+
opt.values&.any? && opt.values.length <= 4 ? opt.values.join('|') : 'ENUM'
|
|
1111
|
+
else
|
|
1112
|
+
if opt.types&.include?(Hash) || !opt.schema.nil?
|
|
1113
|
+
'HASH'
|
|
1114
|
+
elsif opt.types&.include?(Array)
|
|
1115
|
+
'LIST'
|
|
1116
|
+
else
|
|
1117
|
+
'VALUE'
|
|
1118
|
+
end
|
|
866
1119
|
end
|
|
867
1120
|
end
|
|
868
1121
|
|
|
@@ -871,32 +1124,26 @@ module Aspera
|
|
|
871
1124
|
# @param raw_value [String, nil] raw string value from command line, or nil for flag switches
|
|
872
1125
|
def dispatch_option(sym, raw_value)
|
|
873
1126
|
opt = @declared_options[sym]
|
|
874
|
-
if opt.types.eql?(
|
|
1127
|
+
if opt.types.eql?(Type::NONE)
|
|
875
1128
|
opt.block.call
|
|
876
1129
|
else
|
|
877
1130
|
set_option(sym, raw_value, where: SOURCE_USER)
|
|
1131
|
+
# Track options explicitly cleared from CLI (e.g. --opt=@none:) so that
|
|
1132
|
+
# subsequent preset injection does not silently restore the value.
|
|
1133
|
+
# @none: always evaluates to nil; detect it by checking the raw token directly
|
|
1134
|
+
# to avoid re-evaluating side-effectful extended values (e.g. @: consumes positional args).
|
|
1135
|
+
cleared = get_option(sym).nil? || raw_value.eql?('@none:')
|
|
1136
|
+
@explicitly_cleared[sym] = true if cleared
|
|
878
1137
|
end
|
|
879
1138
|
end
|
|
880
1139
|
|
|
881
|
-
#
|
|
882
|
-
# @param
|
|
883
|
-
# @
|
|
884
|
-
|
|
885
|
-
case value
|
|
886
|
-
when 'true' then true
|
|
887
|
-
when 'false' then false
|
|
888
|
-
else
|
|
889
|
-
Integer(value, exception: false) ||
|
|
890
|
-
Float(value, exception: false) ||
|
|
891
|
-
ExtendedValue.instance.evaluate(value, context: 'dotted expression')
|
|
892
|
-
end
|
|
893
|
-
end
|
|
894
|
-
|
|
895
|
-
# generate command line option from option symbol
|
|
1140
|
+
# Generate command line option string from option symbol
|
|
1141
|
+
# @param symbol [Symbol] option name
|
|
1142
|
+
# @param opt_val [String, nil] optional value placeholder
|
|
1143
|
+
# @return [String] formatted option string (e.g. "--option=VALUE")
|
|
896
1144
|
def symbol_to_option(symbol, opt_val = nil)
|
|
897
|
-
result =
|
|
898
|
-
|
|
899
|
-
result
|
|
1145
|
+
result = self.class.option_name_to_line(symbol)
|
|
1146
|
+
opt_val.nil? ? result : "#{result}#{Option::VALUE_SEP}#{opt_val}"
|
|
900
1147
|
end
|
|
901
1148
|
|
|
902
1149
|
# TODO: use formatter
|
|
@@ -923,28 +1170,40 @@ module Aspera
|
|
|
923
1170
|
# @param where [String] label used in log messages and error context
|
|
924
1171
|
# @return [Hash{Symbol => Object}] pairs whose keys were not yet declared (deferred to next round)
|
|
925
1172
|
def consume_option_pairs(option_pairs, where)
|
|
926
|
-
Log.log.trace1{"consume_option_pairs: #{where}"}
|
|
1173
|
+
Log.log.trace1 { "consume_option_pairs: #{where}" }
|
|
927
1174
|
remaining = {}
|
|
928
1175
|
option_pairs.each do |k, v|
|
|
929
1176
|
if @declared_options.key?(k)
|
|
930
1177
|
set_option(k, v, where: where)
|
|
931
1178
|
else
|
|
932
|
-
Log.log.trace1{"unprocessed: #{k}: #{v}"}
|
|
1179
|
+
Log.log.trace1 { "unprocessed: #{k}: #{v}" }
|
|
933
1180
|
remaining[k] = v
|
|
934
1181
|
end
|
|
935
1182
|
end
|
|
936
1183
|
remaining
|
|
937
1184
|
end
|
|
938
1185
|
|
|
939
|
-
#
|
|
940
|
-
|
|
941
|
-
#
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
+
end
|
|
1205
|
+
|
|
1206
|
+
# when this is alone, this stops option processing (same characters as Option::PREFIX but distinct semantics)
|
|
948
1207
|
OPTIONS_STOP = '--'
|
|
949
1208
|
SOURCE_USER = 'cmdline' # cspell:disable-line
|
|
950
1209
|
# Percent selector: select by this field for this value
|
|
@@ -952,7 +1211,7 @@ module Aspera
|
|
|
952
1211
|
# Ask for schema of Extended value
|
|
953
1212
|
HELP = 'help'
|
|
954
1213
|
|
|
955
|
-
private_constant :
|
|
1214
|
+
private_constant :OPTIONS_STOP, :SOURCE_USER, :REGEX_LOOKUP_ID_BY_FIELD, :HELP_BORDER
|
|
956
1215
|
end
|
|
957
1216
|
end
|
|
958
1217
|
end
|