aspera-cli 4.25.6 → 4.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +89 -48
  4. data/CONTRIBUTING.md +1 -1
  5. data/lib/aspera/api/aoc.rb +120 -79
  6. data/lib/aspera/api/node.rb +103 -51
  7. data/lib/aspera/ascp/installation.rb +99 -32
  8. data/lib/aspera/assert.rb +17 -13
  9. data/lib/aspera/cli/extended_value.rb +7 -2
  10. data/lib/aspera/cli/formatter.rb +107 -95
  11. data/lib/aspera/cli/main.rb +69 -10
  12. data/lib/aspera/cli/manager.rb +158 -78
  13. data/lib/aspera/cli/options.schema.yaml +82 -0
  14. data/lib/aspera/cli/plugins/aoc.rb +247 -144
  15. data/lib/aspera/cli/plugins/ats.rb +3 -3
  16. data/lib/aspera/cli/plugins/base.rb +60 -76
  17. data/lib/aspera/cli/plugins/config.rb +14 -12
  18. data/lib/aspera/cli/plugins/console.rb +3 -3
  19. data/lib/aspera/cli/plugins/faspex.rb +6 -6
  20. data/lib/aspera/cli/plugins/faspex5.rb +24 -23
  21. data/lib/aspera/cli/plugins/node.rb +67 -71
  22. data/lib/aspera/cli/plugins/oauth.rb +5 -12
  23. data/lib/aspera/cli/plugins/orchestrator.rb +13 -13
  24. data/lib/aspera/cli/plugins/preview.rb +116 -80
  25. data/lib/aspera/cli/plugins/server.rb +2 -10
  26. data/lib/aspera/cli/plugins/shares.rb +7 -7
  27. data/lib/aspera/cli/sync_actions.rb +1 -1
  28. data/lib/aspera/cli/transfer_agent.rb +17 -15
  29. data/lib/aspera/cli/version.rb +1 -1
  30. data/lib/aspera/command_line_builder.rb +22 -18
  31. data/lib/aspera/dot_container.rb +7 -3
  32. data/lib/aspera/environment.rb +6 -5
  33. data/lib/aspera/formatter_interface.rb +14 -0
  34. data/lib/aspera/hash_ext.rb +6 -0
  35. data/lib/aspera/log.rb +5 -4
  36. data/lib/aspera/markdown.rb +4 -1
  37. data/lib/aspera/oauth/factory.rb +1 -1
  38. data/lib/aspera/preview/file_types.rb +1 -1
  39. data/lib/aspera/preview/generator.rb +146 -91
  40. data/lib/aspera/preview/options.rb +4 -1
  41. data/lib/aspera/preview/terminal.rb +50 -20
  42. data/lib/aspera/preview/utils.rb +76 -34
  43. data/lib/aspera/products/transferd.rb +1 -1
  44. data/lib/aspera/proxy_auto_config.rb +3 -0
  45. data/lib/aspera/rest.rb +2 -1
  46. data/lib/aspera/rest_list.rb +23 -16
  47. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +62801 -0
  48. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +8898 -0
  49. data/lib/aspera/schema/documentation.rb +107 -0
  50. data/lib/aspera/schema/reader.rb +75 -0
  51. data/lib/aspera/schema/registry.rb +63 -0
  52. data/lib/aspera/secret_hider.rb +3 -1
  53. data/lib/aspera/sync/conf.schema.yaml +0 -26
  54. data/lib/aspera/sync/operations.rb +9 -5
  55. data/lib/aspera/transfer/faux_file.rb +1 -1
  56. data/lib/aspera/transfer/resumer.rb +1 -1
  57. data/lib/aspera/transfer/spec.rb +3 -3
  58. data/lib/aspera/transfer/spec.schema.yaml +1 -1
  59. data/lib/aspera/uri_reader.rb +17 -2
  60. data/lib/aspera/yaml.rb +4 -2
  61. data.tar.gz.sig +0 -0
  62. metadata +13 -7
  63. metadata.gz.sig +0 -0
  64. data/lib/aspera/transfer/spec_doc.rb +0 -76
@@ -12,6 +12,20 @@ require 'optparse'
12
12
 
13
13
  module Aspera
14
14
  module Cli
15
+ # Exception raised when schema is asked (`help`)
16
+ class SchemaRequest < Error
17
+ # @return [String, nil] path to schema file
18
+ attr_reader :path
19
+
20
+ # @param type [Symbol] :argument or :option
21
+ # @param name [String] name of the option/argument
22
+ # @param schema_path [String, nil] path to schema file, or `nil` if not available
23
+ def initialize(type, name, schema_path)
24
+ super("#{type}: #{name}")
25
+ @path = schema_path
26
+ end
27
+ end
28
+
15
29
  module BoolValue
16
30
  # boolean options are set to true/false from the following values
17
31
  YES_SYM = :yes
@@ -22,23 +36,24 @@ module Aspera
22
36
  # Boolean values
23
37
  # @return [Array<true, false, :yes, :no>]
24
38
  ALL = (TRUE_VALUES + FALSE_VALUES).freeze
39
+ # `false` and `true`
25
40
  TYPES = [FalseClass, TrueClass].freeze
26
41
  SYMBOLS = [NO_SYM, YES_SYM].freeze
27
42
  # @return `true` if value is a value for `true` in ALL
28
43
  def true?(enum)
29
44
  Aspera.assert_values(enum, ALL){'boolean'}
30
- return TRUE_VALUES.include?(enum)
45
+ TRUE_VALUES.include?(enum)
31
46
  end
32
47
 
33
48
  # @return [:yes, :no]
34
49
  def to_sym(enum)
35
50
  Aspera.assert_values(enum, ALL){'boolean'}
36
- return TRUE_VALUES.include?(enum) ? YES_SYM : NO_SYM
51
+ TRUE_VALUES.include?(enum) ? YES_SYM : NO_SYM
37
52
  end
38
53
 
39
54
  # @return `true` if value is a value for `true` or `false` in ALL
40
55
  def symbol?(sym)
41
- return ALL.include?(sym)
56
+ ALL.include?(sym)
42
57
  end
43
58
  module_function :true?, :to_sym, :symbol?
44
59
  end
@@ -52,30 +67,33 @@ module Aspera
52
67
  # Value will be coerced to int
53
68
  TYPES_INTEGER = [Integer].freeze
54
69
  TYPES_BOOLEAN = BoolValue::TYPES
55
- # no value at all, it's a switch
70
+ # No value at all for the option, it's a switch, like `-N`
56
71
  TYPES_NONE = [].freeze
72
+ # Symbol
57
73
  TYPES_ENUM = [Symbol].freeze
74
+ # String
58
75
  TYPES_STRING = [String].freeze
59
76
  end
60
77
 
61
78
  # Description of option, how to manage
62
79
  class OptionValue
63
80
  # [Array(Class)] List of allowed types
64
- attr_reader :types, :sensitive
81
+ attr_reader :types, :sensitive, :schema, :option
65
82
  # [Array] List of allowed values (Symbols and specific values)
66
83
  attr_accessor :values
67
84
 
68
- # @param option [Symbol] Name of option
85
+ # @param option [Symbol] Name of option
69
86
  # @param description [String] Description for help
70
- # @param allowed [nil,Class,Array<Class>,Array<Symbol>] Allowed values
71
- # @param handler [Hash] Accessor: keys: :o(object) and :m(method)
87
+ # @param allowed [nil,Class,Array<Class>,Array<Symbol>] Allowed values
88
+ # @param handler [Hash] Accessor: keys: :o(object) and :m(method)
72
89
  # @param deprecation [String] Deprecation message
90
+ # @param schema [String] Declaration of schema
73
91
  # `allowed`:
74
92
  # - `nil` No validation, so just a string
75
93
  # - `Class` The single allowed Class
76
94
  # - `Array<Class>` Multiple allowed classes
77
95
  # - `Array<Symbol>` List of allowed values
78
- def initialize(option:, description:, allowed: Allowed::TYPES_STRING, handler: nil, deprecation: nil)
96
+ def initialize(option:, description:, allowed: Allowed::TYPES_STRING, handler: nil, deprecation: nil, schema: nil)
79
97
  Log.log.trace1{"option: #{option}, allowed: #{allowed}"}
80
98
  @option = option
81
99
  @description = description
@@ -86,6 +104,7 @@ module Aspera
86
104
  @read_method = handler&.[](:m)
87
105
  @write_method = @read_method ? "#{@read_method}=".to_sym : nil
88
106
  @deprecation = deprecation
107
+ @schema = schema
89
108
  @access = if @object.nil?
90
109
  :local
91
110
  elsif @object.respond_to?(@write_method)
@@ -102,7 +121,7 @@ module Aspera
102
121
  if allowed.take(Allowed::TYPES_SYMBOL_ARRAY.length) == Allowed::TYPES_SYMBOL_ARRAY
103
122
  # Special case: array of defined symbol values
104
123
  @types = Allowed::TYPES_SYMBOL_ARRAY
105
- @values = allowed[Allowed::TYPES_SYMBOL_ARRAY.length..-1]
124
+ @values = allowed[Allowed::TYPES_SYMBOL_ARRAY.length..]
106
125
  elsif allowed.all?(Class)
107
126
  @types = allowed
108
127
  @values = BoolValue::ALL if allowed.eql?(Allowed::TYPES_BOOLEAN)
@@ -131,12 +150,14 @@ module Aspera
131
150
  when :setter then @object.send(@read_method, @option, :get)
132
151
  end
133
152
  Log.log.trace1{"#{@option} -> (#{current_value.class})#{current_value}"} if log
134
- return current_value
153
+ current_value
135
154
  end
136
155
 
137
156
  # Assign value to option.
138
- # Value can be a String, then evaluated with ExtendedValue, or directly a value.
157
+ # Value can be a `String`, then evaluated with `ExtendedValue`, or directly a value.
139
158
  # @param value [String, Object] Value to assign to option
159
+ # @param where [String] Where the value is assigned from
160
+ # @return [nil]
140
161
  def assign_value(value, where:)
141
162
  Aspera.assert(!@deprecation, type: warn){"Option #{@option} is deprecated: #{@deprecation}"}
142
163
  new_value = ExtendedValue.instance.evaluate(value, context: "option: #{@option}", allowed: @types)
@@ -146,11 +167,10 @@ module Aspera
146
167
  new_value = [new_value] if @types.eql?(Allowed::TYPES_STRING_ARRAY) && new_value.is_a?(String)
147
168
  # Setting a Hash to null set an empty hash
148
169
  new_value = {} if new_value.eql?(nil) && @types&.first.eql?(Hash)
149
- # Setting a Array to null set an empty hash
170
+ # Setting a Array to null set an empty array
150
171
  new_value = [] if new_value.eql?(nil) && @types&.first.eql?(Array)
151
172
  if @types.eql?(Aspera::Cli::Allowed::TYPES_SYMBOL_ARRAY)
152
173
  new_value = [new_value] if new_value.is_a?(String)
153
- Aspera.assert_type(new_value, Array, type: BadArgument)
154
174
  Aspera.assert_array_all(new_value, String, type: BadArgument)
155
175
  new_value = new_value.map{ |v| Manager.get_from_list(v, @option, @values)}
156
176
  end
@@ -166,6 +186,7 @@ module Aspera
166
186
  when :setter then @object.send(@read_method, @option, :set, new_value)
167
187
  end
168
188
  Log.log.trace1{v = value(log: false); "#{@option} <- (#{v.class})#{v}"} # rubocop:disable Style/Semicolon
189
+ nil
169
190
  end
170
191
  end
171
192
 
@@ -184,25 +205,34 @@ module Aspera
184
205
  Aspera.assert(!matching.empty?, multi_choice_assert_msg("unknown value for #{descr}: #{short_value}", allowed_values), type: BadArgument)
185
206
  Aspera.assert(matching.length.eql?(1), multi_choice_assert_msg("ambiguous shortcut for #{descr}: #{short_value}", matching), type: BadArgument)
186
207
  return BoolValue.true?(matching.first) if allowed_values.eql?(BoolValue::ALL)
187
- return matching.first
208
+ matching.first
188
209
  end
189
210
 
190
211
  # Generates error message with list of allowed values
191
- # @param error_msg [String] error message
192
- # @param accept_list [Array] list of allowed values
212
+ # @param error_msg [String] Error message
213
+ # @param accept_list [Array<Symbol>] List of allowed values
193
214
  def multi_choice_assert_msg(error_msg, accept_list)
194
- [error_msg, 'Use:'].concat(accept_list.map{ |c| "- #{c}"}.sort).join("\n")
215
+ [error_msg, 'Use:', *accept_list.map{ |choice| "- #{choice}"}.sort].join("\n")
195
216
  end
196
217
 
197
218
  # Change option name with dash to name with underscore
198
219
  # @param name [String] option name
199
220
  # @return [String]
200
221
  def option_line_to_name(name)
201
- return name.gsub(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
222
+ name.gsub(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
202
223
  end
203
224
 
204
225
  def option_name_to_line(name)
205
- return "#{OPTION_PREFIX}#{name.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)}"
226
+ "#{OPTION_PREFIX}#{name.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)}"
227
+ end
228
+
229
+ # @return [Hash{Symbol => String}, nil] `{field:,value:}` if identifier is a percent selector, else `nil`
230
+ def percent_selector(identifier)
231
+ Aspera.assert_type(identifier, String)
232
+ if (m = identifier.match(REGEX_LOOKUP_ID_BY_FIELD))
233
+ return {field: m[1], value: ExtendedValue.instance.evaluate(m[2], context: "percent selector: #{m[1]}")}
234
+ end
235
+ nil
206
236
  end
207
237
  end
208
238
 
@@ -210,6 +240,8 @@ module Aspera
210
240
  attr_accessor :ask_missing_mandatory, :ask_missing_optional
211
241
  attr_writer :fail_on_missing_mandatory
212
242
 
243
+ # @param program_name [String] Name of the program
244
+ # @param argv [Array<String>, nil] Command line arguments to parse
213
245
  def initialize(program_name, argv = nil)
214
246
  # command line values *not* starting with '-'
215
247
  @unprocessed_cmd_line_arguments = []
@@ -217,7 +249,8 @@ module Aspera
217
249
  @unprocessed_cmd_line_options = []
218
250
  # a copy of all initial options
219
251
  @initial_cli_options = []
220
- # option description: option_symbol => OptionValue
252
+ # Option descriptions: maps option symbol to its OptionValue descriptor
253
+ # @type [Hash{Symbol => OptionValue}]
221
254
  @declared_options = {}
222
255
  # do we ask missing options and arguments to user ?
223
256
  @ask_missing_mandatory = false # STDIN.isatty
@@ -236,7 +269,7 @@ module Aspera
236
269
  # options can also be provided by env vars : --param-name -> ASCLI_PARAM_NAME
237
270
  env_prefix = program_name.upcase + OPTION_SEP_SYMBOL
238
271
  ENV.each do |k, v|
239
- @option_pairs_env[k[env_prefix.length..-1].downcase.to_sym] = v if k.start_with?(env_prefix)
272
+ @option_pairs_env[k.delete_prefix(env_prefix).downcase.to_sym] = v if k.start_with?(env_prefix)
240
273
  end
241
274
  Log.log.debug{"env=#{@option_pairs_env}".red}
242
275
  @unprocessed_cmd_line_options = []
@@ -267,6 +300,14 @@ module Aspera
267
300
  # do not parse options yet, let's wait for option `-h` to be overridden
268
301
  end
269
302
 
303
+ # Add a type to the message if not special types
304
+ # @param types [Array<Class>] types to add
305
+ # @return [String] Types if relevant
306
+ def add_types_info(types)
307
+ return '' if !types || types.empty? || types.eql?(Allowed::TYPES_ENUM) || types.eql?(Allowed::TYPES_BOOLEAN) || types.eql?(Allowed::TYPES_STRING)
308
+ " (#{types.map(&:name).join(', ')})"
309
+ end
310
+
270
311
  # Declare an option
271
312
  # @param option_symbol [Symbol] option name
272
313
  # @param description [String] description for help
@@ -275,13 +316,14 @@ module Aspera
275
316
  # @param default [Object] default value
276
317
  # @param handler [Hash] handler for option value: keys: :o(object) and :m(method)
277
318
  # @param deprecation [String] deprecation
319
+ # @param schema [String] Definition of schema for Hash parameters
278
320
  # @param block [Proc] Block to execute when option is found
279
- def declare(option_symbol, description, short: nil, allowed: nil, default: nil, handler: nil, deprecation: nil, &block)
321
+ def declare(option_symbol, description, short: nil, allowed: nil, default: nil, handler: nil, deprecation: nil, schema: nil, &block)
280
322
  Aspera.assert_type(option_symbol, Symbol)
281
323
  Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
282
324
  Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
283
325
  Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with an uppercase"}
284
- Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :allowed"}
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"}
285
327
  Aspera.assert_type(handler, Hash) if handler
286
328
  Aspera.assert(handler.keys.sort.eql?(%i[m o])) if handler
287
329
  option_attrs = @declared_options[option_symbol] = OptionValue.new(
@@ -289,10 +331,11 @@ module Aspera
289
331
  description: description,
290
332
  allowed: allowed,
291
333
  handler: handler,
292
- deprecation: deprecation
334
+ deprecation: deprecation,
335
+ schema: schema
293
336
  )
294
337
  real_types = option_attrs.types&.reject{ |i| [NilClass, String, Symbol].include?(i)}
295
- description = "#{description} (#{real_types.map(&:name).join(', ')})" if real_types && !real_types.empty? && !real_types.eql?(Allowed::TYPES_ENUM) && !real_types.eql?(Allowed::TYPES_BOOLEAN) && !real_types.eql?(Allowed::TYPES_STRING)
338
+ description += add_types_info(real_types)
296
339
  description = "#{description} (#{'deprecated'.blue}: #{deprecation})" if deprecation
297
340
  set_option(option_symbol, default, where: 'default') unless default.nil?
298
341
  on_args = [description]
@@ -331,32 +374,36 @@ module Aspera
331
374
  end
332
375
 
333
376
  # @param descr [String] description for help
334
- # @param mandatory [Boolean] if true, raise error if option not set
335
- # @param multiple [Boolean] if true, return remaining arguments (Array) until END
336
- # @param accept_list [Array, NilClass] list of allowed values (Symbol)
377
+ # @param mandatory [Boolean] `true`: raise error no more argument
378
+ # @param multiple [Boolean] `true`: return all remaining arguments (Array). String: until marker
379
+ # @param accept_list [Array<Symbol>, NilClass] list of allowed values
337
380
  # @param validation [Class, Array, NilClass] Accepted value type(s) or list of Symbols
338
381
  # @param aliases [Hash] map of aliases: key = alias, value = real value
339
382
  # @param default [Object] default value
340
383
  # @return one value, list or nil (if optional and no default)
341
- def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: Allowed::TYPES_STRING, aliases: nil, default: nil)
384
+ def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: Allowed::TYPES_STRING, aliases: nil, default: nil, schema: nil)
342
385
  Aspera.assert_array_all(accept_list, Symbol) unless accept_list.nil?
343
386
  Aspera.assert_hash_all(aliases, Symbol, Symbol) unless aliases.nil?
344
387
  validation = Symbol unless accept_list.nil?
345
388
  validation = [validation] unless validation.is_a?(Array) || validation.nil?
346
389
  Aspera.assert_array_all(validation, Class){'validation'} unless validation.nil?
347
- descr = "#{descr} (#{validation.join(', ')})" unless validation.nil? || validation.eql?(Allowed::TYPES_STRING)
390
+ descr = "#{descr}#{add_types_info(validation)}"
348
391
  result =
349
392
  if !@unprocessed_cmd_line_arguments.empty?
350
- if multiple
351
- index = @unprocessed_cmd_line_arguments.index(SpecialValues::EOA)
352
- if index.nil?
353
- values = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length)
354
- else
393
+ case multiple
394
+ when true
395
+ values = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length)
396
+ when false
397
+ values = [@unprocessed_cmd_line_arguments.shift]
398
+ when String
399
+ index = @unprocessed_cmd_line_arguments.index(multiple)
400
+ if index
355
401
  values = @unprocessed_cmd_line_arguments.shift(index)
356
- @unprocessed_cmd_line_arguments.shift # remove EOA
402
+ @unprocessed_cmd_line_arguments.shift # remove end marker
403
+ else
404
+ values = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length)
357
405
  end
358
- else
359
- values = [@unprocessed_cmd_line_arguments.shift]
406
+ else Aspera.error_unexpected_value(multiple){'multiple'}
360
407
  end
361
408
  values = values.map{ |v| ExtendedValue.instance.evaluate(v, context: "argument: #{descr}", allowed: validation)}
362
409
  # If expecting list and only one arg of type array : it is the list
@@ -369,7 +416,7 @@ module Aspera
369
416
  multiple ? values : values.first
370
417
  elsif !default.nil? then default
371
418
  # no value provided, either get value interactively, or exception
372
- elsif mandatory then get_interactive(descr, multiple: multiple, accept_list: accept_list)
419
+ elsif mandatory then get_interactive(descr, multiple: multiple, accept_list: accept_list, schema: schema)
373
420
  end
374
421
  if result.is_a?(String) && validation&.eql?(Allowed::TYPES_INTEGER)
375
422
  int_result = Integer(result, exception: false)
@@ -383,23 +430,51 @@ module Aspera
383
430
  if validation && (mandatory || !result.nil?)
384
431
  value_list = multiple ? result : [result]
385
432
  value_list.each do |value|
433
+ raise SchemaRequest.new(:argument, descr, schema) if validation.include?(Hash) && value.eql?(HELP)
386
434
  raise Cli::BadArgument,
387
435
  "Argument #{descr} is a #{value.class} but must be #{'one of: ' if validation.length > 1}#{validation.map(&:name).join(', ')}" unless validation.any?{ |t| value.is_a?(t)}
388
436
  end
389
437
  end
390
- return result
438
+ result
391
439
  end
392
440
 
393
- def get_next_command(command_list, aliases: nil); return get_next_argument('command', accept_list: command_list, aliases: aliases); end
441
+ # Resource identifier as positional parameter
442
+ #
443
+ # @param description [String] description of the identifier
444
+ # @param block [Proc] block to search for identifier based on attribute value
445
+ # @return [String, Array<String>] identifier or list of IDs (if `bulk` option is set)
446
+ # @yieldparam field [String] The field name from percent selector
447
+ # @yieldparam value [String] The value from percent selector
448
+ # @yieldreturn [String] Resolved identifier
449
+ def instance_identifier(description: 'identifier', &block)
450
+ res_id = get_next_argument(description, multiple: get_option(:bulk)) if res_id.nil?
451
+ # Can be an Array
452
+ if res_id.is_a?(String) && (m = Manager.percent_selector(res_id))
453
+ Aspera.assert(block_given?, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
454
+ res_id = yield(m[:field], m[:value])
455
+ end
456
+ res_id
457
+ end
458
+
459
+ def get_next_command(command_list, aliases: nil); get_next_argument('command', accept_list: command_list, aliases: aliases); end
460
+
461
+ # Get an option definition by name
462
+ # @param option_symbol [Symbol]
463
+ # @return [OptionValue] Option definition
464
+ # @raise [Cli::BadArgument] if option not found
465
+ def option_def(option_symbol)
466
+ Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument){"Unknown option: #{option_symbol}"}
467
+ @declared_options[option_symbol]
468
+ end
394
469
 
395
470
  # Get an option value by name
396
471
  # either return value or calls handler, can return nil
397
472
  # ask interactively if requested/required
473
+ # @param option_symbol [Symbol]
398
474
  # @param mandatory [Boolean] if true, raise error if option not set
399
475
  def get_option(option_symbol, mandatory: false)
400
476
  Aspera.assert_type(option_symbol, Symbol)
401
- Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument){"Unknown option: #{option_symbol}"}
402
- option_attrs = @declared_options[option_symbol]
477
+ option_attrs = option_def(option_symbol)
403
478
  result = option_attrs.value
404
479
  # Do not fail for manual generation if option mandatory but not set
405
480
  return :skip_missing_mandatory if result.nil? && mandatory && !@fail_on_missing_mandatory
@@ -408,11 +483,11 @@ module Aspera
408
483
  Aspera.assert(!mandatory, type: Cli::BadArgument){"Missing mandatory option: #{option_symbol}"}
409
484
  elsif @ask_missing_optional || mandatory
410
485
  # ask_missing_mandatory
411
- result = get_interactive(option_symbol.to_s, check_option: true, accept_list: option_attrs.values)
486
+ result = get_interactive(option_symbol.to_s, check_option: true, accept_list: option_attrs.values, schema: option_attrs.schema)
412
487
  set_option(option_symbol, result, where: 'interactive')
413
488
  end
414
489
  end
415
- return result
490
+ result
416
491
  end
417
492
 
418
493
  # Set an option value by name, either store value or call handler
@@ -422,15 +497,15 @@ module Aspera
422
497
  # @param where [String] Where the value comes from
423
498
  def set_option(option_symbol, value, where: 'code override')
424
499
  Aspera.assert_type(option_symbol, Symbol)
425
- Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument){"Unknown option: #{option_symbol}"}
426
- @declared_options[option_symbol].assign_value(value, where: where)
500
+ option = option_def(option_symbol)
501
+ raise SchemaRequest.new(:option, option.option, option.schema) if option.types&.include?(Hash) && value.eql?(HELP)
502
+ option.assign_value(value, where: where)
427
503
  end
428
504
 
429
505
  # Set option to `nil`
430
506
  def clear_option(option_symbol)
431
507
  Aspera.assert_type(option_symbol, Symbol)
432
- Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument){"Unknown option: #{option_symbol}"}
433
- @declared_options[option_symbol].clear
508
+ option_def(option_symbol).clear
434
509
  end
435
510
 
436
511
  # Adds each of the keys of specified hash as an option
@@ -453,7 +528,7 @@ module Aspera
453
528
 
454
529
  # Check if there were unprocessed values to generate error
455
530
  def command_or_arg_empty?
456
- return @unprocessed_cmd_line_arguments.empty?
531
+ @unprocessed_cmd_line_arguments.empty?
457
532
  end
458
533
 
459
534
  # Unprocessed options or arguments ?
@@ -461,7 +536,7 @@ module Aspera
461
536
  result = []
462
537
  result.push("unprocessed options: #{@unprocessed_cmd_line_options}") unless @unprocessed_cmd_line_options.empty?
463
538
  result.push("unprocessed values: #{@unprocessed_cmd_line_arguments}") unless @unprocessed_cmd_line_arguments.empty?
464
- return result
539
+ result
465
540
  end
466
541
 
467
542
  # Get all original options on command line used to generate a config in config file
@@ -471,7 +546,7 @@ module Aspera
471
546
  @initial_cli_options.each do |option_argument|
472
547
  # ignore short options
473
548
  next unless option_argument.start_with?(OPTION_PREFIX)
474
- name, value = option_argument[OPTION_PREFIX.length..-1].split(OPTION_VALUE_SEPARATOR, 2)
549
+ name, value = option_argument.delete_prefix(OPTION_PREFIX).split(OPTION_VALUE_SEPARATOR, 2)
475
550
  # ignore options without value
476
551
  next if value.nil?
477
552
  Log.log.debug{"option #{name}=#{value}"}
@@ -480,7 +555,7 @@ module Aspera
480
555
  DotContainer.dotted_to_container(path, smart_convert(value), result)
481
556
  @unprocessed_cmd_line_options.delete(option_argument)
482
557
  end
483
- return result
558
+ result
484
559
  end
485
560
 
486
561
  # @param only_defined [Boolean] if true, only return options that were defined
@@ -493,7 +568,7 @@ module Aspera
493
568
  rescue => e
494
569
  result[option_symbol] = e.to_s
495
570
  end
496
- return result
571
+ result
497
572
  end
498
573
 
499
574
  # Removes already known options from the list
@@ -508,14 +583,14 @@ module Aspera
508
583
  begin
509
584
  # remove known options one by one, exception if unknown
510
585
  Log.log.trace1('Before parse')
511
- Log.dump(:unprocessed_cmd_line_options, @unprocessed_cmd_line_options)
586
+ Log.dump(:unprocessed_cmd_line_options, @unprocessed_cmd_line_options, level: :trace1)
512
587
  @parser.parse!(@unprocessed_cmd_line_options)
513
588
  Log.log.trace1('After parse')
514
589
  rescue OptionParser::InvalidOption => e
515
590
  Log.log.trace1{"InvalidOption #{e}".red}
516
591
  # An option like --a.b.c=d does: a={"b":{"c":ext_val(d)}}
517
592
  if e.args.first.start_with?(OPTION_PREFIX)
518
- name, value = e.args.first[OPTION_PREFIX.length..-1].split(OPTION_VALUE_SEPARATOR, 2)
593
+ name, value = e.args.first.delete_prefix(OPTION_PREFIX).split(OPTION_VALUE_SEPARATOR, 2)
519
594
  if !value.nil?
520
595
  path = name.split(DotContainer::SEPARATOR)
521
596
  option_sym = self.class.option_line_to_name(path.shift).to_sym
@@ -541,7 +616,7 @@ module Aspera
541
616
  print("#{prompt}> ")
542
617
  line = $stdin.gets
543
618
  Aspera.assert_type(line, String){'Unexpected end of standard input'}
544
- return line.chomp
619
+ line.chomp
545
620
  end
546
621
 
547
622
  # prompt user for input in a list of symbols
@@ -562,20 +637,19 @@ module Aspera
562
637
  # Prompt user for input in a list of symbols
563
638
  # @param descr [String] description for help
564
639
  # @param check_option [Boolean] Check attributes of option with name=descr
565
- # @param multiple [Boolean] true if multiple values expected
566
- # @param accept_list [Array] list of expected values
567
- def get_interactive(descr, check_option: false, multiple: false, accept_list: nil)
640
+ # @param multiple [Boolean, String] `true` if multiple values expected
641
+ # @param accept_list [Array<Symbol>,NilClass] List of expected values
642
+ # @return [String] user input
643
+ def get_interactive(descr, check_option: false, multiple: false, accept_list: nil, schema: nil)
568
644
  option_attrs = @declared_options[descr.to_sym]
569
645
  what = option_attrs ? 'option' : 'argument'
646
+ default_prompt = "#{what}: #{descr}"
570
647
  if !@ask_missing_mandatory
571
- message = "missing #{what}: #{descr}"
572
- if accept_list.nil?
573
- raise Cli::BadArgument, message
574
- else
575
- Aspera.assert(false, self.class.multi_choice_assert_msg(message, accept_list), type: Cli::MissingArgument)
576
- end
648
+ message = "Missing #{default_prompt}"
649
+ message = self.class.multi_choice_assert_msg(message, accept_list) if accept_list
650
+ message += "\nGive `#{HELP}` as argument to retrieve the schema of the missing argument." if schema
651
+ raise Cli::MissingArgument, message
577
652
  end
578
- default_prompt = "#{what}: #{descr}"
579
653
  # ask interactively
580
654
  result = []
581
655
  puts(' (one per line, end with empty line)') if multiple
@@ -589,18 +663,20 @@ module Aspera
589
663
  return entry unless multiple
590
664
  result.push(entry)
591
665
  end
592
- return result
666
+ result
593
667
  end
594
668
 
595
- # Read remaining args and build an Array or Hash
596
- # @param value [nil] Argument to `@:` extended value
597
- def args_as_extended(arg)
669
+ # Read remaining args and build an `Array` or `Hash`
670
+ # @param value [String] Argument to `@:` extended value
671
+ # @return [Hash, Array] Object representing dot-path values
672
+ def args_as_extended(end_marker)
598
673
  # This extended value does not take args (`@:`)
599
- ExtendedValue.assert_no_value(arg, :p)
674
+ # ExtendedValue.assert_no_value(end_marker, :p)
675
+ end_marker = SpecialValues::EOA if end_marker.empty?
600
676
  result = nil
601
- get_next_argument(:args, multiple: true).each do |arg|
602
- Aspera.assert(arg.include?(OPTION_VALUE_SEPARATOR)){"Positional argument: #{arg} does not include #{OPTION_VALUE_SEPARATOR}"}
603
- path, value = arg.split(OPTION_VALUE_SEPARATOR, 2)
677
+ get_next_argument('args', multiple: end_marker).each do |argument|
678
+ Aspera.assert(argument.include?(OPTION_VALUE_SEPARATOR)){"Positional argument: #{argument} does not include #{OPTION_VALUE_SEPARATOR}"}
679
+ path, value = argument.split(OPTION_VALUE_SEPARATOR, 2)
604
680
  result = DotContainer.dotted_to_container(path.split(DotContainer::SEPARATOR), smart_convert(value), result)
605
681
  end
606
682
  result
@@ -627,7 +703,7 @@ module Aspera
627
703
  def symbol_to_option(symbol, opt_val = nil)
628
704
  result = [OPTION_PREFIX, symbol.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)].join
629
705
  result = [result, OPTION_VALUE_SEPARATOR, opt_val].join unless opt_val.nil?
630
- return result
706
+ result
631
707
  end
632
708
 
633
709
  # TODO: use formatter
@@ -678,8 +754,12 @@ module Aspera
678
754
  # when this is alone, this stops option processing
679
755
  OPTIONS_STOP = '--'
680
756
  SOURCE_USER = 'cmdline' # cspell:disable-line
757
+ # Percent selector: select by this field for this value
758
+ REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
759
+ # Ask for schema of Extended value
760
+ HELP = 'help'
681
761
 
682
- private_constant :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER
762
+ private_constant :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER, :REGEX_LOOKUP_ID_BY_FIELD
683
763
  end
684
764
  end
685
765
  end
@@ -0,0 +1,82 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: Aspera CLI Options Schema
4
+ version: 1.0.0
5
+ description: Schema definitions for Aspera CLI options
6
+ paths: {}
7
+ components:
8
+ schemas:
9
+ TransferInfo:
10
+ type: object
11
+ description: Optional parameters to control multi-session transfers, Web Socket Session, resume policy, and additional arguments for `ascp`.
12
+ properties:
13
+ wss:
14
+ type: boolean
15
+ description: Enable Web Socket Session when available.
16
+ default: true
17
+ quiet:
18
+ type: boolean
19
+ description: Suppress the `ascp` progress bar display.
20
+ default: false
21
+ trusted_certs:
22
+ type: array
23
+ description: List of trusted certificate repositories.
24
+ items:
25
+ type: string
26
+ client_ssh_key:
27
+ type: string
28
+ description: SSH key type to use for token-based transfers.
29
+ enum:
30
+ - rsa
31
+ - dsa_rsa
32
+ - per_client
33
+ default: rsa
34
+ ascp_args:
35
+ type: array
36
+ description: List of native `ascp` command-line arguments.
37
+ items:
38
+ type: string
39
+ default: []
40
+ spawn_timeout_sec:
41
+ type: number
42
+ format: float
43
+ description: Multi session - Maximum time (in seconds) to verify that `ascp` is running.
44
+ default: 3
45
+ spawn_delay_sec:
46
+ type: number
47
+ format: float
48
+ description: Multi session - Delay (in seconds) between starting each `ascp` session.
49
+ default: 2
50
+ multi_incr_udp:
51
+ type: boolean
52
+ description: >-
53
+ Multi session - Increment UDP port for each session.
54
+
55
+ If `true`, each session uses a different UDP port starting at `fasp_port` (default: 33001).
56
+
57
+ If `false`, all sessions use the same `fasp_port` (or `ascp` default).
58
+ default: true
59
+ resume:
60
+ type: object
61
+ description: Configuration for automatic transfer resume on interruption.
62
+ properties:
63
+ iter_max:
64
+ type: integer
65
+ description: Maximum number of retry attempts on error.
66
+ default: 7
67
+ sleep_initial:
68
+ type: integer
69
+ description: Initial sleep duration (in seconds) before first retry.
70
+ default: 2
71
+ sleep_factor:
72
+ type: integer
73
+ description: Multiplier applied to sleep duration between consecutive retry attempts.
74
+ default: 2
75
+ sleep_max:
76
+ type: integer
77
+ description: Maximum sleep duration (in seconds) between retry attempts.
78
+ default: 60
79
+ monitor:
80
+ type: boolean
81
+ description: Enable use of the `ascp` management port for transfer monitoring.
82
+ default: true