aspera-cli 4.12.0 → 4.14.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 (80) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +45 -5
  4. data/CONTRIBUTING.md +113 -22
  5. data/README.md +1289 -754
  6. data/bin/ascli +3 -3
  7. data/examples/dascli +1 -1
  8. data/examples/rubyc +24 -0
  9. data/lib/aspera/aoc.rb +63 -74
  10. data/lib/aspera/ascmd.rb +5 -3
  11. data/lib/aspera/cli/basic_auth_plugin.rb +6 -6
  12. data/lib/aspera/cli/extended_value.rb +24 -37
  13. data/lib/aspera/cli/formatter.rb +23 -25
  14. data/lib/aspera/cli/info.rb +2 -4
  15. data/lib/aspera/cli/main.rb +27 -27
  16. data/lib/aspera/cli/manager.rb +143 -120
  17. data/lib/aspera/cli/plugin.rb +88 -43
  18. data/lib/aspera/cli/plugins/alee.rb +2 -2
  19. data/lib/aspera/cli/plugins/aoc.rb +235 -104
  20. data/lib/aspera/cli/plugins/ats.rb +16 -18
  21. data/lib/aspera/cli/plugins/bss.rb +3 -3
  22. data/lib/aspera/cli/plugins/config.rb +190 -373
  23. data/lib/aspera/cli/plugins/console.rb +4 -6
  24. data/lib/aspera/cli/plugins/cos.rb +12 -13
  25. data/lib/aspera/cli/plugins/faspex.rb +21 -21
  26. data/lib/aspera/cli/plugins/faspex5.rb +399 -150
  27. data/lib/aspera/cli/plugins/node.rb +260 -174
  28. data/lib/aspera/cli/plugins/orchestrator.rb +15 -18
  29. data/lib/aspera/cli/plugins/preview.rb +40 -62
  30. data/lib/aspera/cli/plugins/server.rb +33 -16
  31. data/lib/aspera/cli/plugins/shares.rb +24 -33
  32. data/lib/aspera/cli/plugins/sync.rb +6 -6
  33. data/lib/aspera/cli/transfer_agent.rb +47 -30
  34. data/lib/aspera/cli/version.rb +2 -1
  35. data/lib/aspera/colors.rb +9 -7
  36. data/lib/aspera/command_line_builder.rb +2 -1
  37. data/lib/aspera/cos_node.rb +1 -1
  38. data/lib/aspera/data/6 +0 -0
  39. data/lib/aspera/environment.rb +7 -3
  40. data/lib/aspera/fasp/agent_connect.rb +6 -1
  41. data/lib/aspera/fasp/agent_direct.rb +17 -17
  42. data/lib/aspera/fasp/agent_httpgw.rb +138 -60
  43. data/lib/aspera/fasp/agent_node.rb +14 -4
  44. data/lib/aspera/fasp/agent_trsdk.rb +2 -0
  45. data/lib/aspera/fasp/error_info.rb +2 -0
  46. data/lib/aspera/fasp/installation.rb +19 -19
  47. data/lib/aspera/fasp/parameters.rb +29 -20
  48. data/lib/aspera/fasp/parameters.yaml +5 -2
  49. data/lib/aspera/fasp/resume_policy.rb +3 -3
  50. data/lib/aspera/fasp/transfer_spec.rb +8 -5
  51. data/lib/aspera/fasp/uri.rb +23 -21
  52. data/lib/aspera/faspex_gw.rb +1 -0
  53. data/lib/aspera/faspex_postproc.rb +3 -3
  54. data/lib/aspera/hash_ext.rb +12 -2
  55. data/lib/aspera/keychain/macos_security.rb +13 -13
  56. data/lib/aspera/log.rb +1 -0
  57. data/lib/aspera/node.rb +73 -84
  58. data/lib/aspera/oauth.rb +4 -3
  59. data/lib/aspera/persistency_action_once.rb +1 -1
  60. data/lib/aspera/preview/file_types.rb +8 -6
  61. data/lib/aspera/preview/generator.rb +23 -11
  62. data/lib/aspera/preview/options.rb +3 -2
  63. data/lib/aspera/preview/terminal.rb +80 -0
  64. data/lib/aspera/preview/utils.rb +11 -11
  65. data/lib/aspera/proxy_auto_config.js +2 -2
  66. data/lib/aspera/rest.rb +42 -4
  67. data/lib/aspera/rest_call_error.rb +3 -1
  68. data/lib/aspera/secret_hider.rb +10 -5
  69. data/lib/aspera/ssh.rb +1 -1
  70. data/lib/aspera/sync.rb +41 -33
  71. data/lib/aspera/web_server_simple.rb +22 -18
  72. data.tar.gz.sig +0 -0
  73. metadata +40 -48
  74. metadata.gz.sig +0 -0
  75. data/docs/test_env.conf +0 -179
  76. data/examples/aoc.rb +0 -30
  77. data/examples/faspex4.rb +0 -94
  78. data/examples/node.rb +0 -96
  79. data/examples/server.rb +0 -93
  80. data/lib/aspera/data/7 +0 -0
@@ -31,7 +31,7 @@ module Aspera
31
31
  STATUS_FIELD = 'status'
32
32
 
33
33
  # for testing only
34
- SELF_SIGNED_CERT = OpenSSL::SSL.const_get(:enon_yfirev.to_s.upcase.reverse)
34
+ SELF_SIGNED_CERT = OpenSSL::SSL.const_get(:enon_yfirev.to_s.upcase.reverse) # cspell: disable-line
35
35
 
36
36
  class << self
37
37
  # expect some list, but nothing to display
@@ -87,7 +87,7 @@ module Aspera
87
87
  if @option_insecure
88
88
  url = http.inspect.gsub(/^[^ ]* /, 'https://').gsub(/ [^ ]*$/, '')
89
89
  if !@ssl_warned_urls.include?(url)
90
- @formatter.display_message(:error, "#{WARNING_FLASH} ignoring certificate for: #{url}. Do not use unsafe certificates in production.")
90
+ @formatter.display_message(:error, "#{WARNING_FLASH} ignoring certificate for: #{url}. Do not deactivate certificate verification in production.")
91
91
  @ssl_warned_urls.push(url)
92
92
  end
93
93
  http.verify_mode = SELF_SIGNED_CERT
@@ -187,31 +187,25 @@ module Aspera
187
187
  # define header for manual
188
188
  def init_global_options
189
189
  Log.log.debug('init_global_options')
190
- @opt_mgr.add_opt_switch(:help, '-h', 'Show this message.') { @option_help = true }
191
- @opt_mgr.add_opt_switch(:bash_comp, 'generate bash completion for command') { @bash_completion = true }
192
- @opt_mgr.add_opt_switch(:show_config, 'Display parameters used for the provided action.') { @option_show_config = true }
193
- @opt_mgr.add_opt_switch(:rest_debug, '-r', 'more debug for HTTP calls') { @option_rest_debug = true }
194
- @opt_mgr.add_opt_switch(:version, '-v', 'display version') { @formatter.display_message(:data, Aspera::Cli::VERSION); Process.exit(0) } # rubocop:disable Style/Semicolon, Layout/LineLength
195
- @opt_mgr.add_opt_switch(:warnings, '-w', 'check for language warnings') { $VERBOSE = true }
196
- # handler must be set before declaration
197
- @opt_mgr.set_obj_attr(:log_level, Log.instance, :level)
198
- @opt_mgr.set_obj_attr(:logger, Log.instance, :logger_type)
199
- @opt_mgr.set_obj_attr(:insecure, self, :option_insecure, :no)
200
- @opt_mgr.set_obj_attr(:ui, self, :option_ui)
201
- @opt_mgr.set_obj_attr(:http_options, self, :option_http_options)
202
- @opt_mgr.set_obj_attr(:log_secrets, SecretHider, :log_secrets)
203
- @opt_mgr.set_obj_attr(:cache_tokens, self, :option_cache_tokens)
204
- @opt_mgr.add_opt_list(:ui, OpenApplication.user_interfaces, 'method to start browser')
205
- @opt_mgr.add_opt_list(:log_level, Log.levels, 'Log level')
206
- @opt_mgr.add_opt_list(:logger, Log::LOG_TYPES, 'logging method')
207
- @opt_mgr.add_opt_simple(:lock_port, 'prevent dual execution of a command, e.g. in cron')
208
- @opt_mgr.add_opt_simple(:http_options, 'options for http socket (extended value)')
209
- @opt_mgr.add_opt_boolean(:insecure, 'do not validate HTTPS certificate')
210
- @opt_mgr.add_opt_boolean(:once_only, 'process only new items (some commands)')
211
- @opt_mgr.add_opt_boolean(:log_secrets, 'show passwords in logs')
212
- @opt_mgr.add_opt_boolean(:cache_tokens, 'save and reuse Oauth tokens')
213
- @opt_mgr.set_option(:ui, OpenApplication.default_gui_mode)
214
- @opt_mgr.set_option(:once_only, false)
190
+ @opt_mgr.declare(:help, 'Show this message', values: :none, short: 'h') { @option_help = true }
191
+ @opt_mgr.declare(:bash_comp, 'Generate bash completion for command', values: :none) { @bash_completion = true }
192
+ @opt_mgr.declare(:show_config, 'Display parameters used for the provided action', values: :none) { @option_show_config = true }
193
+ @opt_mgr.declare(:rest_debug, 'More debug for HTTP calls (REST)', values: :none, short: 'r') { @option_rest_debug = true }
194
+ @opt_mgr.declare(:version, 'Display version', values: :none, short: 'v') { @formatter.display_message(:data, Aspera::Cli::VERSION); Process.exit(0) } # rubocop:disable Style/Semicolon, Layout/LineLength
195
+ @opt_mgr.declare(:warnings, 'Check for language warnings', values: :none, short: 'w') { $VERBOSE = true }
196
+ @opt_mgr.declare(
197
+ :ui, 'Method to start browser',
198
+ values: OpenApplication.user_interfaces,
199
+ handler: {o: self, m: :option_ui},
200
+ default: OpenApplication.default_gui_mode)
201
+ @opt_mgr.declare(:log_level, 'Log level', values: Log.levels, handler: {o: Log.instance, m: :level})
202
+ @opt_mgr.declare(:logger, 'Logging method', values: Log::LOG_TYPES, handler: {o: Log.instance, m: :logger_type})
203
+ @opt_mgr.declare(:lock_port, 'Prevent dual execution of a command, e.g. in cron')
204
+ @opt_mgr.declare(:http_options, 'Options for http socket', types: Hash, handler: {o: self, m: :option_http_options})
205
+ @opt_mgr.declare(:insecure, 'Do not validate HTTPS certificate', values: :bool, handler: {o: self, m: :option_insecure}, default: :no)
206
+ @opt_mgr.declare(:once_only, 'Process only new items (some commands)', values: :bool, default: false)
207
+ @opt_mgr.declare(:log_secrets, 'Show passwords in logs', values: :bool, handler: {o: SecretHider, m: :log_secrets})
208
+ @opt_mgr.declare(:cache_tokens, 'Save and reuse Oauth tokens', values: :bool, handler: {o: self, m: :option_cache_tokens})
215
209
  # parse declared options
216
210
  @opt_mgr.parse_options!
217
211
  end
@@ -335,6 +329,7 @@ module Aspera
335
329
  # finish
336
330
  @plugin_env[:transfer].shutdown
337
331
  rescue Net::SSH::AuthenticationFailed => e; exception_info = {e: e, t: 'SSH', security: true}
332
+ rescue OpenSSL::SSL::SSLError => e; exception_info = {e: e, t: 'SSL'}
338
333
  rescue CliBadArgument => e; exception_info = {e: e, t: 'Argument', usage: true}
339
334
  rescue CliNoSuchId => e; exception_info = {e: e, t: 'Identifier'}
340
335
  rescue CliError => e; exception_info = {e: e, t: 'Tool', usage: true}
@@ -351,10 +346,15 @@ module Aspera
351
346
  Log.log.warn(exception_info[:e].message) if Aspera::Log.instance.logger_type.eql?(:syslog) && exception_info[:security]
352
347
  @formatter.display_message(:error, "#{ERROR_FLASH} #{exception_info[:t]}: #{exception_info[:e].message}")
353
348
  @formatter.display_message(:error, 'Use option -h to get help.') if exception_info[:usage]
349
+ # Provide hint on FASP errors
354
350
  if exception_info[:e].is_a?(Fasp::Error) && exception_info[:e].message.eql?('Remote host is not who we expected')
355
351
  @formatter.display_message(:error, "For this specific error, refer to:\n"\
356
352
  "#{SRC_URL}#error-remote-host-is-not-who-we-expected\nAdd this to arguments:\n--ts=@json:'{\"sshfp\":null}'")
357
353
  end
354
+ # Provide hint on SSL errors
355
+ if exception_info[:e].is_a?(OpenSSL::SSL::SSLError) && ['does not match the server certificate'].any?{|m|exception_info[:e].message.include?(m)}
356
+ @formatter.display_message(:error, "You can ignore SSL errors with option:\n--insecure=yes")
357
+ end
358
358
  end
359
359
  # 2- processing of command not processed (due to exception or bad command line)
360
360
  if execute_command || @option_show_config
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'aspera/colors'
4
4
  require 'aspera/log'
5
+ require 'aspera/secret_hider'
5
6
  require 'aspera/cli/extended_value'
6
7
  require 'optparse'
7
8
  require 'io/console'
@@ -52,9 +53,9 @@ module Aspera
52
53
  # option name separator on command line
53
54
  OPTION_SEP_LINE = '-'
54
55
  # option name separator in code (symbol)
55
- OPTION_SEP_NAME = '_'
56
+ OPTION_SEP_SYMB = '_'
56
57
 
57
- private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_NAME
58
+ private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMB
58
59
 
59
60
  class << self
60
61
  def enum_to_bool(enum)
@@ -81,6 +82,15 @@ module Aspera
81
82
  def bad_arg_message_multi(error_msg, choices)
82
83
  return [error_msg, 'Use:'].concat(choices.map{|c|"- #{c}"}.sort).join("\n")
83
84
  end
85
+
86
+ # change option name with dash to name with underscore
87
+ def option_line_to_name(name)
88
+ return name.gsub(OPTION_SEP_LINE, OPTION_SEP_SYMB)
89
+ end
90
+
91
+ def option_name_to_line(name)
92
+ return "--#{name.to_s.gsub(OPTION_SEP_SYMB, OPTION_SEP_LINE)}"
93
+ end
84
94
  end
85
95
 
86
96
  attr_reader :parser
@@ -94,7 +104,7 @@ module Aspera
94
104
  @unprocessed_cmd_line_options = []
95
105
  # a copy of all initial options
96
106
  @initial_cli_options = []
97
- # option description: key = option symbol, value=hash, :type, :accessor, :value, :accepted
107
+ # option description: key = option symbol, value=hash, :read_write, :accessor, :value, :accepted
98
108
  @declared_options = {}
99
109
  # do we ask missing options and arguments to user ?
100
110
  @ask_missing_mandatory = false # STDIN.isatty
@@ -108,7 +118,7 @@ module Aspera
108
118
  @parser = OptionParser.new
109
119
  @parser.program_name = program_name
110
120
  # options can also be provided by env vars : --param-name -> ASCLI_PARAM_NAME
111
- env_prefix = program_name.upcase + OPTION_SEP_NAME
121
+ env_prefix = program_name.upcase + OPTION_SEP_SYMB
112
122
  ENV.each do |k, v|
113
123
  if k.start_with?(env_prefix)
114
124
  @unprocessed_env.push([k[env_prefix.length..-1].downcase.to_sym, v])
@@ -121,10 +131,8 @@ module Aspera
121
131
  unless argv.nil?
122
132
  @parser.separator('')
123
133
  @parser.separator('OPTIONS: global')
124
- set_obj_attr(:interactive, self, :ask_missing_mandatory)
125
- set_obj_attr(:ask_options, self, :ask_missing_optional)
126
- add_opt_boolean(:interactive, 'use interactive input of missing params')
127
- add_opt_boolean(:ask_options, 'ask even optional options')
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})
128
136
  parse_options!
129
137
  process_options = true
130
138
  until argv.empty?
@@ -144,21 +152,20 @@ module Aspera
144
152
  Log.log.debug{"add_cmd_line_options:commands/args=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
145
153
  end
146
154
 
147
- def get_next_command(command_list); return get_next_argument('command', expected: command_list); end
148
-
149
155
  # @param expected is
150
156
  # - Array of allowed value (single value)
151
157
  # - :multiple for remaining values
152
158
  # - :single for a single unconstrained value
153
159
  # @param mandatory true/false
154
160
  # @param type expected class for result
161
+ # @param aliases list of aliases for the value
155
162
  # @return value, list or nil
156
- def get_next_argument(descr, expected: :single, mandatory: true, type: nil)
163
+ def get_next_argument(descr, expected: :single, mandatory: true, type: nil, aliases: nil, default: nil)
157
164
  unless type.nil?
158
165
  raise 'internal: type must be a Class' unless type.is_a?(Class)
159
166
  descr = "#{descr} (#{type})"
160
167
  end
161
- result = nil
168
+ result = default
162
169
  if !@unprocessed_cmd_line_arguments.empty?
163
170
  # there are values
164
171
  case expected
@@ -170,66 +177,37 @@ module Aspera
170
177
  if result.length.eql?(1) && result.first.is_a?(Array)
171
178
  result = result.first
172
179
  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)
173
185
  else
174
- result = self.class.get_from_list(@unprocessed_cmd_line_arguments.shift, descr, expected)
186
+ raise 'internal error'
175
187
  end
176
188
  elsif mandatory
177
189
  # no value provided
178
190
  result = get_interactive(:argument, descr, expected: expected)
179
191
  end
180
192
  Log.log.debug{"#{descr}=#{result}"}
193
+ result = aliases[result] if !aliases.nil? && aliases.key?(result)
181
194
  raise "argument shall be #{type.name}" unless type.nil? || result.is_a?(type)
182
195
  return result
183
196
  end
184
197
 
185
- # declare option of type :accessor, or :value
186
- def declare_option(option_symbol, type)
187
- Log.log.debug{"declare_option: #{option_symbol}: #{type}: skip=#{@declared_options.key?(option_symbol)}".green}
188
- if @declared_options.key?(option_symbol)
189
- raise "INTERNAL ERROR: option #{option_symbol} already declared. only accessor can be re-declared and ignored" \
190
- unless @declared_options[option_symbol][:type].eql?(:accessor)
191
- return
192
- end
193
- @declared_options[option_symbol] = {type: type}
194
- # by default passwords and secrets are sensitive, else specify when declaring the option
195
- @declared_options[option_symbol][:sensitive] = true if !%w[password secret key].select{|i| option_symbol.to_s.end_with?(i)}.empty?
196
- end
197
-
198
- # define option with handler
199
- def set_obj_attr(option_symbol, object, attr_symb, default_value=nil)
200
- Log.log.debug{"set attr obj #{option_symbol} (#{object},#{attr_symb})"}
201
- declare_option(option_symbol, :accessor)
202
- @declared_options[option_symbol][:accessor] = AttrAccessor.new(object, attr_symb)
203
- set_option(option_symbol, default_value, 'default obj attr') if !default_value.nil?
204
- end
205
-
206
- # set an option value by name, either store value or call handler
207
- def set_option(option_symbol, value, where='default')
208
- if !@declared_options.key?(option_symbol)
209
- Log.log.debug{"set unknown option: #{option_symbol}"}
210
- raise 'ERROR: cannot set undeclared option'
211
- # declare_option(option_symbol)
212
- end
213
- value = ExtendedValue.instance.evaluate(value)
214
- value = Manager.enum_to_bool(value) if @declared_options[option_symbol][:values].eql?(BOOLEAN_VALUES)
215
- Log.log.debug{"(#{@declared_options[option_symbol][:type]}/#{where}) set #{option_symbol}=#{value}"}
216
- case @declared_options[option_symbol][:type]
217
- when :accessor
218
- @declared_options[option_symbol][:accessor].value = value
219
- when :value
220
- @declared_options[option_symbol][:value] = value
221
- else # nil or other
222
- raise 'error'
223
- end
224
- end
198
+ def get_next_command(command_list, aliases: nil); return get_next_argument('command', expected: command_list, aliases: aliases); end
225
199
 
226
- # get an option value by name
227
- # either return value or call handler, can return nil
200
+ # Get an option value by name
201
+ # either return value or calls handler, can return nil
228
202
  # ask interactively if requested/required
229
- def get_option(option_symbol, is_type: :optional)
203
+ # @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)
230
208
  result = nil
231
209
  if @declared_options.key?(option_symbol)
232
- case @declared_options[option_symbol][:type]
210
+ case @declared_options[option_symbol][:read_write]
233
211
  when :accessor
234
212
  result = @declared_options[option_symbol][:accessor].value
235
213
  when :value
@@ -237,15 +215,15 @@ module Aspera
237
215
  else
238
216
  raise 'unknown type'
239
217
  end
240
- Log.log.debug{"(#{@declared_options[option_symbol][:type]}) get #{option_symbol}=#{result}"}
218
+ Log.log.debug{"(#{@declared_options[option_symbol][:read_write]}) get #{option_symbol}=#{result}"}
241
219
  end
242
220
  # do not fail for manual generation if option mandatory but not set
243
- result = '' if result.nil? && is_type.eql?(:mandatory) && !@fail_on_missing_mandatory
221
+ result = '' if result.nil? && mandatory && !@fail_on_missing_mandatory
244
222
  # Log.log.debug{"interactive=#{@ask_missing_mandatory}"}
245
223
  if result.nil?
246
224
  if !@ask_missing_mandatory
247
- raise CliBadArgument, "Missing mandatory option: #{option_symbol}" if is_type.eql?(:mandatory)
248
- elsif @ask_missing_optional || is_type.eql?(:mandatory)
225
+ raise CliBadArgument, "Missing mandatory option: #{option_symbol}" if mandatory
226
+ elsif @ask_missing_optional || mandatory
249
227
  # ask_missing_mandatory
250
228
  expected = :single
251
229
  # print "please enter: #{option_symbol.to_s}"
@@ -256,72 +234,117 @@ module Aspera
256
234
  set_option(option_symbol, result, 'interactive')
257
235
  end
258
236
  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)}
259
239
  return result
260
240
  end
261
241
 
262
- # param must be hash
263
- def add_option_preset(preset_hash, op: :push)
264
- Log.log.debug{"add_option_preset=#{preset_hash}"}
265
- raise "internal error: setting default with no hash: #{preset_hash.class}" if !preset_hash.is_a?(Hash)
266
- # incremental override
267
- preset_hash.each{|k, v|@unprocessed_defaults.send(op, [k.to_sym, v])}
268
- end
269
-
270
- # define an option with restricted values
271
- def add_opt_list(option_symbol, values, help, *on_args)
272
- declare_option(option_symbol, :value)
273
- Log.log.debug{"add_opt_list #{option_symbol}"}
274
- on_args.unshift(symbol_to_option(option_symbol, 'ENUM'))
275
- # this option value must be a symbol
276
- @declared_options[option_symbol][:values] = values
277
- value = get_option(option_symbol)
278
- help_values = values.map{|i|i.eql?(value) ? highlight_current(i) : i}.join(', ')
279
- if values.eql?(BOOLEAN_VALUES)
280
- help_values = BOOLEAN_SIMPLE.map{|i|(i.eql?(:yes) && value) || (i.eql?(:no) && !value) ? highlight_current(i) : i}.join(', ')
242
+ # set an option value by name, either store value or call handler
243
+ def set_option(option_symbol, value, where='code override')
244
+ raise CliBadArgument, "Unknown option: #{option_symbol}" unless @declared_options.key?(option_symbol)
245
+ attributes = @declared_options[option_symbol]
246
+ Log.log.warn("#{option_symbol}: Option is deprecated: #{attributes[:deprecation]}") if attributes[:deprecation]
247
+ value = ExtendedValue.instance.evaluate(value)
248
+ value = Manager.enum_to_bool(value) if attributes[:values].eql?(BOOLEAN_VALUES)
249
+ Log.log.debug{"(#{attributes[:read_write]}/#{where}) set #{option_symbol}=#{value}"}
250
+ case attributes[:read_write]
251
+ when :accessor
252
+ attributes[:accessor].value = value
253
+ when :value
254
+ attributes[:value] = value
255
+ else # nil or other
256
+ raise 'error'
281
257
  end
282
- on_args.push(values)
283
- on_args.push("#{help}: #{help_values}")
284
- Log.log.debug{"on_args=#{on_args}"}
285
- @parser.on(*on_args){|v|set_option(option_symbol, self.class.get_from_list(v.to_s, help, values), 'cmdline')}
286
- end
287
-
288
- def add_opt_boolean(option_symbol, help, *on_args)
289
- add_opt_list(option_symbol, BOOLEAN_VALUES, help, *on_args)
290
- # if default was defined for obj, it may still be enum (yes/no) instead of boolean
291
- default_value = get_option(option_symbol)
292
- set_option(option_symbol, default_value, 'opt boolean') unless default_value.nil?
293
- end
294
-
295
- # define an option with open values
296
- def add_opt_simple(option_symbol, *on_args)
297
- declare_option(option_symbol, :value)
298
- Log.log.debug{"add_opt_simple #{option_symbol}"}
299
- on_args.unshift(symbol_to_option(option_symbol, 'VALUE'))
300
- Log.log.debug{"on_args=#{on_args}"}
301
- @parser.on(*on_args) { |v| set_option(option_symbol, v, 'cmdline') }
302
258
  end
303
259
 
304
- # define an option with date format
305
- def add_opt_date(option_symbol, *on_args)
306
- declare_option(option_symbol, :value)
307
- Log.log.debug{"add_opt_date #{option_symbol}"}
308
- on_args.unshift(symbol_to_option(option_symbol, 'DATE'))
309
- Log.log.debug{"on_args=#{on_args}"}
310
- @parser.on(*on_args) do |v|
311
- case v
312
- when 'now' then set_option(option_symbol, Manager.time_to_string(Time.now), 'cmdline')
313
- when /^-([0-9]+)h/ then set_option(option_symbol, Manager.time_to_string(Time.now - (Regexp.last_match(1).to_i * 3600)), 'cmdline')
314
- else set_option(option_symbol, v, 'cmdline')
260
+ # declare an option
261
+ # @param option_symbol [Symbol] option name
262
+ # @param description [String] description for help
263
+ # @param handler [Hash] handler for option value: keys: o (object) and m (method)
264
+ # @param default [Object] default value
265
+ # @param values [nil, Array, :bool, :date, :none] list of allowed values, :bool for true/false, :date for dates, :none for on/off switch
266
+ # @param short [String] short option name
267
+ # @param coerce [Class] one of the coerce types accepted par option parser
268
+ # @param types [Class, Array] accepted value type(s)
269
+ # @param block [Proc] block to execute when option is found
270
+ def declare(option_symbol, description, handler: nil, default: nil, values: nil, short: nil, coerce: nil, types: nil, deprecation: nil, &block)
271
+ raise "INTERNAL ERROR: #{option_symbol} already declared" if @declared_options.key?(option_symbol)
272
+ 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')
275
+ opt = @declared_options[option_symbol] = {
276
+ read_write: handler.nil? ? :value : :accessor,
277
+ # by default passwords and secrets are sensitive, else specify when declaring the option
278
+ sensitive: SecretHider.secret?(option_symbol, '')
279
+ }
280
+ if !types.nil?
281
+ types = [types] unless types.is_a?(Array)
282
+ raise "INTERNAL ERROR: types must be classes: #{types}" unless types.all?(Class)
283
+ opt[:types] = types
284
+ description = "#{description} (#{types.map(&:name).join(', ')})"
285
+ end
286
+ if deprecation
287
+ opt[:deprecation] = deprecation
288
+ description = "#{description} (#{'deprecated'.blue}: #{deprecation})"
289
+ end
290
+ Log.log.debug{"declare: #{option_symbol}: #{opt[:read_write]}".green}
291
+ if opt[:read_write].eql?(:accessor)
292
+ raise 'internal error' unless handler.is_a?(Hash)
293
+ raise 'internal error' unless handler.keys.sort.eql?(%i[m o])
294
+ Log.log.debug{"set attr obj #{option_symbol} (#{handler[:o]},#{handler[:m]})"}
295
+ opt[:accessor] = AttrAccessor.new(handler[:o], handler[:m])
296
+ end
297
+ set_option(option_symbol, default, 'default') unless default.nil?
298
+ on_args = [description]
299
+ case values
300
+ when nil
301
+ on_args.push(symbol_to_option(option_symbol, 'VALUE'))
302
+ on_args.push("-#{short}VALUE") unless short.nil?
303
+ on_args.push(coerce) unless coerce.nil?
304
+ @parser.on(*on_args) { |v| set_option(option_symbol, v, 'cmdline') }
305
+ when Array, :bool
306
+ if values.eql?(:bool)
307
+ values = BOOLEAN_VALUES
308
+ set_option(option_symbol, Manager.enum_to_bool(default), 'default') unless default.nil?
309
+ end
310
+ # this option value must be a symbol
311
+ opt[:values] = values
312
+ value = get_option(option_symbol)
313
+ help_values = values.map{|i|i.eql?(value) ? highlight_current(i) : i}.join(', ')
314
+ if values.eql?(BOOLEAN_VALUES)
315
+ help_values = BOOLEAN_SIMPLE.map{|i|(i.eql?(:yes) && value) || (i.eql?(:no) && !value) ? highlight_current(i) : i}.join(', ')
316
+ end
317
+ on_args[0] = "#{description}: #{help_values}"
318
+ on_args.push(symbol_to_option(option_symbol, 'ENUM'))
319
+ 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')}
321
+ when :date
322
+ on_args.push(symbol_to_option(option_symbol, 'DATE'))
323
+ @parser.on(*on_args) do |v|
324
+ time_string = case v
325
+ when 'now' then Manager.time_to_string(Time.now)
326
+ when /^-([0-9]+)h/ then Manager.time_to_string(Time.now - (Regexp.last_match(1).to_i * 3600))
327
+ else v
328
+ end
329
+ set_option(option_symbol, time_string, 'cmdline')
315
330
  end
331
+ when :none
332
+ raise "internal error: missing block for #{option_symbol}" if block.nil?
333
+ on_args.push(symbol_to_option(option_symbol, nil))
334
+ on_args.push("-#{short}") if short.is_a?(String)
335
+ @parser.on(*on_args, &block)
336
+ else raise 'internal error'
316
337
  end
338
+ Log.log.debug{"on_args=#{on_args}"}
317
339
  end
318
340
 
319
- # define an option without value
320
- def add_opt_switch(option_symbol, *on_args, &block)
321
- Log.log.debug{"add_opt_on #{option_symbol}"}
322
- on_args.unshift(symbol_to_option(option_symbol, nil))
323
- Log.log.debug{"on_args=#{on_args}"}
324
- @parser.on(*on_args, &block)
341
+ # Adds each of the keys of specified hash as an option
342
+ # @param preset_hash [Hash] hash of options to add
343
+ def add_option_preset(preset_hash, op: :push)
344
+ Log.log.debug{"add_option_preset=#{preset_hash}"}
345
+ raise "internal error: default expects Hash: #{preset_hash.class}" unless preset_hash.is_a?(Hash)
346
+ # incremental override
347
+ preset_hash.each{|k, v|@unprocessed_defaults.send(op, [k.to_sym, v])}
325
348
  end
326
349
 
327
350
  # check if there were unprocessed values to generate error
@@ -337,7 +360,7 @@ module Aspera
337
360
  return result
338
361
  end
339
362
 
340
- # get all original options on command line used to generate a config in config file
363
+ # get all original options on command line used to generate a config in config file
341
364
  def get_options_table(remove_from_remaining: true)
342
365
  result = {}
343
366
  @initial_cli_options.each do |optionval|
@@ -347,7 +370,7 @@ module Aspera
347
370
  when /^--([^=]+)=(.*)$/
348
371
  name = Regexp.last_match(1)
349
372
  value = Regexp.last_match(2)
350
- name.gsub!(OPTION_SEP_LINE, OPTION_SEP_NAME)
373
+ name.gsub!(OPTION_SEP_LINE, OPTION_SEP_SYMB)
351
374
  value = ExtendedValue.instance.evaluate(value)
352
375
  Log.log.debug{"option #{name}=#{value}"}
353
376
  result[name] = value
@@ -429,7 +452,7 @@ module Aspera
429
452
 
430
453
  # generate command line option from option symbol
431
454
  def symbol_to_option(symbol, opt_val)
432
- result = '--' + symbol.to_s.gsub(OPTION_SEP_NAME, OPTION_SEP_LINE)
455
+ result = '--' + symbol.to_s.gsub(OPTION_SEP_SYMB, OPTION_SEP_LINE)
433
456
  result = result + '=' + opt_val unless opt_val.nil?
434
457
  return result
435
458
  end