aspera-cli 4.26.0 → 4.26.2

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