aspera-cli 4.26.1 → 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 +300 -22
- data/CONTRIBUTING.md +100 -12
- data/bin/ascli +5 -5
- 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 +27 -25
- data/lib/aspera/agent/factory.rb +4 -0
- data/lib/aspera/agent/httpgw.rb +2 -2
- data/lib/aspera/agent/node.rb +34 -2
- data/lib/aspera/agent/transferd.rb +39 -3
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +56 -51
- data/lib/aspera/api/cos_node.rb +2 -2
- data/lib/aspera/api/faspex.rb +22 -20
- data/lib/aspera/api/httpgw.rb +4 -4
- data/lib/aspera/api/node.rb +33 -30
- 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 +25 -23
- data/lib/aspera/ascp/installation.rb +43 -40
- 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 +71 -0
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +5 -3
- data/lib/aspera/cli/formatter.rb +122 -209
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +2 -2
- data/lib/aspera/cli/http.rb +218 -0
- 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} +337 -144
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1173 -849
- data/lib/aspera/cli/plugins/ats.rb +200 -161
- data/lib/aspera/cli/plugins/base.rb +470 -107
- data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
- data/lib/aspera/cli/plugins/config.rb +434 -1032
- 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 +7 -4
- data/lib/aspera/cli/plugins/faspex.rb +296 -259
- data/lib/aspera/cli/plugins/faspex5.rb +592 -344
- 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 +1001 -797
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +111 -139
- data/lib/aspera/cli/plugins/preview.rb +183 -142
- data/lib/aspera/cli/plugins/server.rb +125 -80
- 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 +236 -0
- data/lib/aspera/cli/result.rb +360 -0
- data/lib/aspera/cli/runner.rb +473 -0
- 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 +84 -56
- data/lib/aspera/cli/terminal_formatter.rb +65 -0
- 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 +9 -9
- data/lib/aspera/cli/vault_manager.rb +57 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +26 -26
- data/lib/aspera/command_line_builder.rb +23 -23
- data/lib/aspera/data_repository.rb +6 -1
- data/lib/aspera/dot_container.rb +5 -5
- data/lib/aspera/environment.rb +39 -13
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +2 -2
- 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/base.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +2 -2
- data/lib/aspera/keychain/factory.rb +4 -4
- data/lib/aspera/keychain/macos_security.rb +4 -4
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +23 -5
- data/lib/aspera/markdown.rb +85 -2
- data/lib/aspera/nagios.rb +2 -2
- data/lib/aspera/node_simulator.rb +3 -1
- data/lib/aspera/oauth/base.rb +10 -10
- data/lib/aspera/oauth/boot.rb +43 -0
- data/lib/aspera/oauth/factory.rb +41 -15
- data/lib/aspera/oauth/jwt.rb +4 -4
- data/lib/aspera/oauth/web.rb +4 -4
- data/lib/aspera/oauth.rb +1 -0
- data/lib/aspera/persistency_action_once.rb +10 -10
- data/lib/aspera/persistency_folder.rb +10 -3
- data/lib/aspera/preview/file_types.rb +7 -3
- data/lib/aspera/preview/generator.rb +3 -3
- data/lib/aspera/preview/utils.rb +1 -1
- data/lib/aspera/products/connect.rb +4 -1
- data/lib/aspera/products/other.rb +1 -1
- data/lib/aspera/products/transferd.rb +3 -3
- data/lib/aspera/proxy_auto_config.rb +10 -9
- data/lib/aspera/rest.rb +18 -40
- data/lib/aspera/rest_error_analyzer.rb +7 -3
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +13 -6
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +14 -4
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +2730 -648
- data/lib/aspera/schema/async_tables.yaml +361 -0
- data/lib/aspera/schema/documentation.rb +57 -26
- data/lib/aspera/schema/reader.rb +60 -11
- data/lib/aspera/schema/registry.rb +50 -10
- data/lib/aspera/secret_hider.rb +11 -2
- data/lib/aspera/ssl.rb +4 -4
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/database.rb +14 -10
- data/lib/aspera/sync/operations.rb +15 -18
- data/lib/aspera/temp_file_manager.rb +5 -1
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +27 -25
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +7 -7
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +26 -8
- data/lib/aspera/transfer/uri.rb +1 -1
- 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 +45 -16
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/main.rb +0 -480
- data/lib/aspera/json_rpc.rb +0 -51
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
require 'aspera/cli/extended_value'
|
|
4
4
|
require 'aspera/cli/error'
|
|
5
|
+
require 'aspera/cli/terminal_formatter'
|
|
5
6
|
require 'aspera/colors'
|
|
6
7
|
require 'aspera/secret_hider'
|
|
7
8
|
require 'aspera/log'
|
|
8
9
|
require 'aspera/assert'
|
|
9
10
|
require 'aspera/dot_container'
|
|
11
|
+
require 'aspera/schema/registry'
|
|
10
12
|
require 'io/console'
|
|
11
|
-
require '
|
|
13
|
+
require 'terminal-table'
|
|
12
14
|
|
|
13
15
|
module Aspera
|
|
14
16
|
module Cli
|
|
@@ -39,7 +41,7 @@ module Aspera
|
|
|
39
41
|
# `false` and `true`
|
|
40
42
|
TYPES = [FalseClass, TrueClass].freeze
|
|
41
43
|
SYMBOLS = [NO_SYM, YES_SYM].freeze
|
|
42
|
-
# @return `true` if value is a value for `true` in ALL
|
|
44
|
+
# @return [Boolean] `true` if value is a value for `true` in ALL
|
|
43
45
|
def true?(enum)
|
|
44
46
|
Aspera.assert_values(enum, ALL){'boolean'}
|
|
45
47
|
TRUE_VALUES.include?(enum)
|
|
@@ -51,41 +53,49 @@ module Aspera
|
|
|
51
53
|
TRUE_VALUES.include?(enum) ? YES_SYM : NO_SYM
|
|
52
54
|
end
|
|
53
55
|
|
|
54
|
-
# @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
|
|
55
57
|
def symbol?(sym)
|
|
56
58
|
ALL.include?(sym)
|
|
57
59
|
end
|
|
58
60
|
module_function :true?, :to_sym, :symbol?
|
|
59
61
|
end
|
|
60
62
|
|
|
61
|
-
# 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
|
|
62
68
|
module Allowed
|
|
63
69
|
# This option can be set to a single string or array, multiple times, and gives Array of String
|
|
64
70
|
TYPES_STRING_ARRAY = [Array, String].freeze
|
|
65
|
-
# A list of symbols with constrained values
|
|
71
|
+
# A list of symbols with constrained values; use as prefix: TYPES_SYMBOL_ARRAY + [:val1, :val2]
|
|
66
72
|
TYPES_SYMBOL_ARRAY = [Array, Symbol].freeze
|
|
67
73
|
# Value will be coerced to int
|
|
68
74
|
TYPES_INTEGER = [Integer].freeze
|
|
69
75
|
TYPES_BOOLEAN = BoolValue::TYPES
|
|
70
76
|
# No value at all for the option, it's a switch, like `-N`
|
|
71
77
|
TYPES_NONE = [].freeze
|
|
72
|
-
# Symbol
|
|
73
|
-
TYPES_ENUM
|
|
74
|
-
# 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
|
|
75
81
|
TYPES_STRING = [String].freeze
|
|
76
82
|
end
|
|
77
83
|
|
|
78
84
|
# Description of option, how to manage
|
|
79
85
|
class OptionValue
|
|
80
86
|
# [Array(Class)] List of allowed types
|
|
81
|
-
attr_reader :types, :sensitive, :schema, :option
|
|
87
|
+
attr_reader :types, :sensitive, :schema, :option, :deprecation
|
|
82
88
|
# [Array] List of allowed values (Symbols and specific values)
|
|
83
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
|
|
84
94
|
|
|
85
95
|
# @param option [Symbol] Name of option
|
|
86
|
-
# @param description [String] Description for help
|
|
96
|
+
# @param description [String, nil] Description for help; if nil, derived from schema
|
|
87
97
|
# @param allowed [nil,Class,Array<Class>,Array<Symbol>] Allowed values
|
|
88
|
-
# @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
|
|
89
99
|
# @param deprecation [String] Deprecation message
|
|
90
100
|
# @param schema [String] Declaration of schema
|
|
91
101
|
# `allowed`:
|
|
@@ -93,28 +103,34 @@ module Aspera
|
|
|
93
103
|
# - `Class` The single allowed Class
|
|
94
104
|
# - `Array<Class>` Multiple allowed classes
|
|
95
105
|
# - `Array<Symbol>` List of allowed values
|
|
96
|
-
def initialize(option:, description
|
|
106
|
+
def initialize(option:, description: nil, allowed: Allowed::TYPES_STRING, handler: nil, deprecation: nil, schema: nil)
|
|
97
107
|
Log.log.trace1{"option: #{option}, allowed: #{allowed}"}
|
|
98
108
|
@option = option
|
|
99
109
|
@description = description
|
|
110
|
+
@group = nil
|
|
111
|
+
@block = nil
|
|
100
112
|
# by default passwords and secrets are sensitive, else specify when declaring the option
|
|
101
113
|
@sensitive = SecretHider.instance.secret?(@option, '')
|
|
102
|
-
# either the value, or object giving value
|
|
103
|
-
@object = handler&.[](:o)
|
|
104
|
-
@read_method = handler&.[](:m)
|
|
105
|
-
@write_method = @read_method ? "#{@read_method}=".to_sym : nil
|
|
106
114
|
@deprecation = deprecation
|
|
107
115
|
@schema = schema
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
end
|
|
115
|
-
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?
|
|
116
122
|
@types = nil
|
|
117
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
|
|
118
134
|
if !allowed.nil?
|
|
119
135
|
allowed = [allowed] if allowed.is_a?(Class)
|
|
120
136
|
Aspera.assert_type(allowed, Array)
|
|
@@ -122,12 +138,17 @@ module Aspera
|
|
|
122
138
|
# Special case: array of defined symbol values
|
|
123
139
|
@types = Allowed::TYPES_SYMBOL_ARRAY
|
|
124
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?
|
|
125
143
|
elsif allowed.all?(Class)
|
|
126
144
|
@types = allowed
|
|
127
145
|
@values = BoolValue::ALL if allowed.eql?(Allowed::TYPES_BOOLEAN)
|
|
128
|
-
# Default value for array
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
131
152
|
elsif allowed.all?(Symbol)
|
|
132
153
|
@types = Allowed::TYPES_ENUM
|
|
133
154
|
@values = allowed
|
|
@@ -138,6 +159,40 @@ module Aspera
|
|
|
138
159
|
Log.log.trace1{"declare: #{@option}: #{@access} #{@object.class}.#{@read_method}".green}
|
|
139
160
|
end
|
|
140
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
|
+
|
|
141
196
|
def clear
|
|
142
197
|
@object = nil
|
|
143
198
|
end
|
|
@@ -157,22 +212,42 @@ module Aspera
|
|
|
157
212
|
# Value can be a `String`, then evaluated with `ExtendedValue`, or directly a value.
|
|
158
213
|
# @param value [String, Object] Value to assign to option
|
|
159
214
|
# @param where [String] Where the value is assigned from
|
|
215
|
+
# @param warn_deprecation [Boolean] Emit deprecation warning (false for internal transfers)
|
|
160
216
|
# @return [nil]
|
|
161
|
-
def assign_value(value, where:)
|
|
162
|
-
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
|
|
163
219
|
new_value = ExtendedValue.instance.evaluate(value, context: "option: #{@option}", allowed: @types)
|
|
164
220
|
Log.log.trace1{"#{where}: #{@option} <- (#{new_value.class})#{new_value}"}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
173
234
|
new_value = [new_value] if new_value.is_a?(String)
|
|
174
235
|
Aspera.assert_array_all(new_value, String, type: BadArgument)
|
|
175
|
-
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
|
|
176
251
|
end
|
|
177
252
|
Aspera.assert_type(new_value, *@types, type: BadArgument){"Option #{@option}"} if @types
|
|
178
253
|
if new_value.is_a?(Hash) || new_value.is_a?(Array)
|
|
@@ -193,7 +268,7 @@ module Aspera
|
|
|
193
268
|
# parse command line options
|
|
194
269
|
# arguments options start with '-', others are commands
|
|
195
270
|
# resolves on extended value syntax
|
|
196
|
-
class
|
|
271
|
+
class Parser
|
|
197
272
|
class << self
|
|
198
273
|
# Find shortened string value in allowed symbol list
|
|
199
274
|
def get_from_list(short_value, descr, allowed_values)
|
|
@@ -202,12 +277,21 @@ module Aspera
|
|
|
202
277
|
matching_exact = allowed_values.select{ |i| i.to_s.eql?(short_value)}
|
|
203
278
|
return matching_exact.first if matching_exact.length == 1
|
|
204
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)
|
|
205
281
|
Aspera.assert(!matching.empty?, multi_choice_assert_msg("unknown value for #{descr}: #{short_value}", allowed_values), type: BadArgument)
|
|
206
282
|
Aspera.assert(matching.length.eql?(1), multi_choice_assert_msg("ambiguous shortcut for #{descr}: #{short_value}", matching), type: BadArgument)
|
|
207
283
|
return BoolValue.true?(matching.first) if allowed_values.eql?(BoolValue::ALL)
|
|
208
284
|
matching.first
|
|
209
285
|
end
|
|
210
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
|
+
|
|
211
295
|
# Generates error message with list of allowed values
|
|
212
296
|
# @param error_msg [String] Error message
|
|
213
297
|
# @param accept_list [Array<Symbol>] List of allowed values
|
|
@@ -216,8 +300,8 @@ module Aspera
|
|
|
216
300
|
end
|
|
217
301
|
|
|
218
302
|
# Change option name with dash to name with underscore
|
|
219
|
-
# @param name [String] option name
|
|
220
|
-
# @return [String]
|
|
303
|
+
# @param name [String] option name with dash separators
|
|
304
|
+
# @return [String] option name with underscore separators
|
|
221
305
|
def option_line_to_name(name)
|
|
222
306
|
name.gsub(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
|
|
223
307
|
end
|
|
@@ -236,19 +320,12 @@ module Aspera
|
|
|
236
320
|
end
|
|
237
321
|
end
|
|
238
322
|
|
|
239
|
-
|
|
240
|
-
attr_accessor :ask_missing_mandatory, :ask_missing_optional
|
|
323
|
+
attr_accessor :ask_missing_mandatory, :ask_missing_optional, :help_requested
|
|
241
324
|
attr_writer :fail_on_missing_mandatory
|
|
242
325
|
|
|
243
326
|
# @param program_name [String] Name of the program
|
|
244
327
|
# @param argv [Array<String>, nil] Command line arguments to parse
|
|
245
328
|
def initialize(program_name, argv = nil)
|
|
246
|
-
# command line values *not* starting with '-'
|
|
247
|
-
@unprocessed_cmd_line_arguments = []
|
|
248
|
-
# command line values starting with at least one '-'
|
|
249
|
-
@unprocessed_cmd_line_options = []
|
|
250
|
-
# a copy of all initial options
|
|
251
|
-
@initial_cli_options = []
|
|
252
329
|
# Option descriptions: maps option symbol to its OptionValue descriptor
|
|
253
330
|
# @type [Hash{Symbol => OptionValue}]
|
|
254
331
|
@declared_options = {}
|
|
@@ -258,45 +335,57 @@ module Aspera
|
|
|
258
335
|
@ask_missing_optional = false
|
|
259
336
|
# get_option fails if a mandatory parameter is asked
|
|
260
337
|
@fail_on_missing_mandatory = true
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
#
|
|
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
|
|
264
341
|
@option_pairs_batch = {}
|
|
265
342
|
@option_pairs_env = {}
|
|
266
|
-
#
|
|
267
|
-
@
|
|
268
|
-
|
|
269
|
-
|
|
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'
|
|
270
347
|
env_prefix = program_name.upcase + OPTION_SEP_SYMBOL
|
|
271
348
|
ENV.each do |k, v|
|
|
272
349
|
@option_pairs_env[k.delete_prefix(env_prefix).downcase.to_sym] = v if k.start_with?(env_prefix)
|
|
273
350
|
end
|
|
274
351
|
Log.log.debug{"env=#{@option_pairs_env}".red}
|
|
352
|
+
# command line values starting with at least one '-'
|
|
275
353
|
@unprocessed_cmd_line_options = []
|
|
354
|
+
# command line values *not* starting with '-'
|
|
276
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 = {}
|
|
277
362
|
return if argv.nil?
|
|
278
363
|
# true until `--` is found (stop options)
|
|
279
364
|
process_options = true
|
|
280
|
-
|
|
281
|
-
|
|
365
|
+
arg_count = 0
|
|
366
|
+
argv.each do |value|
|
|
282
367
|
if process_options && value.start_with?('-')
|
|
283
368
|
Log.log.trace1{"opt: #{value}"}
|
|
284
369
|
if value.eql?(OPTIONS_STOP)
|
|
285
370
|
process_options = false
|
|
286
371
|
else
|
|
287
372
|
@unprocessed_cmd_line_options.push(value)
|
|
373
|
+
(@args_before_option[value] ||= []).push(arg_count)
|
|
288
374
|
end
|
|
289
375
|
else
|
|
290
376
|
Log.log.trace1{"arg: #{value}"}
|
|
291
377
|
@unprocessed_cmd_line_arguments.push(value)
|
|
378
|
+
arg_count += 1
|
|
292
379
|
end
|
|
293
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
|
|
294
385
|
@initial_cli_options = @unprocessed_cmd_line_options.dup.freeze
|
|
295
386
|
Log.log.trace1{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
declare(:interactive, 'Use interactive input of missing params', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :ask_missing_mandatory})
|
|
299
|
-
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})
|
|
300
389
|
# do not parse options yet, let's wait for option `-h` to be overridden
|
|
301
390
|
end
|
|
302
391
|
|
|
@@ -310,7 +399,7 @@ module Aspera
|
|
|
310
399
|
|
|
311
400
|
# Declare an option
|
|
312
401
|
# @param option_symbol [Symbol] option name
|
|
313
|
-
# @param description [String] description for help
|
|
402
|
+
# @param description [String, nil] description for help; if nil, derived from schema
|
|
314
403
|
# @param short [String] short option name
|
|
315
404
|
# @param allowed [Object] Allowed values, see `OptionValue`
|
|
316
405
|
# @param default [Object] default value
|
|
@@ -318,14 +407,11 @@ module Aspera
|
|
|
318
407
|
# @param deprecation [String] deprecation
|
|
319
408
|
# @param schema [String] Definition of schema for Hash parameters
|
|
320
409
|
# @param block [Proc] Block to execute when option is found
|
|
321
|
-
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)
|
|
322
411
|
Aspera.assert_type(option_symbol, Symbol)
|
|
323
412
|
Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
|
|
324
|
-
Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
|
|
325
|
-
Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with an uppercase"}
|
|
326
|
-
Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :allowed instead of hash/extended value in option description"}
|
|
327
413
|
Aspera.assert_type(handler, Hash) if handler
|
|
328
|
-
Aspera.assert(handler.keys.sort.eql?(%i[m o])) if handler
|
|
414
|
+
Aspera.assert(handler.keys.sort.eql?(%i[m o]), 'handler must have keys :m and :o') if handler
|
|
329
415
|
option_attrs = @declared_options[option_symbol] = OptionValue.new(
|
|
330
416
|
option: option_symbol,
|
|
331
417
|
description: description,
|
|
@@ -334,45 +420,44 @@ module Aspera
|
|
|
334
420
|
deprecation: deprecation,
|
|
335
421
|
schema: schema
|
|
336
422
|
)
|
|
337
|
-
|
|
338
|
-
description
|
|
339
|
-
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"}
|
|
340
429
|
set_option(option_symbol, default, where: 'default') unless default.nil?
|
|
341
|
-
on_args = [description]
|
|
342
430
|
case option_attrs.types
|
|
343
431
|
when Allowed::TYPES_ENUM, Allowed::TYPES_BOOLEAN
|
|
344
432
|
# This option value must be a symbol (or array of symbols)
|
|
345
433
|
set_option(option_symbol, BoolValue.true?(default), where: 'default') if option_attrs.values.eql?(BoolValue::ALL) && !default.nil?
|
|
346
|
-
value = get_option(option_symbol)
|
|
347
|
-
help_values =
|
|
348
|
-
if option_attrs.types.eql?(Allowed::TYPES_BOOLEAN)
|
|
349
|
-
highlight_current_in_list(BoolValue::SYMBOLS, BoolValue.to_sym(value))
|
|
350
|
-
else
|
|
351
|
-
highlight_current_in_list(option_attrs.values, value)
|
|
352
|
-
end
|
|
353
|
-
on_args[0] = "#{description}: #{help_values}"
|
|
354
|
-
on_args.push(symbol_to_option(option_symbol, 'ENUM'))
|
|
355
|
-
# on_args.push(option_attrs.values)
|
|
356
|
-
@parser.on(*on_args) do |v|
|
|
357
|
-
set_option(option_symbol, self.class.get_from_list(v.to_s, description, option_attrs.values), where: SOURCE_USER)
|
|
358
|
-
end
|
|
359
434
|
when Allowed::TYPES_NONE
|
|
360
435
|
Aspera.assert_type(block, Proc){"missing execution block for #{option_symbol}"}
|
|
361
|
-
|
|
362
|
-
on_args.push("-#{short}") if short.is_a?(String)
|
|
363
|
-
@parser.on(*on_args, &block)
|
|
364
|
-
else
|
|
365
|
-
on_args.push(symbol_to_option(option_symbol, 'VALUE'))
|
|
366
|
-
on_args.push("-#{short}VALUE") unless short.nil?
|
|
367
|
-
# coerce integer
|
|
368
|
-
on_args.push(Integer) if option_attrs.types.eql?(Allowed::TYPES_INTEGER)
|
|
369
|
-
@parser.on(*on_args) do |v|
|
|
370
|
-
set_option(option_symbol, v, where: SOURCE_USER)
|
|
371
|
-
end
|
|
436
|
+
option_attrs.block = block
|
|
372
437
|
end
|
|
373
|
-
|
|
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
|
|
374
455
|
end
|
|
375
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
|
|
376
461
|
# @param descr [String] description for help
|
|
377
462
|
# @param mandatory [Boolean] `true`: raise error no more argument
|
|
378
463
|
# @param multiple [Boolean] `true`: return all remaining arguments (Array). String: until marker
|
|
@@ -380,7 +465,7 @@ module Aspera
|
|
|
380
465
|
# @param validation [Class, Array, NilClass] Accepted value type(s) or list of Symbols
|
|
381
466
|
# @param aliases [Hash] map of aliases: key = alias, value = real value
|
|
382
467
|
# @param default [Object] default value
|
|
383
|
-
# @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)
|
|
384
469
|
def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: Allowed::TYPES_STRING, aliases: nil, default: nil, schema: nil)
|
|
385
470
|
Aspera.assert_array_all(accept_list, Symbol) unless accept_list.nil?
|
|
386
471
|
Aspera.assert_hash_all(aliases, Symbol, Symbol) unless aliases.nil?
|
|
@@ -442,14 +527,14 @@ module Aspera
|
|
|
442
527
|
#
|
|
443
528
|
# @param description [String] description of the identifier
|
|
444
529
|
# @param block [Proc] block to search for identifier based on attribute value
|
|
445
|
-
# @return
|
|
530
|
+
# @return [String, Array<String>] identifier or list of IDs (if `bulk` option is set)
|
|
446
531
|
# @yieldparam field [String] The field name from percent selector
|
|
447
532
|
# @yieldparam value [String] The value from percent selector
|
|
448
533
|
# @yieldreturn [String] Resolved identifier
|
|
449
534
|
def instance_identifier(description: 'identifier', &block)
|
|
450
|
-
res_id = get_next_argument(description, multiple: get_option(:bulk))
|
|
535
|
+
res_id = get_next_argument(description, multiple: get_option(:bulk))
|
|
451
536
|
# Can be an Array
|
|
452
|
-
if res_id.is_a?(String) && (m =
|
|
537
|
+
if res_id.is_a?(String) && (m = Parser.percent_selector(res_id))
|
|
453
538
|
Aspera.assert(block_given?, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
|
|
454
539
|
res_id = yield(m[:field], m[:value])
|
|
455
540
|
end
|
|
@@ -458,8 +543,18 @@ module Aspera
|
|
|
458
543
|
|
|
459
544
|
def get_next_command(command_list, aliases: nil); get_next_argument('command', accept_list: command_list, aliases: aliases); end
|
|
460
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
|
+
|
|
461
556
|
# Get an option definition by name
|
|
462
|
-
# @param option_symbol [Symbol]
|
|
557
|
+
# @param option_symbol [Symbol] name of the option
|
|
463
558
|
# @return [OptionValue] Option definition
|
|
464
559
|
# @raise [Cli::BadArgument] if option not found
|
|
465
560
|
def option_def(option_symbol)
|
|
@@ -470,12 +565,16 @@ module Aspera
|
|
|
470
565
|
# Get an option value by name
|
|
471
566
|
# either return value or calls handler, can return nil
|
|
472
567
|
# ask interactively if requested/required
|
|
473
|
-
# @param option_symbol [Symbol]
|
|
568
|
+
# @param option_symbol [Symbol] name of the option to retrieve
|
|
474
569
|
# @param mandatory [Boolean] if true, raise error if option not set
|
|
475
|
-
|
|
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)
|
|
476
573
|
Aspera.assert_type(option_symbol, Symbol)
|
|
477
574
|
option_attrs = option_def(option_symbol)
|
|
478
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)
|
|
479
578
|
# Do not fail for manual generation if option mandatory but not set
|
|
480
579
|
return :skip_missing_mandatory if result.nil? && mandatory && !@fail_on_missing_mandatory
|
|
481
580
|
if result.nil?
|
|
@@ -498,7 +597,10 @@ module Aspera
|
|
|
498
597
|
def set_option(option_symbol, value, where: 'code override')
|
|
499
598
|
Aspera.assert_type(option_symbol, Symbol)
|
|
500
599
|
option = option_def(option_symbol)
|
|
501
|
-
|
|
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
|
|
502
604
|
option.assign_value(value, where: where)
|
|
503
605
|
end
|
|
504
606
|
|
|
@@ -508,6 +610,18 @@ module Aspera
|
|
|
508
610
|
option_def(option_symbol).clear
|
|
509
611
|
end
|
|
510
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
|
+
|
|
511
625
|
# Adds each of the keys of specified hash as an option
|
|
512
626
|
# @param preset_hash [Hash] Options to add
|
|
513
627
|
# @param where [String] Where the value comes from
|
|
@@ -575,37 +689,52 @@ module Aspera
|
|
|
575
689
|
def parse_options!
|
|
576
690
|
Log.log.trace1('parse_options!'.red)
|
|
577
691
|
# First options from conf file
|
|
578
|
-
consume_option_pairs(@option_pairs_batch, 'set')
|
|
692
|
+
@option_pairs_batch = consume_option_pairs(@option_pairs_batch, 'set')
|
|
579
693
|
# Then, env var (to override)
|
|
580
|
-
consume_option_pairs(@option_pairs_env, 'env')
|
|
581
|
-
# 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).
|
|
582
697
|
unknown_options = []
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
|
602
724
|
end
|
|
725
|
+
# Unknown option: defer to next parse_options! round, do not consume the recorded offset
|
|
726
|
+
unknown_options.push(opt)
|
|
603
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)
|
|
604
734
|
end
|
|
605
|
-
# Save for later processing
|
|
606
|
-
unknown_options.push(e.args.first)
|
|
607
|
-
retry
|
|
608
735
|
end
|
|
736
|
+
@current_option_args_offset = nil
|
|
737
|
+
Log.log.trace1('After parse')
|
|
609
738
|
Log.log.trace1{"remains: #{unknown_options}"}
|
|
610
739
|
# Set unprocessed options for next time
|
|
611
740
|
@unprocessed_cmd_line_options = unknown_options
|
|
@@ -647,7 +776,7 @@ module Aspera
|
|
|
647
776
|
if !@ask_missing_mandatory
|
|
648
777
|
message = "Missing #{default_prompt}"
|
|
649
778
|
message = self.class.multi_choice_assert_msg(message, accept_list) if accept_list
|
|
650
|
-
message += "\
|
|
779
|
+
message += "\n#{TerminalFormatter::HINT}Give `#{HELP}` as argument to retrieve the schema of the missing argument." if schema
|
|
651
780
|
raise Cli::MissingArgument, message
|
|
652
781
|
end
|
|
653
782
|
# ask interactively
|
|
@@ -673,21 +802,85 @@ module Aspera
|
|
|
673
802
|
# This extended value does not take args (`@:`)
|
|
674
803
|
# ExtendedValue.assert_no_value(end_marker, :p)
|
|
675
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?
|
|
676
815
|
result = nil
|
|
677
816
|
get_next_argument('args', multiple: end_marker).each do |argument|
|
|
678
817
|
Aspera.assert(argument.include?(OPTION_VALUE_SEPARATOR)){"Positional argument: #{argument} does not include #{OPTION_VALUE_SEPARATOR}"}
|
|
679
818
|
path, value = argument.split(OPTION_VALUE_SEPARATOR, 2)
|
|
680
819
|
result = DotContainer.dotted_to_container(path.split(DotContainer::SEPARATOR), smart_convert(value), result)
|
|
681
820
|
end
|
|
821
|
+
# Restore skipped args so they remain available for command dispatching
|
|
822
|
+
@unprocessed_cmd_line_arguments.unshift(*skipped) unless skipped.empty?
|
|
682
823
|
result
|
|
683
824
|
end
|
|
684
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
|
+
|
|
685
846
|
# ======================================================
|
|
686
847
|
private
|
|
687
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
|
+
|
|
688
881
|
# Using dotted hash notation, convert value to bool, int, float or extended value
|
|
689
882
|
# @param value [String] The value to convert to appropriate type
|
|
690
|
-
# @return the converted value
|
|
883
|
+
# @return [Boolean, Integer, Float, String, Array, Hash] the converted value
|
|
691
884
|
def smart_convert(value)
|
|
692
885
|
case value
|
|
693
886
|
when 'true' then true
|
|
@@ -724,23 +917,23 @@ module Aspera
|
|
|
724
917
|
# Try to evaluate options set in batch
|
|
725
918
|
# @param unprocessed_options [Array] list of options to apply (key_sym,value)
|
|
726
919
|
# @param where [String] where the options come from
|
|
727
|
-
|
|
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)
|
|
728
926
|
Log.log.trace1{"consume_option_pairs: #{where}"}
|
|
729
|
-
|
|
730
|
-
|
|
927
|
+
remaining = {}
|
|
928
|
+
option_pairs.each do |k, v|
|
|
731
929
|
if @declared_options.key?(k)
|
|
732
|
-
|
|
733
|
-
v = self.class.get_from_list(v, "#{k} in #{where}", @declared_options[k].values) if @declared_options[k].values && v.is_a?(String)
|
|
734
|
-
options_to_set[k] = v
|
|
930
|
+
set_option(k, v, where: where)
|
|
735
931
|
else
|
|
736
932
|
Log.log.trace1{"unprocessed: #{k}: #{v}"}
|
|
933
|
+
remaining[k] = v
|
|
737
934
|
end
|
|
738
935
|
end
|
|
739
|
-
|
|
740
|
-
set_option(k, v, where: where)
|
|
741
|
-
# keep only unprocessed values for next parse
|
|
742
|
-
unprocessed_options.delete(k)
|
|
743
|
-
end
|
|
936
|
+
remaining
|
|
744
937
|
end
|
|
745
938
|
|
|
746
939
|
# Option name separator on command line, e.g. in --option-blah, third "-"
|
|
@@ -759,7 +952,7 @@ module Aspera
|
|
|
759
952
|
# Ask for schema of Extended value
|
|
760
953
|
HELP = 'help'
|
|
761
954
|
|
|
762
|
-
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
|
|
763
956
|
end
|
|
764
957
|
end
|
|
765
958
|
end
|