aspera-cli 4.26.2 → 4.27.0
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 +281 -21
- data/CONTRIBUTING.md +98 -7
- data/bin/ascli +3 -3
- data/docs/README.md +10757 -0
- data/docs/test-mcp-with-ai.md +202 -0
- data/lib/aspera/agent/base.rb +20 -6
- data/lib/aspera/agent/connect.rb +53 -16
- data/lib/aspera/agent/desktop.rb +52 -17
- data/lib/aspera/agent/direct.rb +26 -24
- data/lib/aspera/agent/httpgw.rb +1 -1
- data/lib/aspera/agent/node.rb +32 -0
- data/lib/aspera/agent/transferd.rb +39 -3
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +14 -16
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/faspex.rb +15 -14
- data/lib/aspera/api/httpgw.rb +4 -4
- data/lib/aspera/api/node.rb +26 -23
- data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
- data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
- data/lib/aspera/ascmd.rb +21 -19
- data/lib/aspera/ascp/installation.rb +37 -38
- data/lib/aspera/ascp/management.rb +2 -3
- data/lib/aspera/cli/ascp_actions.rb +155 -0
- data/lib/aspera/cli/async_transfer_store.rb +81 -0
- data/lib/aspera/cli/bootstrapper.rb +197 -0
- data/lib/aspera/cli/command_registry.rb +145 -0
- data/lib/aspera/cli/command_spec.rb +138 -0
- data/lib/aspera/cli/context.rb +16 -3
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +2 -1
- data/lib/aspera/cli/formatter.rb +41 -27
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +2 -2
- data/lib/aspera/cli/http.rb +70 -19
- data/lib/aspera/cli/info.rb +2 -0
- data/lib/aspera/cli/mailer.rb +97 -0
- data/lib/aspera/cli/mcp_tool.rb +198 -0
- data/lib/aspera/cli/options.schema.yaml +364 -7
- data/lib/aspera/cli/{manager.rb → parser.rb} +334 -142
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1166 -853
- data/lib/aspera/cli/plugins/ats.rb +200 -161
- data/lib/aspera/cli/plugins/base.rb +457 -100
- data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
- data/lib/aspera/cli/plugins/config.rb +428 -754
- data/lib/aspera/cli/plugins/console.rb +106 -64
- data/lib/aspera/cli/plugins/cos.rb +44 -32
- data/lib/aspera/cli/plugins/factory.rb +4 -4
- data/lib/aspera/cli/plugins/faspex.rb +294 -257
- data/lib/aspera/cli/plugins/faspex5.rb +585 -357
- data/lib/aspera/cli/plugins/faspio.rb +49 -51
- data/lib/aspera/cli/plugins/httpgw.rb +18 -25
- data/lib/aspera/cli/plugins/mcp.rb +279 -0
- data/lib/aspera/cli/plugins/node.rb +998 -794
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +111 -134
- data/lib/aspera/cli/plugins/preview.rb +180 -139
- data/lib/aspera/cli/plugins/server.rb +124 -79
- data/lib/aspera/cli/plugins/shares.rb +301 -107
- data/lib/aspera/cli/preset_actions.rb +139 -0
- data/lib/aspera/cli/preset_manager.rb +13 -12
- data/lib/aspera/cli/result.rb +61 -11
- data/lib/aspera/cli/runner.rb +275 -141
- data/lib/aspera/cli/secret_finder.rb +40 -0
- data/lib/aspera/cli/special_values.rb +1 -0
- data/lib/aspera/cli/sync_actions.rb +83 -59
- data/lib/aspera/cli/terminal_formatter.rb +1 -1
- data/lib/aspera/cli/transfer_actions.rb +83 -0
- data/lib/aspera/cli/transfer_agent.rb +116 -51
- data/lib/aspera/cli/transfer_progress.rb +3 -3
- data/lib/aspera/cli/vault_manager.rb +57 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +21 -20
- data/lib/aspera/command_line_builder.rb +22 -22
- data/lib/aspera/data_repository.rb +2 -1
- data/lib/aspera/dot_container.rb +4 -4
- data/lib/aspera/environment.rb +18 -11
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/faspex_postproc.rb +4 -3
- data/lib/aspera/graphql.rb +35 -0
- data/lib/aspera/hash_ext.rb +6 -0
- data/lib/aspera/json_rpc/client.rb +62 -0
- data/lib/aspera/json_rpc/version.rb +7 -0
- data/lib/aspera/keychain/encrypted_hash.rb +1 -1
- data/lib/aspera/keychain/factory.rb +3 -3
- data/lib/aspera/keychain/macos_security.rb +3 -3
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +17 -3
- data/lib/aspera/markdown.rb +85 -2
- data/lib/aspera/node_simulator.rb +3 -1
- data/lib/aspera/oauth/base.rb +5 -5
- data/lib/aspera/oauth/boot.rb +2 -2
- data/lib/aspera/oauth/factory.rb +7 -5
- data/lib/aspera/oauth/jwt.rb +4 -4
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/persistency_action_once.rb +8 -8
- data/lib/aspera/persistency_folder.rb +10 -3
- data/lib/aspera/preview/file_types.rb +3 -3
- data/lib/aspera/preview/generator.rb +3 -3
- data/lib/aspera/preview/utils.rb +1 -1
- data/lib/aspera/products/connect.rb +1 -1
- data/lib/aspera/products/other.rb +1 -1
- data/lib/aspera/products/transferd.rb +1 -1
- data/lib/aspera/proxy_auto_config.rb +7 -6
- data/lib/aspera/rest.rb +10 -36
- data/lib/aspera/rest_error_analyzer.rb +3 -3
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +3 -3
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
- data/lib/aspera/schema/documentation.rb +57 -26
- data/lib/aspera/schema/reader.rb +59 -10
- data/lib/aspera/schema/registry.rb +39 -6
- data/lib/aspera/secret_hider.rb +7 -2
- data/lib/aspera/ssl.rb +4 -4
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/operations.rb +10 -13
- data/lib/aspera/temp_file_manager.rb +1 -1
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +23 -21
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +6 -6
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +12 -3
- data/lib/aspera/uri_reader.rb +2 -2
- data/lib/aspera/web_auth.rb +2 -2
- data/lib/aspera/web_server_simple.rb +8 -5
- data/lib/aspera/yaml.rb +2 -1
- data.tar.gz.sig +0 -0
- metadata +25 -3
- metadata.gz.sig +0 -0
- data/lib/aspera/json_rpc.rb +0 -52
|
@@ -8,8 +8,9 @@ require 'aspera/secret_hider'
|
|
|
8
8
|
require 'aspera/log'
|
|
9
9
|
require 'aspera/assert'
|
|
10
10
|
require 'aspera/dot_container'
|
|
11
|
+
require 'aspera/schema/registry'
|
|
11
12
|
require 'io/console'
|
|
12
|
-
require '
|
|
13
|
+
require 'terminal-table'
|
|
13
14
|
|
|
14
15
|
module Aspera
|
|
15
16
|
module Cli
|
|
@@ -40,7 +41,7 @@ module Aspera
|
|
|
40
41
|
# `false` and `true`
|
|
41
42
|
TYPES = [FalseClass, TrueClass].freeze
|
|
42
43
|
SYMBOLS = [NO_SYM, YES_SYM].freeze
|
|
43
|
-
# @return `true` if value is a value for `true` in ALL
|
|
44
|
+
# @return [Boolean] `true` if value is a value for `true` in ALL
|
|
44
45
|
def true?(enum)
|
|
45
46
|
Aspera.assert_values(enum, ALL){'boolean'}
|
|
46
47
|
TRUE_VALUES.include?(enum)
|
|
@@ -52,41 +53,49 @@ module Aspera
|
|
|
52
53
|
TRUE_VALUES.include?(enum) ? YES_SYM : NO_SYM
|
|
53
54
|
end
|
|
54
55
|
|
|
55
|
-
# @return `true` if value is a value for `true` or `false` in ALL
|
|
56
|
+
# @return [Boolean] `true` if value is a value for `true` or `false` in ALL
|
|
56
57
|
def symbol?(sym)
|
|
57
58
|
ALL.include?(sym)
|
|
58
59
|
end
|
|
59
60
|
module_function :true?, :to_sym, :symbol?
|
|
60
61
|
end
|
|
61
62
|
|
|
62
|
-
# Constants to be used as parameter `allowed:` for `OptionValue
|
|
63
|
+
# Constants to be used as parameter `allowed:` for `OptionValue`.
|
|
64
|
+
# Public API: TYPES_STRING_ARRAY, TYPES_SYMBOL_ARRAY, TYPES_INTEGER, TYPES_BOOLEAN, TYPES_NONE.
|
|
65
|
+
# Internal (do not pass as `allowed:`):
|
|
66
|
+
# TYPES_ENUM - derived internally when `allowed:` is an Array<Symbol> (enum list)
|
|
67
|
+
# TYPES_STRING - the implicit default; equivalent to omitting `allowed:` entirely
|
|
63
68
|
module Allowed
|
|
64
69
|
# This option can be set to a single string or array, multiple times, and gives Array of String
|
|
65
70
|
TYPES_STRING_ARRAY = [Array, String].freeze
|
|
66
|
-
# A list of symbols with constrained values
|
|
71
|
+
# A list of symbols with constrained values; use as prefix: TYPES_SYMBOL_ARRAY + [:val1, :val2]
|
|
67
72
|
TYPES_SYMBOL_ARRAY = [Array, Symbol].freeze
|
|
68
73
|
# Value will be coerced to int
|
|
69
74
|
TYPES_INTEGER = [Integer].freeze
|
|
70
75
|
TYPES_BOOLEAN = BoolValue::TYPES
|
|
71
76
|
# No value at all for the option, it's a switch, like `-N`
|
|
72
77
|
TYPES_NONE = [].freeze
|
|
73
|
-
# Symbol
|
|
74
|
-
TYPES_ENUM
|
|
75
|
-
# String
|
|
78
|
+
# Internal: derived when allowed: is an Array<Symbol>; do not pass directly
|
|
79
|
+
TYPES_ENUM = [Symbol].freeze
|
|
80
|
+
# Internal: implicit default (String); equivalent to omitting allowed: entirely
|
|
76
81
|
TYPES_STRING = [String].freeze
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
# Description of option, how to manage
|
|
80
85
|
class OptionValue
|
|
81
86
|
# [Array(Class)] List of allowed types
|
|
82
|
-
attr_reader :types, :sensitive, :schema, :option
|
|
87
|
+
attr_reader :types, :sensitive, :schema, :option, :deprecation
|
|
83
88
|
# [Array] List of allowed values (Symbols and specific values)
|
|
84
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
|
|
85
94
|
|
|
86
95
|
# @param option [Symbol] Name of option
|
|
87
|
-
# @param description [String] Description for help
|
|
96
|
+
# @param description [String, nil] Description for help; if nil, derived from schema
|
|
88
97
|
# @param allowed [nil,Class,Array<Class>,Array<Symbol>] Allowed values
|
|
89
|
-
# @param handler [Hash] Accessor: keys: :o(object) and :m(method)
|
|
98
|
+
# @param handler [Hash, nil] Accessor: keys: :o(object) and :m(method); nil for local storage
|
|
90
99
|
# @param deprecation [String] Deprecation message
|
|
91
100
|
# @param schema [String] Declaration of schema
|
|
92
101
|
# `allowed`:
|
|
@@ -94,28 +103,34 @@ module Aspera
|
|
|
94
103
|
# - `Class` The single allowed Class
|
|
95
104
|
# - `Array<Class>` Multiple allowed classes
|
|
96
105
|
# - `Array<Symbol>` List of allowed values
|
|
97
|
-
def initialize(option:, description
|
|
106
|
+
def initialize(option:, description: nil, allowed: Allowed::TYPES_STRING, handler: nil, deprecation: nil, schema: nil)
|
|
98
107
|
Log.log.trace1{"option: #{option}, allowed: #{allowed}"}
|
|
99
108
|
@option = option
|
|
100
109
|
@description = description
|
|
110
|
+
@group = nil
|
|
111
|
+
@block = nil
|
|
101
112
|
# by default passwords and secrets are sensitive, else specify when declaring the option
|
|
102
113
|
@sensitive = SecretHider.instance.secret?(@option, '')
|
|
103
|
-
# either the value, or object giving value
|
|
104
|
-
@object = handler&.[](:o)
|
|
105
|
-
@read_method = handler&.[](:m)
|
|
106
|
-
@write_method = @read_method ? "#{@read_method}=".to_sym : nil
|
|
107
114
|
@deprecation = deprecation
|
|
108
115
|
@schema = schema
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
end
|
|
116
|
-
Aspera.assert(@object.respond_to?(@read_method)){"#{@object} does not respond to #{@read_method}"} unless @access.eql?(:local)
|
|
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?
|
|
117
122
|
@types = nil
|
|
118
123
|
@values = nil
|
|
124
|
+
# Derive allowed type from schema when not explicitly provided
|
|
125
|
+
if (allowed.nil? || allowed.eql?(Allowed::TYPES_STRING)) && schema
|
|
126
|
+
schema_node = Schema::Registry.instance.reader(schema).current rescue nil
|
|
127
|
+
if schema_node
|
|
128
|
+
case schema_node['type']
|
|
129
|
+
when 'object' then allowed = Hash
|
|
130
|
+
when 'array' then allowed = Array
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
119
134
|
if !allowed.nil?
|
|
120
135
|
allowed = [allowed] if allowed.is_a?(Class)
|
|
121
136
|
Aspera.assert_type(allowed, Array)
|
|
@@ -123,12 +138,17 @@ module Aspera
|
|
|
123
138
|
# Special case: array of defined symbol values
|
|
124
139
|
@types = Allowed::TYPES_SYMBOL_ARRAY
|
|
125
140
|
@values = allowed[Allowed::TYPES_SYMBOL_ARRAY.length..]
|
|
141
|
+
# Default value for symbol array when no value has been set yet
|
|
142
|
+
assign_value([], where: 'array default', warn_deprecation: false) if value(log: false).nil?
|
|
126
143
|
elsif allowed.all?(Class)
|
|
127
144
|
@types = allowed
|
|
128
145
|
@values = BoolValue::ALL if allowed.eql?(Allowed::TYPES_BOOLEAN)
|
|
129
|
-
# Default value for array
|
|
130
|
-
|
|
131
|
-
|
|
146
|
+
# Default value for array/hash when no value has been set yet
|
|
147
|
+
if @types.first.eql?(Array) && !@types.include?(NilClass) && value(log: false).nil?
|
|
148
|
+
assign_value([], where: 'array default', warn_deprecation: false)
|
|
149
|
+
elsif @types.first.eql?(Hash) && !@types.include?(NilClass) && value(log: false).nil?
|
|
150
|
+
assign_value({}, where: 'hash default', warn_deprecation: false)
|
|
151
|
+
end
|
|
132
152
|
elsif allowed.all?(Symbol)
|
|
133
153
|
@types = Allowed::TYPES_ENUM
|
|
134
154
|
@values = allowed
|
|
@@ -139,6 +159,40 @@ module Aspera
|
|
|
139
159
|
Log.log.trace1{"declare: #{@option}: #{@access} #{@object.class}.#{@read_method}".green}
|
|
140
160
|
end
|
|
141
161
|
|
|
162
|
+
# Wire (or re-wire) the getter/setter delegation for this option.
|
|
163
|
+
# Safe to call after construction - used by Parser#set_handler to bind a composed
|
|
164
|
+
# instance variable that did not exist at class-load time (Category C handlers).
|
|
165
|
+
# @param handler [Hash] Accessor hash with keys :o (object) and :m (method symbol)
|
|
166
|
+
# @return [nil]
|
|
167
|
+
def bind_handler(handler)
|
|
168
|
+
Aspera.assert_type(handler, Hash){'handler'}
|
|
169
|
+
# Capture any value already stored locally before switching to delegated storage.
|
|
170
|
+
# This transfers defaults (and any preset values already applied) to the new target.
|
|
171
|
+
pending_value = @access.eql?(:local) ? @object : nil
|
|
172
|
+
@object = handler[:o]
|
|
173
|
+
@read_method = handler[:m]
|
|
174
|
+
@write_method = "#{@read_method}=".to_sym
|
|
175
|
+
@access = if @object.respond_to?(@write_method)
|
|
176
|
+
:write
|
|
177
|
+
else
|
|
178
|
+
:setter
|
|
179
|
+
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}
|
|
182
|
+
# Push the pending local value to the new target if one was stored
|
|
183
|
+
assign_value(pending_value, where: 'bind_handler', warn_deprecation: false) unless pending_value.nil?
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @return [String] description of the option: explicit one, or first line of schema description
|
|
188
|
+
def description
|
|
189
|
+
return @description unless @description.nil?
|
|
190
|
+
return if @schema.nil?
|
|
191
|
+
schema_node = Schema::Registry.instance.reader(@schema).current
|
|
192
|
+
first_line = (schema_node['title'] || schema_node['description'].to_s).lines.first.to_s.strip
|
|
193
|
+
first_line.end_with?('.') ? first_line[0..-2] : first_line
|
|
194
|
+
end
|
|
195
|
+
|
|
142
196
|
def clear
|
|
143
197
|
@object = nil
|
|
144
198
|
end
|
|
@@ -158,22 +212,42 @@ module Aspera
|
|
|
158
212
|
# Value can be a `String`, then evaluated with `ExtendedValue`, or directly a value.
|
|
159
213
|
# @param value [String, Object] Value to assign to option
|
|
160
214
|
# @param where [String] Where the value is assigned from
|
|
215
|
+
# @param warn_deprecation [Boolean] Emit deprecation warning (false for internal transfers)
|
|
161
216
|
# @return [nil]
|
|
162
|
-
def assign_value(value, where:)
|
|
163
|
-
Aspera.assert(!@deprecation, type: warn){"Option #{@option} is deprecated: #{@deprecation}"}
|
|
217
|
+
def assign_value(value, where:, warn_deprecation: true)
|
|
218
|
+
Aspera.assert(!@deprecation, type: :warn){"Option #{@option} is deprecated: #{@deprecation}"} if warn_deprecation
|
|
164
219
|
new_value = ExtendedValue.instance.evaluate(value, context: "option: #{@option}", allowed: @types)
|
|
165
220
|
Log.log.trace1{"#{where}: #{@option} <- (#{new_value.class})#{new_value}"}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
221
|
+
# Per-type coercion: String input from CLI/env/preset is normalized to the expected type.
|
|
222
|
+
# Centralized here so all sources (CLI dispatch, preset, env) go through the same path.
|
|
223
|
+
case @types
|
|
224
|
+
when Allowed::TYPES_ENUM
|
|
225
|
+
new_value = Parser.get_from_list(new_value, @option, @values) if new_value.is_a?(String)
|
|
226
|
+
when Allowed::TYPES_BOOLEAN
|
|
227
|
+
new_value = Parser.get_from_list(new_value, @option, BoolValue::ALL) if new_value.is_a?(String)
|
|
228
|
+
new_value = BoolValue.true?(new_value)
|
|
229
|
+
when Allowed::TYPES_INTEGER
|
|
230
|
+
new_value = Integer(new_value)
|
|
231
|
+
when Allowed::TYPES_STRING_ARRAY
|
|
232
|
+
new_value = [new_value] if new_value.is_a?(String)
|
|
233
|
+
when Allowed::TYPES_SYMBOL_ARRAY
|
|
174
234
|
new_value = [new_value] if new_value.is_a?(String)
|
|
175
235
|
Aspera.assert_array_all(new_value, String, type: BadArgument)
|
|
176
|
-
new_value = new_value.map{ |v|
|
|
236
|
+
new_value = new_value.map{ |v| Parser.get_from_list(v, @option, @values)}
|
|
237
|
+
else
|
|
238
|
+
# nil (setting nil on a Hash/Array option resets to empty container)
|
|
239
|
+
new_value = {} if new_value.nil? && @types&.first.eql?(Hash)
|
|
240
|
+
new_value = [] if new_value.nil? && @types&.first.eql?(Array)
|
|
241
|
+
end
|
|
242
|
+
# Skip type validation for the special 'help' value on Hash options: store it as-is
|
|
243
|
+
# so that get_option(schema:) can raise SchemaRequest with the contextual schema later.
|
|
244
|
+
if new_value.eql?(Parser::HELP) && @types&.include?(Hash) && !@schema
|
|
245
|
+
case @access
|
|
246
|
+
when :local then @object = new_value
|
|
247
|
+
when :write then @object.send(@write_method, new_value)
|
|
248
|
+
when :setter then @object.send(@read_method, @option, :set, new_value)
|
|
249
|
+
end
|
|
250
|
+
return
|
|
177
251
|
end
|
|
178
252
|
Aspera.assert_type(new_value, *@types, type: BadArgument){"Option #{@option}"} if @types
|
|
179
253
|
if new_value.is_a?(Hash) || new_value.is_a?(Array)
|
|
@@ -194,7 +268,7 @@ module Aspera
|
|
|
194
268
|
# parse command line options
|
|
195
269
|
# arguments options start with '-', others are commands
|
|
196
270
|
# resolves on extended value syntax
|
|
197
|
-
class
|
|
271
|
+
class Parser
|
|
198
272
|
class << self
|
|
199
273
|
# Find shortened string value in allowed symbol list
|
|
200
274
|
def get_from_list(short_value, descr, allowed_values)
|
|
@@ -203,12 +277,21 @@ module Aspera
|
|
|
203
277
|
matching_exact = allowed_values.select{ |i| i.to_s.eql?(short_value)}
|
|
204
278
|
return matching_exact.first if matching_exact.length == 1
|
|
205
279
|
matching = allowed_values.select{ |i| i.to_s.start_with?(short_value)}
|
|
280
|
+
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)
|
|
206
281
|
Aspera.assert(!matching.empty?, multi_choice_assert_msg("unknown value for #{descr}: #{short_value}", allowed_values), type: BadArgument)
|
|
207
282
|
Aspera.assert(matching.length.eql?(1), multi_choice_assert_msg("ambiguous shortcut for #{descr}: #{short_value}", matching), type: BadArgument)
|
|
208
283
|
return BoolValue.true?(matching.first) if allowed_values.eql?(BoolValue::ALL)
|
|
209
284
|
matching.first
|
|
210
285
|
end
|
|
211
286
|
|
|
287
|
+
# Find a key in a list by exact match or unique prefix match
|
|
288
|
+
# @return [Object, nil] the matching key, or nil if none or ambiguous
|
|
289
|
+
def match_prefix(short_value, allowed_values)
|
|
290
|
+
return short_value if allowed_values.include?(short_value)
|
|
291
|
+
matches = allowed_values.select{ |k| k.to_s.start_with?(short_value.to_s)}
|
|
292
|
+
matches.length == 1 ? matches.first : nil
|
|
293
|
+
end
|
|
294
|
+
|
|
212
295
|
# Generates error message with list of allowed values
|
|
213
296
|
# @param error_msg [String] Error message
|
|
214
297
|
# @param accept_list [Array<Symbol>] List of allowed values
|
|
@@ -217,8 +300,8 @@ module Aspera
|
|
|
217
300
|
end
|
|
218
301
|
|
|
219
302
|
# Change option name with dash to name with underscore
|
|
220
|
-
# @param name [String] option name
|
|
221
|
-
# @return [String]
|
|
303
|
+
# @param name [String] option name with dash separators
|
|
304
|
+
# @return [String] option name with underscore separators
|
|
222
305
|
def option_line_to_name(name)
|
|
223
306
|
name.gsub(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
|
|
224
307
|
end
|
|
@@ -237,19 +320,12 @@ module Aspera
|
|
|
237
320
|
end
|
|
238
321
|
end
|
|
239
322
|
|
|
240
|
-
|
|
241
|
-
attr_accessor :ask_missing_mandatory, :ask_missing_optional
|
|
323
|
+
attr_accessor :ask_missing_mandatory, :ask_missing_optional, :help_requested
|
|
242
324
|
attr_writer :fail_on_missing_mandatory
|
|
243
325
|
|
|
244
326
|
# @param program_name [String] Name of the program
|
|
245
327
|
# @param argv [Array<String>, nil] Command line arguments to parse
|
|
246
328
|
def initialize(program_name, argv = nil)
|
|
247
|
-
# command line values *not* starting with '-'
|
|
248
|
-
@unprocessed_cmd_line_arguments = []
|
|
249
|
-
# command line values starting with at least one '-'
|
|
250
|
-
@unprocessed_cmd_line_options = []
|
|
251
|
-
# a copy of all initial options
|
|
252
|
-
@initial_cli_options = []
|
|
253
329
|
# Option descriptions: maps option symbol to its OptionValue descriptor
|
|
254
330
|
# @type [Hash{Symbol => OptionValue}]
|
|
255
331
|
@declared_options = {}
|
|
@@ -259,45 +335,57 @@ module Aspera
|
|
|
259
335
|
@ask_missing_optional = false
|
|
260
336
|
# get_option fails if a mandatory parameter is asked
|
|
261
337
|
@fail_on_missing_mandatory = true
|
|
262
|
-
#
|
|
263
|
-
|
|
264
|
-
#
|
|
338
|
+
# set to true when --help / -h is parsed
|
|
339
|
+
@help_requested = false
|
|
340
|
+
# options can also be provided by env vars : --param-name -> ASCLI_PARAM_NAME
|
|
265
341
|
@option_pairs_batch = {}
|
|
266
342
|
@option_pairs_env = {}
|
|
267
|
-
#
|
|
268
|
-
@
|
|
269
|
-
|
|
270
|
-
|
|
343
|
+
# Short option char -> option symbol, e.g. {'h' => :help, 'v' => :version}
|
|
344
|
+
@short_options = {}
|
|
345
|
+
# Current help section group name, set by #group
|
|
346
|
+
@current_group = 'global'
|
|
271
347
|
env_prefix = program_name.upcase + OPTION_SEP_SYMBOL
|
|
272
348
|
ENV.each do |k, v|
|
|
273
349
|
@option_pairs_env[k.delete_prefix(env_prefix).downcase.to_sym] = v if k.start_with?(env_prefix)
|
|
274
350
|
end
|
|
275
351
|
Log.log.debug{"env=#{@option_pairs_env}".red}
|
|
352
|
+
# command line values starting with at least one '-'
|
|
276
353
|
@unprocessed_cmd_line_options = []
|
|
354
|
+
# command line values *not* starting with '-'
|
|
277
355
|
@unprocessed_cmd_line_arguments = []
|
|
356
|
+
# a copy of all initial options
|
|
357
|
+
@initial_cli_options = []
|
|
358
|
+
# For each option string: list (one entry per occurrence) of the number of positional args
|
|
359
|
+
# that appear before it in original argv. Used by `@:` in option values to skip preceding args.
|
|
360
|
+
# @type [Hash{String => Array<Integer>}]
|
|
361
|
+
@args_before_option = {}
|
|
278
362
|
return if argv.nil?
|
|
279
363
|
# true until `--` is found (stop options)
|
|
280
364
|
process_options = true
|
|
281
|
-
|
|
282
|
-
|
|
365
|
+
arg_count = 0
|
|
366
|
+
argv.each do |value|
|
|
283
367
|
if process_options && value.start_with?('-')
|
|
284
368
|
Log.log.trace1{"opt: #{value}"}
|
|
285
369
|
if value.eql?(OPTIONS_STOP)
|
|
286
370
|
process_options = false
|
|
287
371
|
else
|
|
288
372
|
@unprocessed_cmd_line_options.push(value)
|
|
373
|
+
(@args_before_option[value] ||= []).push(arg_count)
|
|
289
374
|
end
|
|
290
375
|
else
|
|
291
376
|
Log.log.trace1{"arg: #{value}"}
|
|
292
377
|
@unprocessed_cmd_line_arguments.push(value)
|
|
378
|
+
arg_count += 1
|
|
293
379
|
end
|
|
294
380
|
end
|
|
381
|
+
# Total positional args at parse time - used in args_as_extended to compute how many to skip.
|
|
382
|
+
@arg_total_count = @unprocessed_cmd_line_arguments.length
|
|
383
|
+
# Number of original positional args before the option currently being parsed (nil = positional context).
|
|
384
|
+
@current_option_args_offset = nil
|
|
295
385
|
@initial_cli_options = @unprocessed_cmd_line_options.dup.freeze
|
|
296
386
|
Log.log.trace1{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
declare(:interactive, 'Use interactive input of missing params', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :ask_missing_mandatory})
|
|
300
|
-
declare(:ask_options, 'Ask even optional options', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :ask_missing_optional})
|
|
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})
|
|
301
389
|
# do not parse options yet, let's wait for option `-h` to be overridden
|
|
302
390
|
end
|
|
303
391
|
|
|
@@ -311,7 +399,7 @@ module Aspera
|
|
|
311
399
|
|
|
312
400
|
# Declare an option
|
|
313
401
|
# @param option_symbol [Symbol] option name
|
|
314
|
-
# @param description [String] description for help
|
|
402
|
+
# @param description [String, nil] description for help; if nil, derived from schema
|
|
315
403
|
# @param short [String] short option name
|
|
316
404
|
# @param allowed [Object] Allowed values, see `OptionValue`
|
|
317
405
|
# @param default [Object] default value
|
|
@@ -319,12 +407,9 @@ module Aspera
|
|
|
319
407
|
# @param deprecation [String] deprecation
|
|
320
408
|
# @param schema [String] Definition of schema for Hash parameters
|
|
321
409
|
# @param block [Proc] Block to execute when option is found
|
|
322
|
-
def declare(option_symbol, description, short: nil, allowed: nil, default: nil, handler: nil, deprecation: nil, schema: nil, &block)
|
|
410
|
+
def declare(option_symbol, description: nil, short: nil, allowed: nil, default: nil, handler: nil, deprecation: nil, schema: nil, &block)
|
|
323
411
|
Aspera.assert_type(option_symbol, Symbol)
|
|
324
412
|
Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
|
|
325
|
-
Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
|
|
326
|
-
Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with an uppercase"}
|
|
327
|
-
Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :allowed instead of hash/extended value in option description"}
|
|
328
413
|
Aspera.assert_type(handler, Hash) if handler
|
|
329
414
|
Aspera.assert(handler.keys.sort.eql?(%i[m o]), 'handler must have keys :m and :o') if handler
|
|
330
415
|
option_attrs = @declared_options[option_symbol] = OptionValue.new(
|
|
@@ -335,45 +420,44 @@ module Aspera
|
|
|
335
420
|
deprecation: deprecation,
|
|
336
421
|
schema: schema
|
|
337
422
|
)
|
|
338
|
-
|
|
339
|
-
description
|
|
340
|
-
description
|
|
423
|
+
option_attrs.group = @current_group
|
|
424
|
+
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"}
|
|
341
429
|
set_option(option_symbol, default, where: 'default') unless default.nil?
|
|
342
|
-
on_args = [description]
|
|
343
430
|
case option_attrs.types
|
|
344
431
|
when Allowed::TYPES_ENUM, Allowed::TYPES_BOOLEAN
|
|
345
432
|
# This option value must be a symbol (or array of symbols)
|
|
346
433
|
set_option(option_symbol, BoolValue.true?(default), where: 'default') if option_attrs.values.eql?(BoolValue::ALL) && !default.nil?
|
|
347
|
-
value = get_option(option_symbol)
|
|
348
|
-
help_values =
|
|
349
|
-
if option_attrs.types.eql?(Allowed::TYPES_BOOLEAN)
|
|
350
|
-
highlight_current_in_list(BoolValue::SYMBOLS, BoolValue.to_sym(value))
|
|
351
|
-
else
|
|
352
|
-
highlight_current_in_list(option_attrs.values, value)
|
|
353
|
-
end
|
|
354
|
-
on_args[0] = "#{description}: #{help_values}"
|
|
355
|
-
on_args.push(symbol_to_option(option_symbol, 'ENUM'))
|
|
356
|
-
# on_args.push(option_attrs.values)
|
|
357
|
-
@parser.on(*on_args) do |v|
|
|
358
|
-
set_option(option_symbol, self.class.get_from_list(v.to_s, description, option_attrs.values), where: SOURCE_USER)
|
|
359
|
-
end
|
|
360
434
|
when Allowed::TYPES_NONE
|
|
361
435
|
Aspera.assert_type(block, Proc){"missing execution block for #{option_symbol}"}
|
|
362
|
-
|
|
363
|
-
on_args.push("-#{short}") if short.is_a?(String)
|
|
364
|
-
@parser.on(*on_args, &block)
|
|
365
|
-
else
|
|
366
|
-
on_args.push(symbol_to_option(option_symbol, 'VALUE'))
|
|
367
|
-
on_args.push("-#{short}VALUE") unless short.nil?
|
|
368
|
-
# coerce integer
|
|
369
|
-
on_args.push(Integer) if option_attrs.types.eql?(Allowed::TYPES_INTEGER)
|
|
370
|
-
@parser.on(*on_args) do |v|
|
|
371
|
-
set_option(option_symbol, v, where: SOURCE_USER)
|
|
372
|
-
end
|
|
436
|
+
option_attrs.block = block
|
|
373
437
|
end
|
|
374
|
-
|
|
438
|
+
@short_options[short] = option_symbol unless short.nil?
|
|
439
|
+
Log.log.trace1{"declare: #{option_symbol}, group: #{@current_group}, short: #{short}"}
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Set the current help section group name for subsequent declarations
|
|
443
|
+
# @param name [String] group name, shown as section header in help text
|
|
444
|
+
def group(name)
|
|
445
|
+
@current_group = name
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Rename all options currently tagged with @current_group to a new name,
|
|
449
|
+
# then update @current_group. Used by add_manual_header when a plugin
|
|
450
|
+
# declares its options before its group name is known (e.g. Plugins::Config).
|
|
451
|
+
# @param name [String] new group name
|
|
452
|
+
def rename_current_group(name)
|
|
453
|
+
@declared_options.each_value{ |opt| opt.group = name if opt.group.eql?(@current_group)}
|
|
454
|
+
@current_group = name
|
|
375
455
|
end
|
|
376
456
|
|
|
457
|
+
# Low-level positional argument reader. Prefer +Base#resolve_argument+ from action methods.
|
|
458
|
+
# Direct calls from outside +Parser+ are legacy exceptions documented in ST12/ST13
|
|
459
|
+
# (mixins without DSL: sync_actions, ascp_actions; setup callbacks: aoc.rb).
|
|
460
|
+
# @api private
|
|
377
461
|
# @param descr [String] description for help
|
|
378
462
|
# @param mandatory [Boolean] `true`: raise error no more argument
|
|
379
463
|
# @param multiple [Boolean] `true`: return all remaining arguments (Array). String: until marker
|
|
@@ -381,7 +465,7 @@ module Aspera
|
|
|
381
465
|
# @param validation [Class, Array, NilClass] Accepted value type(s) or list of Symbols
|
|
382
466
|
# @param aliases [Hash] map of aliases: key = alias, value = real value
|
|
383
467
|
# @param default [Object] default value
|
|
384
|
-
# @return one value, list or nil (if optional and no default)
|
|
468
|
+
# @return [Object, Array, nil] one value, list or nil (if optional and no default)
|
|
385
469
|
def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: Allowed::TYPES_STRING, aliases: nil, default: nil, schema: nil)
|
|
386
470
|
Aspera.assert_array_all(accept_list, Symbol) unless accept_list.nil?
|
|
387
471
|
Aspera.assert_hash_all(aliases, Symbol, Symbol) unless aliases.nil?
|
|
@@ -443,14 +527,14 @@ module Aspera
|
|
|
443
527
|
#
|
|
444
528
|
# @param description [String] description of the identifier
|
|
445
529
|
# @param block [Proc] block to search for identifier based on attribute value
|
|
446
|
-
# @return
|
|
530
|
+
# @return [String, Array<String>] identifier or list of IDs (if `bulk` option is set)
|
|
447
531
|
# @yieldparam field [String] The field name from percent selector
|
|
448
532
|
# @yieldparam value [String] The value from percent selector
|
|
449
533
|
# @yieldreturn [String] Resolved identifier
|
|
450
534
|
def instance_identifier(description: 'identifier', &block)
|
|
451
|
-
res_id = get_next_argument(description, multiple: get_option(:bulk))
|
|
535
|
+
res_id = get_next_argument(description, multiple: get_option(:bulk))
|
|
452
536
|
# Can be an Array
|
|
453
|
-
if res_id.is_a?(String) && (m =
|
|
537
|
+
if res_id.is_a?(String) && (m = Parser.percent_selector(res_id))
|
|
454
538
|
Aspera.assert(block_given?, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
|
|
455
539
|
res_id = yield(m[:field], m[:value])
|
|
456
540
|
end
|
|
@@ -459,8 +543,18 @@ module Aspera
|
|
|
459
543
|
|
|
460
544
|
def get_next_command(command_list, aliases: nil); get_next_argument('command', accept_list: command_list, aliases: aliases); end
|
|
461
545
|
|
|
546
|
+
# Check whether an option has already been declared in this manager
|
|
547
|
+
# @param option_symbol [Symbol] name of the option
|
|
548
|
+
# @return [Boolean]
|
|
549
|
+
def option_declared?(option_symbol)
|
|
550
|
+
@declared_options.key?(option_symbol)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# @return [Hash{Symbol => OptionValue}] all declared options (read-only view)
|
|
554
|
+
attr_reader :declared_options
|
|
555
|
+
|
|
462
556
|
# Get an option definition by name
|
|
463
|
-
# @param option_symbol [Symbol]
|
|
557
|
+
# @param option_symbol [Symbol] name of the option
|
|
464
558
|
# @return [OptionValue] Option definition
|
|
465
559
|
# @raise [Cli::BadArgument] if option not found
|
|
466
560
|
def option_def(option_symbol)
|
|
@@ -471,12 +565,16 @@ module Aspera
|
|
|
471
565
|
# Get an option value by name
|
|
472
566
|
# either return value or calls handler, can return nil
|
|
473
567
|
# ask interactively if requested/required
|
|
474
|
-
# @param option_symbol [Symbol]
|
|
568
|
+
# @param option_symbol [Symbol] name of the option to retrieve
|
|
475
569
|
# @param mandatory [Boolean] if true, raise error if option not set
|
|
476
|
-
|
|
570
|
+
# @param schema [String, nil] contextual schema path override; when set, raises SchemaRequest
|
|
571
|
+
# if the option value is 'help' (used for --query whose schema depends on the current command)
|
|
572
|
+
def get_option(option_symbol, mandatory: false, schema: nil)
|
|
477
573
|
Aspera.assert_type(option_symbol, Symbol)
|
|
478
574
|
option_attrs = option_def(option_symbol)
|
|
479
575
|
result = option_attrs.value
|
|
576
|
+
# Contextual schema: raise SchemaRequest when value is 'help'
|
|
577
|
+
raise SchemaRequest.new(:option, option_symbol.to_s, schema) if schema && result.eql?(HELP)
|
|
480
578
|
# Do not fail for manual generation if option mandatory but not set
|
|
481
579
|
return :skip_missing_mandatory if result.nil? && mandatory && !@fail_on_missing_mandatory
|
|
482
580
|
if result.nil?
|
|
@@ -499,7 +597,10 @@ module Aspera
|
|
|
499
597
|
def set_option(option_symbol, value, where: 'code override')
|
|
500
598
|
Aspera.assert_type(option_symbol, Symbol)
|
|
501
599
|
option = option_def(option_symbol)
|
|
502
|
-
|
|
600
|
+
# Raise immediately only when the option has a static schema: the schema is known at parse time.
|
|
601
|
+
# When schema is nil (e.g. --query), 'help' is stored as-is and SchemaRequest is raised later
|
|
602
|
+
# in get_option() with the contextual schema provided by the calling command.
|
|
603
|
+
raise SchemaRequest.new(:option, option.option, option.schema) if option.types&.include?(Hash) && value.eql?(HELP) && option.schema
|
|
503
604
|
option.assign_value(value, where: where)
|
|
504
605
|
end
|
|
505
606
|
|
|
@@ -509,6 +610,18 @@ module Aspera
|
|
|
509
610
|
option_def(option_symbol).clear
|
|
510
611
|
end
|
|
511
612
|
|
|
613
|
+
# Bind (or re-bind) a runtime handler to an already-declared option.
|
|
614
|
+
# Called from plugin initialize() for Category C handlers whose target object
|
|
615
|
+
# (e.g. @gen_options) is created after class-load time.
|
|
616
|
+
# @param option_symbol [Symbol] name of the already-declared option
|
|
617
|
+
# @param object [Object] the target object for get/set delegation
|
|
618
|
+
# @param method [Symbol] accessor method name on object
|
|
619
|
+
# @return [nil]
|
|
620
|
+
def set_handler(option_symbol, object:, method:)
|
|
621
|
+
Aspera.assert_type(option_symbol, Symbol)
|
|
622
|
+
option_def(option_symbol).bind_handler(o: object, m: method)
|
|
623
|
+
end
|
|
624
|
+
|
|
512
625
|
# Adds each of the keys of specified hash as an option
|
|
513
626
|
# @param preset_hash [Hash] Options to add
|
|
514
627
|
# @param where [String] Where the value comes from
|
|
@@ -576,37 +689,52 @@ module Aspera
|
|
|
576
689
|
def parse_options!
|
|
577
690
|
Log.log.trace1('parse_options!'.red)
|
|
578
691
|
# First options from conf file
|
|
579
|
-
consume_option_pairs(@option_pairs_batch, 'set')
|
|
692
|
+
@option_pairs_batch = consume_option_pairs(@option_pairs_batch, 'set')
|
|
580
693
|
# Then, env var (to override)
|
|
581
|
-
consume_option_pairs(@option_pairs_env, 'env')
|
|
582
|
-
# Then, command line override
|
|
694
|
+
@option_pairs_env = consume_option_pairs(@option_pairs_env, 'env')
|
|
695
|
+
# Then, command line override: process one option at a time so that @current_option_args_offset
|
|
696
|
+
# can be set before each option is evaluated (used by `@:` extended value in option values).
|
|
583
697
|
unknown_options = []
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
698
|
+
Log.log.trace1('Before parse')
|
|
699
|
+
Log.dump(:unprocessed_cmd_line_options, @unprocessed_cmd_line_options, level: :trace1)
|
|
700
|
+
until @unprocessed_cmd_line_options.empty?
|
|
701
|
+
opt = @unprocessed_cmd_line_options.shift
|
|
702
|
+
# Expose args_before for this option so `args_as_extended` can skip args preceding it.
|
|
703
|
+
# Peek (first) without consuming - consumed only if this option is processed (not deferred).
|
|
704
|
+
@current_option_args_offset = @args_before_option[opt]&.first
|
|
705
|
+
if opt.start_with?(OPTION_PREFIX)
|
|
706
|
+
# Long option: --name or --name=value
|
|
707
|
+
name_raw, raw_value = opt.delete_prefix(OPTION_PREFIX).split(OPTION_VALUE_SEPARATOR, 2)
|
|
708
|
+
option_sym = self.class.option_line_to_name(name_raw).to_sym
|
|
709
|
+
resolved_sym = self.class.match_prefix(option_sym, @declared_options.keys)
|
|
710
|
+
if resolved_sym
|
|
711
|
+
dispatch_option(resolved_sym, raw_value)
|
|
712
|
+
@args_before_option[opt]&.shift # consumed: advance to next occurrence
|
|
713
|
+
else
|
|
714
|
+
# Dotted notation: --a.b.c=d does: a={"b":{"c":ext_val(d)}}
|
|
715
|
+
Log.log.trace1{"Unknown long option: #{opt}".red}
|
|
716
|
+
if !raw_value.nil?
|
|
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
|
|
603
724
|
end
|
|
725
|
+
# Unknown option: defer to next parse_options! round, do not consume the recorded offset
|
|
726
|
+
unknown_options.push(opt)
|
|
604
727
|
end
|
|
728
|
+
elsif opt.start_with?('-') && (option_sym = @short_options[opt[1]])
|
|
729
|
+
# Short option: -h, -v, or -Pvalue (value glued to flag)
|
|
730
|
+
dispatch_option(option_sym, opt.length > 2 ? opt[2..] : nil)
|
|
731
|
+
@args_before_option[opt]&.shift # consumed: advance to next occurrence
|
|
732
|
+
else
|
|
733
|
+
unknown_options.push(opt)
|
|
605
734
|
end
|
|
606
|
-
# Save for later processing
|
|
607
|
-
unknown_options.push(e.args.first)
|
|
608
|
-
retry
|
|
609
735
|
end
|
|
736
|
+
@current_option_args_offset = nil
|
|
737
|
+
Log.log.trace1('After parse')
|
|
610
738
|
Log.log.trace1{"remains: #{unknown_options}"}
|
|
611
739
|
# Set unprocessed options for next time
|
|
612
740
|
@unprocessed_cmd_line_options = unknown_options
|
|
@@ -674,21 +802,85 @@ module Aspera
|
|
|
674
802
|
# This extended value does not take args (`@:`)
|
|
675
803
|
# ExtendedValue.assert_no_value(end_marker, :p)
|
|
676
804
|
end_marker = SpecialValues::EOA if end_marker.empty?
|
|
805
|
+
# When called from an option value, skip positional args that appear before the option in argv.
|
|
806
|
+
# @current_option_args_offset holds the number of original args before the option (nil = positional context).
|
|
807
|
+
# The number to actually skip = args_before_option - args_already_consumed (clamped to 0).
|
|
808
|
+
skip_count = if @current_option_args_offset
|
|
809
|
+
[@current_option_args_offset - (@arg_total_count - @unprocessed_cmd_line_arguments.length), 0].max
|
|
810
|
+
else
|
|
811
|
+
0
|
|
812
|
+
end
|
|
813
|
+
skipped = skip_count.positive? ? @unprocessed_cmd_line_arguments.shift(skip_count) : []
|
|
814
|
+
Log.log.trace1{"args_as_extended: skipping #{skipped.length} args before option: #{skipped}"} unless skipped.empty?
|
|
677
815
|
result = nil
|
|
678
816
|
get_next_argument('args', multiple: end_marker).each do |argument|
|
|
679
817
|
Aspera.assert(argument.include?(OPTION_VALUE_SEPARATOR)){"Positional argument: #{argument} does not include #{OPTION_VALUE_SEPARATOR}"}
|
|
680
818
|
path, value = argument.split(OPTION_VALUE_SEPARATOR, 2)
|
|
681
819
|
result = DotContainer.dotted_to_container(path.split(DotContainer::SEPARATOR), smart_convert(value), result)
|
|
682
820
|
end
|
|
821
|
+
# Restore skipped args so they remain available for command dispatching
|
|
822
|
+
@unprocessed_cmd_line_arguments.unshift(*skipped) unless skipped.empty?
|
|
683
823
|
result
|
|
684
824
|
end
|
|
685
825
|
|
|
826
|
+
# Generate help text for all declared options, grouped by section.
|
|
827
|
+
# @param banner [String, nil] Optional banner text to prepend
|
|
828
|
+
# @return [String] Formatted help text
|
|
829
|
+
def help_text(banner: nil)
|
|
830
|
+
rows = []
|
|
831
|
+
current_group = nil
|
|
832
|
+
@declared_options.each do |sym, opt|
|
|
833
|
+
if opt.group != current_group
|
|
834
|
+
current_group = opt.group
|
|
835
|
+
rows << [{value: "OPTIONS: #{current_group}", colspan: 2}]
|
|
836
|
+
end
|
|
837
|
+
short_char = @short_options.key(sym)
|
|
838
|
+
short_part = short_char ? "-#{short_char}, " : ' '
|
|
839
|
+
flag = "#{short_part}#{symbol_to_option(sym, option_display_value(opt))}"
|
|
840
|
+
rows << [flag, opt.description]
|
|
841
|
+
end
|
|
842
|
+
table = Terminal::Table.new(rows: rows, style: {border: HELP_BORDER, padding_left: 0, padding_right: 2})
|
|
843
|
+
banner.nil? ? table.to_s : "#{banner}\n#{table}"
|
|
844
|
+
end
|
|
845
|
+
|
|
686
846
|
# ======================================================
|
|
687
847
|
private
|
|
688
848
|
|
|
849
|
+
# AsciiBorder with all visible characters removed - used by help_text
|
|
850
|
+
HELP_BORDER = Terminal::Table::AsciiBorder.new.tap do |b|
|
|
851
|
+
b.top = false
|
|
852
|
+
b.bottom = false
|
|
853
|
+
b.left = false
|
|
854
|
+
b.right = false
|
|
855
|
+
b.remove_verticals
|
|
856
|
+
b.remove_horizontals
|
|
857
|
+
end.freeze
|
|
858
|
+
|
|
859
|
+
# @param opt [OptionValue] option descriptor
|
|
860
|
+
# @return [String, nil] placeholder shown in flag column: 'ENUM', 'VALUE', or nil for flag switches
|
|
861
|
+
def option_display_value(opt)
|
|
862
|
+
case opt.types
|
|
863
|
+
when Allowed::TYPES_NONE then nil
|
|
864
|
+
when Allowed::TYPES_ENUM, Allowed::TYPES_BOOLEAN then 'ENUM'
|
|
865
|
+
else 'VALUE'
|
|
866
|
+
end
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
# Dispatch a parsed CLI option to its handler.
|
|
870
|
+
# @param sym [Symbol] option symbol
|
|
871
|
+
# @param raw_value [String, nil] raw string value from command line, or nil for flag switches
|
|
872
|
+
def dispatch_option(sym, raw_value)
|
|
873
|
+
opt = @declared_options[sym]
|
|
874
|
+
if opt.types.eql?(Allowed::TYPES_NONE)
|
|
875
|
+
opt.block.call
|
|
876
|
+
else
|
|
877
|
+
set_option(sym, raw_value, where: SOURCE_USER)
|
|
878
|
+
end
|
|
879
|
+
end
|
|
880
|
+
|
|
689
881
|
# Using dotted hash notation, convert value to bool, int, float or extended value
|
|
690
882
|
# @param value [String] The value to convert to appropriate type
|
|
691
|
-
# @return the converted value
|
|
883
|
+
# @return [Boolean, Integer, Float, String, Array, Hash] the converted value
|
|
692
884
|
def smart_convert(value)
|
|
693
885
|
case value
|
|
694
886
|
when 'true' then true
|
|
@@ -725,23 +917,23 @@ module Aspera
|
|
|
725
917
|
# Try to evaluate options set in batch
|
|
726
918
|
# @param unprocessed_options [Array] list of options to apply (key_sym,value)
|
|
727
919
|
# @param where [String] where the options come from
|
|
728
|
-
|
|
920
|
+
# Apply all known options from the given pairs hash and return the remaining (unknown) pairs.
|
|
921
|
+
# Pure: does not mutate the argument; the caller is responsible for storing the returned value.
|
|
922
|
+
# @param option_pairs [Hash{Symbol => Object}] candidate key/value pairs
|
|
923
|
+
# @param where [String] label used in log messages and error context
|
|
924
|
+
# @return [Hash{Symbol => Object}] pairs whose keys were not yet declared (deferred to next round)
|
|
925
|
+
def consume_option_pairs(option_pairs, where)
|
|
729
926
|
Log.log.trace1{"consume_option_pairs: #{where}"}
|
|
730
|
-
|
|
731
|
-
|
|
927
|
+
remaining = {}
|
|
928
|
+
option_pairs.each do |k, v|
|
|
732
929
|
if @declared_options.key?(k)
|
|
733
|
-
|
|
734
|
-
v = self.class.get_from_list(v, "#{k} in #{where}", @declared_options[k].values) if @declared_options[k].values && v.is_a?(String)
|
|
735
|
-
options_to_set[k] = v
|
|
930
|
+
set_option(k, v, where: where)
|
|
736
931
|
else
|
|
737
932
|
Log.log.trace1{"unprocessed: #{k}: #{v}"}
|
|
933
|
+
remaining[k] = v
|
|
738
934
|
end
|
|
739
935
|
end
|
|
740
|
-
|
|
741
|
-
set_option(k, v, where: where)
|
|
742
|
-
# keep only unprocessed values for next parse
|
|
743
|
-
unprocessed_options.delete(k)
|
|
744
|
-
end
|
|
936
|
+
remaining
|
|
745
937
|
end
|
|
746
938
|
|
|
747
939
|
# Option name separator on command line, e.g. in --option-blah, third "-"
|
|
@@ -760,7 +952,7 @@ module Aspera
|
|
|
760
952
|
# Ask for schema of Extended value
|
|
761
953
|
HELP = 'help'
|
|
762
954
|
|
|
763
|
-
private_constant :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER, :REGEX_LOOKUP_ID_BY_FIELD
|
|
955
|
+
private_constant :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER, :REGEX_LOOKUP_ID_BY_FIELD, :HELP_BORDER
|
|
764
956
|
end
|
|
765
957
|
end
|
|
766
958
|
end
|