aspera-cli 4.26.0 → 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 (47) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +17 -3
  4. data/lib/aspera/api/aoc.rb +2 -1
  5. data/lib/aspera/api/node.rb +2 -2
  6. data/lib/aspera/ascp/installation.rb +7 -4
  7. data/lib/aspera/assert.rb +17 -13
  8. data/lib/aspera/cli/extended_value.rb +6 -2
  9. data/lib/aspera/cli/formatter.rb +65 -60
  10. data/lib/aspera/cli/main.rb +69 -10
  11. data/lib/aspera/cli/manager.rb +130 -76
  12. data/lib/aspera/cli/options.schema.yaml +82 -0
  13. data/lib/aspera/cli/plugins/aoc.rb +36 -11
  14. data/lib/aspera/cli/plugins/base.rb +46 -37
  15. data/lib/aspera/cli/plugins/config.rb +9 -9
  16. data/lib/aspera/cli/plugins/faspex.rb +1 -1
  17. data/lib/aspera/cli/plugins/faspex5.rb +4 -5
  18. data/lib/aspera/cli/plugins/node.rb +1 -1
  19. data/lib/aspera/cli/sync_actions.rb +1 -1
  20. data/lib/aspera/cli/transfer_agent.rb +17 -15
  21. data/lib/aspera/cli/version.rb +1 -1
  22. data/lib/aspera/command_line_builder.rb +22 -18
  23. data/lib/aspera/environment.rb +3 -3
  24. data/lib/aspera/formatter_interface.rb +14 -0
  25. data/lib/aspera/hash_ext.rb +6 -0
  26. data/lib/aspera/log.rb +4 -3
  27. data/lib/aspera/markdown.rb +4 -1
  28. data/lib/aspera/oauth/factory.rb +1 -1
  29. data/lib/aspera/proxy_auto_config.rb +3 -0
  30. data/lib/aspera/rest.rb +1 -1
  31. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +62801 -0
  32. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +8898 -0
  33. data/lib/aspera/schema/documentation.rb +107 -0
  34. data/lib/aspera/schema/reader.rb +75 -0
  35. data/lib/aspera/schema/registry.rb +63 -0
  36. data/lib/aspera/sync/conf.schema.yaml +0 -26
  37. data/lib/aspera/sync/operations.rb +9 -5
  38. data/lib/aspera/transfer/faux_file.rb +1 -1
  39. data/lib/aspera/transfer/resumer.rb +1 -1
  40. data/lib/aspera/transfer/spec.rb +3 -3
  41. data/lib/aspera/transfer/spec.schema.yaml +1 -1
  42. data/lib/aspera/uri_reader.rb +1 -1
  43. data/lib/aspera/yaml.rb +4 -2
  44. data.tar.gz.sig +0 -0
  45. metadata +9 -3
  46. metadata.gz.sig +0 -0
  47. 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)
@@ -150,7 +171,6 @@ module Aspera
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,25 @@ 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)}"
206
227
  end
207
228
 
208
229
  # @return [Hash{Symbol => String}, nil] `{field:,value:}` if identifier is a percent selector, else `nil`
@@ -219,6 +240,8 @@ module Aspera
219
240
  attr_accessor :ask_missing_mandatory, :ask_missing_optional
220
241
  attr_writer :fail_on_missing_mandatory
221
242
 
243
+ # @param program_name [String] Name of the program
244
+ # @param argv [Array<String>, nil] Command line arguments to parse
222
245
  def initialize(program_name, argv = nil)
223
246
  # command line values *not* starting with '-'
224
247
  @unprocessed_cmd_line_arguments = []
@@ -226,7 +249,8 @@ module Aspera
226
249
  @unprocessed_cmd_line_options = []
227
250
  # a copy of all initial options
228
251
  @initial_cli_options = []
229
- # option description: option_symbol => OptionValue
252
+ # Option descriptions: maps option symbol to its OptionValue descriptor
253
+ # @type [Hash{Symbol => OptionValue}]
230
254
  @declared_options = {}
231
255
  # do we ask missing options and arguments to user ?
232
256
  @ask_missing_mandatory = false # STDIN.isatty
@@ -245,7 +269,7 @@ module Aspera
245
269
  # options can also be provided by env vars : --param-name -> ASCLI_PARAM_NAME
246
270
  env_prefix = program_name.upcase + OPTION_SEP_SYMBOL
247
271
  ENV.each do |k, v|
248
- @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)
249
273
  end
250
274
  Log.log.debug{"env=#{@option_pairs_env}".red}
251
275
  @unprocessed_cmd_line_options = []
@@ -276,6 +300,14 @@ module Aspera
276
300
  # do not parse options yet, let's wait for option `-h` to be overridden
277
301
  end
278
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
+
279
311
  # Declare an option
280
312
  # @param option_symbol [Symbol] option name
281
313
  # @param description [String] description for help
@@ -284,8 +316,9 @@ module Aspera
284
316
  # @param default [Object] default value
285
317
  # @param handler [Hash] handler for option value: keys: :o(object) and :m(method)
286
318
  # @param deprecation [String] deprecation
319
+ # @param schema [String] Definition of schema for Hash parameters
287
320
  # @param block [Proc] Block to execute when option is found
288
- 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)
289
322
  Aspera.assert_type(option_symbol, Symbol)
290
323
  Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
291
324
  Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
@@ -298,10 +331,11 @@ module Aspera
298
331
  description: description,
299
332
  allowed: allowed,
300
333
  handler: handler,
301
- deprecation: deprecation
334
+ deprecation: deprecation,
335
+ schema: schema
302
336
  )
303
337
  real_types = option_attrs.types&.reject{ |i| [NilClass, String, Symbol].include?(i)}
304
- 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)
305
339
  description = "#{description} (#{'deprecated'.blue}: #{deprecation})" if deprecation
306
340
  set_option(option_symbol, default, where: 'default') unless default.nil?
307
341
  on_args = [description]
@@ -340,32 +374,36 @@ module Aspera
340
374
  end
341
375
 
342
376
  # @param descr [String] description for help
343
- # @param mandatory [Boolean] if true, raise error if option not set
344
- # @param multiple [Boolean] if true, return remaining arguments (Array) until END
345
- # @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
346
380
  # @param validation [Class, Array, NilClass] Accepted value type(s) or list of Symbols
347
381
  # @param aliases [Hash] map of aliases: key = alias, value = real value
348
382
  # @param default [Object] default value
349
383
  # @return one value, list or nil (if optional and no default)
350
- 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)
351
385
  Aspera.assert_array_all(accept_list, Symbol) unless accept_list.nil?
352
386
  Aspera.assert_hash_all(aliases, Symbol, Symbol) unless aliases.nil?
353
387
  validation = Symbol unless accept_list.nil?
354
388
  validation = [validation] unless validation.is_a?(Array) || validation.nil?
355
389
  Aspera.assert_array_all(validation, Class){'validation'} unless validation.nil?
356
- descr = "#{descr} (#{validation.join(', ')})" unless validation.nil? || validation.eql?(Allowed::TYPES_STRING)
390
+ descr = "#{descr}#{add_types_info(validation)}"
357
391
  result =
358
392
  if !@unprocessed_cmd_line_arguments.empty?
359
- if multiple
360
- index = @unprocessed_cmd_line_arguments.index(SpecialValues::EOA)
361
- if index.nil?
362
- values = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length)
363
- 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
364
401
  values = @unprocessed_cmd_line_arguments.shift(index)
365
- @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)
366
405
  end
367
- else
368
- values = [@unprocessed_cmd_line_arguments.shift]
406
+ else Aspera.error_unexpected_value(multiple){'multiple'}
369
407
  end
370
408
  values = values.map{ |v| ExtendedValue.instance.evaluate(v, context: "argument: #{descr}", allowed: validation)}
371
409
  # If expecting list and only one arg of type array : it is the list
@@ -378,7 +416,7 @@ module Aspera
378
416
  multiple ? values : values.first
379
417
  elsif !default.nil? then default
380
418
  # no value provided, either get value interactively, or exception
381
- 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)
382
420
  end
383
421
  if result.is_a?(String) && validation&.eql?(Allowed::TYPES_INTEGER)
384
422
  int_result = Integer(result, exception: false)
@@ -392,18 +430,22 @@ module Aspera
392
430
  if validation && (mandatory || !result.nil?)
393
431
  value_list = multiple ? result : [result]
394
432
  value_list.each do |value|
433
+ raise SchemaRequest.new(:argument, descr, schema) if validation.include?(Hash) && value.eql?(HELP)
395
434
  raise Cli::BadArgument,
396
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)}
397
436
  end
398
437
  end
399
- return result
438
+ result
400
439
  end
401
440
 
402
441
  # Resource identifier as positional parameter
403
442
  #
404
443
  # @param description [String] description of the identifier
405
444
  # @param block [Proc] block to search for identifier based on attribute value
406
- # @return [String, Array] identifier or list of ids
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
407
449
  def instance_identifier(description: 'identifier', &block)
408
450
  res_id = get_next_argument(description, multiple: get_option(:bulk)) if res_id.nil?
409
451
  # Can be an Array
@@ -411,19 +453,28 @@ module Aspera
411
453
  Aspera.assert(block_given?, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
412
454
  res_id = yield(m[:field], m[:value])
413
455
  end
414
- return res_id
456
+ res_id
415
457
  end
416
458
 
417
- def get_next_command(command_list, aliases: nil); return get_next_argument('command', accept_list: command_list, aliases: aliases); end
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
418
469
 
419
470
  # Get an option value by name
420
471
  # either return value or calls handler, can return nil
421
472
  # ask interactively if requested/required
473
+ # @param option_symbol [Symbol]
422
474
  # @param mandatory [Boolean] if true, raise error if option not set
423
475
  def get_option(option_symbol, mandatory: false)
424
476
  Aspera.assert_type(option_symbol, Symbol)
425
- Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument){"Unknown option: #{option_symbol}"}
426
- option_attrs = @declared_options[option_symbol]
477
+ option_attrs = option_def(option_symbol)
427
478
  result = option_attrs.value
428
479
  # Do not fail for manual generation if option mandatory but not set
429
480
  return :skip_missing_mandatory if result.nil? && mandatory && !@fail_on_missing_mandatory
@@ -432,11 +483,11 @@ module Aspera
432
483
  Aspera.assert(!mandatory, type: Cli::BadArgument){"Missing mandatory option: #{option_symbol}"}
433
484
  elsif @ask_missing_optional || mandatory
434
485
  # ask_missing_mandatory
435
- 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)
436
487
  set_option(option_symbol, result, where: 'interactive')
437
488
  end
438
489
  end
439
- return result
490
+ result
440
491
  end
441
492
 
442
493
  # Set an option value by name, either store value or call handler
@@ -446,15 +497,15 @@ module Aspera
446
497
  # @param where [String] Where the value comes from
447
498
  def set_option(option_symbol, value, where: 'code override')
448
499
  Aspera.assert_type(option_symbol, Symbol)
449
- Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument){"Unknown option: #{option_symbol}"}
450
- @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)
451
503
  end
452
504
 
453
505
  # Set option to `nil`
454
506
  def clear_option(option_symbol)
455
507
  Aspera.assert_type(option_symbol, Symbol)
456
- Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument){"Unknown option: #{option_symbol}"}
457
- @declared_options[option_symbol].clear
508
+ option_def(option_symbol).clear
458
509
  end
459
510
 
460
511
  # Adds each of the keys of specified hash as an option
@@ -477,7 +528,7 @@ module Aspera
477
528
 
478
529
  # Check if there were unprocessed values to generate error
479
530
  def command_or_arg_empty?
480
- return @unprocessed_cmd_line_arguments.empty?
531
+ @unprocessed_cmd_line_arguments.empty?
481
532
  end
482
533
 
483
534
  # Unprocessed options or arguments ?
@@ -485,7 +536,7 @@ module Aspera
485
536
  result = []
486
537
  result.push("unprocessed options: #{@unprocessed_cmd_line_options}") unless @unprocessed_cmd_line_options.empty?
487
538
  result.push("unprocessed values: #{@unprocessed_cmd_line_arguments}") unless @unprocessed_cmd_line_arguments.empty?
488
- return result
539
+ result
489
540
  end
490
541
 
491
542
  # Get all original options on command line used to generate a config in config file
@@ -495,7 +546,7 @@ module Aspera
495
546
  @initial_cli_options.each do |option_argument|
496
547
  # ignore short options
497
548
  next unless option_argument.start_with?(OPTION_PREFIX)
498
- 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)
499
550
  # ignore options without value
500
551
  next if value.nil?
501
552
  Log.log.debug{"option #{name}=#{value}"}
@@ -504,7 +555,7 @@ module Aspera
504
555
  DotContainer.dotted_to_container(path, smart_convert(value), result)
505
556
  @unprocessed_cmd_line_options.delete(option_argument)
506
557
  end
507
- return result
558
+ result
508
559
  end
509
560
 
510
561
  # @param only_defined [Boolean] if true, only return options that were defined
@@ -517,7 +568,7 @@ module Aspera
517
568
  rescue => e
518
569
  result[option_symbol] = e.to_s
519
570
  end
520
- return result
571
+ result
521
572
  end
522
573
 
523
574
  # Removes already known options from the list
@@ -539,7 +590,7 @@ module Aspera
539
590
  Log.log.trace1{"InvalidOption #{e}".red}
540
591
  # An option like --a.b.c=d does: a={"b":{"c":ext_val(d)}}
541
592
  if e.args.first.start_with?(OPTION_PREFIX)
542
- 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)
543
594
  if !value.nil?
544
595
  path = name.split(DotContainer::SEPARATOR)
545
596
  option_sym = self.class.option_line_to_name(path.shift).to_sym
@@ -565,7 +616,7 @@ module Aspera
565
616
  print("#{prompt}> ")
566
617
  line = $stdin.gets
567
618
  Aspera.assert_type(line, String){'Unexpected end of standard input'}
568
- return line.chomp
619
+ line.chomp
569
620
  end
570
621
 
571
622
  # prompt user for input in a list of symbols
@@ -586,20 +637,19 @@ module Aspera
586
637
  # Prompt user for input in a list of symbols
587
638
  # @param descr [String] description for help
588
639
  # @param check_option [Boolean] Check attributes of option with name=descr
589
- # @param multiple [Boolean] true if multiple values expected
590
- # @param accept_list [Array] list of expected values
591
- 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)
592
644
  option_attrs = @declared_options[descr.to_sym]
593
645
  what = option_attrs ? 'option' : 'argument'
646
+ default_prompt = "#{what}: #{descr}"
594
647
  if !@ask_missing_mandatory
595
- message = "missing #{what}: #{descr}"
596
- if accept_list.nil?
597
- raise Cli::BadArgument, message
598
- else
599
- Aspera.assert(false, self.class.multi_choice_assert_msg(message, accept_list), type: Cli::MissingArgument)
600
- 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
601
652
  end
602
- default_prompt = "#{what}: #{descr}"
603
653
  # ask interactively
604
654
  result = []
605
655
  puts(' (one per line, end with empty line)') if multiple
@@ -613,18 +663,20 @@ module Aspera
613
663
  return entry unless multiple
614
664
  result.push(entry)
615
665
  end
616
- return result
666
+ result
617
667
  end
618
668
 
619
- # Read remaining args and build an Array or Hash
620
- # @param value [nil] Argument to `@:` extended value
621
- 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)
622
673
  # This extended value does not take args (`@:`)
623
- ExtendedValue.assert_no_value(arg, :p)
674
+ # ExtendedValue.assert_no_value(end_marker, :p)
675
+ end_marker = SpecialValues::EOA if end_marker.empty?
624
676
  result = nil
625
- get_next_argument(:args, multiple: true).each do |arg|
626
- Aspera.assert(arg.include?(OPTION_VALUE_SEPARATOR)){"Positional argument: #{arg} does not include #{OPTION_VALUE_SEPARATOR}"}
627
- 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)
628
680
  result = DotContainer.dotted_to_container(path.split(DotContainer::SEPARATOR), smart_convert(value), result)
629
681
  end
630
682
  result
@@ -651,7 +703,7 @@ module Aspera
651
703
  def symbol_to_option(symbol, opt_val = nil)
652
704
  result = [OPTION_PREFIX, symbol.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)].join
653
705
  result = [result, OPTION_VALUE_SEPARATOR, opt_val].join unless opt_val.nil?
654
- return result
706
+ result
655
707
  end
656
708
 
657
709
  # TODO: use formatter
@@ -704,6 +756,8 @@ module Aspera
704
756
  SOURCE_USER = 'cmdline' # cspell:disable-line
705
757
  # Percent selector: select by this field for this value
706
758
  REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
759
+ # Ask for schema of Extended value
760
+ HELP = 'help'
707
761
 
708
762
  private_constant :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER, :REGEX_LOOKUP_ID_BY_FIELD
709
763
  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