aspera-cli 4.17.0 → 4.18.1

Sign up to get free protection for your applications and to get access to all the features.
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