aspera-cli 4.14.0 → 4.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +54 -3
  4. data/CONTRIBUTING.md +7 -7
  5. data/README.md +1457 -880
  6. data/bin/ascli +18 -9
  7. data/bin/asession +12 -14
  8. data/examples/proxy.pac +1 -1
  9. data/lib/aspera/aoc.rb +198 -127
  10. data/lib/aspera/ascmd.rb +24 -14
  11. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  12. data/lib/aspera/cli/error.rb +17 -0
  13. data/lib/aspera/cli/extended_value.rb +47 -12
  14. data/lib/aspera/cli/formatter.rb +260 -171
  15. data/lib/aspera/cli/hints.rb +80 -0
  16. data/lib/aspera/cli/main.rb +101 -147
  17. data/lib/aspera/cli/manager.rb +160 -124
  18. data/lib/aspera/cli/plugin.rb +70 -59
  19. data/lib/aspera/cli/plugins/alee.rb +0 -1
  20. data/lib/aspera/cli/plugins/aoc.rb +239 -273
  21. data/lib/aspera/cli/plugins/ats.rb +8 -5
  22. data/lib/aspera/cli/plugins/bss.rb +2 -2
  23. data/lib/aspera/cli/plugins/config.rb +516 -375
  24. data/lib/aspera/cli/plugins/console.rb +40 -0
  25. data/lib/aspera/cli/plugins/cos.rb +4 -5
  26. data/lib/aspera/cli/plugins/faspex.rb +99 -84
  27. data/lib/aspera/cli/plugins/faspex5.rb +179 -148
  28. data/lib/aspera/cli/plugins/node.rb +219 -153
  29. data/lib/aspera/cli/plugins/orchestrator.rb +52 -17
  30. data/lib/aspera/cli/plugins/preview.rb +46 -32
  31. data/lib/aspera/cli/plugins/server.rb +57 -17
  32. data/lib/aspera/cli/plugins/shares.rb +34 -12
  33. data/lib/aspera/cli/sync_actions.rb +68 -0
  34. data/lib/aspera/cli/transfer_agent.rb +45 -55
  35. data/lib/aspera/cli/transfer_progress.rb +74 -0
  36. data/lib/aspera/cli/version.rb +1 -1
  37. data/lib/aspera/colors.rb +3 -1
  38. data/lib/aspera/command_line_builder.rb +14 -11
  39. data/lib/aspera/cos_node.rb +3 -2
  40. data/lib/aspera/environment.rb +17 -6
  41. data/lib/aspera/fasp/agent_aspera.rb +126 -0
  42. data/lib/aspera/fasp/agent_base.rb +31 -77
  43. data/lib/aspera/fasp/agent_connect.rb +21 -22
  44. data/lib/aspera/fasp/agent_direct.rb +88 -102
  45. data/lib/aspera/fasp/agent_httpgw.rb +196 -192
  46. data/lib/aspera/fasp/agent_node.rb +41 -34
  47. data/lib/aspera/fasp/agent_trsdk.rb +75 -34
  48. data/lib/aspera/fasp/error_info.rb +2 -2
  49. data/lib/aspera/fasp/faux_file.rb +52 -0
  50. data/lib/aspera/fasp/installation.rb +43 -184
  51. data/lib/aspera/fasp/management.rb +244 -0
  52. data/lib/aspera/fasp/parameters.rb +59 -26
  53. data/lib/aspera/fasp/parameters.yaml +75 -8
  54. data/lib/aspera/fasp/products.rb +162 -0
  55. data/lib/aspera/fasp/transfer_spec.rb +1 -1
  56. data/lib/aspera/fasp/uri.rb +4 -4
  57. data/lib/aspera/faspex_gw.rb +2 -2
  58. data/lib/aspera/faspex_postproc.rb +2 -2
  59. data/lib/aspera/hash_ext.rb +2 -2
  60. data/lib/aspera/json_rpc.rb +49 -0
  61. data/lib/aspera/line_logger.rb +23 -0
  62. data/lib/aspera/log.rb +57 -16
  63. data/lib/aspera/node.rb +97 -14
  64. data/lib/aspera/oauth.rb +36 -18
  65. data/lib/aspera/open_application.rb +4 -4
  66. data/lib/aspera/persistency_folder.rb +2 -2
  67. data/lib/aspera/preview/file_types.rb +4 -2
  68. data/lib/aspera/preview/generator.rb +22 -35
  69. data/lib/aspera/preview/options.rb +2 -0
  70. data/lib/aspera/preview/terminal.rb +24 -13
  71. data/lib/aspera/preview/utils.rb +19 -26
  72. data/lib/aspera/rest.rb +103 -72
  73. data/lib/aspera/rest_call_error.rb +1 -1
  74. data/lib/aspera/rest_error_analyzer.rb +15 -14
  75. data/lib/aspera/rest_errors_aspera.rb +37 -34
  76. data/lib/aspera/secret_hider.rb +14 -16
  77. data/lib/aspera/ssh.rb +4 -1
  78. data/lib/aspera/sync.rb +128 -122
  79. data/lib/aspera/temp_file_manager.rb +10 -3
  80. data/lib/aspera/web_auth.rb +10 -7
  81. data/lib/aspera/web_server_simple.rb +9 -4
  82. data.tar.gz.sig +0 -0
  83. metadata +33 -15
  84. metadata.gz.sig +0 -0
  85. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  86. data/lib/aspera/cli/listener/logger.rb +0 -22
  87. data/lib/aspera/cli/listener/progress.rb +0 -50
  88. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  89. data/lib/aspera/cli/plugins/sync.rb +0 -44
  90. data/lib/aspera/fasp/listener.rb +0 -13
@@ -1,42 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/cli/extended_value'
4
+ require 'aspera/cli/error'
3
5
  require 'aspera/colors'
4
- require 'aspera/log'
5
6
  require 'aspera/secret_hider'
6
- require 'aspera/cli/extended_value'
7
- require 'optparse'
7
+ require 'aspera/log'
8
8
  require 'io/console'
9
+ require 'optparse'
9
10
 
10
11
  module Aspera
11
12
  module Cli
12
- # raised by cli on error conditions
13
- class CliError < StandardError; end
14
-
15
- # raised when an unexpected argument is provided
16
- class CliBadArgument < Aspera::Cli::CliError; end
17
-
18
- class CliNoSuchId < Aspera::Cli::CliError
19
- def initialize(res_type, res_id)
20
- msg = "No such #{res_type} identifier: #{res_id}"
21
- super(msg)
22
- end
23
- end
24
-
25
13
  # option is retrieved from another object using accessor
26
14
  class AttrAccessor
27
15
  # attr_accessor :object
28
- # attr_accessor :attr_symb
29
- def initialize(object, attr_symb)
16
+ # attr_accessor :method_name
17
+ def initialize(object, method_name, option_name)
30
18
  @object = object
31
- @attr_symb = attr_symb
19
+ @method = method_name
20
+ @option_name = option_name
21
+ @has_writer = @object.respond_to?(writer_method)
22
+ Log.log.debug{"AttrAccessor: #{@option_name}: #{@object.class}.#{@method}: writer=#{@has_writer}"}
23
+ raise "internal error: #{object} does not respond to #{method_name}" unless @object.respond_to?(@method)
32
24
  end
33
25
 
34
26
  def value
35
- return @object.send(@attr_symb)
27
+ return @object.send(@method) if @has_writer
28
+ return @object.send(@method, @option_name, :get)
36
29
  end
37
30
 
38
31
  def value=(val)
39
- @object.send("#{@attr_symb}=", val)
32
+ Log.log.trace1{"AttrAccessor: = #{@method} #{@option_name} :set #{val}, writer=#{@has_writer}"}
33
+ return @object.send(writer_method, val) if @has_writer
34
+ return @object.send(@method, @option_name, :set, val)
35
+ end
36
+
37
+ def writer_method
38
+ return "#{@method}="
40
39
  end
41
40
  end
42
41
 
@@ -53,9 +52,10 @@ module Aspera
53
52
  # option name separator on command line
54
53
  OPTION_SEP_LINE = '-'
55
54
  # option name separator in code (symbol)
56
- OPTION_SEP_SYMB = '_'
55
+ OPTION_SEP_SYMBOL = '_'
56
+ SOURCE_USER = 'cmdline' # cspell:disable-line
57
57
 
58
- private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMB
58
+ private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :SOURCE_USER
59
59
 
60
60
  class << self
61
61
  def enum_to_bool(enum)
@@ -68,13 +68,13 @@ module Aspera
68
68
  end
69
69
 
70
70
  # find shortened string value in allowed symbol list
71
- def get_from_list(shortval, descr, allowed_values)
71
+ def get_from_list(short_value, descr, allowed_values)
72
72
  # we accept shortcuts
73
- matching_exact = allowed_values.select{|i| i.to_s.eql?(shortval)}
73
+ matching_exact = allowed_values.select{|i| i.to_s.eql?(short_value)}
74
74
  return matching_exact.first if matching_exact.length == 1
75
- matching = allowed_values.select{|i| i.to_s.start_with?(shortval)}
76
- raise CliBadArgument, bad_arg_message_multi("unknown value for #{descr}: #{shortval}", allowed_values) if matching.empty?
77
- raise CliBadArgument, bad_arg_message_multi("ambiguous shortcut for #{descr}: #{shortval}", matching) unless matching.length.eql?(1)
75
+ matching = allowed_values.select{|i| i.to_s.start_with?(short_value)}
76
+ raise Cli::BadArgument, bad_arg_message_multi("unknown value for #{descr}: #{short_value}", allowed_values) if matching.empty?
77
+ raise Cli::BadArgument, bad_arg_message_multi("ambiguous shortcut for #{descr}: #{short_value}", matching) unless matching.length.eql?(1)
78
78
  return enum_to_bool(matching.first) if allowed_values.eql?(BOOLEAN_VALUES)
79
79
  return matching.first
80
80
  end
@@ -85,11 +85,19 @@ module Aspera
85
85
 
86
86
  # change option name with dash to name with underscore
87
87
  def option_line_to_name(name)
88
- return name.gsub(OPTION_SEP_LINE, OPTION_SEP_SYMB)
88
+ return name.gsub(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
89
89
  end
90
90
 
91
91
  def option_name_to_line(name)
92
- return "--#{name.to_s.gsub(OPTION_SEP_SYMB, OPTION_SEP_LINE)}"
92
+ return "--#{name.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)}"
93
+ end
94
+
95
+ def validate_type(what, descr, value, type_list)
96
+ return nil if type_list.nil?
97
+ raise 'internal error: types must be a Class Array' unless type_list.is_a?(Array) && type_list.all?(Class)
98
+ raise Cli::BadArgument,
99
+ "#{what.to_s.capitalize} #{descr} is a #{value.class} but must be #{type_list.length > 1 ? 'one of ' : ''}#{type_list.map(&:name).join(',')}" unless \
100
+ type_list.any?{|t|value.is_a?(t)}
93
101
  end
94
102
  end
95
103
 
@@ -97,7 +105,7 @@ module Aspera
97
105
  attr_accessor :ask_missing_mandatory, :ask_missing_optional
98
106
  attr_writer :fail_on_missing_mandatory
99
107
 
100
- def initialize(program_name, argv: nil)
108
+ def initialize(program_name)
101
109
  # command line values not starting with '-'
102
110
  @unprocessed_cmd_line_arguments = []
103
111
  # command line values starting with '-'
@@ -118,7 +126,7 @@ module Aspera
118
126
  @parser = OptionParser.new
119
127
  @parser.program_name = program_name
120
128
  # options can also be provided by env vars : --param-name -> ASCLI_PARAM_NAME
121
- env_prefix = program_name.upcase + OPTION_SEP_SYMB
129
+ env_prefix = program_name.upcase + OPTION_SEP_SYMBOL
122
130
  ENV.each do |k, v|
123
131
  if k.start_with?(env_prefix)
124
132
  @unprocessed_env.push([k[env_prefix.length..-1].downcase.to_sym, v])
@@ -127,71 +135,81 @@ module Aspera
127
135
  Log.log.debug{"env=#{@unprocessed_env}".red}
128
136
  @unprocessed_cmd_line_options = []
129
137
  @unprocessed_cmd_line_arguments = []
130
- # argv is nil when help is generated for every plugin
131
- unless argv.nil?
132
- @parser.separator('')
133
- @parser.separator('OPTIONS: global')
134
- declare(:interactive, 'Use interactive input of missing params', values: :bool, handler: {o: self, m: :ask_missing_mandatory})
135
- declare(:ask_options, 'Ask even optional options', values: :bool, handler: {o: self, m: :ask_missing_optional})
136
- parse_options!
137
- process_options = true
138
- until argv.empty?
139
- value = argv.shift
140
- if process_options && value.start_with?('-')
141
- if value.eql?('--')
142
- process_options = false
143
- else
144
- @unprocessed_cmd_line_options.push(value)
145
- end
138
+ end
139
+
140
+ def parse_command_line(argv)
141
+ @parser.separator('')
142
+ @parser.separator('OPTIONS: global')
143
+ declare(:interactive, 'Use interactive input of missing params', values: :bool, handler: {o: self, m: :ask_missing_mandatory})
144
+ declare(:ask_options, 'Ask even optional options', values: :bool, handler: {o: self, m: :ask_missing_optional})
145
+ parse_options!
146
+ process_options = true
147
+ until argv.empty?
148
+ value = argv.shift
149
+ if process_options && value.start_with?('-')
150
+ if value.eql?('--')
151
+ process_options = false
146
152
  else
147
- @unprocessed_cmd_line_arguments.push(value)
153
+ @unprocessed_cmd_line_options.push(value)
148
154
  end
155
+ else
156
+ @unprocessed_cmd_line_arguments.push(value)
149
157
  end
150
158
  end
151
159
  @initial_cli_options = @unprocessed_cmd_line_options.dup
152
- Log.log.debug{"add_cmd_line_options:commands/args=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
160
+ Log.log.debug{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
153
161
  end
154
162
 
163
+ # @param descr [String] description for help
155
164
  # @param expected is
156
- # - Array of allowed value (single value)
157
- # - :multiple for remaining values
158
- # - :single for a single unconstrained value
159
- # @param mandatory true/false
160
- # @param type expected class for result
161
- # @param aliases list of aliases for the value
165
+ # - Array of allowed value (single value)
166
+ # - :multiple for remaining values
167
+ # - :single for a single unconstrained value
168
+ # - :integer for a single integer value
169
+ # @param mandatory [Boolean] if true, raise error if option not set
170
+ # @param type [Class, Array] accepted value type(s)
171
+ # @param aliases [Hash] map of aliases: key = alias, value = real value
172
+ # @param default [Object] default value
162
173
  # @return value, list or nil
163
174
  def get_next_argument(descr, expected: :single, mandatory: true, type: nil, aliases: nil, default: nil)
164
175
  unless type.nil?
165
- raise 'internal: type must be a Class' unless type.is_a?(Class)
176
+ type = [type] unless type.is_a?(Array)
177
+ raise "INTERNAL ERROR: type must be Array of Class: #{type}" unless type.all?(Class)
166
178
  descr = "#{descr} (#{type})"
167
179
  end
168
- result = default
169
- if !@unprocessed_cmd_line_arguments.empty?
170
- # there are values
171
- case expected
172
- when :single
173
- result = ExtendedValue.instance.evaluate(@unprocessed_cmd_line_arguments.shift)
174
- when :multiple
175
- result = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length).map{|v|ExtendedValue.instance.evaluate(v)}
176
- # if expecting list and only one arg of type array : it is the list
177
- if result.length.eql?(1) && result.first.is_a?(Array)
178
- result = result.first
180
+ result =
181
+ if !@unprocessed_cmd_line_arguments.empty?
182
+ # there are values
183
+ case expected
184
+ when :single
185
+ ExtendedValue.instance.evaluate(@unprocessed_cmd_line_arguments.shift)
186
+ when :multiple
187
+ value = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length).map{|v|ExtendedValue.instance.evaluate(v)}
188
+ # if expecting list and only one arg of type array : it is the list
189
+ if value.length.eql?(1) && value.first.is_a?(Array)
190
+ value = value.first
191
+ end
192
+ value
193
+ when Array
194
+ allowed_values = [].concat(expected)
195
+ allowed_values.concat(aliases.keys) unless aliases.nil?
196
+ raise "internal error: only symbols allowed: #{allowed_values}" unless allowed_values.all?(Symbol)
197
+ self.class.get_from_list(@unprocessed_cmd_line_arguments.shift, descr, allowed_values)
198
+ else
199
+ raise 'Internal error: expected: must be single, multiple, or value array'
179
200
  end
180
- when Array
181
- allowed_values = [].concat(expected)
182
- allowed_values.concat(aliases.keys) unless aliases.nil?
183
- raise "internal error: only symbols allowed: #{allowed_values}" unless allowed_values.all?(Symbol)
184
- result = self.class.get_from_list(@unprocessed_cmd_line_arguments.shift, descr, allowed_values)
185
- else
186
- raise 'internal error'
201
+ elsif !default.nil? then default
202
+ # no value provided, either get value interactively, or exception
203
+ elsif mandatory then get_interactive(:argument, descr, expected: expected)
187
204
  end
188
- elsif mandatory
189
- # no value provided
190
- result = get_interactive(:argument, descr, expected: expected)
205
+ if result.is_a?(String) && type.eql?([Integer])
206
+ str_result = result
207
+ result = Integer(str_result, exception: false)
208
+ raise Cli::BadArgument, "Invalid integer: #{str_result}" if result.nil?
191
209
  end
192
210
  Log.log.debug{"#{descr}=#{result}"}
193
211
  result = aliases[result] if !aliases.nil? && aliases.key?(result)
194
- raise "argument shall be #{type.name}" unless type.nil? || result.is_a?(type)
212
+ self.class.validate_type(:argument, descr, result, type) unless result.nil? && !mandatory
195
213
  return result
196
214
  end
197
215
 
@@ -201,52 +219,50 @@ module Aspera
201
219
  # either return value or calls handler, can return nil
202
220
  # ask interactively if requested/required
203
221
  # @param mandatory [Boolean] if true, raise error if option not set
204
- # @param allowed_types [Array] list of allowed types
205
- def get_option(option_symbol, mandatory: false, allowed_types: nil)
206
- allowed_types = [allowed_types] if allowed_types.is_a?(Class)
207
- raise 'Internal Error: allowed_types must be an Array of Class or a Class' unless allowed_types.nil? || allowed_types.is_a?(Array)
222
+ def get_option(option_symbol, mandatory: false, default: nil)
223
+ attributes = @declared_options[option_symbol]
224
+ raise "INTERNAL ERROR: option not declared: #{option_symbol}" unless attributes
208
225
  result = nil
209
- if @declared_options.key?(option_symbol)
210
- case @declared_options[option_symbol][:read_write]
211
- when :accessor
212
- result = @declared_options[option_symbol][:accessor].value
213
- when :value
214
- result = @declared_options[option_symbol][:value]
215
- else
216
- raise 'unknown type'
217
- end
218
- Log.log.debug{"(#{@declared_options[option_symbol][:read_write]}) get #{option_symbol}=#{result}"}
226
+ case attributes[:read_write]
227
+ when :accessor
228
+ result = attributes[:accessor].value
229
+ when :value
230
+ result = attributes[:value]
231
+ else
232
+ raise 'unknown type'
219
233
  end
234
+ Log.log.debug{"(#{attributes[:read_write]}) get #{option_symbol}=#{result}"}
235
+ result = default if result.nil?
220
236
  # do not fail for manual generation if option mandatory but not set
221
237
  result = '' if result.nil? && mandatory && !@fail_on_missing_mandatory
222
238
  # Log.log.debug{"interactive=#{@ask_missing_mandatory}"}
223
239
  if result.nil?
224
240
  if !@ask_missing_mandatory
225
- raise CliBadArgument, "Missing mandatory option: #{option_symbol}" if mandatory
241
+ raise Cli::BadArgument, "Missing mandatory option: #{option_symbol}" if mandatory
226
242
  elsif @ask_missing_optional || mandatory
227
243
  # ask_missing_mandatory
228
244
  expected = :single
229
245
  # print "please enter: #{option_symbol.to_s}"
230
- if @declared_options.key?(option_symbol) && @declared_options[option_symbol].key?(:values)
231
- expected = @declared_options[option_symbol][:values]
246
+ if @declared_options.key?(option_symbol) && attributes.key?(:values)
247
+ expected = attributes[:values]
232
248
  end
233
249
  result = get_interactive(:option, option_symbol.to_s, expected: expected)
234
250
  set_option(option_symbol, result, 'interactive')
235
251
  end
236
252
  end
237
- raise "option #{option_symbol} is #{result.class} but must be one of #{allowed_types}" unless \
238
- !mandatory || allowed_types.nil? || allowed_types.any? { |t|result.is_a?(t)}
253
+ self.class.validate_type(:option, option_symbol, result, attributes[:types]) unless result.nil? && !mandatory
239
254
  return result
240
255
  end
241
256
 
242
257
  # set an option value by name, either store value or call handler
243
258
  def set_option(option_symbol, value, where='code override')
244
- raise CliBadArgument, "Unknown option: #{option_symbol}" unless @declared_options.key?(option_symbol)
259
+ raise Cli::BadArgument, "Unknown option: #{option_symbol}" unless @declared_options.key?(option_symbol)
245
260
  attributes = @declared_options[option_symbol]
246
261
  Log.log.warn("#{option_symbol}: Option is deprecated: #{attributes[:deprecation]}") if attributes[:deprecation]
247
262
  value = ExtendedValue.instance.evaluate(value)
248
263
  value = Manager.enum_to_bool(value) if attributes[:values].eql?(BOOLEAN_VALUES)
249
264
  Log.log.debug{"(#{attributes[:read_write]}/#{where}) set #{option_symbol}=#{value}"}
265
+ self.class.validate_type(:option, option_symbol, value, attributes[:types])
250
266
  case attributes[:read_write]
251
267
  when :accessor
252
268
  attributes[:accessor].value = value
@@ -269,9 +285,11 @@ module Aspera
269
285
  # @param block [Proc] block to execute when option is found
270
286
  def declare(option_symbol, description, handler: nil, default: nil, values: nil, short: nil, coerce: nil, types: nil, deprecation: nil, &block)
271
287
  raise "INTERNAL ERROR: #{option_symbol} already declared" if @declared_options.key?(option_symbol)
288
+ # raise "INTERNAL ERROR: #{option_symbol} clash with another option" if
289
+ # @declared_options.keys.map(&:to_s).any?{|k|k.start_with?(option_symbol.to_s) || option_symbol.to_s.start_with?(k)}
272
290
  raise "INTERNAL ERROR: #{option_symbol} ends with dot" unless description[-1] != '.'
273
- raise "INTERNAL ERROR: #{option_symbol} does not start with capital" unless description[0] == description[0].upcase
274
- raise "INTERNAL ERROR: #{option_symbol} shall use :types" if description.downcase.include?('hash') || description.downcase.include?('extended value')
291
+ raise "INTERNAL ERROR: #{option_symbol} description does not start with capital" unless description[0] == description[0].upcase
292
+ raise "INTERNAL ERROR: #{option_symbol} shall use :types" if ['hash', 'extended value'].any?{|s|description.downcase.include?(s) }
275
293
  opt = @declared_options[option_symbol] = {
276
294
  read_write: handler.nil? ? :value : :accessor,
277
295
  # by default passwords and secrets are sensitive, else specify when declaring the option
@@ -279,7 +297,7 @@ module Aspera
279
297
  }
280
298
  if !types.nil?
281
299
  types = [types] unless types.is_a?(Array)
282
- raise "INTERNAL ERROR: types must be classes: #{types}" unless types.all?(Class)
300
+ raise "INTERNAL ERROR: types must be Array of Class: #{types}" unless types.all?(Class)
283
301
  opt[:types] = types
284
302
  description = "#{description} (#{types.map(&:name).join(', ')})"
285
303
  end
@@ -292,7 +310,7 @@ module Aspera
292
310
  raise 'internal error' unless handler.is_a?(Hash)
293
311
  raise 'internal error' unless handler.keys.sort.eql?(%i[m o])
294
312
  Log.log.debug{"set attr obj #{option_symbol} (#{handler[:o]},#{handler[:m]})"}
295
- opt[:accessor] = AttrAccessor.new(handler[:o], handler[:m])
313
+ opt[:accessor] = AttrAccessor.new(handler[:o], handler[:m], option_symbol)
296
314
  end
297
315
  set_option(option_symbol, default, 'default') unless default.nil?
298
316
  on_args = [description]
@@ -301,7 +319,7 @@ module Aspera
301
319
  on_args.push(symbol_to_option(option_symbol, 'VALUE'))
302
320
  on_args.push("-#{short}VALUE") unless short.nil?
303
321
  on_args.push(coerce) unless coerce.nil?
304
- @parser.on(*on_args) { |v| set_option(option_symbol, v, 'cmdline') }
322
+ @parser.on(*on_args) { |v| set_option(option_symbol, v, SOURCE_USER) }
305
323
  when Array, :bool
306
324
  if values.eql?(:bool)
307
325
  values = BOOLEAN_VALUES
@@ -317,7 +335,7 @@ module Aspera
317
335
  on_args[0] = "#{description}: #{help_values}"
318
336
  on_args.push(symbol_to_option(option_symbol, 'ENUM'))
319
337
  on_args.push(values)
320
- @parser.on(*on_args){|v|set_option(option_symbol, self.class.get_from_list(v.to_s, description, values), 'cmdline')}
338
+ @parser.on(*on_args){|v|set_option(option_symbol, self.class.get_from_list(v.to_s, description, values), SOURCE_USER)}
321
339
  when :date
322
340
  on_args.push(symbol_to_option(option_symbol, 'DATE'))
323
341
  @parser.on(*on_args) do |v|
@@ -326,14 +344,14 @@ module Aspera
326
344
  when /^-([0-9]+)h/ then Manager.time_to_string(Time.now - (Regexp.last_match(1).to_i * 3600))
327
345
  else v
328
346
  end
329
- set_option(option_symbol, time_string, 'cmdline')
347
+ set_option(option_symbol, time_string, SOURCE_USER)
330
348
  end
331
349
  when :none
332
350
  raise "internal error: missing block for #{option_symbol}" if block.nil?
333
351
  on_args.push(symbol_to_option(option_symbol, nil))
334
352
  on_args.push("-#{short}") if short.is_a?(String)
335
353
  @parser.on(*on_args, &block)
336
- else raise 'internal error'
354
+ else raise "internal error: Unknown type for values: #{values} / #{values.class}"
337
355
  end
338
356
  Log.log.debug{"on_args=#{on_args}"}
339
357
  end
@@ -363,31 +381,32 @@ module Aspera
363
381
  # get all original options on command line used to generate a config in config file
364
382
  def get_options_table(remove_from_remaining: true)
365
383
  result = {}
366
- @initial_cli_options.each do |optionval|
367
- case optionval
384
+ @initial_cli_options.each do |option_value|
385
+ case option_value
368
386
  when /^--([^=]+)$/
369
387
  # ignore
370
388
  when /^--([^=]+)=(.*)$/
371
389
  name = Regexp.last_match(1)
372
390
  value = Regexp.last_match(2)
373
- name.gsub!(OPTION_SEP_LINE, OPTION_SEP_SYMB)
391
+ name.gsub!(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
374
392
  value = ExtendedValue.instance.evaluate(value)
375
393
  Log.log.debug{"option #{name}=#{value}"}
376
394
  result[name] = value
377
- @unprocessed_cmd_line_options.delete(optionval) if remove_from_remaining
395
+ @unprocessed_cmd_line_options.delete(option_value) if remove_from_remaining
378
396
  else
379
- raise CliBadArgument, "wrong option format: #{optionval}"
397
+ raise Cli::BadArgument, "wrong option format: #{option_value}"
380
398
  end
381
399
  end
382
400
  return result
383
401
  end
384
402
 
385
- # return options as taken from config file and command line just before command execution
386
- def declared_options(only_defined: false)
403
+ # @param only_defined [Boolean] if true, only return options that were defined
404
+ # @return [Hash] options as taken from config file and command line just before command execution
405
+ def known_options(only_defined: false)
387
406
  result = {}
388
- @declared_options.each_key do |option_symb|
389
- v = get_option(option_symb)
390
- result[option_symb.to_s] = v unless only_defined && v.nil?
407
+ @declared_options.each_key do |option_symbol|
408
+ v = get_option(option_symbol)
409
+ result[option_symbol] = v unless only_defined && v.nil?
391
410
  end
392
411
  return result
393
412
  end
@@ -416,21 +435,36 @@ module Aspera
416
435
  @unprocessed_cmd_line_options = unknown_options
417
436
  end
418
437
 
419
- private
420
-
421
438
  def prompt_user_input(prompt, sensitive)
422
439
  return $stdin.getpass("#{prompt}> ") if sensitive
423
440
  print("#{prompt}> ")
424
- return $stdin.gets.chomp
441
+ line = $stdin.gets
442
+ raise 'Unexpected end of standard input' if line.nil?
443
+ return line.chomp
444
+ end
445
+
446
+ # prompt user for input in a list of symbols
447
+ # @param prompt [String] prompt to display
448
+ # @param sym_list [Array] list of symbols to select from
449
+ # @return [Symbol] selected symbol
450
+ def prompt_user_input_in_list(prompt, sym_list)
451
+ loop do
452
+ input = prompt_user_input(prompt, false).to_sym
453
+ if sym_list.any?{|a|a.eql?(input)}
454
+ return input
455
+ else
456
+ $stderr.puts("No such #{prompt}: #{input}, select one of: #{sym_list.join(', ')}")
457
+ end
458
+ end
425
459
  end
426
460
 
427
461
  def get_interactive(type, descr, expected: :single)
428
462
  if !@ask_missing_mandatory
429
- raise CliBadArgument, self.class.bad_arg_message_multi("missing: #{descr}", expected) if expected.is_a?(Array)
430
- raise CliBadArgument, "missing argument (#{expected}): #{descr}"
463
+ raise Cli::BadArgument, self.class.bad_arg_message_multi("missing: #{descr}", expected) if expected.is_a?(Array)
464
+ raise Cli::BadArgument, "missing argument (#{expected}): #{descr}"
431
465
  end
432
466
  result = nil
433
- sensitive = type.eql?(:option) && @declared_options[descr.to_sym][:sensitive]
467
+ sensitive = type.eql?(:option) && @declared_options[descr.to_sym].is_a?(Hash) && @declared_options[descr.to_sym][:sensitive]
434
468
  default_prompt = "#{type}: #{descr}"
435
469
  # ask interactively
436
470
  case expected
@@ -450,9 +484,11 @@ module Aspera
450
484
  return result
451
485
  end
452
486
 
487
+ private
488
+
453
489
  # generate command line option from option symbol
454
490
  def symbol_to_option(symbol, opt_val)
455
- result = '--' + symbol.to_s.gsub(OPTION_SEP_SYMB, OPTION_SEP_LINE)
491
+ result = '--' + symbol.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)
456
492
  result = result + '=' + opt_val unless opt_val.nil?
457
493
  return result
458
494
  end