Class: Aspera::Cli::Formatter

Inherits:
Object
  • Object
show all
Defined in:
lib/aspera/cli/formatter.rb

Overview

Take care of CLI output on terminal

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeFormatter

initialize the formatter



65
66
67
68
# File 'lib/aspera/cli/formatter.rb', line 65

def initialize
  @options = {}
  @spinner = nil
end

Class Method Details

.all_but(list) ⇒ Object



59
60
61
# File 'lib/aspera/cli/formatter.rb', line 59

def all_but(list)
  Array(list).map{ |i| "#{FIELDS_LESS}#{i}"}.unshift(SpecialValues::ALL)
end

.replace_specific_for_terminal(input_hash, string_list_separator) ⇒ Object

Replace special values with a readable version on terminal



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/aspera/cli/formatter.rb', line 26

def replace_specific_for_terminal(input_hash, string_list_separator)
  hash_to_process = [input_hash]
  until hash_to_process.empty?
    current = hash_to_process.pop
    current.each do |key, value|
      case value
      when NilClass
        current[key] = TerminalFormatter.special_format('null')
      when String
        current[key] = TerminalFormatter.special_format('empty string') if value.empty?
      when Proc
        current[key] = TerminalFormatter.special_format('lambda')
      when Array
        if value.empty?
          current[key] = TerminalFormatter.special_format('empty list')
        elsif value.all?(String)
          current[key] = value.join(string_list_separator)
        else
          value.each do |item|
            hash_to_process.push(item) if item.is_a?(Hash)
          end
        end
      when Hash
        if value.empty?
          current[key] = TerminalFormatter.special_format('empty dict')
        else
          hash_to_process.push(value)
        end
      end
    end
  end
end

Instance Method Details

#all_fields(data) ⇒ Object

Returns all fields of all objects in list of objects.

Returns:

  • all fields of all objects in list of objects



246
247
248
# File 'lib/aspera/cli/formatter.rb', line 246

def all_fields(data)
  data.each_with_object({}){ |v, m| v.each_key{ |c| m[c] = true}}.keys
end

#compute_fields(data, default) ⇒ Object

Returns the list of fields to display.

Parameters:

  • data (Array<Hash>)

    data to display

  • default (Array<String>, Proc)

    list of fields to display by default (may contain special values)

Returns:

  • the list of fields to display



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/aspera/cli/formatter.rb', line 253

def compute_fields(data, default)
  Log.log.debug{"compute_fields: data:#{data.class} default:#{default.class} #{default}"}
  Log.dump(:compute_fields_default, default, level: :trace1)
  # the requested list of fields, but if can contain special values
  request =
    case @options[:fields]
    # when NilClass then [SpecialValues::DEF]
    when String then @options[:fields].split(',')
    when Array then @options[:fields]
    when Regexp then return all_fields(data).select{ |i| i.match(@options[:fields])}
    when Proc then return all_fields(data).select{ |i| @options[:fields].call(i)}
    else Aspera.error_unexpected_value(@options[:fields])
    end
  Aspera.assert_array_all(request, String)
  result = []
  until request.empty?
    item = request.shift
    removal = false
    if item[0].eql?(FIELDS_LESS)
      removal = true
      item = item.delete_prefix(FIELDS_LESS)
    end
    case item
    when SpecialValues::ALL
      # get the list of all column names used in all lines, not just first one, as all lines may have different columns
      request.unshift(*all_fields(data))
    when SpecialValues::DEF
      default = all_fields(data).select{ |i| default.call(i)} if default.is_a?(Proc)
      default = all_fields(data) if default.nil?
      request.unshift(*default)
    else
      if removal
        result = result.reject{ |i| i.eql?(item)}
      else
        result.push(item)
      end
    end
  end
  Log.dump(:compute_fields, result, level: :trace1)
  return result
end

#declare_options(options) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/aspera/cli/formatter.rb', line 111

def declare_options(options)
  options.declare(:display, 'Output only some information', allowed: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :data)
  options.declare(:format, 'Output format', allowed: DISPLAY_FORMATS, handler: {o: self, m: :option_handler}, default: :table)
  options.declare(:output, 'Destination for results', handler: {o: self, m: :option_handler})
  options.declare(
    :fields, "Comma separated list of: fields, or #{SpecialValues::ALL}, or #{SpecialValues::DEF}", handler: {o: self, m: :option_handler},
    allowed: [String, Array, Regexp, Proc],
    default: SpecialValues::DEF
  )
  options.declare(:select, 'Select only some items in lists: column, value', allowed: [Hash, Proc], handler: {o: self, m: :option_handler})
  options.declare(:table_style, '(Table) Display style', allowed: [Hash], handler: {o: self, m: :option_handler}, default: {})
  options.declare(:flat_hash, '(Table) Display deep values as additional keys', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_handler}, default: true)
  options.declare(
    :multi_single, '(Table) Control how object list is displayed as single table, or multiple objects', allowed: %i[no yes single],
    handler: {o: self, m: :option_handler}, default: :no
  )
  options.declare(:show_secrets, 'Show secrets on command output', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :option_handler}, default: false)
  options.declare(:image, 'Options for image display', allowed: Hash, handler: {o: self, m: :option_handler}, default: {})
end

#display_item_count(count, total) ⇒ Object

Display item count if total is provided



71
72
73
74
75
76
77
78
79
# File 'lib/aspera/cli/formatter.rb', line 71

def display_item_count(count, total)
  return if total.nil?
  count = count.to_i
  total = total.to_i
  return if total.eql?(0) && count.eql?(0)
  count_msg = "Items: #{count}/#{total}"
  count_msg = count_msg.bg_red unless count.eql?(total)
  display_status(count_msg)
end

#display_message(message_level, message, hide_secrets: true) ⇒ void

Note:

Message display behavior depends on the message_level:

  • :data messages are displayed unless display level is :error
  • :info messages are only displayed when display level is :info
  • :error messages are always displayed on stderr

This method returns an undefined value.

Main output method for displaying messages to the user This method is used by Result classes to output formatted data

Parameters:

  • message_level (Symbol)

    The level of the message - must be one of: :data, :info, :error

  • message (String)

    The message to display

  • hide_secrets (Boolean) (defaults to: true)

    Whether to hide secrets in the message (default: true)



171
172
173
174
175
176
177
178
179
# File 'lib/aspera/cli/formatter.rb', line 171

def display_message(message_level, message, hide_secrets: true)
  message = SecretHider.instance.hide_secrets_in_string(message) if hide_secrets && message.is_a?(String) && hide_secrets?
  case message_level
  when :data then $stdout.puts(message) unless @options[:display].eql?(:error)
  when :info then $stdout.puts(message) if @options[:display].eql?(:info)
  when :error then $stderr.puts(message) # rubocop:disable Style/StderrPuts
  else Aspera.error_unexpected_value(message_level)
  end
end

#display_results(result) ⇒ Object

Display results using the Visitor pattern Each Result subclass knows how to format itself by calling the appropriate formatter method, eliminating the need for type checking in the formatter.

Parameters:

  • result (Result)

    Result object to display



199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/aspera/cli/formatter.rb', line 199

def display_results(result)
  require 'aspera/cli/result'
  Aspera.assert_type(result, Cli::Result){'result must be a Result object'}

  Log.log.debug{"display_results: result class=#{result.class.name}"}
  Log.dump(:data, result.data, level: :trace1)
  Log.dump(:fields, result.fields, level: :trace1)

  # Hide secrets in data
  hide_secrets(result.data)

  # Use the Visitor pattern: delegate formatting to the result object
  result.format(self)
end

#display_status(status, **kwargs) ⇒ Object



181
182
183
# File 'lib/aspera/cli/formatter.rb', line 181

def display_status(status, **kwargs)
  display_message(:info, status, **kwargs)
end

#display_table(object_array, fields, single: false) ⇒ Object

Displays a list of objects

Parameters:

  • object_array (Array<Hash>)

    Array of hash

  • fields (Array<String>)

    List of column names

  • single (Boolean) (defaults to: false)

    Contains a single object to display



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/aspera/cli/formatter.rb', line 328

def display_table(object_array, fields, single: false)
  Aspera.assert_array_all(object_array, Hash)
  Aspera.assert_array_all(fields, String)
  if object_array.empty?
    # no  display for csv
    display_message(:info, TerminalFormatter.special_format('empty')) if @options[:format].eql?(:table)
    return
  end
  filter_columns_on_select(object_array)
  format_style = @options[:table_style].symbolize_keys
  string_list_separator = format_style.delete(:str_lst_sep) || STR_LST_SEP_VERT
  # convert data to string, and keep only display fields
  object_array.each{ |i| self.class.replace_specific_for_terminal(i, string_list_separator)}
  # if table has only one element, and only one field, display the value
  if object_array.length == 1 && fields.length == 1
    Log.log.debug("single element, field: #{fields.first}")
    data = object_array.first[fields.first]
    unless data.is_a?(Array) && data.all?(Hash)
      display_message(:data, data)
      return
    end
    object_array = data
    fields = all_fields(object_array)
    single = false
  end
  Log.dump(:object_array, object_array)
  Log.dump(:fields, fields)
  # convert data to string, and keep only display fields
  final_table_rows = object_array.map{ |r| fields.map{ |c| r[c].to_s}}
  # remove empty rows
  final_table_rows.select!{ |i| !(i.is_a?(Hash) && i.empty?)}
  # here : fields : list of column names
  case @options[:format]
  when :table
    format_style[:border] = :unicode_round if Environment.terminal_supports_unicode?
    if single || @options[:multi_single].eql?(:yes) ||
        (@options[:multi_single].eql?(:single) && final_table_rows.length.eql?(1))
      # display multiple objects as multiple transposed tables
      final_table_rows.each do |row|
        display_message(:data, Terminal::Table.new(
          headings:  SINGLE_OBJECT_COLUMN_NAMES,
          rows:      fields.zip(row),
          style:     format_style
        ))
      end
    else
      # display the table as single table
      display_message(:data, Terminal::Table.new(
        headings:  fields,
        rows:      final_table_rows,
        style:     format_style
      ))
    end
  when :csv
    add_headers = format_style.delete(:headers)
    output = CSV.generate(**format_style) do |csv|
      csv << fields if add_headers
      final_table_rows.each do |row|
        csv << row
      end
    end
    display_message(:data, output)
  else
    raise "not expected: #{@options[:format]}"
  end
  nil
end

#filter_columns_on_select(data) ⇒ Object

filter the list of items on the select option

Parameters:

  • data (Array<Hash>)

    list of items



311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/aspera/cli/formatter.rb', line 311

def filter_columns_on_select(data)
  case @options[:select]
  when Proc
    begin
      data.select!{ |i| @options[:select].call(i)}
    rescue Exception => e # rubocop:disable Lint/RescueException
      raise Cli::BadArgument, "Error in user-provided ruby lambda code during select: #{e.message}"
    end
  when Hash
    @options[:select].each{ |k, v| data.select!{ |i| i[k].eql?(v)}}
  end
end

#filter_list_on_fields(data) ⇒ Object

filter the list of items on the fields option



296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/aspera/cli/formatter.rb', line 296

def filter_list_on_fields(data)
  # no filter for single element
  return data unless data.is_a?(Array)
  # by default, keep all data intact
  return data if @options[:fields].eql?(SpecialValues::DEF) && @options[:select].nil?
  Aspera.assert_array_all(data, Hash){'filter or select'}
  filter_columns_on_select(data)
  return data if @options[:fields].eql?(SpecialValues::DEF)
  selected_fields = compute_fields(data, @options[:fields])
  return data.map{ |i| i[selected_fields.first]} if selected_fields.length == 1
  return data.map{ |i| i.slice(*selected_fields)}
end

#flat_hash?Boolean

Check if flat_hash option is enabled

Returns:

  • (Boolean)


236
237
238
# File 'lib/aspera/cli/formatter.rb', line 236

def flat_hash?
  @options[:flat_hash]
end

#format_typeObject

Get the current format type



216
217
218
# File 'lib/aspera/cli/formatter.rb', line 216

def format_type
  @options[:format]
end

#hide_secrets(data) ⇒ Object

Hides secrets in Hash or Array



191
192
193
# File 'lib/aspera/cli/formatter.rb', line 191

def hide_secrets(data)
  SecretHider.instance.deep_remove_secret(data) if hide_secrets?
end

#hide_secrets?Boolean

Check if secrets should be hidden

Returns:

  • (Boolean)


186
187
188
# File 'lib/aspera/cli/formatter.rb', line 186

def hide_secrets?
  !@options[:show_secrets] && !@options[:display].eql?(:data)
end

#hide_secrets_in_string(string) ⇒ Object

Hide secrets in a string



226
227
228
# File 'lib/aspera/cli/formatter.rb', line 226

def hide_secrets_in_string(string)
  SecretHider.instance.hide_secrets_in_string(string)
end

#image_optionsObject

Get image options



241
242
243
# File 'lib/aspera/cli/formatter.rb', line 241

def image_options
  @options[:image].symbolize_keys
end

#long_operation(title = nil, action: :spin) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/aspera/cli/formatter.rb', line 81

def long_operation(title = nil, action: :spin)
  return unless Environment.terminal?
  return if %i[error data].include?(@options[:display])

  # Handle the "delayed start" state
  return @spinner = :starting if action == :spin && @spinner.nil?

  # Cleanup if we try to stop a spinner that never actually started
  @spinner = nil if action != :spin && @spinner == :starting
  return if @spinner.nil?

  # Initialize the real TTY object if it's currently just the :starting symbol
  if @spinner == :starting
    @spinner = TTY::Spinner.new('[:spinner] :title', format: :classic)
    @spinner.update(title: '')
    @spinner.start
  end

  @spinner.update(title: title) if title

  case action
  when :spin
    @spinner.spin
  when :success, :fail
    action == :success ? @spinner.success : @spinner.error
    @spinner.stop
    @spinner = nil
  end
end

#option_handler(option_symbol, operation, value = nil) ⇒ Object

method accessed by option manager options are: format, output, display, fields, select, table_style, flat_hash, multi_single



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/aspera/cli/formatter.rb', line 133

def option_handler(option_symbol, operation, value = nil)
  Aspera.assert_values(operation, %i[set get])
  case operation
  when :set
    @options[option_symbol] = value
    # special handling of some options
    case option_symbol
    when :format
      @options[:display] = value.eql?(:table) ? :info : :data
    when :output
      $stdout = if value.eql?('-')
        STDOUT # rubocop:disable Style/GlobalStdStream
      else
        File.open(value, 'w')
      end
    when :image
      # get list if key arguments of method
      allowed_options = Preview::Terminal.method(:build).parameters.select{ |i| i[0].eql?(:key)}.map{ |i| i[1]}
      # check that only supported options are given
      unknown_options = value.keys.map(&:to_sym) - allowed_options
      raise "Invalid parameter(s) for option image: #{unknown_options.join(', ')}, use #{allowed_options.join(', ')}" unless unknown_options.empty?
    end
  when :get then return @options[option_symbol]
  else Aspera.error_unreachable_line
  end
  nil
end

#set_format_type(format) ⇒ Object

Set the format type (used by Image result)



221
222
223
# File 'lib/aspera/cli/formatter.rb', line 221

def set_format_type(format)
  @options[:format] = format
end

#special_format(text) ⇒ Object

Get special format string



231
232
233
# File 'lib/aspera/cli/formatter.rb', line 231

def special_format(text)
  TerminalFormatter.special_format(text)
end