Module: GreenHat::ShellHelper

Defined in:
lib/greenhat/shell/filter_help.rb,
lib/greenhat/shell/log.rb,
lib/greenhat/shell/list.rb,
lib/greenhat/shell/page.rb,
lib/greenhat/shell/faststats.rb,
lib/greenhat/shell/color_string.rb,
lib/greenhat/shell/shell_helper.rb

Overview

Common Helpers rubocop:disable Metrics/ModuleLength

Defined Under Namespace

Modules: Faststats, Filter, List, Log, Page, StringColor

Class Method Summary collapse

Class Method Details

.common_optsObject

General Helper for ‘show`



641
642
643
644
645
646
647
648
649
650
651
# File 'lib/greenhat/shell/shell_helper.rb', line 641

def self.common_opts
  puts 'Common Options'.pastel(:blue)
  puts '  --raw'.pastel(:green)
  puts '    Do not use less/paging'
  puts

  puts '  --archive'.pastel(:green)
  puts '    Limit to specific archive name (inclusive). Matching SOS tar.gz name'
  puts '    Ex: --archive=dev-gitlab_20210622154626, --archive=202106,202107'
  puts
end

.entry_show(flags, entry, key = nil) ⇒ Object

Entry Shower / Top Level



69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/greenhat/shell/shell_helper.rb', line 69

def self.entry_show(flags, entry, key = nil)
  LogBot.debug('Entry Show', entry.class) if ENV['DEBUG']
  case entry
  when Hash then render_table(entry, flags)
  when Float, Integer, Array
    format_table_entry(flags, entry, key)
  # Ignore Special Formatting for Strings / Usually already formatted
  when String
    entry
  else
    LogBot.warn('Shell Show', "Unknown #{entry.class}")
    nil
  end
end

.entry_truncate(entry, truncate) ⇒ Object



317
318
319
320
321
322
323
324
325
326
# File 'lib/greenhat/shell/shell_helper.rb', line 317

def self.entry_truncate(entry, truncate)
  # Ignore if Truncation Off
  return entry if truncate.zero?

  # Only truncate large strings
  return entry unless entry.instance_of?(String) && entry.size > truncate

  # Include  '...' to indicate truncation
  "#{entry.to_s[0..truncate]} #{'...'.pastel(:bright_blue)}"
end

.field_table(list, columns = 4) ⇒ Object



479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# File 'lib/greenhat/shell/shell_helper.rb', line 479

def self.field_table(list, columns = 4)
  return nil if list.size.zero?

  # Keep Alphabetical Sort
  groups = list.each_slice((list.size / columns.to_f).round).to_a

  table = TTY::Table.new do |t|
    loop do
      break if groups.all?(&:empty?)

      t << groups.map(&:shift)
    end
  end

  table.render(:unicode, padding: [0, 1, 0, 1])
end

.fields_print(results) ⇒ Object

Total Count Helper



471
472
473
474
475
476
477
# File 'lib/greenhat/shell/shell_helper.rb', line 471

def self.fields_print(results)
  results.each do |k, v|
    puts k
    puts field_table(v.map(&:keys).flatten.uniq.sort)
    puts
  end
end

.file_output(files, flags = {}) ⇒ Object

Use File Process for Output



6
7
8
9
10
11
12
13
14
15
16
# File 'lib/greenhat/shell/shell_helper.rb', line 6

def self.file_output(files, flags = {})
  results = file_process(files) do |file|
    [
      file.friendly_name,
      file.output(false),
      "\n"
    ]
  end

  ShellHelper.show(results.flatten, flags)
end

.file_process(files, &block) ⇒ Object



18
19
20
21
22
23
24
# File 'lib/greenhat/shell/shell_helper.rb', line 18

def self.file_process(files, &block)
  files.map do |file|
    next if file.output(false).empty?

    block.call(file)
  end.flatten
end

.files(file_list, base_list = nil, flags = {}) ⇒ Object

Unified Files Interface



497
498
499
500
501
502
503
504
505
# File 'lib/greenhat/shell/shell_helper.rb', line 497

def self.files(file_list, base_list = nil, flags = {})
  base_list ||= Thing.all

  # Prepare Log List
  file_list = prepare_list(file_list, base_list)

  # Convert to Things
  find_things(file_list, flags)
end

.filter(data, flags = {}, args = {}) ⇒ Object

Filter Logic TODO: Simplify rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/greenhat/shell/shell_helper.rb', line 186

def self.filter(data, flags = {}, args = {})
  # results = data.clone.flatten.compact

  # Experimenting with deep clone
  results = Marshal.load(Marshal.dump(data))
  results.select! do |row|
    args.send(flags.logic) do |arg|
      filter_row_key(row, arg, flags)
    end
  end

  # Ensure presecense of a specific field
  results = filter_exists(results, flags[:exists]) if flags.key?(:exists)

  # Time Zone
  results = filter_modify_timezone(results, flags[:time_zone]) if flags.key?(:time_zone)

  # Time Filtering
  results = filter_time(results, flags) if flags.key?(:start) || flags.key?(:end)

  # Strip Results if Slice is defined
  results = filter_slice(results, flags[:slice]) if flags.key?(:slice)

  # Strip Results if Except is defined
  results = filter_except(results, flags[:except]) if flags.key?(:except)

  # Remove Blank from either slice or except
  results.reject!(&:empty?)

  # Sort
  results.sort_by! { |x| x.slice(*flags[:sort]).values } if flags.key?(:sort)

  # JSON Formatting
  results = results.map { |x| Oj.dump(x) } if flags.key?(:json)

  # Show Unique Only
  results = filter_uniq(results, flags[:uniq]) if flags.key?(:uniq)

  # Reverse
  results.reverse! if flags[:reverse]

  # Count occurrences / Skip Results
  return filter_stats(results, flags) if flags.key?(:stats)

  # Limit before Pluck / Flattening
  results = filter_limit(results, flags[:limit]) if flags.key?(:limit)

  # Pluck
  results = filter_pluck(results, flags[:pluck]) if flags.key?(:pluck)

  results
end

.filter_and(data, params = {}) ⇒ Object

TODO: Needed?



624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
# File 'lib/greenhat/shell/shell_helper.rb', line 624

def self.filter_and(data, params = {})
  result = data.clone.flatten.compact
  params.each do |k, v|
    result.select! do |row|
      if row.key? k.to_sym
        row[k.to_sym].include? v
      else
        false
      end
    end
    next
  end

  result
end

.filter_count_occurrences(results, field) ⇒ Object

Helper to Count occurrences



408
409
410
411
412
413
414
415
416
417
418
# File 'lib/greenhat/shell/shell_helper.rb', line 408

def self.filter_count_occurrences(results, field)
  results.each_with_object(Hash.new(0)) do |entry, counts|
    if entry.key? field
      counts[entry[field]] += 1
    else
      counts['None'.pastel(:bright_black)] += 1
    end

    counts
  end
end

.filter_empty_arg(arg) ⇒ Object



420
421
422
423
424
425
426
# File 'lib/greenhat/shell/shell_helper.rb', line 420

def self.filter_empty_arg(arg)
  puts [
    'Ignoring'.pastel(:bright_yellow),
    "--#{arg}".pastel(:cyan),
    'it requires an argument'.pastel(:red)
  ].join(' ')
end

.filter_except(results, except) ⇒ Object

rubocop:enable Metrics/MethodLength



297
298
299
300
301
302
303
304
305
# File 'lib/greenhat/shell/shell_helper.rb', line 297

def self.filter_except(results, except)
  # Avoid Empty Results
  if except.empty?
    filter_empty_arg('except')
    return results
  end

  results.map { |row| row.except(*except) }
end

.filter_exists(results, exists) ⇒ Object



307
308
309
310
311
312
313
314
315
# File 'lib/greenhat/shell/shell_helper.rb', line 307

def self.filter_exists(results, exists)
  # Avoid Empty Results
  if exists.empty?
    filter_empty_arg('exists')
    return results
  end

  results.select { |row| (exists - row.keys).empty? }
end

.filter_limit(results, limit) ⇒ Object

Limit / Ensure Exists and Valid Number



241
242
243
244
245
# File 'lib/greenhat/shell/shell_helper.rb', line 241

def self.filter_limit(results, limit)
  return results unless limit.integer? && limit.positive?

  results.shift limit
end

.filter_modify_timezone(results, time_zone) ⇒ Object



247
248
249
250
251
252
253
254
255
# File 'lib/greenhat/shell/shell_helper.rb', line 247

def self.filter_modify_timezone(results, time_zone)
  results.each do |x|
    next unless x.key? :time

    x[:time] = x[:time].in_time_zone time_zone
  end

  results
end

.filter_pluck(results, pluck) ⇒ Object



338
339
340
341
342
343
344
345
346
# File 'lib/greenhat/shell/shell_helper.rb', line 338

def self.filter_pluck(results, pluck)
  # Avoid Empty Results
  if pluck.empty?
    filter_empty_arg('pluck')
    return results
  end

  results.map { |x| x.slice(*pluck).values }.flatten
end

.filter_row_entry(entry, arg, flags) ⇒ Object

Field Partial / Case / Exact search



450
451
452
453
454
455
456
457
458
459
# File 'lib/greenhat/shell/shell_helper.rb', line 450

def self.filter_row_entry(entry, arg, flags)
  # Exact Matching / Unless doing full text search
  return entry.to_s == arg.value.to_s if flags.key?(:exact) && arg.field != :text

  if flags.key?(:case)
    entry.include? arg.value.to_s
  else
    entry.downcase.include? arg.value.to_s.downcase
  end
end

.filter_row_key(row, arg, flags) ⇒ Object

Break out filter row logic into separate method



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'lib/greenhat/shell/shell_helper.rb', line 430

def self.filter_row_key(row, arg, flags)
  # Ignore Other Logic if Field isn't even included / Full Text Searching
  return false unless row.key?(arg[:field]) || arg[:field] == :text

  # Sensitivity Check / Check for Match / Full Text Searching
  included = if arg[:field] == :text
               filter_row_entry(row.to_s, arg, flags)
             else
               filter_row_entry(row[arg.field].to_s, arg, flags)
             end

  # Pivot of off include vs exclude
  if arg.bang
    !included
  else
    included
  end
end

.filter_slice(results, slice) ⇒ Object



328
329
330
331
332
333
334
335
336
# File 'lib/greenhat/shell/shell_helper.rb', line 328

def self.filter_slice(results, slice)
  # Avoid Empty Results
  if slice.empty?
    filter_empty_arg('slice')
    return results
  end

  results.map { |row| row.slice(*slice) }
end

.filter_start(files, flags, args) ⇒ Object

Main Entry Point for Filtering



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/greenhat/shell/shell_helper.rb', line 154

def self.filter_start(files, flags, args)
  # Convert to Things
  logs = ShellHelper.find_things(files, flags).select(&:processed?)

  # Ignore Archive/Host Dividers
  if flags[:combine]
    results = logs.reject(&:blank?).map(&:data).flatten.compact
    ShellHelper.filter(results, flags, args)
  else
    # Iterate and Preserve Archive/Host Index
    logs.each_with_object({}) do |log, obj|
      # Ignore Empty Results / No Thing
      next if log&.blank?

      # Include Total Count in Name
      results = ShellHelper.filter(log.data, flags, args)
      title = [
        log.friendly_name,
        " #{results.count}".pastel(:bright_black)
      ]

      # Save unless empty
      obj[title.join] = results unless results.count.zero?

      obj
    end
  end
end

.filter_stats(results, flags) ⇒ Object



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
395
396
397
398
399
400
# File 'lib/greenhat/shell/shell_helper.rb', line 360

def self.filter_stats(results, flags)
  stats = flags[:stats]

  # Avoid Empty Results
  if stats.empty?
    filter_empty_arg('stats')
    return results
  end

  # Loop through Stats, Separate Hash/Tables
  stats.map do |field|
    occurrences = filter_count_occurrences(results, field)

    # Total Occurences
    total = occurrences.values.sum

    # Percs
    occurrences.transform_values! do |count|
      [
        count,
        " #{percent(count, total)}%".pastel(:bright_black)
      ]
    end

    # Sort by total occurances / New Variable for Total
    output = occurrences.sort_by(&:last).to_h.transform_values!(&:join).to_a

    # Append Header / Total with field name
    output.unshift([field.to_s.pastel(:bright_black), total])

    # Use Truncate For Long Keys
    if flags[:truncate]
      output.map! do |key, value|
        [key.to_s[0..flags[:truncate]], value]
      end
    end

    # Format
    output.to_h
  end
end

.filter_time(results, flags) ⇒ Object

Filter Start and End Times rubocop:disable Metrics/MethodLength TODO: This is a bit icky, simplify/dry



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
294
# File 'lib/greenhat/shell/shell_helper.rb', line 260

def self.filter_time(results, flags)
  if flags.key?(:start)
    begin
      time_start = Time.parse(flags[:start])

      results.select! do |x|
        if x.time
          time_start < x.time
        else
          true
        end
      end
    rescue StandardError
      puts 'Unable to Process Start Time Filter'.pastel(:red)
    end
  end

  if flags.key?(:end)
    begin
      time_start = Time.parse(flags[:end])

      results.select! do |x|
        if x.time
          time_start > x.time
        else
          true
        end
      end
    rescue StandardError
      puts 'Unable to Process End Time Filter'.pastel(:red)
    end
  end

  results
end

.filter_uniq(results, unique) ⇒ Object



348
349
350
351
352
353
354
355
356
357
358
# File 'lib/greenhat/shell/shell_helper.rb', line 348

def self.filter_uniq(results, unique)
  # Avoid Empty Results
  if unique.empty?
    filter_empty_arg('uniq')
    return results
  end

  unique.map do |field|
    results.uniq { |x| x[field] }
  end.inject(:&)
end

.find_things(files, flags = {}) ⇒ Object

Shortcut find things



528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/greenhat/shell/shell_helper.rb', line 528

def self.find_things(files, flags = {})
  things = files.uniq.flat_map do |file|
    # If Thing, Return Thing
    return file if file.instance_of?(Thing)

    if flags.fuzzy_file_match
      Thing.all.select { |x| x.name.include? file }
    else
      Thing.where name: file
    end
  end.uniq

  # Host / Archive
  things.select! { |x| x.archive? flags.archive } if flags.key?(:archive)

  things
end

.format_table_entry(flags, entry, key = nil) ⇒ Object

Format Table Entries



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
110
111
112
113
114
115
116
# File 'lib/greenhat/shell/shell_helper.rb', line 85

def self.format_table_entry(flags, entry, key = nil)
  formatted_entry = case entry
                    # Rounding
                    when Float, Integer || entry.numeric?
                      flags.key?(:round) ? entry.to_f.round(flags.round).ai : entry.ai

                      # General Inspecting
                    when Hash then entry.ai(ruby19_syntax: true)

                    # Arrays often contain Hashes. Dangerous Recursive?
                    when Array
                      entry.map { |x| format_table_entry(flags, x) }.join("\n")

                    when Time
                      entry.to_s.pastel(:bright_white)

                    # Default String Formatting
                    else
                      StringColor.do(key, entry)
                    end

  if flags[:truncate]
    entry_truncate(formatted_entry, flags[:truncate])
  else
    formatted_entry
  end
rescue StandardError => e
  if ENV['DEBUG']
    LogBot.warn('Table Format Entry', message: e.message)
    ap e.backtrace
  end
end

.human_size_to_number(string) ⇒ Object



604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
# File 'lib/greenhat/shell/shell_helper.rb', line 604

def self.human_size_to_number(string)
  size, unit = string.scan(/(\d*\.?\d+)\s?(Bytes?|KB|MB|GB|TB)/i).first
  number = size.to_f

  number = case unit.downcase
           when 'byte', 'bytes'
             number
           when 'kb'
             number * 1024
           when 'mb'
             number * 1024 * 1024
           when 'gb'
             number * 1024 * 1024 * 1024
           when 'tb'
             number * 1024 * 1024 * 1024 * 1024
           end
  number.round
end

.page(data) ⇒ Object

Pagination Helper



27
28
29
30
31
32
33
# File 'lib/greenhat/shell/shell_helper.rb', line 27

def self.page(data)
  TTY::Pager.page do |pager|
    data.flatten.each do |output|
      pager.write("\n#{output}") # write line to the pager
    end
  end
end

.percent(value, total) ⇒ Object

Percent Helper



403
404
405
# File 'lib/greenhat/shell/shell_helper.rb', line 403

def self.percent(value, total)
  ((value / total.to_f) * 100).round
end

.prepare_list(log_list, base_list = nil, _flags = {}) ⇒ Object

Total Log List Manipulator



508
509
510
511
512
513
514
515
516
517
518
# File 'lib/greenhat/shell/shell_helper.rb', line 508

def self.prepare_list(log_list, base_list = nil, _flags = {})
  base_list ||= GreenHat::ShellHelper::Log.list

  # Assume all
  log_list.push '*' if log_list.empty?

  # Map for All
  log_list = base_list.map(&:name) if log_list == ['*']

  log_list
end

.render_table(entry, flags) ⇒ Object

Print the Table in a Nice way



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/greenhat/shell/shell_helper.rb', line 119

def self.render_table(entry, flags)
  entry = entry.map { |k, v| [k, format_table_entry(flags, v, k)] }.to_h
  # Pre-format Entry

  table_style = flags[:table_style]&.to_sym || :unicode

  table = TTY::Table.new(header: entry.keys, rows: [entry], orientation: :vertical)

  LogBot.debug('Rendering Entries') if ENV['DEBUG']
  table.render(table_style, padding: [0, 1, 0, 1], multiline: true) do |renderer|
    renderer.border.style = :cyan
  end

  # LogBot.debug('Finish Render Table') if ENV['DEBUG']
  # Fall Back to Amazing Inspect
rescue StandardError => e
  if ENV['DEBUG']
    LogBot.warn('Table', message: e.message)
    ap e.backtrace
  end

  [
    entry.ai,
    ('_' * (TTY::Screen.width / 3)).pastel(:cyan),
    "\n"
  ].join("\n")
end

.render_table_entry(val, col_index, flags) ⇒ Object



147
148
149
150
151
# File 'lib/greenhat/shell/shell_helper.rb', line 147

def self.render_table_entry(val, col_index, flags)
  return val.to_s unless col_index == 1

  format_table_entry(flags, val)
end

.search(data, flags = {}, args = {}) ⇒ Object

Generic Search Helper / String/Regex



563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
# File 'lib/greenhat/shell/shell_helper.rb', line 563

def self.search(data, flags = {}, args = {})
  results = data.clone.flatten.compact
  results.select! do |row|
    args.send(flags.logic) do |arg|
      search_row(row, arg, flags)
    end
  end

  # Strip Results if Slice is defined
  results.map! { |row| row.slice(*flags[:slice]) } if flags[:slice]

  # Strip Results if Except is defined
  results.map! { |row| row.except(*flags[:except]) } if flags[:except]

  # Remove Blank from either slice or except
  results.reject!(&:empty?)

  results
end

.search_row(row, arg, flags) ⇒ Object

Break out filter row logic into separate method



584
585
586
587
588
589
590
591
592
593
594
# File 'lib/greenhat/shell/shell_helper.rb', line 584

def self.search_row(row, arg, flags)
  # Sensitivity Check / Check for Match
  included = filter_row_entry(row.to_s, arg, flags)

  # Pivot of off include vs exclude
  if arg.bang
    !included
  else
    included
  end
end

.search_start(files, flags, args) ⇒ Object

Main Entry Point for Searching def self.search_start(log_list, filter_type, args, opts)



548
549
550
551
552
553
554
555
556
557
558
559
560
# File 'lib/greenhat/shell/shell_helper.rb', line 548

def self.search_start(files, flags, args)
  # Convert to Things
  logs = ShellHelper.find_things(files, flags)

  logs.each_with_object({}) do |log, obj|
    # Ignore Empty Results / No Thing
    next if log&.data.blank?

    obj[log.friendly_name] = ShellHelper.search(log.data, flags, args)

    obj
  end
end

.show(data, flags = {}) ⇒ Object

Show Data / Auto Paginate Helper



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/greenhat/shell/shell_helper.rb', line 36

def self.show(data, flags = {})
  # If Block of String
  if data.instance_of?(String)
    TTY::Pager.page data
    return true
  end

  # If raw just print out
  if flags[:raw]
    puts data.join("\n")
    return true
  end

  # Check if content needs to paged, or if auto_height is off
  if Page.skip?(flags, data)
    puts data.map { |entry| entry_show(flags, entry) }.compact.join("\n")
    return true
  end

  # Default Pager
  TTY::Pager.page do |pager|
    data.each do |entry|
      output = entry_show(flags, entry)

      # Breaks any intentional spaces
      # next if output.blank?

      pager.write("\n#{output}") # write line to the pager
    end
  end
end

.thing_listObject

Fuzzy match for things



521
522
523
524
525
# File 'lib/greenhat/shell/shell_helper.rb', line 521

def self.thing_list
  @thing_list ||= Thing.all.map(&:name)

  @thing_list
end

.total_count(results) ⇒ Object

Total Count Helper



462
463
464
465
466
467
468
# File 'lib/greenhat/shell/shell_helper.rb', line 462

def self.total_count(results)
  results.each do |k, v|
    puts k
    puts "Total: #{v.count.to_s.pastel(:blue)}"
    puts
  end
end