aspera-cli 4.14.0 → 4.15.0

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 (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