aspera-cli 4.17.0 → 4.18.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 (85) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +3 -4
  3. data/CHANGELOG.md +33 -0
  4. data/CONTRIBUTING.md +15 -1
  5. data/README.md +711 -432
  6. data/bin/ascli +5 -0
  7. data/bin/asession +2 -2
  8. data/examples/build_package.sh +28 -0
  9. data/lib/aspera/agent/alpha.rb +10 -8
  10. data/lib/aspera/agent/base.rb +9 -6
  11. data/lib/aspera/agent/connect.rb +7 -8
  12. data/lib/aspera/agent/direct.rb +56 -37
  13. data/lib/aspera/agent/httpgw.rb +23 -324
  14. data/lib/aspera/agent/node.rb +19 -20
  15. data/lib/aspera/agent/trsdk.rb +19 -20
  16. data/lib/aspera/api/aoc.rb +17 -14
  17. data/lib/aspera/api/cos_node.rb +4 -4
  18. data/lib/aspera/api/httpgw.rb +342 -0
  19. data/lib/aspera/api/node.rb +135 -89
  20. data/lib/aspera/ascmd.rb +4 -3
  21. data/lib/aspera/ascp/installation.rb +15 -7
  22. data/lib/aspera/ascp/management.rb +2 -2
  23. data/lib/aspera/ascp/products.rb +1 -1
  24. data/lib/aspera/cli/basic_auth_plugin.rb +5 -9
  25. data/lib/aspera/cli/extended_value.rb +35 -16
  26. data/lib/aspera/cli/formatter.rb +161 -70
  27. data/lib/aspera/cli/hints.rb +18 -0
  28. data/lib/aspera/cli/main.rb +32 -39
  29. data/lib/aspera/cli/manager.rb +151 -119
  30. data/lib/aspera/cli/plugin.rb +27 -21
  31. data/lib/aspera/cli/plugin_factory.rb +31 -20
  32. data/lib/aspera/cli/plugins/alee.rb +14 -2
  33. data/lib/aspera/cli/plugins/aoc.rb +152 -141
  34. data/lib/aspera/cli/plugins/ats.rb +1 -1
  35. data/lib/aspera/cli/plugins/config.rb +72 -65
  36. data/lib/aspera/cli/plugins/console.rb +8 -5
  37. data/lib/aspera/cli/plugins/faspex.rb +32 -23
  38. data/lib/aspera/cli/plugins/faspex5.rb +232 -156
  39. data/lib/aspera/cli/plugins/faspio.rb +85 -0
  40. data/lib/aspera/cli/plugins/httpgw.rb +55 -0
  41. data/lib/aspera/cli/plugins/node.rb +129 -64
  42. data/lib/aspera/cli/plugins/orchestrator.rb +33 -30
  43. data/lib/aspera/cli/plugins/preview.rb +7 -3
  44. data/lib/aspera/cli/plugins/server.rb +6 -6
  45. data/lib/aspera/cli/plugins/shares.rb +16 -14
  46. data/lib/aspera/cli/special_values.rb +13 -0
  47. data/lib/aspera/cli/sync_actions.rb +10 -10
  48. data/lib/aspera/cli/transfer_agent.rb +7 -6
  49. data/lib/aspera/cli/version.rb +1 -1
  50. data/lib/aspera/environment.rb +70 -9
  51. data/lib/aspera/faspex_gw.rb +5 -4
  52. data/lib/aspera/faspex_postproc.rb +2 -2
  53. data/lib/aspera/log.rb +6 -3
  54. data/lib/aspera/node_simulator.rb +2 -2
  55. data/lib/aspera/oauth/base.rb +31 -19
  56. data/lib/aspera/oauth/factory.rb +12 -13
  57. data/lib/aspera/oauth/generic.rb +1 -0
  58. data/lib/aspera/oauth/jwt.rb +18 -15
  59. data/lib/aspera/oauth/url_json.rb +8 -6
  60. data/lib/aspera/oauth/web.rb +2 -2
  61. data/lib/aspera/persistency_folder.rb +2 -2
  62. data/lib/aspera/preview/generator.rb +3 -3
  63. data/lib/aspera/preview/options.rb +3 -3
  64. data/lib/aspera/preview/terminal.rb +4 -4
  65. data/lib/aspera/preview/utils.rb +3 -3
  66. data/lib/aspera/proxy_auto_config.rb +5 -1
  67. data/lib/aspera/rest.rb +105 -88
  68. data/lib/aspera/rest_call_error.rb +1 -1
  69. data/lib/aspera/rest_error_analyzer.rb +2 -2
  70. data/lib/aspera/rest_errors_aspera.rb +1 -1
  71. data/lib/aspera/resumer.rb +1 -1
  72. data/lib/aspera/secret_hider.rb +2 -4
  73. data/lib/aspera/ssh.rb +1 -1
  74. data/lib/aspera/transfer/parameters.rb +39 -36
  75. data/lib/aspera/transfer/spec.rb +2 -0
  76. data/lib/aspera/transfer/sync.rb +2 -1
  77. data/lib/aspera/transfer/uri.rb +1 -1
  78. data/lib/aspera/uri_reader.rb +5 -4
  79. data/lib/aspera/web_auth.rb +1 -1
  80. data/lib/aspera/web_server_simple.rb +4 -3
  81. data.tar.gz.sig +0 -0
  82. metadata +7 -4
  83. metadata.gz.sig +0 -0
  84. data/lib/aspera/cli/plugins/bss.rb +0 -71
  85. data/lib/aspera/open_application.rb +0 -71
@@ -55,9 +55,13 @@ module Aspera
55
55
  # option name separator in code (symbol)
56
56
  OPTION_SEP_SYMBOL = '_'
57
57
  SOURCE_USER = 'cmdline' # cspell:disable-line
58
- TYPE_INTEGER = [Integer].freeze
58
+ OPTION_VALUE_SEPARATOR = '='
59
+ OPTION_PREFIX = '--'
60
+ OPTIONS_STOP = '--'
59
61
 
60
- private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :SOURCE_USER, :TYPE_INTEGER
62
+ DEFAULT_PARSER_TYPES = [Array, Hash].freeze
63
+
64
+ private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :SOURCE_USER
61
65
 
62
66
  class << self
63
67
  def enum_to_bool(enum)
@@ -83,9 +87,9 @@ module Aspera
83
87
 
84
88
  # Generates error message with list of allowed values
85
89
  # @param error_msg [String] error message
86
- # @param choices [Array] list of allowed values
87
- def multi_choice_assert(assertion, error_msg, choices)
88
- raise Cli::BadArgument, [error_msg, 'Use:'].concat(choices.map{|c|"- #{c}"}.sort).join("\n") unless assertion
90
+ # @param accept_list [Array] list of allowed values
91
+ def multi_choice_assert(assertion, error_msg, accept_list)
92
+ raise Cli::BadArgument, [error_msg, 'Use:'].concat(accept_list.map{|c|"- #{c}"}.sort).join("\n") unless assertion
89
93
  end
90
94
 
91
95
  # change option name with dash to name with underscore
@@ -94,19 +98,22 @@ module Aspera
94
98
  end
95
99
 
96
100
  def option_name_to_line(name)
97
- return "--#{name.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)}"
101
+ return "#{OPTION_PREFIX}#{name.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)}"
98
102
  end
99
103
 
100
104
  # @param what [Symbol] :option or :argument
101
105
  # @param descr [String] description for help
102
- # @param value [Object] value to check
106
+ # @param to_check [Object] value to check
103
107
  # @param type_list [NilClass, Class, Array[Class]] accepted value type(s)
104
- def validate_type(what, descr, value, type_list)
108
+ def validate_type(what, descr, to_check, type_list, check_array: false)
105
109
  return nil if type_list.nil?
106
110
  Aspera.assert(type_list.is_a?(Array) && type_list.all?(Class)){'types must be a Class Array'}
107
- raise Cli::BadArgument,
108
- "#{what.to_s.capitalize} #{descr} is a #{value.class} but must be #{type_list.length > 1 ? 'one of ' : ''}#{type_list.map(&:name).join(',')}" unless \
109
- type_list.any?{|t|value.is_a?(t)}
111
+ value_list = check_array ? to_check : [to_check]
112
+ value_list.each do |value|
113
+ raise Cli::BadArgument,
114
+ "#{what.to_s.capitalize} #{descr} is a #{value.class} but must be #{type_list.length > 1 ? 'one of ' : ''}#{type_list.map(&:name).join(',')}" unless \
115
+ type_list.any?{|t|value.is_a?(t)}
116
+ end
110
117
  end
111
118
  end
112
119
 
@@ -114,23 +121,25 @@ module Aspera
114
121
  attr_accessor :ask_missing_mandatory, :ask_missing_optional
115
122
  attr_writer :fail_on_missing_mandatory
116
123
 
117
- def initialize(program_name)
118
- # command line values not starting with '-'
124
+ def initialize(program_name, argv = nil)
125
+ # command line values *not* starting with '-'
119
126
  @unprocessed_cmd_line_arguments = []
120
127
  # command line values starting with '-'
121
128
  @unprocessed_cmd_line_options = []
122
129
  # a copy of all initial options
123
130
  @initial_cli_options = []
124
- # option description: key = option symbol, value=hash, :read_write, :accessor, :value, :accepted
131
+ # option description: key = option symbol, value=Hash, :read_write, :accessor, :value, :accepted
125
132
  @declared_options = {}
126
133
  # do we ask missing options and arguments to user ?
127
134
  @ask_missing_mandatory = false # STDIN.isatty
128
135
  # ask optional options if not provided and in interactive
129
136
  @ask_missing_optional = false
130
137
  @fail_on_missing_mandatory = true
131
- # those must be set before parse, parse consumes those defined only
132
- @unprocessed_defaults = []
133
- @unprocessed_env = []
138
+ # Array of [key(sym), value]
139
+ # those must be set before parse
140
+ # parse consumes those defined only
141
+ @option_pairs_batch = {}
142
+ @option_pairs_env = {}
134
143
  # NOTE: was initially inherited but it is preferred to have specific methods
135
144
  @parser = OptionParser.new
136
145
  @parser.program_name = program_name
@@ -138,100 +147,94 @@ module Aspera
138
147
  env_prefix = program_name.upcase + OPTION_SEP_SYMBOL
139
148
  ENV.each do |k, v|
140
149
  if k.start_with?(env_prefix)
141
- @unprocessed_env.push([k[env_prefix.length..-1].downcase.to_sym, v])
150
+ @option_pairs_env[k[env_prefix.length..-1].downcase.to_sym] = v
142
151
  end
143
152
  end
144
- Log.log.debug{"env=#{@unprocessed_env}".red}
153
+ Log.log.debug{"env=#{@option_pairs_env}".red}
145
154
  @unprocessed_cmd_line_options = []
146
155
  @unprocessed_cmd_line_arguments = []
147
- end
148
-
149
- def parse_command_line(argv)
150
- @parser.separator('')
151
- @parser.separator('OPTIONS: global')
152
- declare(:interactive, 'Use interactive input of missing params', values: :bool, handler: {o: self, m: :ask_missing_mandatory})
153
- declare(:ask_options, 'Ask even optional options', values: :bool, handler: {o: self, m: :ask_missing_optional})
154
- parse_options!
156
+ return if argv.nil?
155
157
  process_options = true
156
158
  until argv.empty?
157
159
  value = argv.shift
158
160
  if process_options && value.start_with?('-')
159
- if value.eql?('--')
161
+ Log.log.trace1{"opt: #{value}"}
162
+ if value.eql?(OPTIONS_STOP)
160
163
  process_options = false
161
164
  else
162
165
  @unprocessed_cmd_line_options.push(value)
163
166
  end
164
167
  else
168
+ Log.log.trace1{"arg: #{value}"}
165
169
  @unprocessed_cmd_line_arguments.push(value)
166
170
  end
167
171
  end
168
- @initial_cli_options = @unprocessed_cmd_line_options.dup
172
+ @initial_cli_options = @unprocessed_cmd_line_options.dup.freeze
169
173
  Log.log.debug{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
174
+ @parser.separator('')
175
+ @parser.separator('OPTIONS: global')
176
+ declare(:interactive, 'Use interactive input of missing params', values: :bool, handler: {o: self, m: :ask_missing_mandatory})
177
+ declare(:ask_options, 'Ask even optional options', values: :bool, handler: {o: self, m: :ask_missing_optional})
178
+ declare(:struct_parser, 'Default parser when expected value is a struct', values: %i[json ruby])
179
+ # do not parse options yet, let's wait for option `-h` to be overriden
170
180
  end
171
181
 
172
182
  # @param descr [String] description for help
173
- # @param expected is
174
- # - Array of allowed value (single value)
175
- # - :multiple for remaining values
176
- # - :single for a single unconstrained value
177
- # - :integer for a single integer value
178
183
  # @param mandatory [Boolean] if true, raise error if option not set
179
- # @param type [Class, Array] accepted value type(s)
184
+ # @param multiple [Boolean] if true, return remaining arguments
185
+ # @param accept_list [Array] list of allowed values (Symbol)
186
+ # @param validation [Class, Array] accepted value type(s) or list of Symbols
180
187
  # @param aliases [Hash] map of aliases: key = alias, value = real value
181
188
  # @param default [Object] default value
182
- # @return value, list or nil
183
- def get_next_argument(descr, expected: :single, mandatory: true, type: nil, aliases: nil, default: nil)
184
- Aspera.assert(%i[single multiple].include?(expected) || (expected.is_a?(Array) && expected.all?(Symbol))) do
185
- 'expected must be single, multiple, or array of symbol'
186
- end
187
- Aspera.assert(type.nil? || type.is_a?(Class) || (type.is_a?(Array) && type.all?(Class))){'type must be Class or Array of Class'}
188
- Aspera.assert(aliases.nil? || (aliases.is_a?(Hash) && aliases.keys.all?(Symbol) && aliases.values.all?(Symbol))){'aliases must be Hash'}
189
- allowed_types = type
189
+ # @return one value, list or nil (if optional and no default)
190
+ def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: String, aliases: nil, default: nil)
191
+ Aspera.assert(accept_list.nil? || (accept_list.is_a?(Array) && accept_list.all?(Symbol)))
192
+ validation = Symbol if accept_list
193
+ Aspera.assert(validation.nil? || validation.is_a?(Class) || (validation.is_a?(Array) && validation.all?(Class))){'validation must be Class or Array of Class'}
194
+ Aspera.assert(aliases.nil? || (aliases.is_a?(Hash) && aliases.keys.all?(Symbol) && aliases.values.all?(Symbol))){'aliases must be Hash:Symbol: Symbol'}
195
+ allowed_types = validation
190
196
  unless allowed_types.nil?
191
197
  allowed_types = [allowed_types] unless allowed_types.is_a?(Array)
192
198
  descr = "#{descr} (#{allowed_types.join(', ')})"
193
199
  end
194
200
  result =
195
201
  if !@unprocessed_cmd_line_arguments.empty?
196
- # there are values
197
- case expected
198
- when :single
199
- ExtendedValue.instance.evaluate(@unprocessed_cmd_line_arguments.shift)
200
- when :multiple
201
- value = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length).map{|v|ExtendedValue.instance.evaluate(v)}
202
- # if expecting list and only one arg of type array : it is the list
203
- if value.length.eql?(1) && value.first.is_a?(Array)
204
- value = value.first
205
- end
206
- value
207
- when Array
208
- allowed_values = [].concat(expected)
202
+ how_many = multiple ? @unprocessed_cmd_line_arguments.length : 1
203
+ values = @unprocessed_cmd_line_arguments.shift(how_many)
204
+ values = values.map{|v|evaluate_extended_value(v, allowed_types)}
205
+ # if expecting list and only one arg of type array : it is the list
206
+ values = values.first if values.length.eql?(1) && values.first.is_a?(Array)
207
+ if accept_list
208
+ allowed_values = [].concat(accept_list)
209
209
  allowed_values.concat(aliases.keys) unless aliases.nil?
210
- self.class.get_from_list(@unprocessed_cmd_line_arguments.shift, descr, allowed_values)
211
- else Aspera.error_unexpected_value(expected)
210
+ values = values.map{|v|self.class.get_from_list(v, descr, allowed_values)}
212
211
  end
212
+ multiple ? values : values.first
213
213
  elsif !default.nil? then default
214
214
  # no value provided, either get value interactively, or exception
215
- elsif mandatory then get_interactive(:argument, descr, expected: expected)
215
+ elsif mandatory then get_interactive(descr, multiple: multiple, accept_list: accept_list)
216
216
  end
217
- if result.is_a?(String) && allowed_types.eql?(TYPE_INTEGER)
217
+ if result.is_a?(String) && validation.eql?(Integer)
218
218
  int_result = Integer(result, exception: false)
219
219
  raise Cli::BadArgument, "Invalid integer: #{result}" if int_result.nil?
220
220
  result = int_result
221
221
  end
222
222
  Log.log.debug{"#{descr}=#{result}"}
223
223
  result = aliases[result] if aliases&.key?(result)
224
- self.class.validate_type(:argument, descr, result, allowed_types) unless result.nil? && !mandatory
224
+ # if value comes from JSON/YAML, it may come as Integer
225
+ result = result.to_s if result.is_a?(Integer) && validation.eql?(String)
226
+ self.class.validate_type(:argument, descr, result, allowed_types, check_array: multiple) unless result.nil? && !mandatory
225
227
  return result
226
228
  end
227
229
 
228
- def get_next_command(command_list, aliases: nil); return get_next_argument('command', expected: command_list, aliases: aliases); end
230
+ def get_next_command(command_list, aliases: nil); return get_next_argument('command', accept_list: command_list, aliases: aliases); end
229
231
 
230
232
  # Get an option value by name
231
233
  # either return value or calls handler, can return nil
232
234
  # ask interactively if requested/required
233
235
  # @param mandatory [Boolean] if true, raise error if option not set
234
236
  def get_option(option_symbol, mandatory: false, default: nil)
237
+ Aspera.assert_type(option_symbol, Symbol)
235
238
  attributes = @declared_options[option_symbol]
236
239
  Aspera.assert(attributes){"option not declared: #{option_symbol}"}
237
240
  result = nil
@@ -253,13 +256,13 @@ module Aspera
253
256
  raise Cli::BadArgument, "Missing mandatory option: #{option_symbol}" if mandatory
254
257
  elsif @ask_missing_optional || mandatory
255
258
  # ask_missing_mandatory
256
- expected = :single
259
+ accept_list = nil
257
260
  # print "please enter: #{option_symbol.to_s}"
258
261
  if @declared_options.key?(option_symbol) && attributes.key?(:values)
259
- expected = attributes[:values]
262
+ accept_list = attributes[:values]
260
263
  end
261
- result = get_interactive(:option, option_symbol.to_s, expected: expected)
262
- set_option(option_symbol, result, 'interactive')
264
+ result = get_interactive(option_symbol.to_s, option: true, accept_list: accept_list)
265
+ set_option(option_symbol, result, where: 'interactive')
263
266
  end
264
267
  end
265
268
  self.class.validate_type(:option, option_symbol, result, attributes[:types]) unless result.nil? && !mandatory
@@ -267,11 +270,16 @@ module Aspera
267
270
  end
268
271
 
269
272
  # set an option value by name, either store value or call handler
270
- def set_option(option_symbol, value, where='code override')
273
+ # @param option_symbol [Symbol] option name
274
+ # @param value [String] value to set
275
+ # @param where [String] where the value comes from
276
+ # @param expect [Class, Array] expected value type(s)
277
+ def set_option(option_symbol, value, where: 'code override')
278
+ Aspera.assert_type(option_symbol, Symbol)
271
279
  raise Cli::BadArgument, "Unknown option: #{option_symbol}" unless @declared_options.key?(option_symbol)
272
280
  attributes = @declared_options[option_symbol]
273
281
  Log.log.warn("#{option_symbol}: Option is deprecated: #{attributes[:deprecation]}") if attributes[:deprecation]
274
- value = ExtendedValue.instance.evaluate(value)
282
+ value = evaluate_extended_value(value, attributes[:types])
275
283
  value = Manager.enum_to_bool(value) if attributes[:values].eql?(BOOLEAN_VALUES)
276
284
  Log.log.debug{"(#{attributes[:read_write]}/#{where}) set #{option_symbol}=#{value}"}
277
285
  self.class.validate_type(:option, option_symbol, value, attributes[:types])
@@ -292,10 +300,11 @@ module Aspera
292
300
  # @param default [Object] default value
293
301
  # @param values [nil, Array, :bool, :date, :none] list of allowed values, :bool for true/false, :date for dates, :none for on/off switch
294
302
  # @param short [String] short option name
295
- # @param coerce [Class] one of the coerce types accepted par option parser
303
+ # @param coerce [Class] one of the coerce types accepted by option parser
296
304
  # @param types [Class, Array] accepted value type(s)
297
305
  # @param block [Proc] block to execute when option is found
298
306
  def declare(option_symbol, description, handler: nil, default: nil, values: nil, short: nil, coerce: nil, types: nil, deprecation: nil, &block)
307
+ Aspera.assert_type(option_symbol, Symbol)
299
308
  Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
300
309
  Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
301
310
  Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with capital"}
@@ -307,7 +316,7 @@ module Aspera
307
316
  }
308
317
  if !types.nil?
309
318
  types = [types] unless types.is_a?(Array)
310
- Aspera.assert(types.all?(Class)){"types must be Array of Class: #{types}"}
319
+ Aspera.assert(types.all?(Class)){"types must be (Array of) Class: #{types}"}
311
320
  opt[:types] = types
312
321
  description = "#{description} (#{types.map(&:name).join(', ')})"
313
322
  end
@@ -322,18 +331,18 @@ module Aspera
322
331
  Log.log.debug{"set attr obj #{option_symbol} (#{handler[:o]},#{handler[:m]})"}
323
332
  opt[:accessor] = AttrAccessor.new(handler[:o], handler[:m], option_symbol)
324
333
  end
325
- set_option(option_symbol, default, 'default') unless default.nil?
334
+ set_option(option_symbol, default, where: 'default') unless default.nil?
326
335
  on_args = [description]
327
336
  case values
328
337
  when nil
329
338
  on_args.push(symbol_to_option(option_symbol, 'VALUE'))
330
339
  on_args.push("-#{short}VALUE") unless short.nil?
331
340
  on_args.push(coerce) unless coerce.nil?
332
- @parser.on(*on_args) { |v| set_option(option_symbol, v, SOURCE_USER) }
341
+ @parser.on(*on_args) { |v| set_option(option_symbol, v, where: SOURCE_USER) }
333
342
  when Array, :bool
334
343
  if values.eql?(:bool)
335
344
  values = BOOLEAN_VALUES
336
- set_option(option_symbol, Manager.enum_to_bool(default), 'default') unless default.nil?
345
+ set_option(option_symbol, Manager.enum_to_bool(default), where: 'default') unless default.nil?
337
346
  end
338
347
  # this option value must be a symbol
339
348
  opt[:values] = values
@@ -345,7 +354,7 @@ module Aspera
345
354
  on_args[0] = "#{description}: #{help_values}"
346
355
  on_args.push(symbol_to_option(option_symbol, 'ENUM'))
347
356
  on_args.push(values)
348
- @parser.on(*on_args){|v|set_option(option_symbol, self.class.get_from_list(v.to_s, description, values), SOURCE_USER)}
357
+ @parser.on(*on_args){|v|set_option(option_symbol, self.class.get_from_list(v.to_s, description, values), where: SOURCE_USER)}
349
358
  when :date
350
359
  on_args.push(symbol_to_option(option_symbol, 'DATE'))
351
360
  @parser.on(*on_args) do |v|
@@ -354,11 +363,11 @@ module Aspera
354
363
  when /^-([0-9]+)h/ then Manager.time_to_string(Time.now - (Regexp.last_match(1).to_i * 3600))
355
364
  else v
356
365
  end
357
- set_option(option_symbol, time_string, SOURCE_USER)
366
+ set_option(option_symbol, time_string, where: SOURCE_USER)
358
367
  end
359
368
  when :none
360
369
  Aspera.assert(!block.nil?){"missing block for #{option_symbol}"}
361
- on_args.push(symbol_to_option(option_symbol, nil))
370
+ on_args.push(symbol_to_option(option_symbol))
362
371
  on_args.push("-#{short}") if short.is_a?(String)
363
372
  @parser.on(*on_args, &block)
364
373
  else Aspera.error_unexpected_value(values)
@@ -368,11 +377,13 @@ module Aspera
368
377
 
369
378
  # Adds each of the keys of specified hash as an option
370
379
  # @param preset_hash [Hash] hash of options to add
371
- def add_option_preset(preset_hash, op: :push)
380
+ def add_option_preset(preset_hash, where, override: true)
372
381
  Aspera.assert_type(preset_hash, Hash)
373
- Log.log.debug{"add_option_preset=#{preset_hash}"}
374
- # incremental override
375
- preset_hash.each{|k, v|@unprocessed_defaults.send(op, [k.to_sym, v])}
382
+ Log.log.debug{"add_option_preset: #{preset_hash}, #{where}, #{override}"}
383
+ preset_hash.each do |k, v|
384
+ option_symbol = k.to_sym
385
+ @option_pairs_batch[option_symbol] = v if override || !@option_pairs_batch.key?(option_symbol)
386
+ end
376
387
  end
377
388
 
378
389
  # check if there were unprocessed values to generate error
@@ -389,20 +400,21 @@ module Aspera
389
400
  end
390
401
 
391
402
  # get all original options on command line used to generate a config in config file
392
- def get_options_table(remove_from_remaining: true)
403
+ # @return [Hash] options as taken from config file and command line just before command execution
404
+ def unprocessed_options_with_value
393
405
  result = {}
394
406
  @initial_cli_options.each do |option_value|
395
407
  case option_value
396
- when /^--([^=]+)$/
408
+ when /^#{OPTION_PREFIX}([^=]+)$/o
397
409
  # ignore
398
- when /^--([^=]+)=(.*)$/
410
+ when /^#{OPTION_PREFIX}([^=]+)=(.*)$/o
399
411
  name = Regexp.last_match(1)
400
412
  value = Regexp.last_match(2)
401
413
  name.gsub!(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
402
414
  value = ExtendedValue.instance.evaluate(value)
403
415
  Log.log.debug{"option #{name}=#{value}"}
404
416
  result[name] = value
405
- @unprocessed_cmd_line_options.delete(option_value) if remove_from_remaining
417
+ @unprocessed_cmd_line_options.delete(option_value)
406
418
  else
407
419
  raise Cli::BadArgument, "wrong option format: #{option_value}"
408
420
  end
@@ -425,8 +437,8 @@ module Aspera
425
437
  def parse_options!
426
438
  Log.log.debug('parse_options!'.red)
427
439
  # first conf file, then env var
428
- apply_options_preset(@unprocessed_defaults, 'file')
429
- apply_options_preset(@unprocessed_env, 'env')
440
+ consume_option_pairs(@option_pairs_batch, 'set')
441
+ consume_option_pairs(@option_pairs_env, 'env')
430
442
  # command line override
431
443
  unknown_options = []
432
444
  begin
@@ -445,7 +457,7 @@ module Aspera
445
457
  @unprocessed_cmd_line_options = unknown_options
446
458
  end
447
459
 
448
- def prompt_user_input(prompt, sensitive)
460
+ def prompt_user_input(prompt, sensitive: false)
449
461
  return $stdin.getpass("#{prompt}> ") if sensitive
450
462
  print("#{prompt}> ")
451
463
  line = $stdin.gets
@@ -459,7 +471,7 @@ module Aspera
459
471
  # @return [Symbol] selected symbol
460
472
  def prompt_user_input_in_list(prompt, sym_list)
461
473
  loop do
462
- input = prompt_user_input(prompt, false).to_sym
474
+ input = prompt_user_input(prompt).to_sym
463
475
  if sym_list.any?{|a|a.eql?(input)}
464
476
  return input
465
477
  else
@@ -468,38 +480,53 @@ module Aspera
468
480
  end
469
481
  end
470
482
 
471
- def get_interactive(type, descr, expected: :single)
483
+ # Prompt user for input in a list of symbols
484
+ # @param descr [String] description for help
485
+ # @param option [Boolean] true if command line option
486
+ # @param multiple [Boolean] true if multiple values expected
487
+ # @param accept_list [Array] list of expected values
488
+ def get_interactive(descr, option: false, multiple: false, accept_list: nil)
489
+ what = option ? 'option' : 'argument'
472
490
  if !@ask_missing_mandatory
473
- raise Cli::BadArgument, "missing argument (#{expected}): #{descr}" unless expected.is_a?(Array)
474
- self.class.multi_choice_assert(false, "missing: #{descr}", expected)
491
+ message = "missing #{what}: #{descr}"
492
+ if accept_list.nil?
493
+ raise Cli::BadArgument, message
494
+ else
495
+ self.class.multi_choice_assert(false, message, accept_list)
496
+ end
475
497
  end
476
498
  result = nil
477
- sensitive = type.eql?(:option) && @declared_options[descr.to_sym].is_a?(Hash) && @declared_options[descr.to_sym][:sensitive]
478
- default_prompt = "#{type}: #{descr}"
499
+ sensitive = option && @declared_options[descr.to_sym].is_a?(Hash) && @declared_options[descr.to_sym][:sensitive]
500
+ default_prompt = "#{what}: #{descr}"
479
501
  # ask interactively
480
- case expected
481
- when :multiple
482
- result = []
483
- puts(' (one per line, end with empty line)')
484
- loop do
485
- entry = prompt_user_input(default_prompt, sensitive)
486
- break if entry.empty?
487
- result.push(ExtendedValue.instance.evaluate(entry))
488
- end
489
- when :single
490
- result = ExtendedValue.instance.evaluate(prompt_user_input(default_prompt, sensitive))
491
- else # one fixed
492
- result = self.class.get_from_list(prompt_user_input("#{expected.join(' ')}\n#{default_prompt}", sensitive), descr, expected)
502
+ result = []
503
+ puts(' (one per line, end with empty line)') if multiple
504
+ loop do
505
+ prompt = default_prompt
506
+ prompt = "#{accept_list.join(' ')}\n#{default_prompt}" if accept_list
507
+ entry = prompt_user_input(prompt, sensitive: sensitive)
508
+ break if entry.empty? && multiple
509
+ entry = ExtendedValue.instance.evaluate(entry)
510
+ entry = self.class.get_from_list(entry, descr, accept_list) if accept_list
511
+ return entry unless multiple
512
+ result.push(entry)
493
513
  end
494
514
  return result
495
515
  end
496
516
 
497
517
  private
498
518
 
519
+ def evaluate_extended_value(value, types)
520
+ if DEFAULT_PARSER_TYPES.include?(types) || (types.is_a?(Array) && types.all?{|t|DEFAULT_PARSER_TYPES.include?(t)})
521
+ return ExtendedValue.instance.evaluate_with_default(value)
522
+ end
523
+ return ExtendedValue.instance.evaluate(value)
524
+ end
525
+
499
526
  # generate command line option from option symbol
500
- def symbol_to_option(symbol, opt_val)
501
- result = '--' + symbol.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)
502
- result = result + '=' + opt_val unless opt_val.nil?
527
+ def symbol_to_option(symbol, opt_val = nil)
528
+ result = [OPTION_PREFIX, symbol.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)].join
529
+ result = [result, OPTION_VALUE_SEPARATOR, opt_val].join unless opt_val.nil?
503
530
  return result
504
531
  end
505
532
 
@@ -507,23 +534,28 @@ module Aspera
507
534
  $stdout.isatty ? value.to_s.red.bold : "[#{value}]"
508
535
  end
509
536
 
510
- def apply_options_preset(preset, where)
511
- unprocessed = []
512
- preset.each do |pair|
513
- k, v = *pair
537
+ # try to evaluate options set ib batch
538
+ # @param unprocessed_options [Array] list of options to apply (key_sym,value)
539
+ # @param where [String] where the options come from
540
+ def consume_option_pairs(unprocessed_options, where)
541
+ Log.log.debug{"consume_option_pairs: #{where}"}
542
+ options_to_set = {}
543
+ unprocessed_options.each do |k, v|
514
544
  if @declared_options.key?(k)
515
545
  # constrained parameters as string are revert to symbol
516
546
  if @declared_options[k].key?(:values) && v.is_a?(String)
517
- v = self.class.get_from_list(v, k.to_s + " in #{where}", @declared_options[k][:values])
547
+ v = self.class.get_from_list(v, "#{k} in #{where}", @declared_options[k][:values])
518
548
  end
519
- set_option(k, v, where)
549
+ options_to_set[k] = v
520
550
  else
521
- unprocessed.push(pair)
551
+ Log.log.debug{"unprocessed: #{k}: #{v}"}
522
552
  end
523
553
  end
524
- # keep only unprocessed values for next parse
525
- preset.clear
526
- preset.push(*unprocessed)
554
+ options_to_set.each do |k, v|
555
+ set_option(k, v, where: where)
556
+ # keep only unprocessed values for next parse
557
+ unprocessed_options.delete(k)
558
+ end
527
559
  end
528
560
  end
529
561
  end
@@ -19,6 +19,7 @@ module Aspera
19
19
  MAX_PAGES = 'pmax'
20
20
  # special identifier format: look for this name to find where supported
21
21
  REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/.freeze
22
+ # instance variables, also constructor parameters
22
23
  INIT_PARAMS = %i[options transfer config formatter persistency only_manual].freeze
23
24
 
24
25
  class << self
@@ -28,7 +29,6 @@ module Aspera
28
29
  :value, 'Value for create, update, list filter', types: Hash,
29
30
  deprecation: '(4.14) Use positional value for create/modify or option: query for list/delete')
30
31
  options.declare(:property, 'Name of property to set (modify operation)')
31
- options.declare(:id, 'Resource identifier', deprecation: "(4.14) Use positional identifier after verb (#{INSTANCE_OPS.join(',')})")
32
32
  options.declare(:bulk, 'Bulk operation (only some)', values: :bool, default: :no)
33
33
  options.declare(:bfail, 'Bulk operation error handling', values: :bool, default: :yes)
34
34
  end
@@ -62,15 +62,14 @@ module Aspera
62
62
  # @param description [String] description of the identifier
63
63
  # @param as_option [Symbol] option name to use if identifier is an option
64
64
  # @param block [Proc] block to search for identifier based on attribute value
65
- # @return [String] identifier
65
+ # @return [String, Array] identifier or list of ids
66
66
  def instance_identifier(description: 'identifier', as_option: nil, &block)
67
67
  if as_option.nil?
68
- res_id = options.get_option(:id)
69
- res_id = options.get_next_argument(description) if res_id.nil?
68
+ res_id = options.get_next_argument(description, multiple: options.get_option(:bulk)) if res_id.nil?
70
69
  else
71
70
  res_id = options.get_option(as_option)
72
71
  end
73
- # cab be an Array
72
+ # can be an Array
74
73
  if res_id.is_a?(String) && (m = res_id.match(REGEX_LOOKUP_ID_BY_FIELD))
75
74
  if block
76
75
  res_id = yield(m[1], ExtendedValue.instance.evaluate(m[2]))
@@ -108,7 +107,9 @@ module Aspera
108
107
  # execute custom code
109
108
  res = yield(param)
110
109
  # if block returns a hash, let's use this (create)
111
- result = res if param.is_a?(Hash)
110
+ result = res if res.is_a?(Hash)
111
+ # TODO: remove when faspio gw api fixes this
112
+ result = res.first if res.is_a?(Array) && res.first.is_a?(Hash)
112
113
  # create -> created
113
114
  result['status'] = "#{command}#{'e' unless command.to_s.end_with?('e')}d".gsub(/yed$/, 'ied')
114
115
  rescue StandardError => e
@@ -133,9 +134,16 @@ module Aspera
133
134
  # @param item_list_key [String] result is in a sub key of the json
134
135
  # @param id_as_arg [String] if set, the id is provided as url argument ?<id_as_arg>=<id>
135
136
  # @param is_singleton [Boolean] if true, res_class_path is the full path to the resource
137
+ # @param delete_style [String] if set, the delete operation by array in payload
136
138
  # @param block [Proc] block to search for identifier based on attribute value
137
139
  # @return result suitable for CLI result
138
- def entity_command(command, rest_api, res_class_path, display_fields: nil, item_list_key: false, id_as_arg: false, is_singleton: false, &block)
140
+ def entity_command(command, rest_api, res_class_path,
141
+ display_fields: nil,
142
+ item_list_key: false,
143
+ id_as_arg: false,
144
+ is_singleton: false,
145
+ delete_style: nil,
146
+ &block)
139
147
  if is_singleton
140
148
  one_res_path = res_class_path
141
149
  elsif INSTANCE_OPS.include?(command)
@@ -152,14 +160,20 @@ module Aspera
152
160
  end
153
161
  when :delete
154
162
  raise 'cannot delete singleton' if is_singleton
163
+ if !delete_style.nil?
164
+ one_res_id = [one_res_id] unless one_res_id.is_a?(Array)
165
+ Aspera.assert_type(one_res_id, Array, exception_class: Cli::BadArgument)
166
+ rest_api.call(operation: 'DELETE', subpath: res_class_path, headers: {'Accept' => 'application/json'}, body: {delete_style => one_res_id}, body_type: :json)
167
+ return Main.result_status('deleted')
168
+ end
155
169
  return do_bulk_operation(command: command, descr: 'identifier', values: one_res_id) do |one_id|
156
- rest_api.delete("#{res_class_path}/#{one_id}", old_query_read_delete)
170
+ rest_api.delete("#{res_class_path}/#{one_id}", query_read_delete)
157
171
  {'id' => one_id}
158
172
  end
159
173
  when :show
160
174
  return {type: :single_object, data: rest_api.read(one_res_path)[:data], fields: display_fields}
161
175
  when :list
162
- resp = rest_api.read(res_class_path, old_query_read_delete)
176
+ resp = rest_api.read(res_class_path, query_read_delete)
163
177
  return Main.result_empty if resp[:http].code == '204'
164
178
  data = resp[:data]
165
179
  # TODO: not generic : which application is this for ?
@@ -215,14 +229,6 @@ module Aspera
215
229
  return query
216
230
  end
217
231
 
218
- # TODO: when deprecation of `value` is completed: remove this method, replace with query_read_delete
219
- # deprecation: 4.14
220
- def old_query_read_delete
221
- query = options.get_option(:value) # legacy, deprecated, remove, one day...
222
- query = query_read_delete if query.nil?
223
- return query
224
- end
225
-
226
232
  # TODO: when deprecation of `value` is completed: remove this method, replace with options.get_option(:query)
227
233
  # deprecation: 4.14
228
234
  def query_option(mandatory: false, default: nil)
@@ -246,7 +252,7 @@ module Aspera
246
252
  Log.log.warn("option `value` is deprecated. Use positional parameter for #{command}") unless value.nil?
247
253
  value = options.get_next_argument(
248
254
  "parameters for #{command}#{description.nil? ? '' : " (#{description})"}", mandatory: default.nil?,
249
- type: bulk ? Array : type) if value.nil?
255
+ validation: bulk ? Array : type) if value.nil?
250
256
  value = default if value.nil?
251
257
  unless type.nil?
252
258
  type = [type] unless type.is_a?(Array)
@@ -262,6 +268,6 @@ module Aspera
262
268
  end
263
269
  return value
264
270
  end
265
- end # Plugin
266
- end # Cli
267
- end # Aspera
271
+ end
272
+ end
273
+ end