aspera-cli 4.18.0 → 4.19.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 (60) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +23 -0
  4. data/CONTRIBUTING.md +5 -12
  5. data/README.md +152 -84
  6. data/examples/build_exec +85 -0
  7. data/examples/build_package.sh +28 -0
  8. data/lib/aspera/agent/alpha.rb +4 -4
  9. data/lib/aspera/agent/base.rb +2 -0
  10. data/lib/aspera/agent/connect.rb +3 -4
  11. data/lib/aspera/agent/direct.rb +108 -104
  12. data/lib/aspera/agent/httpgw.rb +1 -1
  13. data/lib/aspera/api/aoc.rb +2 -2
  14. data/lib/aspera/api/httpgw.rb +95 -57
  15. data/lib/aspera/api/node.rb +110 -77
  16. data/lib/aspera/ascp/installation.rb +47 -32
  17. data/lib/aspera/ascp/management.rb +4 -1
  18. data/lib/aspera/ascp/products.rb +2 -8
  19. data/lib/aspera/cli/extended_value.rb +27 -14
  20. data/lib/aspera/cli/formatter.rb +35 -28
  21. data/lib/aspera/cli/main.rb +11 -11
  22. data/lib/aspera/cli/manager.rb +109 -94
  23. data/lib/aspera/cli/plugin.rb +4 -7
  24. data/lib/aspera/cli/plugin_factory.rb +10 -1
  25. data/lib/aspera/cli/plugins/aoc.rb +15 -14
  26. data/lib/aspera/cli/plugins/config.rb +35 -29
  27. data/lib/aspera/cli/plugins/faspex.rb +5 -4
  28. data/lib/aspera/cli/plugins/faspex5.rb +16 -13
  29. data/lib/aspera/cli/plugins/node.rb +50 -41
  30. data/lib/aspera/cli/plugins/orchestrator.rb +3 -2
  31. data/lib/aspera/cli/plugins/preview.rb +1 -1
  32. data/lib/aspera/cli/plugins/server.rb +2 -2
  33. data/lib/aspera/cli/plugins/shares.rb +11 -7
  34. data/lib/aspera/cli/special_values.rb +13 -0
  35. data/lib/aspera/cli/sync_actions.rb +73 -32
  36. data/lib/aspera/cli/transfer_agent.rb +3 -2
  37. data/lib/aspera/cli/transfer_progress.rb +1 -1
  38. data/lib/aspera/cli/version.rb +1 -1
  39. data/lib/aspera/environment.rb +100 -7
  40. data/lib/aspera/faspex_gw.rb +1 -1
  41. data/lib/aspera/keychain/encrypted_hash.rb +2 -0
  42. data/lib/aspera/log.rb +1 -0
  43. data/lib/aspera/node_simulator.rb +1 -1
  44. data/lib/aspera/oauth/jwt.rb +1 -1
  45. data/lib/aspera/oauth/url_json.rb +2 -0
  46. data/lib/aspera/oauth/web.rb +7 -6
  47. data/lib/aspera/rest.rb +46 -15
  48. data/lib/aspera/secret_hider.rb +3 -2
  49. data/lib/aspera/ssh.rb +1 -1
  50. data/lib/aspera/transfer/faux_file.rb +7 -5
  51. data/lib/aspera/transfer/parameters.rb +27 -19
  52. data/lib/aspera/transfer/spec.rb +8 -10
  53. data/lib/aspera/transfer/sync.rb +52 -47
  54. data/lib/aspera/web_auth.rb +0 -1
  55. data/lib/aspera/web_server_simple.rb +24 -13
  56. data.tar.gz.sig +0 -0
  57. metadata +5 -4
  58. metadata.gz.sig +0 -0
  59. data/examples/rubyc +0 -24
  60. data/lib/aspera/open_application.rb +0 -69
@@ -41,8 +41,11 @@ module Aspera
41
41
  </CONF>
42
42
  END_OF_CONFIG_FILE
43
43
  # all ascp files (in SDK)
44
- FILES = %i[ascp ascp4 transferd ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].freeze
44
+ EXE_FILES = %i[ascp ascp4 async].freeze
45
+ FILES = %i[transferd ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
45
46
  private_constant :EXT_RUBY_PROTOBUF, :RB_SDK_FOLDER, :DEFAULT_ASPERA_CONF, :FILES
47
+ # options for SSH client private key
48
+ CLIENT_SSH_KEY_OPTIONS = %i{dsa_rsa rsa per_client}.freeze
46
49
  # set ascp executable path
47
50
  def ascp_path=(v)
48
51
  @path_to_ascp = v
@@ -110,14 +113,14 @@ module Aspera
110
113
  def path(k)
111
114
  file_is_optional = false
112
115
  case k
113
- when :ascp, :ascp4
116
+ when *EXE_FILES
117
+ file_is_optional = k.eql?(:async)
114
118
  use_ascp_from_product(FIRST_FOUND) if @path_to_ascp.nil?
115
- file = @path_to_ascp
116
119
  # NOTE: that there might be a .exe at the end
117
- file = file.gsub('ascp', 'ascp4') if k.eql?(:ascp4)
120
+ file = @path_to_ascp.gsub('ascp', k.to_s)
118
121
  when :transferd
119
- file = transferd_filepath
120
122
  file_is_optional = true
123
+ file = transferd_filepath
121
124
  when :ssh_private_dsa, :ssh_private_rsa
122
125
  # assume last 3 letters are type
123
126
  type = k.to_s[-3..-1].to_sym
@@ -151,8 +154,16 @@ module Aspera
151
154
  return DataRepository.instance.item(:uuid)
152
155
  end
153
156
 
154
- def aspera_token_ssh_key_paths
155
- return %i[ssh_private_dsa ssh_private_rsa].map{|i|Installation.instance.path(i)}
157
+ # get paths of SSH keys to use for ascp client
158
+ # @param types [Symbol] types to use
159
+ def aspera_token_ssh_key_paths(types)
160
+ Aspera.assert_values(types, CLIENT_SSH_KEY_OPTIONS)
161
+ return case types
162
+ when :dsa_rsa, :rsa
163
+ types.to_s.split('_').map{|i|Installation.instance.path("ssh_private_#{i}".to_sym)}
164
+ when :per_client
165
+ raise 'Not yet implemented'
166
+ end
156
167
  end
157
168
 
158
169
  # use in plugin `config`
@@ -169,13 +180,14 @@ module Aspera
169
180
  raise "An error occurred when testing #{ascp_filename}: #{cmd_out}" unless $CHILD_STATUS == 0
170
181
  # get version from ascp, only after full extract, as windows requires DLLs (SSL/TLS/etc...)
171
182
  m = cmd_out.match(/ version ([0-9.]+)/)
172
- exe_version = m[1] unless m.nil?
183
+ exe_version = m[1].gsub(/\.$/, '') unless m.nil?
173
184
  return exe_version
174
185
  end
175
186
 
176
- def ascp_add_pvcl(data)
187
+ def ascp_pvcl_info
188
+ data = {}
177
189
  # read PATHs from ascp directly, and pvcl modules as well
178
- Open3.popen3(data['ascp'], '-DDL-') do |_stdin, _stdout, stderr, thread|
190
+ Open3.popen3(ascp_path, '-DDL-') do |_stdin, _stdout, stderr, thread|
179
191
  last_line = ''
180
192
  while (line = stderr.gets)
181
193
  line.chomp!
@@ -194,32 +206,32 @@ module Aspera
194
206
  data['product_name'] = Regexp.last_match(1)
195
207
  data['product_version'] = Regexp.last_match(2)
196
208
  when /^LOG Initializing FASP version ([^,]+),/
197
- data['ascp_version'] = Regexp.last_match(1)
209
+ data['sdk_ascp_version'] = Regexp.last_match(1)
198
210
  end
199
211
  end
200
212
  if !thread.value.exitstatus.eql?(1) && !data.key?('root')
201
213
  raise last_line
202
214
  end
203
215
  end
216
+ return data
204
217
  end
205
218
 
206
219
  # extract some stings from ascp binary
207
- def ascp_add_openssl(data)
208
- ascp_file = data['ascp']
209
- File.binread(ascp_file).scan(/[\x20-\x7E]{10,}/) do |bin_string|
220
+ def ascp_ssl_info
221
+ data = {}
222
+ File.binread(ascp_path).scan(/[\x20-\x7E]{10,}/) do |bin_string|
210
223
  if (m = bin_string.match(/OPENSSLDIR.*"(.*)"/))
211
224
  data['openssldir'] = m[1]
212
225
  elsif (m = bin_string.match(/OpenSSL (\d[^ -]+)/))
213
226
  data['openssl_version'] = m[1]
214
227
  end
215
- end if File.file?(ascp_file)
228
+ end if File.file?(ascp_path)
229
+ return data
216
230
  end
217
231
 
218
232
  def ascp_info
219
- data = file_paths
220
- ascp_add_pvcl(data)
221
- ascp_add_openssl(data)
222
- return data
233
+ files = file_paths
234
+ return files.merge(ascp_pvcl_info).merge(ascp_ssl_info)
223
235
  end
224
236
 
225
237
  # download aspera SDK or use local file
@@ -263,19 +275,22 @@ module Aspera
263
275
  # ensure license file are generated so that ascp invocation for version works
264
276
  path(:aspera_license)
265
277
  path(:aspera_conf)
266
- ascp_file = Products.ascp_filename
267
- ascp_path = File.join(sdk_folder, ascp_file)
268
- raise "No #{ascp_file} found in SDK archive" unless File.exist?(ascp_path)
269
- Environment.restrict_file_access(ascp_path, mode: 0o755)
270
- Environment.restrict_file_access(ascp_path.gsub('ascp', 'ascp4'), mode: 0o755)
271
- ascp_version = get_ascp_version(ascp_path)
272
- trd_path = transferd_filepath
273
- Log.log.warn{"No #{trd_path} in SDK archive"} unless File.exist?(trd_path)
274
- Environment.restrict_file_access(trd_path, mode: 0o755) if File.exist?(trd_path)
275
- transferd_version = get_exe_version(trd_path, 'version')
276
- sdk_version = transferd_version || ascp_version
277
- File.write(File.join(sdk_folder, Products::INFO_META_FILE), "<product><name>IBM Aspera SDK</name><version>#{sdk_version}</version></product>")
278
- return sdk_version
278
+ sdk_ascp_file = Products.ascp_filename
279
+ sdk_ascp_path = File.join(sdk_folder, sdk_ascp_file)
280
+ raise "No #{sdk_ascp_file} found in SDK archive" unless File.exist?(sdk_ascp_path)
281
+ EXE_FILES.each do |exe_sym|
282
+ exe_path = sdk_ascp_path.gsub('ascp', exe_sym.to_s)
283
+ Environment.restrict_file_access(exe_path, mode: 0o755) if File.exist?(exe_path)
284
+ end
285
+ sdk_ascp_version = get_ascp_version(sdk_ascp_path)
286
+ sdk_daemon_path = transferd_filepath
287
+ Log.log.warn{"No #{sdk_daemon_path} in SDK archive"} unless File.exist?(sdk_daemon_path)
288
+ Environment.restrict_file_access(sdk_daemon_path, mode: 0o755) if File.exist?(sdk_daemon_path)
289
+ transferd_version = get_exe_version(sdk_daemon_path, 'version')
290
+ sdk_name = 'IBM Aspera Transfer SDK'
291
+ sdk_version = transferd_version || sdk_ascp_version
292
+ File.write(File.join(sdk_folder, Products::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
293
+ return sdk_name, sdk_version
279
294
  end
280
295
 
281
296
  private
@@ -188,7 +188,7 @@ module Aspera
188
188
  # empty line is separator to end event information
189
189
  MGT_FRAME_SEPARATOR = ''
190
190
  # fields description for JSON generation
191
- # spellchecker: disable
191
+ # cspell: disable
192
192
  INTEGER_FIELDS = %w[Bytescont FaspFileArgIndex StartByte Rate MinRate Port Priority RateCap MinRateCap TCPPort CreatePolicy TimePolicy
193
193
  DatagramSize XoptFlags VLinkVersion PeerVLinkVersion DSPipelineDepth PeerDSPipelineDepth ReadBlockSize WriteBlockSize
194
194
  ClusterNumNodes ClusterNodeId Size Written Loss FileBytes PreTransferBytes TransferBytes PMTU Elapsedusec ArgScansAttempted
@@ -219,6 +219,9 @@ module Aspera
219
219
  end
220
220
  attr_reader :last_event
221
221
 
222
+ # process line of mgt port event
223
+ # @param line [String] line of mgt port event
224
+ # @return [Hash] event hash or nil if event is not yet complete
222
225
  def process_line(line)
223
226
  # Log.log.debug{"line=[#{line}]"}
224
227
  case line
@@ -44,7 +44,7 @@ module Aspera
44
44
  app_root: File.join('C:', 'Program Files', 'Aspera', 'Enterprise Server'),
45
45
  log_root: File.join('C:', 'Program Files', 'Aspera', 'Enterprise Server', 'var', 'log')
46
46
  }]
47
- when Aspera::Environment::OS_X then [{
47
+ when Aspera::Environment::OS_MACOS then [{
48
48
  expected: CONNECT,
49
49
  app_root: File.join(Dir.home, 'Applications', 'Aspera Connect.app'),
50
50
  log_root: File.join(Dir.home, 'Library', 'Logs', 'Aspera_Connect'),
@@ -126,7 +126,7 @@ module Aspera
126
126
 
127
127
  # filename for ascp with optional extension (Windows)
128
128
  def ascp_filename
129
- return 'ascp' + Environment.exe_extension
129
+ return "ascp#{Environment.exe_extension}"
130
130
  end
131
131
 
132
132
  # @return folder paths for specified applications
@@ -150,12 +150,6 @@ module Aspera
150
150
  end
151
151
  raise "no connect uri file found in #{folder}"
152
152
  end
153
-
154
- # @ return path to configuration file of aspera CLI
155
- # def cli_conf_file
156
- # connect = folders(PRODUCT_CLI_V1)
157
- # return File.join(connect[:app_root], BIN_SUBFOLDER, '.aspera_cli_conf')
158
- # end
159
153
  end
160
154
  end
161
155
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # cspell:ignore csvt jsonpp
3
+ # cspell:ignore csvt jsonpp stdbin
4
4
  require 'aspera/uri_reader'
5
5
  require 'aspera/environment'
6
6
  require 'aspera/log'
@@ -17,11 +17,6 @@ module Aspera
17
17
  class ExtendedValue
18
18
  include Singleton
19
19
 
20
- # special values
21
- INIT = 'INIT'
22
- ALL = 'ALL'
23
- DEF = 'DEF'
24
-
25
20
  MARKER_START = '@'
26
21
  MARKER_END = ':'
27
22
  MARKER_IN_END = '@'
@@ -74,10 +69,22 @@ module Aspera
74
69
  zlib: lambda{|v|Zlib::Inflate.inflate(v)},
75
70
  extend: lambda{|v|ExtendedValue.instance.evaluate_all(v)}
76
71
  }
72
+ @default_decoder = nil
73
+ end
74
+
75
+ # Regex to match an extended value
76
+ def handler_regex_string
77
+ "#{MARKER_START}(#{modifiers.join('|')})#{MARKER_END}"
77
78
  end
78
79
 
79
80
  public
80
81
 
82
+ def default_decoder=(value)
83
+ Log.log.debug{"setting default decoder to #{value} (#{value.class})"}
84
+ Aspera.assert(value.nil? || @handlers.key?(value))
85
+ @default_decoder = value
86
+ end
87
+
81
88
  def modifiers; @handlers.keys; end
82
89
 
83
90
  # add a new handler
@@ -87,16 +94,13 @@ module Aspera
87
94
  @handlers[name] = method
88
95
  end
89
96
 
90
- # Regex to match an extended value
91
- def ext_re
92
- "#{MARKER_START}(#{modifiers.join('|')})#{MARKER_END}"
93
- end
94
-
95
- # parse an option value if it is a String using supported extended value modifiers
97
+ # parse an string value to extended value, if it is a String using supported extended value modifiers
96
98
  # other value types are returned as is
99
+ # @param value [String] the value to parse
100
+ # @param expect [Class,Array] one or a list of expected types
97
101
  def evaluate(value)
98
102
  return value unless value.is_a?(String)
99
- regex = Regexp.new("^#{ext_re}(.*)$", Regexp::MULTILINE)
103
+ regex = Regexp.new("^#{handler_regex_string}(.*)$", Regexp::MULTILINE)
100
104
  # first determine decoders, in reversed order
101
105
  handlers_reversed = []
102
106
  while (m = value.match(regex))
@@ -113,9 +117,18 @@ module Aspera
113
117
  return value
114
118
  end
115
119
 
120
+ # parse string value as extended value
121
+ # use default decoder if none is specified
122
+ def evaluate_with_default(value)
123
+ if value.is_a?(String) && value.match(/^#{handler_regex_string}.*$/).nil? && !@default_decoder.nil?
124
+ value = [MARKER_START, @default_decoder, MARKER_END, value].join
125
+ end
126
+ return evaluate(value)
127
+ end
128
+
116
129
  # find inner extended values
117
130
  def evaluate_all(value)
118
- regex = Regexp.new("^(.*)#{ext_re}([^#{MARKER_IN_END}]*)#{MARKER_IN_END}(.*)$", Regexp::MULTILINE)
131
+ regex = Regexp.new("^(.*)#{handler_regex_string}([^#{MARKER_IN_END}]*)#{MARKER_IN_END}(.*)$", Regexp::MULTILINE)
119
132
  while (m = value.match(regex))
120
133
  sub_value = "@#{m[2]}:#{m[3]}"
121
134
  Log.log.debug{"evaluating #{sub_value}"}
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # cspell:ignore jsonpp
4
+ require 'aspera/cli/special_values'
4
5
  require 'aspera/preview/terminal'
5
6
  require 'aspera/secret_hider'
6
7
  require 'aspera/environment'
@@ -97,11 +98,12 @@ module Aspera
97
98
  CSV_RECORD_SEPARATOR = "\n"
98
99
  CSV_FIELD_SEPARATOR = ','
99
100
  # supported output formats
100
- DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv image].freeze
101
+ DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table multi csv image].freeze
101
102
  # user output levels
102
103
  DISPLAY_LEVELS = %i[info data error].freeze
104
+ FIELD_VALUE_HEADINGS = %i[key value].freeze
103
105
 
104
- private_constant :DISPLAY_FORMATS, :DISPLAY_LEVELS, :CSV_RECORD_SEPARATOR, :CSV_FIELD_SEPARATOR
106
+ private_constant :DISPLAY_FORMATS, :DISPLAY_LEVELS, :CSV_RECORD_SEPARATOR, :CSV_FIELD_SEPARATOR, :FIELD_VALUE_HEADINGS
105
107
  # prefix to display error messages in user messages (terminal)
106
108
  ERROR_FLASH = 'ERROR:'.bg_red.gray.blink.freeze
107
109
  WARNING_FLASH = 'WARNING:'.bg_brown.black.blink.freeze
@@ -110,12 +112,12 @@ module Aspera
110
112
  class << self
111
113
  def all_but(list)
112
114
  list = [list] unless list.is_a?(Array)
113
- return list.map{|i|"#{FIELDS_LESS}#{i}"}.unshift(ExtendedValue::ALL)
115
+ return list.map{|i|"#{FIELDS_LESS}#{i}"}.unshift(SpecialValues::ALL)
114
116
  end
115
117
 
116
118
  def tick(yes)
117
119
  result =
118
- if Environment.terminal_supports_unicode?
120
+ if Environment.instance.terminal_supports_unicode?
119
121
  if yes
120
122
  "\u2713"
121
123
  else
@@ -149,16 +151,9 @@ module Aspera
149
151
  end
150
152
 
151
153
  # Highlight special values
152
- def special_format(what, use_colors: $stdout.isatty)
153
- result = $stdout.isatty ? "<#{what}>" : "&lt;#{what}&gt;"
154
- if use_colors
155
- result = if %w[null empty].any?{|s|what.include?(s)}
156
- result.dim
157
- else
158
- result.reverse_color
159
- end
160
- end
161
- return result
154
+ def special_format(what)
155
+ result = "<#{what}>"
156
+ return %w[null empty].any?{|s|what.include?(s)} ? result.dim : result.reverse_color
162
157
  end
163
158
 
164
159
  # call this after REST calls if several api calls are expected
@@ -197,7 +192,7 @@ module Aspera
197
192
  end
198
193
 
199
194
  def declare_options(options)
200
- default_table_style = if Environment.terminal_supports_unicode?
195
+ default_table_style = if Environment.instance.terminal_supports_unicode?
201
196
  {border: :unicode_round}
202
197
  else
203
198
  {}
@@ -207,9 +202,9 @@ module Aspera
207
202
  options.declare(:output, 'Destination for results', types: String, handler: {o: self, m: :option_handler})
208
203
  options.declare(:display, 'Output only some information', values: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :info)
209
204
  options.declare(
210
- :fields, "Comma separated list of: fields, or #{ExtendedValue::ALL}, or #{ExtendedValue::DEF}", handler: {o: self, m: :option_handler},
205
+ :fields, "Comma separated list of: fields, or #{SpecialValues::ALL}, or #{SpecialValues::DEF}", handler: {o: self, m: :option_handler},
211
206
  types: [String, Array, Regexp, Proc],
212
- default: ExtendedValue::DEF)
207
+ default: SpecialValues::DEF)
213
208
  options.declare(:select, 'Select only some items in lists: column, value', types: [Hash, Proc], handler: {o: self, m: :option_handler})
214
209
  options.declare(:table_style, 'Table display style', types: [Hash], handler: {o: self, m: :option_handler}, default: default_table_style)
215
210
  options.declare(:flat_hash, 'Display deep values as additional keys', values: :bool, handler: {o: self, m: :option_handler}, default: true)
@@ -256,7 +251,7 @@ module Aspera
256
251
  # the requested list of fields, but if can contain special values
257
252
  request =
258
253
  case @options[:fields]
259
- # when NilClass then [ExtendedValue::DEF]
254
+ # when NilClass then [SpecialValues::DEF]
260
255
  when String then @options[:fields].split(',')
261
256
  when Array then @options[:fields]
262
257
  when Regexp then return all_fields(data).select{|i|i.match(@options[:fields])}
@@ -272,10 +267,10 @@ module Aspera
272
267
  item = item[1..-1]
273
268
  end
274
269
  case item
275
- when ExtendedValue::ALL
270
+ when SpecialValues::ALL
276
271
  # get the list of all column names used in all lines, not just first one, as all lines may have different columns
277
272
  request.unshift(*all_fields(data))
278
- when ExtendedValue::DEF
273
+ when SpecialValues::DEF
279
274
  default = all_fields(data).select{|i|default.call(i)} if default.is_a?(Proc)
280
275
  default = all_fields(data) if default.nil?
281
276
  request.unshift(*default)
@@ -293,11 +288,11 @@ module Aspera
293
288
  # filter the list of items on the fields option
294
289
  def filter_list_on_fields(data)
295
290
  # by default, keep all data intact
296
- return data if @options[:fields].eql?(ExtendedValue::DEF) && @options[:select].nil?
291
+ return data if @options[:fields].eql?(SpecialValues::DEF) && @options[:select].nil?
297
292
  Aspera.assert_type(data, Array){'Filtering fields or select requires result is an Array of Hash'}
298
293
  Aspera.assert(data.all?(Hash)){'Filtering fields or select requires result is an Array of Hash'}
299
294
  filter_columns_on_select(data)
300
- return data if @options[:fields].eql?(ExtendedValue::DEF)
295
+ return data if @options[:fields].eql?(SpecialValues::DEF)
301
296
  selected_fields = compute_fields(data, @options[:fields])
302
297
  return data.map{|i|i[selected_fields.first]} if selected_fields.length == 1
303
298
  return data.map{|i|i.select{|k, _|selected_fields.include?(k)}}
@@ -325,16 +320,16 @@ module Aspera
325
320
  display_message(:info, special_format('empty')) if @options[:format].eql?(:table)
326
321
  return
327
322
  end
323
+ # if table has only one element, and only one field, display the value
328
324
  if object_array.length == 1 && fields.length == 1
329
325
  display_message(:data, object_array.first[fields.first])
330
326
  return
331
327
  end
332
328
  # Special case if only one row (it could be object_list or single_object)
333
329
  if @options[:transpose_single] && object_array.length == 1
334
- new_columns = %i[key value]
335
330
  single = object_array.first
336
- object_array = fields.map { |i| new_columns.zip([i, single[i]]).to_h }
337
- fields = new_columns
331
+ object_array = fields.map { |i| FIELD_VALUE_HEADINGS.zip([i, single[i]]).to_h }
332
+ fields = FIELD_VALUE_HEADINGS
338
333
  end
339
334
  Log.log.debug{Log.dump(:object_array, object_array)}
340
335
  # convert data to string, and keep only display fields
@@ -347,8 +342,18 @@ module Aspera
347
342
  headings: fields,
348
343
  rows: final_table_rows,
349
344
  style: @options[:table_style]&.symbolize_keys))
345
+ when :multi
346
+ final_table_rows.each do |row|
347
+ Log.log.debug{Log.dump(:row, row)}
348
+ display_message(:data, Terminal::Table.new(
349
+ headings: FIELD_VALUE_HEADINGS,
350
+ rows: fields.zip(row),
351
+ style: @options[:table_style]&.symbolize_keys))
352
+ end
350
353
  when :csv
351
354
  display_message(:data, final_table_rows.map{|t| t.join(CSV_FIELD_SEPARATOR)}.join(CSV_RECORD_SEPARATOR))
355
+ else
356
+ raise "not expected: #{@options[:format]}"
352
357
  end
353
358
  end
354
359
 
@@ -358,8 +363,8 @@ module Aspera
358
363
  raise URI::InvalidURIError, 'not uri' if !(blob =~ /\A#{URI::DEFAULT_PARSER.make_regexp}\z/)
359
364
  # it's a url
360
365
  url = blob
361
- unless OpenApplication.instance.url_method.eql?(:text)
362
- OpenApplication.instance.uri(url)
366
+ unless Environment.instance.url_method.eql?(:text)
367
+ Environment.instance.open_uri(url)
363
368
  return ''
364
369
  end
365
370
  # remote_image = Rest.new(base_url: url).read('')
@@ -419,7 +424,7 @@ module Aspera
419
424
  end
420
425
  raise "not url: #{url.class} #{url}" unless url.is_a?(String)
421
426
  display_message(:data, status_image(url))
422
- when :table, :csv
427
+ when :table, :csv, :multi
423
428
  case type
424
429
  when :config_over
425
430
  display_table(Flattener.new(self).config_over(data), CONF_OVERVIEW_KEYS)
@@ -451,6 +456,8 @@ module Aspera
451
456
  else
452
457
  raise "unknown data type: #{type}"
453
458
  end
459
+ else
460
+ raise "not expected: #{@options[:format]}"
454
461
  end
455
462
  end
456
463
  end
@@ -93,11 +93,11 @@ module Aspera
93
93
  @plug_init[:only_manual] = false
94
94
  # create formatter, in case there is an exception, it is used to display.
95
95
  @plug_init[:formatter] = Formatter.new
96
- @plug_init[:options] = Manager.new(PROGRAM_NAME)
97
- # give command line arguments to option manager
98
- options.parse_command_line(@argv)
96
+ # create command line manager with arguments
97
+ @plug_init[:options] = Manager.new(PROGRAM_NAME, @argv)
99
98
  # formatter adds options
100
- formatter.declare_options(options)
99
+ @plug_init[:formatter].declare_options(options)
100
+ ExtendedValue.instance.default_decoder = options.get_option(:struct_parser)
101
101
  # compare $0 with expected name
102
102
  current_prog_name = File.basename($PROGRAM_NAME)
103
103
  formatter.display_message(
@@ -162,9 +162,9 @@ module Aspera
162
162
  options.declare(:version, 'Display version', values: :none, short: 'v') { formatter.display_message(:data, Cli::VERSION); Process.exit(0) } # rubocop:disable Style/Semicolon, Layout/LineLength
163
163
  options.declare(
164
164
  :ui, 'Method to start browser',
165
- values: OpenApplication::USER_INTERFACES,
166
- handler: {o: OpenApplication.instance, m: :url_method},
167
- default: OpenApplication.default_gui_mode)
165
+ values: Environment::USER_INTERFACES,
166
+ handler: {o: Environment.instance, m: :url_method},
167
+ default: Environment.default_gui_mode)
168
168
  options.declare(:log_level, 'Log level', values: Log.levels, handler: {o: Log.instance, m: :level})
169
169
  options.declare(:logger, 'Logging method', values: Log::LOG_TYPES, handler: {o: Log.instance, m: :logger_type})
170
170
  options.declare(:lock_port, 'Prevent dual execution of a command, e.g. in cron', coerce: Integer, types: Integer)
@@ -189,7 +189,7 @@ module Aspera
189
189
  end
190
190
 
191
191
  def generate_bash_completion
192
- if options.get_next_argument('', expected: :multiple, mandatory: false).nil?
192
+ if options.get_next_argument('', multiple: true, mandatory: false).nil?
193
193
  PluginFactory.instance.plugin_list.each{|p|puts p}
194
194
  else
195
195
  Log.log.warn('only first level completion so far')
@@ -197,11 +197,11 @@ module Aspera
197
197
  Process.exit(0)
198
198
  end
199
199
 
200
- def exit_with_usage(all_plugins)
201
- Log.log.debug('exit_with_usage'.bg_red)
200
+ def exit_with_usage(include_all_plugins)
201
+ Log.log.debug{"exit_with_usage(#{include_all_plugins})".bg_red}
202
202
  # display main plugin options
203
203
  formatter.display_message(:error, options.parser)
204
- if all_plugins
204
+ if include_all_plugins
205
205
  # list plugins that have a "require" field, i.e. all but main plugin
206
206
  PluginFactory.instance.plugin_list.each do |plugin_name_sym|
207
207
  next if plugin_name_sym.eql?(COMMAND_CONFIG)