aspera-cli 4.23.0 → 4.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +37 -1
  4. data/CONTRIBUTING.md +86 -29
  5. data/README.md +2109 -1300
  6. data/bin/ascli +2 -1
  7. data/bin/asession +4 -4
  8. data/lib/aspera/agent/base.rb +4 -0
  9. data/lib/aspera/agent/connect.rb +20 -18
  10. data/lib/aspera/agent/desktop.rb +14 -11
  11. data/lib/aspera/agent/direct.rb +39 -31
  12. data/lib/aspera/agent/httpgw.rb +2 -2
  13. data/lib/aspera/agent/node.rb +9 -11
  14. data/lib/aspera/agent/transferd.rb +18 -11
  15. data/lib/aspera/api/aoc.rb +44 -31
  16. data/lib/aspera/api/cos_node.rb +7 -5
  17. data/lib/aspera/api/httpgw.rb +15 -18
  18. data/lib/aspera/api/node.rb +104 -22
  19. data/lib/aspera/ascmd.rb +22 -16
  20. data/lib/aspera/ascp/installation.rb +37 -40
  21. data/lib/aspera/ascp/management.rb +5 -4
  22. data/lib/aspera/assert.rb +54 -23
  23. data/lib/aspera/cli/basic_auth_plugin.rb +8 -7
  24. data/lib/aspera/cli/error.rb +1 -1
  25. data/lib/aspera/cli/extended_value.rb +28 -29
  26. data/lib/aspera/cli/formatter.rb +191 -168
  27. data/lib/aspera/cli/hints.rb +29 -3
  28. data/lib/aspera/cli/main.rb +138 -107
  29. data/lib/aspera/cli/manager.rb +50 -30
  30. data/lib/aspera/cli/plugin.rb +148 -77
  31. data/lib/aspera/cli/plugin_factory.rb +2 -2
  32. data/lib/aspera/cli/plugins/aoc.rb +189 -70
  33. data/lib/aspera/cli/plugins/ats.rb +15 -13
  34. data/lib/aspera/cli/plugins/config.rb +100 -214
  35. data/lib/aspera/cli/plugins/console.rb +49 -18
  36. data/lib/aspera/cli/plugins/cos.rb +4 -4
  37. data/lib/aspera/cli/plugins/faspex.rb +45 -51
  38. data/lib/aspera/cli/plugins/faspex5.rb +164 -165
  39. data/lib/aspera/cli/plugins/faspio.rb +6 -5
  40. data/lib/aspera/cli/plugins/httpgw.rb +2 -2
  41. data/lib/aspera/cli/plugins/node.rb +144 -162
  42. data/lib/aspera/cli/plugins/orchestrator.rb +10 -14
  43. data/lib/aspera/cli/plugins/preview.rb +26 -29
  44. data/lib/aspera/cli/plugins/server.rb +28 -28
  45. data/lib/aspera/cli/plugins/shares.rb +40 -28
  46. data/lib/aspera/cli/sync_actions.rb +101 -80
  47. data/lib/aspera/cli/transfer_agent.rb +51 -50
  48. data/lib/aspera/cli/transfer_progress.rb +29 -20
  49. data/lib/aspera/cli/version.rb +1 -1
  50. data/lib/aspera/cli/wizard.rb +157 -0
  51. data/lib/aspera/colors.rb +13 -8
  52. data/lib/aspera/command_line_builder.rb +28 -22
  53. data/lib/aspera/command_line_converter.rb +31 -0
  54. data/lib/aspera/environment.rb +145 -101
  55. data/lib/aspera/faspex_gw.rb +1 -1
  56. data/lib/aspera/faspex_postproc.rb +3 -2
  57. data/lib/aspera/hash_ext.rb +1 -1
  58. data/lib/aspera/id_generator.rb +10 -10
  59. data/lib/aspera/keychain/base.rb +18 -0
  60. data/lib/aspera/keychain/encrypted_hash.rb +6 -12
  61. data/lib/aspera/keychain/factory.rb +9 -3
  62. data/lib/aspera/keychain/hashicorp_vault.rb +9 -6
  63. data/lib/aspera/keychain/macos_security.rb +13 -13
  64. data/lib/aspera/log.rb +91 -19
  65. data/lib/aspera/nagios.rb +5 -6
  66. data/lib/aspera/node_simulator.rb +12 -7
  67. data/lib/aspera/oauth/base.rb +5 -3
  68. data/lib/aspera/oauth/factory.rb +24 -18
  69. data/lib/aspera/oauth/jwt.rb +13 -1
  70. data/lib/aspera/oauth/url_json.rb +3 -3
  71. data/lib/aspera/oauth/web.rb +5 -3
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/file_types.rb +4 -3
  74. data/lib/aspera/preview/generator.rb +25 -12
  75. data/lib/aspera/preview/terminal.rb +10 -7
  76. data/lib/aspera/preview/utils.rb +11 -9
  77. data/lib/aspera/products/connect.rb +1 -1
  78. data/lib/aspera/products/desktop.rb +1 -1
  79. data/lib/aspera/products/other.rb +2 -2
  80. data/lib/aspera/products/transferd.rb +8 -6
  81. data/lib/aspera/proxy_auto_config.rb +1 -1
  82. data/lib/aspera/rest.rb +29 -22
  83. data/lib/aspera/rest_call_error.rb +1 -1
  84. data/lib/aspera/resumer.rb +1 -1
  85. data/lib/aspera/secret_hider.rb +46 -40
  86. data/lib/aspera/ssh.rb +13 -3
  87. data/lib/aspera/sync/args.schema.yaml +102 -0
  88. data/lib/aspera/sync/conf.schema.yaml +701 -0
  89. data/lib/aspera/sync/database.rb +83 -0
  90. data/lib/aspera/sync/operations.rb +296 -0
  91. data/lib/aspera/temp_file_manager.rb +3 -2
  92. data/lib/aspera/transfer/error.rb +1 -1
  93. data/lib/aspera/transfer/error_info.rb +1 -2
  94. data/lib/aspera/transfer/faux_file.rb +11 -10
  95. data/lib/aspera/transfer/parameters.rb +6 -5
  96. data/lib/aspera/transfer/spec.rb +15 -1
  97. data/lib/aspera/transfer/spec.schema.yaml +316 -293
  98. data/lib/aspera/transfer/spec_doc.rb +34 -16
  99. data/lib/aspera/transfer/uri.rb +5 -5
  100. data/lib/aspera/uri_reader.rb +14 -10
  101. data/lib/aspera/web_auth.rb +2 -2
  102. data/lib/aspera/web_server_simple.rb +2 -2
  103. data.tar.gz.sig +0 -0
  104. metadata +15 -13
  105. metadata.gz.sig +0 -0
  106. data/lib/aspera/transfer/async_conf.schema.yaml +0 -716
  107. data/lib/aspera/transfer/convert.rb +0 -29
  108. data/lib/aspera/transfer/sync.rb +0 -232
  109. data/lib/aspera/transfer/sync_instance.schema.yaml +0 -20
  110. data/lib/aspera/transfer/sync_session.schema.yaml +0 -86
@@ -16,10 +16,14 @@ require 'aspera/assert'
16
16
  module Aspera
17
17
  module Cli
18
18
  # Global objects shared with plugins
19
- class Objects
20
- MEMBERS = %i[options transfer config formatter persistency].freeze
19
+ class Context
20
+ MEMBERS = %i[options transfer config formatter persistency man_header].freeze
21
21
  attr_accessor(*MEMBERS)
22
22
 
23
+ def initialize
24
+ @man_header = true
25
+ end
26
+
23
27
  def validate
24
28
  MEMBERS.each do |i|
25
29
  Aspera.assert(instance_variable_defined?(:"@#{i}"))
@@ -42,19 +46,42 @@ module Aspera
42
46
  STATUS_FIELD = 'status'
43
47
  COMMAND_CONFIG = :config
44
48
  COMMAND_HELP = :help
49
+ # types that go to result of type = text
45
50
  SCALAR_TYPES = [String, Integer, Symbol].freeze
51
+ USER_INTERFACES = %i[text graphical].freeze
46
52
 
47
- private_constant :COMMAND_CONFIG, :COMMAND_HELP, :SCALAR_TYPES
53
+ private_constant :COMMAND_CONFIG, :COMMAND_HELP, :SCALAR_TYPES, :USER_INTERFACES
48
54
 
49
55
  class << self
50
- # expect some list, but nothing to display
51
- def result_empty; return {type: :empty, data: :nil}; end
56
+ # early debug for parser
57
+ # Note: does not accept shortcuts
58
+ def early_debug_setup(argv)
59
+ Log.instance.program_name = Info::CMD_NAME
60
+ argv.each do |arg|
61
+ case arg
62
+ when '--' then break
63
+ when /^--log-level=(.*)/ then Log.instance.level = Regexp.last_match(1).to_sym
64
+ when /^--logger=(.*)/ then Log.instance.logger_type = Regexp.last_match(1).to_sym
65
+ end
66
+ rescue => e
67
+ $stderr.puts("Error: #{e}") # rubocop:disable Style/StderrPuts
68
+ Process.exit(1)
69
+ end
70
+ end
52
71
 
53
- # nothing expected
54
- def result_nothing; return {type: :nothing, data: :nil}; end
72
+ def result_special(how); {type: :special, data: how}; end
55
73
 
74
+ # Expect some list, but nothing to display
75
+ def result_empty; result_special(:empty); end
76
+
77
+ # Nothing expected
78
+ def result_nothing; result_special(:nothing); end
79
+
80
+ # Result is some status, such as "complete", "deleted"...
81
+ # @param status [String] The status
56
82
  def result_status(status); return {type: :status, data: status}; end
57
83
 
84
+ # text result coming from command result
58
85
  def result_text(data); return {type: :text, data: data}; end
59
86
 
60
87
  def result_success; return result_status('complete'); end
@@ -68,7 +95,7 @@ module Aspera
68
95
  return Main.result_nothing
69
96
  end
70
97
 
71
- # used when one command executes several transfer jobs (each job being possibly multi session)
98
+ # Used when one command executes several transfer jobs (each job being possibly multi session)
72
99
  # @param status_table [Array] [{STATUS_FIELD=>[status array],...},...]
73
100
  # @return a status object suitable as command result
74
101
  # each element has a key STATUS_FIELD which contains the result of possibly multiple sessions
@@ -84,32 +111,40 @@ module Aspera
84
111
  return result_object_list(status_table)
85
112
  end
86
113
 
87
- def result_image(blob, formatter:)
88
- return Main.result_status(formatter.status_image(blob))
114
+ # Display image for that URL or directly blob
115
+ def result_image(url_or_blob)
116
+ return {type: :image, data: url_or_blob}
89
117
  end
90
118
 
119
+ # A single object, must be Hash
91
120
  def result_single_object(data, fields: nil)
92
121
  return {type: :single_object, data: data, fields: fields}
93
122
  end
94
123
 
124
+ # An Array of Hash
95
125
  def result_object_list(data, fields: nil, total: nil)
96
126
  return {type: :object_list, data: data, fields: fields, total: total}
97
127
  end
98
128
 
99
- def result_value_list(data, name)
129
+ # A list of values
130
+ def result_value_list(data, name: 'id')
131
+ Aspera.assert_type(data, Array)
132
+ Aspera.assert_type(name, String)
100
133
  return {type: :value_list, data: data, name: name}
101
134
  end
102
135
 
103
136
  # Determines type of result based on data
104
137
  def result_auto(data)
105
138
  case data
139
+ when NilClass
140
+ return result_special(:null)
106
141
  when Hash
107
142
  return result_single_object(data)
108
143
  when Array
109
144
  all_types = data.map(&:class).uniq
110
145
  return result_object_list(data) if all_types.eql?([Hash])
111
146
  unsupported_types = all_types - SCALAR_TYPES
112
- return result_value_list(data, 'list') if unsupported_types.empty?
147
+ return result_value_list(data, name: 'list') if unsupported_types.empty?
113
148
  Aspera.error_unexpected_value(unsupported_types){'list item types'}
114
149
  when *SCALAR_TYPES
115
150
  return result_text(data)
@@ -121,12 +156,11 @@ module Aspera
121
156
  # minimum initialization, no exception raised
122
157
  def initialize(argv)
123
158
  @argv = argv
124
- early_debug_setup
125
- Log.log.trace2{Log.dump(:argv, @argv)}
159
+ Log.dump(:argv, @argv, level: :trace2)
126
160
  @option_help = false
127
161
  @option_show_config = false
128
162
  @bash_completion = false
129
- @env = Objects.new
163
+ @context = Context.new
130
164
  end
131
165
 
132
166
  # this is the main function called by initial script just after constructor
@@ -137,41 +171,39 @@ module Aspera
137
171
  execute_command = true
138
172
  # catch exceptions
139
173
  begin
140
- init_agents_and_options
141
- # find plugins, shall be after parse! ?
142
- PluginFactory.instance.add_plugins_from_lookup_folders
174
+ init_agents_options_plugins
143
175
  # help requested without command ? (plugins must be known here)
144
- exit_with_usage(true) if @option_help && @env.options.command_or_arg_empty?
176
+ show_usage if @option_help && @context.options.command_or_arg_empty?
145
177
  generate_bash_completion if @bash_completion
146
- @env.config.periodic_check_newer_gem_version
178
+ @context.config.periodic_check_newer_gem_version
147
179
  command_sym =
148
- if @option_show_config && @env.options.command_or_arg_empty?
180
+ if @option_show_config && @context.options.command_or_arg_empty?
149
181
  COMMAND_CONFIG
150
182
  else
151
- @env.options.get_next_command(PluginFactory.instance.plugin_list.unshift(COMMAND_HELP))
183
+ @context.options.get_next_command(PluginFactory.instance.plugin_list.unshift(COMMAND_HELP))
152
184
  end
153
185
  # command will not be executed, but we need manual
154
- @env.options.fail_on_missing_mandatory = false if @option_help || @option_show_config
186
+ @context.options.fail_on_missing_mandatory = false if @option_help || @option_show_config
155
187
  # main plugin is not dynamically instantiated
156
188
  case command_sym
157
189
  when COMMAND_HELP
158
- exit_with_usage(true)
190
+ show_usage
159
191
  when COMMAND_CONFIG
160
- command_plugin = @env.config
192
+ command_plugin = @context.config
161
193
  else
162
194
  # get plugin, set options, etc
163
195
  command_plugin = get_plugin_instance_with_options(command_sym)
164
196
  # parse plugin specific options
165
- @env.options.parse_options!
197
+ @context.options.parse_options!
166
198
  end
167
199
  # help requested for current plugin
168
- exit_with_usage(false) if @option_help
200
+ show_usage(all: false) if @option_help
169
201
  if @option_show_config
170
- @env.formatter.display_results(type: :single_object, data: @env.options.known_options(only_defined: true).stringify_keys)
202
+ @context.formatter.display_results(type: :single_object, data: @context.options.known_options(only_defined: true).stringify_keys)
171
203
  execute_command = false
172
204
  end
173
205
  # locking for single execution (only after "per plugin" option, in case lock port is there)
174
- lock_port = @env.options.get_option(:lock_port)
206
+ lock_port = @context.options.get_option(:lock_port)
175
207
  if !lock_port.nil?
176
208
  begin
177
209
  # no need to close later, will be freed on process exit. must save in member else it is garbage collected
@@ -183,18 +215,18 @@ module Aspera
183
215
  Log.log.warn{"Another instance is already running (#{e.message})."}
184
216
  end
185
217
  end
186
- pid_file = @env.options.get_option(:pid_file)
218
+ pid_file = @context.options.get_option(:pid_file)
187
219
  if !pid_file.nil?
188
220
  File.write(pid_file, Process.pid)
189
221
  Log.log.debug{"Wrote pid #{Process.pid} to #{pid_file}"}
190
222
  at_exit{File.delete(pid_file)}
191
223
  end
192
224
  # execute and display (if not exclusive execution)
193
- @env.formatter.display_results(**command_plugin.execute_action) if execute_command
225
+ @context.formatter.display_results(**command_plugin.execute_action) if execute_command
194
226
  # save config file if command modified it
195
- @env.config.save_config_file_if_needed
227
+ @context.config.save_config_file_if_needed
196
228
  # finish
197
- @env.transfer.shutdown
229
+ @context.transfer.shutdown
198
230
  rescue Net::SSH::AuthenticationFailed => e; exception_info = {e: e, t: 'SSH', security: true}
199
231
  rescue OpenSSL::SSL::SSLError => e; exception_info = {e: e, t: 'SSL'}
200
232
  rescue Cli::BadArgument => e; exception_info = {e: e, t: 'Argument', usage: true}
@@ -211,15 +243,15 @@ module Aspera
211
243
  # 1- processing of error condition
212
244
  unless exception_info.nil?
213
245
  Log.log.warn(exception_info[:e].message) if Log.instance.logger_type.eql?(:syslog) && exception_info[:security]
214
- @env.formatter.display_message(:error, "#{Formatter::ERROR_FLASH} #{exception_info[:t]}: #{exception_info[:e].message}")
215
- @env.formatter.display_message(:error, 'Use option -h to get help.') if exception_info[:usage]
246
+ @context.formatter.display_message(:error, "#{Formatter::ERROR_FLASH} #{exception_info[:t]}: #{exception_info[:e].message}")
247
+ @context.formatter.display_message(:error, 'Use option -h to get help.') if exception_info[:usage]
216
248
  # Is that a known error condition with proposal for remediation ?
217
- Hints.hint_for(exception_info[:e], @env.formatter)
249
+ Hints.hint_for(exception_info[:e], @context.formatter)
218
250
  end
219
251
  # 2- processing of command not processed (due to exception or bad command line)
220
252
  if execute_command || @option_show_config
221
- @env.options.final_errors.each do |msg|
222
- @env.formatter.display_message(:error, "#{Formatter::ERROR_FLASH} Argument: #{msg}")
253
+ @context.options.final_errors.each do |msg|
254
+ @context.formatter.display_message(:error, "#{Formatter::ERROR_FLASH} Argument: #{msg}")
223
255
  # add code as exception if there is not already an error
224
256
  exception_info = {e: Exception.new(msg), t: 'UnusedArg'} if exception_info.nil?
225
257
  end
@@ -229,10 +261,36 @@ module Aspera
229
261
  # show stack trace in debug mode
230
262
  raise exception_info[:e] if Log.log.debug?
231
263
  # else give hint and exit
232
- @env.formatter.display_message(:error, 'Use --log-level=debug to get more details.') if exception_info[:debug]
264
+ @context.formatter.display_message(:error, 'Use --log-level=debug to get more details.') if exception_info[:debug]
233
265
  Process.exit(1)
234
266
  end
235
- return nil
267
+ return
268
+ end
269
+
270
+ def init_agents_options_plugins
271
+ init_agents_and_options
272
+ # find plugins, shall be after parse! ?
273
+ PluginFactory.instance.add_plugins_from_lookup_folders
274
+ end
275
+
276
+ def show_usage(all: true, exit: true)
277
+ # display main plugin options (+config)
278
+ @context.formatter.display_message(:error, @context.options.parser)
279
+ if all
280
+ @context.only_manual
281
+ # list plugins that have a "require" field, i.e. all but main plugin
282
+ PluginFactory.instance.plugin_list.each do |plugin_name_sym|
283
+ # config was already included in the global options
284
+ next if plugin_name_sym.eql?(COMMAND_CONFIG)
285
+ # override main option parser with a brand new, to avoid having global options
286
+ @context.options = Manager.new(Info::CMD_NAME)
287
+ @context.options.parser.banner = '' # remove default banner
288
+ get_plugin_instance_with_options(plugin_name_sym)
289
+ # display generated help for plugin options
290
+ @context.formatter.display_message(:error, @context.options.parser.help)
291
+ end
292
+ end
293
+ Process.exit(0) if exit
236
294
  end
237
295
 
238
296
  private
@@ -240,30 +298,34 @@ module Aspera
240
298
  # This can throw exception if there is a problem with the environment, needs to be caught by execute method
241
299
  def init_agents_and_options
242
300
  # create formatter, in case there is an exception, it is used to display.
243
- @env.formatter = Formatter.new
301
+ @context.formatter = Formatter.new
244
302
  # create command line manager with arguments
245
- @env.options = Manager.new(Info::CMD_NAME, @argv)
303
+ @context.options = Manager.new(Info::CMD_NAME, @argv)
246
304
  # formatter adds options
247
- @env.formatter.declare_options(@env.options)
248
- ExtendedValue.instance.default_decoder = @env.options.get_option(:struct_parser)
305
+ @context.formatter.declare_options(@context.options)
306
+ ExtendedValue.instance.default_decoder = @context.options.get_option(:struct_parser)
249
307
  # compare $0 with expected name
250
308
  current_prog_name = File.basename($PROGRAM_NAME)
251
- @env.formatter.display_message(
309
+ @context.formatter.display_message(
252
310
  :error,
253
- "#{Formatter::WARNING_FLASH} Please use '#{Info::CMD_NAME}' instead of '#{current_prog_name}'") unless current_prog_name.eql?(Info::CMD_NAME)
311
+ "#{Formatter::WARNING_FLASH} Please use '#{Info::CMD_NAME}' instead of '#{current_prog_name}'"
312
+ ) unless current_prog_name.eql?(Info::CMD_NAME)
254
313
  # declare and parse global options
255
314
  declare_global_options
315
+ # do not display config commands if help is asked
316
+ @context.man_header = false
256
317
  # the Config plugin adds the @preset parser, so declare before TransferAgent which may use it
257
- @env.config = Plugins::Config.new(broker: @env, man_header: false)
318
+ @context.config = Plugins::Config.new(context: @context)
319
+ @context.man_header = true
258
320
  # data persistency is set in config
259
- Aspera.assert(@env.persistency){'missing persistency object'}
321
+ Aspera.assert(@context.persistency){'missing persistency object'}
260
322
  # the TransferAgent plugin may use the @preset parser
261
- @env.transfer = TransferAgent.new(@env.options, @env.config)
323
+ @context.transfer = TransferAgent.new(@context.options, @context.config)
262
324
  # add commands for config plugin after all options have been added
263
- @env.config.add_manual_header(false)
264
- @env.validate
325
+ @context.config.add_manual_header(false)
326
+ @context.validate
265
327
  # set banner when all environment is created so that additional extended value modifiers are known, e.g. @preset
266
- @env.options.parser.banner = app_banner
328
+ @context.options.parser.banner = app_banner
267
329
  end
268
330
 
269
331
  def app_banner
@@ -303,24 +365,29 @@ module Aspera
303
365
  # define header for manual
304
366
  def declare_global_options
305
367
  Log.log.debug('declare_global_options')
306
- @env.options.declare(:help, 'Show this message', values: :none, short: 'h'){@option_help = true}
307
- @env.options.declare(:bash_comp, 'Generate bash completion for command', values: :none){@bash_completion = true}
308
- @env.options.declare(:show_config, 'Display parameters used for the provided action', values: :none){@option_show_config = true}
309
- @env.options.declare(:version, 'Display version', values: :none, short: 'v'){@env.formatter.display_message(:data, Cli::VERSION); Process.exit(0)} # rubocop:disable Style/Semicolon, Layout/LineLength
310
- @env.options.declare(
368
+ @context.options.declare(:help, 'Show this message', values: :none, short: 'h'){@option_help = true}
369
+ @context.options.declare(:bash_comp, 'Generate bash completion for command', values: :none){@bash_completion = true}
370
+ @context.options.declare(:show_config, 'Display parameters used for the provided action', values: :none){@option_show_config = true}
371
+ @context.options.declare(:version, 'Display version', values: :none, short: 'v'){@context.formatter.display_message(:data, Cli::VERSION); Process.exit(0)} # rubocop:disable Style/Semicolon
372
+ @context.options.declare(
311
373
  :ui, 'Method to start browser',
312
- values: Environment::USER_INTERFACES,
313
- handler: {o: Environment.instance, m: :url_method},
314
- default: Environment.default_gui_mode)
315
- @env.options.declare(:log_level, 'Log level', values: Log.levels, handler: {o: Log.instance, m: :level})
316
- @env.options.declare(:logger, 'Logging method', values: Log::LOG_TYPES, handler: {o: Log.instance, m: :logger_type})
317
- @env.options.declare(:lock_port, 'Prevent dual execution of a command, e.g. in cron', coerce: Integer, types: Integer)
318
- @env.options.declare(:once_only, 'Process only new items (some commands)', values: :bool, default: false)
319
- @env.options.declare(:log_secrets, 'Show passwords in logs', values: :bool, handler: {o: SecretHider, m: :log_secrets})
320
- @env.options.declare(:clean_temp, 'Cleanup temporary files on exit', values: :bool, handler: {o: TempFileManager.instance, m: :cleanup_on_exit})
321
- @env.options.declare(:pid_file, 'Write process identifier to file, delete on exit', types: String)
374
+ values: USER_INTERFACES,
375
+ handler: {o: Environment.instance, m: :url_method}
376
+ )
377
+ @context.options.declare(
378
+ :invalid_characters, 'Replacement character and invalid filename characters',
379
+ handler: {o: Environment.instance, m: :file_illegal_characters}
380
+ )
381
+ @context.options.declare(:log_level, 'Log level', values: Log::LEVELS, handler: {o: Log.instance, m: :level})
382
+ @context.options.declare(:log_format, 'Log formatter', types: [Proc, Logger::Formatter, String], handler: {o: Log.instance, m: :formatter})
383
+ @context.options.declare(:logger, 'Logging method', values: Log::LOG_TYPES, handler: {o: Log.instance, m: :logger_type})
384
+ @context.options.declare(:lock_port, 'Prevent dual execution of a command, e.g. in cron', coerce: Integer, types: Integer)
385
+ @context.options.declare(:once_only, 'Process only new items (some commands)', values: :bool, default: false)
386
+ @context.options.declare(:log_secrets, 'Show passwords in logs', values: :bool, handler: {o: SecretHider.instance, m: :log_secrets})
387
+ @context.options.declare(:clean_temp, 'Cleanup temporary files on exit', values: :bool, handler: {o: TempFileManager.instance, m: :cleanup_on_exit})
388
+ @context.options.declare(:pid_file, 'Write process identifier to file, delete on exit', types: String)
322
389
  # parse declared options
323
- @env.options.parse_options!
390
+ @context.options.parse_options!
324
391
  end
325
392
 
326
393
  # @return the plugin instance, based on name
@@ -329,55 +396,19 @@ module Aspera
329
396
  def get_plugin_instance_with_options(plugin_name_sym)
330
397
  Log.log.debug{"get_plugin_instance_with_options(#{plugin_name_sym})"}
331
398
  # load default params only if no param already loaded before plugin instantiation
332
- @env.config.add_plugin_default_preset(plugin_name_sym)
333
- command_plugin = PluginFactory.instance.create(plugin_name_sym, broker: @env)
399
+ @context.config.add_plugin_default_preset(plugin_name_sym)
400
+ command_plugin = PluginFactory.instance.create(plugin_name_sym, context: @context)
334
401
  return command_plugin
335
402
  end
336
403
 
337
404
  def generate_bash_completion
338
- if @env.options.get_next_argument('', multiple: true, mandatory: false).nil?
405
+ if @context.options.get_next_argument('', multiple: true, mandatory: false).nil?
339
406
  PluginFactory.instance.plugin_list.each{ |p| puts p}
340
407
  else
341
408
  Log.log.warn('only first level completion so far')
342
409
  end
343
410
  Process.exit(0)
344
411
  end
345
-
346
- def exit_with_usage(include_all_plugins)
347
- Log.log.debug{"exit_with_usage(#{include_all_plugins})".bg_red}
348
- # display main plugin options (+config)
349
- @env.formatter.display_message(:error, @env.options.parser)
350
- if include_all_plugins
351
- @env.only_manual
352
- # list plugins that have a "require" field, i.e. all but main plugin
353
- PluginFactory.instance.plugin_list.each do |plugin_name_sym|
354
- # config was already included in the global options
355
- next if plugin_name_sym.eql?(COMMAND_CONFIG)
356
- # override main option parser with a brand new, to avoid having global options
357
- @env.options = Manager.new(Info::CMD_NAME)
358
- @env.options.parser.banner = '' # remove default banner
359
- get_plugin_instance_with_options(plugin_name_sym)
360
- # display generated help for plugin options
361
- @env.formatter.display_message(:error, @env.options.parser.help)
362
- end
363
- end
364
- Process.exit(0)
365
- end
366
-
367
- # early debug for parser
368
- # Note: does not accept shortcuts
369
- def early_debug_setup
370
- Log.instance.program_name = Info::CMD_NAME
371
- @argv.each do |arg|
372
- case arg
373
- when '--' then break
374
- when /^--log-level=(.*)/ then Log.instance.level = Regexp.last_match(1).to_sym
375
- when /^--logger=(.*)/ then Log.instance.logger_type = Regexp.last_match(1).to_sym
376
- end
377
- rescue => e
378
- $stderr.puts("Error: #{e}")
379
- end
380
- end
381
412
  end
382
413
  end
383
414
  end
@@ -54,14 +54,19 @@ module Aspera
54
54
  OPTION_SEP_LINE = '-'
55
55
  # option name separator in code (symbol)
56
56
  OPTION_SEP_SYMBOL = '_'
57
- SOURCE_USER = 'cmdline' # cspell:disable-line
58
57
  OPTION_VALUE_SEPARATOR = '='
58
+ # an option like --a.b.c=d does: a={"b":{"c":ext_val(d)}}
59
+ # TODO: all Hash are additive, + way to reset Hash (e.g. --opt=@none:)
60
+ OPTION_HASH_SEPARATOR = '.'
61
+ # starts an option
59
62
  OPTION_PREFIX = '--'
63
+ # when this is alone, this stops option processing
60
64
  OPTIONS_STOP = '--'
65
+ SOURCE_USER = 'cmdline' # cspell:disable-line
61
66
 
62
67
  DEFAULT_PARSER_TYPES = [Array, Hash].freeze
63
68
 
64
- private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :SOURCE_USER
69
+ private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_HASH_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER, :DEFAULT_PARSER_TYPES
65
70
 
66
71
  class << self
67
72
  def enum_to_bool(enum)
@@ -75,6 +80,7 @@ module Aspera
75
80
 
76
81
  # find shortened string value in allowed symbol list
77
82
  def get_from_list(short_value, descr, allowed_values)
83
+ Aspera.assert_type(short_value, String)
78
84
  # we accept shortcuts
79
85
  matching_exact = allowed_values.select{ |i| i.to_s.eql?(short_value)}
80
86
  return matching_exact.first if matching_exact.length == 1
@@ -107,7 +113,7 @@ module Aspera
107
113
  # @param type_list [NilClass, Class, Array[Class]] accepted value type(s)
108
114
  # @param check_array [bool] set to true if it is a list of values to check
109
115
  def validate_type(what, descr, to_check, type_list, check_array: false)
110
- return nil if type_list.nil?
116
+ return if type_list.nil?
111
117
  Aspera.assert(type_list.is_a?(Array) && type_list.all?(Class)){'types must be a Class Array'}
112
118
  value_list = check_array ? to_check : [to_check]
113
119
  value_list.each do |value|
@@ -147,14 +153,13 @@ module Aspera
147
153
  # options can also be provided by env vars : --param-name -> ASCLI_PARAM_NAME
148
154
  env_prefix = program_name.upcase + OPTION_SEP_SYMBOL
149
155
  ENV.each do |k, v|
150
- if k.start_with?(env_prefix)
151
- @option_pairs_env[k[env_prefix.length..-1].downcase.to_sym] = v
152
- end
156
+ @option_pairs_env[k[env_prefix.length..-1].downcase.to_sym] = v if k.start_with?(env_prefix)
153
157
  end
154
158
  Log.log.debug{"env=#{@option_pairs_env}".red}
155
159
  @unprocessed_cmd_line_options = []
156
160
  @unprocessed_cmd_line_arguments = []
157
161
  return if argv.nil?
162
+ # true until `--` is found (stop options)
158
163
  process_options = true
159
164
  until argv.empty?
160
165
  value = argv.shift
@@ -171,7 +176,7 @@ module Aspera
171
176
  end
172
177
  end
173
178
  @initial_cli_options = @unprocessed_cmd_line_options.dup.freeze
174
- Log.log.debug{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
179
+ Log.log.trace1{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
175
180
  @parser.separator('')
176
181
  @parser.separator('OPTIONS: global')
177
182
  declare(:interactive, 'Use interactive input of missing params', values: :bool, handler: {o: self, m: :ask_missing_mandatory})
@@ -244,10 +249,9 @@ module Aspera
244
249
  result = attributes[:accessor].value
245
250
  when :value
246
251
  result = attributes[:value]
247
- else
248
- raise 'unknown type'
252
+ else Aspera.error_unexpected_value(attributes[:read_write]){'attribute read/write'}
249
253
  end
250
- Log.log.debug{"(#{attributes[:read_write]}) get #{option_symbol}=#{result}"}
254
+ Log.log.trace1{"(#{attributes[:read_write]}) get #{option_symbol}=#{result}"}
251
255
  result = default if result.nil?
252
256
  # do not fail for manual generation if option mandatory but not set
253
257
  result = '' if result.nil? && mandatory && !@fail_on_missing_mandatory
@@ -259,9 +263,7 @@ module Aspera
259
263
  # ask_missing_mandatory
260
264
  accept_list = nil
261
265
  # print "please enter: #{option_symbol.to_s}"
262
- if @declared_options.key?(option_symbol) && attributes.key?(:values)
263
- accept_list = attributes[:values]
264
- end
266
+ accept_list = attributes[:values] if @declared_options.key?(option_symbol) && attributes.key?(:values)
265
267
  result = get_interactive(option_symbol.to_s, option: true, accept_list: accept_list)
266
268
  set_option(option_symbol, result, where: 'interactive')
267
269
  end
@@ -282,15 +284,14 @@ module Aspera
282
284
  Log.log.warn("#{option_symbol}: Option is deprecated: #{attributes[:deprecation]}") if attributes[:deprecation]
283
285
  value = evaluate_extended_value(value, attributes[:types])
284
286
  value = Manager.enum_to_bool(value) if attributes[:values].eql?(BOOLEAN_VALUES)
285
- Log.log.debug{"(#{attributes[:read_write]}/#{where}) set #{option_symbol}=#{value}"}
287
+ Log.log.trace1{"(#{attributes[:read_write]}/#{where}) set #{option_symbol}=#{value}"}
286
288
  self.class.validate_type(:option, option_symbol, value, attributes[:types])
287
289
  case attributes[:read_write]
288
290
  when :accessor
289
291
  attributes[:accessor].value = value
290
292
  when :value
291
293
  attributes[:value] = value
292
- else # nil or other
293
- raise 'error'
294
+ else Aspera.error_unexpected_value(attributes[:read_write]){'attribute read/write'}
294
295
  end
295
296
  end
296
297
 
@@ -308,12 +309,12 @@ module Aspera
308
309
  Aspera.assert_type(option_symbol, Symbol)
309
310
  Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
310
311
  Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
311
- Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with capital"}
312
+ Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with an uppercase"}
312
313
  Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :types"}
313
314
  opt = @declared_options[option_symbol] = {
314
315
  read_write: handler.nil? ? :value : :accessor,
315
316
  # by default passwords and secrets are sensitive, else specify when declaring the option
316
- sensitive: SecretHider.secret?(option_symbol, '')
317
+ sensitive: SecretHider.instance.secret?(option_symbol, '')
317
318
  }
318
319
  if !types.nil?
319
320
  types = [types] unless types.is_a?(Array)
@@ -325,7 +326,7 @@ module Aspera
325
326
  opt[:deprecation] = deprecation
326
327
  description = "#{description} (#{'deprecated'.blue}: #{deprecation})"
327
328
  end
328
- Log.log.debug{"declare: #{option_symbol}: #{opt[:read_write]}".green}
329
+ Log.log.trace1{"declare: #{option_symbol}: #{opt[:read_write]}".green}
329
330
  if opt[:read_write].eql?(:accessor)
330
331
  Aspera.assert_type(handler, Hash)
331
332
  Aspera.assert(handler.keys.sort.eql?(%i[m o]))
@@ -449,11 +450,20 @@ module Aspera
449
450
  unknown_options = []
450
451
  begin
451
452
  # remove known options one by one, exception if unknown
452
- Log.log.trace1('before parse'.red)
453
+ Log.log.trace1('Before parse')
453
454
  @parser.parse!(@unprocessed_cmd_line_options)
454
- Log.log.trace1('After parse'.red)
455
+ Log.log.trace1('After parse')
455
456
  rescue OptionParser::InvalidOption => e
456
457
  Log.log.trace1{"InvalidOption #{e}".red}
458
+ if (m = e.args.first.match(/^--([a-z\-]+)\.([^=]+)=(.+)$/))
459
+ option, path, raw_value = m.captures
460
+ option_sym = self.class.option_line_to_name(option).to_sym
461
+ if @declared_options.key?(option_sym)
462
+ value = path.split(OPTION_HASH_SEPARATOR).reverse.inject(smart_convert(raw_value)){ |v, k| {k => v}}
463
+ set_option(option_sym, value, where: 'dotted')
464
+ retry
465
+ end
466
+ end
457
467
  # save for later processing
458
468
  unknown_options.push(e.args.first)
459
469
  retry
@@ -467,7 +477,7 @@ module Aspera
467
477
  return $stdin.getpass("#{prompt}> ") if sensitive
468
478
  print("#{prompt}> ")
469
479
  line = $stdin.gets
470
- raise 'Unexpected end of standard input' if line.nil?
480
+ Aspera.assert_type(line, String){'Unexpected end of standard input'}
471
481
  return line.chomp
472
482
  end
473
483
 
@@ -481,7 +491,7 @@ module Aspera
481
491
  if sym_list.any?{ |a| a.eql?(input)}
482
492
  return input
483
493
  else
484
- $stderr.puts("No such #{prompt}: #{input}, select one of: #{sym_list.join(', ')}")
494
+ $stderr.puts("No such #{prompt}: #{input}, select one of: #{sym_list.join(', ')}") # rubocop:disable Style/StderrPuts
485
495
  end
486
496
  end
487
497
  end
@@ -521,10 +531,21 @@ module Aspera
521
531
 
522
532
  private
523
533
 
524
- def evaluate_extended_value(value, types)
525
- if DEFAULT_PARSER_TYPES.include?(types) || (types.is_a?(Array) && types.all?{ |t| DEFAULT_PARSER_TYPES.include?(t)})
526
- return ExtendedValue.instance.evaluate_with_default(value)
534
+ # Using dotted hash notation, convert value to bool, int, float or extended value
535
+ def smart_convert(value)
536
+ return true if value == 'true'
537
+ return false if value == 'false'
538
+ Integer(value)
539
+ rescue ArgumentError
540
+ begin
541
+ Float(value)
542
+ rescue ArgumentError
543
+ evaluate_extended_value(value, nil)
527
544
  end
545
+ end
546
+
547
+ def evaluate_extended_value(value, types)
548
+ return ExtendedValue.instance.evaluate_with_default(value) if DEFAULT_PARSER_TYPES.include?(types) || (types.is_a?(Array) && types.all?{ |t| DEFAULT_PARSER_TYPES.include?(t)})
528
549
  return ExtendedValue.instance.evaluate(value)
529
550
  end
530
551
 
@@ -535,11 +556,12 @@ module Aspera
535
556
  return result
536
557
  end
537
558
 
559
+ # TODO: use formatter
538
560
  def highlight_current(value)
539
561
  $stdout.isatty ? value.to_s.red.bold : "[#{value}]"
540
562
  end
541
563
 
542
- # try to evaluate options set ib batch
564
+ # try to evaluate options set in batch
543
565
  # @param unprocessed_options [Array] list of options to apply (key_sym,value)
544
566
  # @param where [String] where the options come from
545
567
  def consume_option_pairs(unprocessed_options, where)
@@ -548,9 +570,7 @@ module Aspera
548
570
  unprocessed_options.each do |k, v|
549
571
  if @declared_options.key?(k)
550
572
  # constrained parameters as string are revert to symbol
551
- if @declared_options[k].key?(:values) && v.is_a?(String)
552
- v = self.class.get_from_list(v, "#{k} in #{where}", @declared_options[k][:values])
553
- end
573
+ v = self.class.get_from_list(v, "#{k} in #{where}", @declared_options[k][:values]) if @declared_options[k].key?(:values) && v.is_a?(String)
554
574
  options_to_set[k] = v
555
575
  else
556
576
  Log.log.trace1{"unprocessed: #{k}: #{v}"}