wavefront-cli 0.0.2

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 (82) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +20 -0
  3. data/.gitignore +4 -0
  4. data/.travis.yml +16 -0
  5. data/Gemfile +2 -0
  6. data/Gemfile.lock +65 -0
  7. data/README.md +221 -0
  8. data/Rakefile +18 -0
  9. data/bin/wavefront +14 -0
  10. data/lib/wavefront-cli/alert.rb +60 -0
  11. data/lib/wavefront-cli/base.rb +320 -0
  12. data/lib/wavefront-cli/cloudintegration.rb +12 -0
  13. data/lib/wavefront-cli/commands/alert.rb +38 -0
  14. data/lib/wavefront-cli/commands/base.rb +105 -0
  15. data/lib/wavefront-cli/commands/dashboard.rb +29 -0
  16. data/lib/wavefront-cli/commands/event.rb +44 -0
  17. data/lib/wavefront-cli/commands/integration.rb +33 -0
  18. data/lib/wavefront-cli/commands/link.rb +34 -0
  19. data/lib/wavefront-cli/commands/message.rb +23 -0
  20. data/lib/wavefront-cli/commands/metric.rb +20 -0
  21. data/lib/wavefront-cli/commands/proxy.rb +25 -0
  22. data/lib/wavefront-cli/commands/query.rb +32 -0
  23. data/lib/wavefront-cli/commands/savedsearch.rb +32 -0
  24. data/lib/wavefront-cli/commands/source.rb +27 -0
  25. data/lib/wavefront-cli/commands/user.rb +24 -0
  26. data/lib/wavefront-cli/commands/webhook.rb +25 -0
  27. data/lib/wavefront-cli/commands/window.rb +33 -0
  28. data/lib/wavefront-cli/commands/write.rb +35 -0
  29. data/lib/wavefront-cli/constants.rb +17 -0
  30. data/lib/wavefront-cli/controller.rb +134 -0
  31. data/lib/wavefront-cli/dashboard.rb +27 -0
  32. data/lib/wavefront-cli/display/alert.rb +44 -0
  33. data/lib/wavefront-cli/display/base.rb +304 -0
  34. data/lib/wavefront-cli/display/cloudintegration.rb +18 -0
  35. data/lib/wavefront-cli/display/dashboard.rb +21 -0
  36. data/lib/wavefront-cli/display/event.rb +19 -0
  37. data/lib/wavefront-cli/display/externallink.rb +13 -0
  38. data/lib/wavefront-cli/display/maintenancewindow.rb +19 -0
  39. data/lib/wavefront-cli/display/message.rb +8 -0
  40. data/lib/wavefront-cli/display/metric.rb +22 -0
  41. data/lib/wavefront-cli/display/proxy.rb +13 -0
  42. data/lib/wavefront-cli/display/query.rb +69 -0
  43. data/lib/wavefront-cli/display/savedsearch.rb +17 -0
  44. data/lib/wavefront-cli/display/source.rb +26 -0
  45. data/lib/wavefront-cli/display/user.rb +16 -0
  46. data/lib/wavefront-cli/display/webhook.rb +24 -0
  47. data/lib/wavefront-cli/display/write.rb +19 -0
  48. data/lib/wavefront-cli/event.rb +162 -0
  49. data/lib/wavefront-cli/exception.rb +5 -0
  50. data/lib/wavefront-cli/externallink.rb +16 -0
  51. data/lib/wavefront-cli/maintenancewindow.rb +16 -0
  52. data/lib/wavefront-cli/message.rb +19 -0
  53. data/lib/wavefront-cli/metric.rb +24 -0
  54. data/lib/wavefront-cli/opt_handler.rb +62 -0
  55. data/lib/wavefront-cli/proxy.rb +22 -0
  56. data/lib/wavefront-cli/query.rb +74 -0
  57. data/lib/wavefront-cli/savedsearch.rb +24 -0
  58. data/lib/wavefront-cli/source.rb +20 -0
  59. data/lib/wavefront-cli/user.rb +25 -0
  60. data/lib/wavefront-cli/version.rb +1 -0
  61. data/lib/wavefront-cli/webhook.rb +8 -0
  62. data/lib/wavefront-cli/write.rb +244 -0
  63. data/spec/spec_helper.rb +197 -0
  64. data/spec/wavefront-cli/alert_spec.rb +44 -0
  65. data/spec/wavefront-cli/base_spec.rb +47 -0
  66. data/spec/wavefront-cli/cli_help_spec.rb +47 -0
  67. data/spec/wavefront-cli/cloudintegration_spec.rb +24 -0
  68. data/spec/wavefront-cli/dashboard_spec.rb +37 -0
  69. data/spec/wavefront-cli/event_spec.rb +19 -0
  70. data/spec/wavefront-cli/externallink_spec.rb +18 -0
  71. data/spec/wavefront-cli/maintanancewindow_spec.rb +19 -0
  72. data/spec/wavefront-cli/message_spec.rb +28 -0
  73. data/spec/wavefront-cli/metric_spec.rb +22 -0
  74. data/spec/wavefront-cli/proxy_spec.rb +26 -0
  75. data/spec/wavefront-cli/query_spec.rb +63 -0
  76. data/spec/wavefront-cli/resources/conf.yaml +10 -0
  77. data/spec/wavefront-cli/savedsearch_spec.rb +18 -0
  78. data/spec/wavefront-cli/source_spec.rb +18 -0
  79. data/spec/wavefront-cli/user_spec.rb +31 -0
  80. data/spec/wavefront-cli/webhook_spec.rb +17 -0
  81. data/wavefront-cli.gemspec +36 -0
  82. metadata +279 -0
@@ -0,0 +1,27 @@
1
+ require_relative './base'
2
+
3
+ # Define the source command.
4
+ #
5
+ class WavefrontCommandSource < WavefrontCommandBase
6
+ def description
7
+ 'view and manage source tags and descriptions'
8
+ end
9
+
10
+ def _commands
11
+ ["list #{CMN} [-l] [-f format] [-o offset] [-L limit] [-a]",
12
+ "describe #{CMN} [-f format] <id>",
13
+ "description set #{CMN} <id> <description>",
14
+ "description clear #{CMN} <id>",
15
+ "clear #{CMN} <id>",
16
+ tag_commands]
17
+ end
18
+
19
+ def _options
20
+ [common_options,
21
+ '-l, --long list sources in detail',
22
+ '-o, --offset=n start list from nth source',
23
+ '-L, --limit=COUNT number of sources to list',
24
+ '-a, --all list all sources, including cluster',
25
+ '-f, --format=STRING output format']
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ require_relative './base'
2
+
3
+ # Define the user command.
4
+ #
5
+ class WavefrontCommandUser < WavefrontCommandBase
6
+ def description
7
+ 'view and manage Wavefront users'
8
+ end
9
+
10
+ def _commands
11
+ ["list #{CMN} [-l]",
12
+ "describe #{CMN} [-f format] <id>",
13
+ "delete #{CMN} <id>",
14
+ "import #{CMN} <file>",
15
+ "grant #{CMN} <privilege> to <id>",
16
+ "revoke #{CMN} <privilege> from <id>"]
17
+ end
18
+
19
+ def _options
20
+ [common_options,
21
+ '-l, --long list users in detail',
22
+ '-f, --format=STRING output format']
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ require_relative './base'
2
+
3
+ # Define the webhook command.
4
+ #
5
+ class WavefrontCommandWebhook < WavefrontCommandBase
6
+ def description
7
+ 'view and manage webhooks'
8
+ end
9
+
10
+ def _commands
11
+ ["list #{CMN} [-l] [-f format] [-o offset] [-L limit]",
12
+ "describe #{CMN} [-f format] <id>",
13
+ "delete #{CMN} <id>",
14
+ "import #{CMN} <file>",
15
+ "update #{CMN} <key=value> <id>"]
16
+ end
17
+
18
+ def _options
19
+ [common_options,
20
+ '-l, --long list webhooks in detail',
21
+ '-o, --offset=n start list from nth webhook',
22
+ '-L, --limit=COUNT number of webhooks to list',
23
+ '-f, --format=STRING output format']
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ require_relative './base'
2
+
3
+ # Define the maintenance window command.
4
+ #
5
+ class WavefrontCommandWindow < WavefrontCommandBase
6
+ def description
7
+ 'view and manage maintenance windows'
8
+ end
9
+
10
+ def sdk_file
11
+ 'maintenancewindow'
12
+ end
13
+
14
+ def sdk_class
15
+ 'MaintenanceWindow'
16
+ end
17
+
18
+ def _commands
19
+ ["list #{CMN} [-l] [-f format] [-o offset] [-L limit]",
20
+ "describe #{CMN} [-f format] <id>",
21
+ "delete #{CMN} <id>",
22
+ "import #{CMN} <file>",
23
+ "update #{CMN} <key=value> <id>"]
24
+ end
25
+
26
+ def _options
27
+ [common_options,
28
+ '-l, --long list maintenance windows in detail',
29
+ '-o, --offset=n start from nth maintenance window',
30
+ '-L, --limit=COUNT number of maintenance windows to list',
31
+ '-f, --format=STRING output format']
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ require_relative './base'
2
+
3
+ # Define the write command.
4
+ #
5
+ class WavefrontCommandWrite < WavefrontCommandBase
6
+ def description
7
+ 'send data to a Wavefront proxy'
8
+ end
9
+
10
+ def _commands
11
+ ['point [-DnV] [-c file] [-P profile] [-E proxy] [-t time] ' \
12
+ '[-p port] [-H host] [-n] [-T tag...] <metric> <value>',
13
+ 'file [-DnV] [-c file] [-P profile] [-E proxy] [-H host] ' \
14
+ '[-p port] [-n] [-F format] [-m metric] [-T tag...] <file>']
15
+ end
16
+
17
+ def _options
18
+ ['-E, --proxy=URI proxy endpoint',
19
+ '-t, --time=TIME time of data point (omit to use ' \
20
+ 'current time)',
21
+ '-H, --host=STRING source host', \
22
+ '-p, --port=INT Wavefront proxy port',
23
+ '-T, --tag=TAG point tag in key=value form',
24
+ '-F, --infileformat=STRING format of input file or stdin',
25
+ '-m, --metric=STRING the metric path to which contents of ' \
26
+ 'a file will be assigned. If the file contains a metric name, ' \
27
+ 'the two will be concatenated']
28
+ end
29
+
30
+ def postscript
31
+ %(Files are whitespace separated, and fields can be defined
32
+ with the -F option. Use 't' for timestamp; 'm' for metric
33
+ name; 'v' for value and 'T' for tags. Put 'T' last.)
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ module WavefrontCli
2
+
3
+ # Universal truths
4
+ #
5
+ module Constants
6
+ HUMAN_TIME_FORMAT = '%F %T'.freeze
7
+ HUMAN_TIME_FORMAT_MS = '%F %T.%3N'.freeze
8
+
9
+ # The CLI will use these options if they are not supplied on the
10
+ # command line or in a config file.
11
+ #
12
+ DEFAULT_OPTS = {
13
+ endpoint: 'metrics.wavefront.com',
14
+ format: :human,
15
+ }.freeze
16
+ end
17
+ end
@@ -0,0 +1,134 @@
1
+ require 'pathname'
2
+ require 'pp'
3
+ require 'docopt'
4
+ require_relative './version'
5
+ require_relative './opt_handler'
6
+ require_relative './exception'
7
+
8
+ $LOAD_PATH.<< Pathname.new(__FILE__).dirname.realpath.parent.parent
9
+ .parent + 'lib'
10
+ $LOAD_PATH.<< Pathname.new(__FILE__).dirname.realpath.parent.parent
11
+ .parent + 'wavefront-sdk' + 'lib'
12
+
13
+ CMD_DIR = Pathname.new(__FILE__).dirname + 'commands'
14
+
15
+ # Dynamically generate a CLI interface from files which describe
16
+ # each subcomand.
17
+ #
18
+ class WavefrontCliController
19
+ attr_reader :args, :usage, :opts, :cmds, :tw
20
+
21
+ def initialize(args)
22
+ @args = args
23
+ @cmds = load_commands
24
+ @usage = docopt_hash
25
+ cmd, opts = parse_args
26
+ @opts = parse_opts(opts)
27
+ pp @opts if @opts[:debug]
28
+ hook = load_sdk(cmd, @opts)
29
+ run_command(hook)
30
+ end
31
+
32
+ # What you see when you do 'wavefront --help'
33
+ #
34
+ def default_help
35
+ s = "Wavefront CLI\n\nUsage:\n #{CMD} command [options]\n" \
36
+ " #{CMD} --version\n #{CMD} --help\n\nCommands:\n"
37
+
38
+ cmds.sort.each { |k, v| s.<< format(" %-15s %s\n", k, v.description) }
39
+ s.<< "\nUse '#{CMD} <command> --help' for further information.\n"
40
+ end
41
+
42
+ # Make a hash of command descriptions for docopt.
43
+ #
44
+ def docopt_hash
45
+ cmds.each_with_object(default: default_help) do |(k, v), ret|
46
+ ret[k.to_sym] = v.docopt
47
+ end
48
+ end
49
+
50
+ # Parse the input. The first Docopt.docopt handles the default
51
+ # options, the second works on the command.
52
+ #
53
+ def parse_args
54
+ Docopt.docopt(usage[:default], version: WF_CLI_VERSION, argv: args)
55
+ rescue Docopt::Exit => e
56
+ cmd = args.empty? ? nil : args.first.to_sym
57
+
58
+ abort e.message unless usage.keys.include?(cmd)
59
+
60
+ begin
61
+ [cmd, sanitize_keys(Docopt.docopt(usage[cmd], argv: args))]
62
+ rescue Docopt::Exit => e
63
+ abort e.message
64
+ end
65
+ end
66
+
67
+ def parse_opts(o)
68
+ WavefrontCli::OptHandler.new(conf_file, o).opts
69
+ end
70
+
71
+ # Get the SDK class we need to run the command we've been given.
72
+ #
73
+ def load_sdk(cmd, opts)
74
+ require_relative File.join('.', cmds[cmd].sdk_file)
75
+ Object.const_get('WavefrontCli').const_get(cmds[cmd].sdk_class).new(opts)
76
+ rescue WavefrontCli::Exception::UnhandledCommand
77
+ abort 'Fatal error. Unsupported command.'
78
+ rescue => e
79
+ p e
80
+ end
81
+
82
+ def run_command(hook)
83
+ hook.validate_opts
84
+ hook.run
85
+ rescue => e
86
+ $stderr.puts "general error: #{e}"
87
+ $stderr.puts "re-run with '-D' for stack trace." unless opts[:debug]
88
+ $stderr.puts "Backtrace:\n\t#{e.backtrace.join("\n\t")}" if opts[:debug]
89
+ abort
90
+ end
91
+
92
+ # Each command is defined in its own file. Dynamically load all
93
+ # those commands.
94
+ #
95
+ def load_commands
96
+ CMD_DIR.children.each_with_object({}) do |f, ret|
97
+ k = import_command(f)
98
+ ret[k.word.to_sym] = k if k
99
+ end
100
+ end
101
+
102
+ # Load a command description from a file. Each is in its own class
103
+ #
104
+ # @param f [Pathname] path of file to load
105
+ # return [Class] new class object defining command.
106
+ #
107
+ def import_command(f)
108
+ return if f.extname != '.rb' || f.basename.to_s == 'base.rb'
109
+ k_name = f.basename.to_s[0..-4]
110
+ require(CMD_DIR + k_name)
111
+ Object.const_get("WavefrontCommand#{k_name.capitalize}").new
112
+ end
113
+
114
+ # The default config file path.
115
+ #
116
+ # @return [Pathname] where we excpect to find a config file
117
+ #
118
+ def conf_file
119
+ if ENV['HOME']
120
+ Pathname.new(ENV['HOME']) + '.wavefront'
121
+ else
122
+ Pathname.new('/etc/wavefront/client.conf')
123
+ end
124
+ end
125
+
126
+ # Symbolize, and remove dashes from option keys
127
+ #
128
+ # @param h [Hash] options hash
129
+ # return [Hash] h with modified keys
130
+ #
131
+ def sanitize_keys(h)
132
+ h.each_with_object({}) { |(k, v), r| r[k.delete('-').to_sym] = v }
133
+ end
134
+ end
@@ -0,0 +1,27 @@
1
+ require_relative './base'
2
+
3
+ module WavefrontCli
4
+ #
5
+ # CLI coverage for the v2 'dashboard' API.
6
+ #
7
+ class Dashboard < WavefrontCli::Base
8
+ def do_describe
9
+ wf.describe(options[:'<id>'], options[:version])
10
+ end
11
+
12
+ def do_delete
13
+ print (if wf.describe(options[:'<id>']).status.code == 200
14
+ 'Soft'
15
+ else
16
+ 'Permanently'
17
+ end)
18
+
19
+ puts " deleting dashboard '#{options[:'<id>']}'."
20
+ wf.delete(options[:'<id>'])
21
+ end
22
+
23
+ def do_history
24
+ wf.history(options[:'<id>'])
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ require_relative './base'
2
+
3
+ module WavefrontDisplay
4
+ #
5
+ # Format human-readable output for alerts.
6
+ #
7
+ class Alert < Base
8
+ def do_list
9
+ long_output [:id, :minutes, :target, :status, :tags, :hostsUsed,
10
+ :condition, :displayExpression, :severity,
11
+ :additionalInformation]
12
+ end
13
+
14
+ def do_list_brief
15
+ multicolumn(:id, :status, :name)
16
+ end
17
+
18
+ def do_describe
19
+ readable_time(:created, :lastProcessedMillis,
20
+ :lastNotificationMillis, :createdEpochMillis,
21
+ :updatedEpochMillis, :updated)
22
+ drop_fields(:conditionQBEnabled, :displayExpressionQBEnabled,
23
+ :displayExpressionQBSerialization)
24
+ long_output
25
+ end
26
+
27
+ def do_snooze
28
+ print "Snoozed alert '#{options[:'<id>']}' "
29
+
30
+ puts options[:time] ? "for #{options[:time]} seconds." :
31
+ 'indefinitely.'
32
+ end
33
+
34
+ def do_unsnooze
35
+ puts "Unsnoozed alert '#{options[:'<id>']}'."
36
+ end
37
+
38
+ def do_summary
39
+ kw = data.keys.map(&:size).max + 2
40
+ data.delete_if { |_k, v| v.zero? } unless options[:all]
41
+ data.sort.each { |k, v| puts format("%-#{kw}s%s", k, v) }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,304 @@
1
+ require_relative '../constants'
2
+
3
+ module WavefrontDisplay
4
+ #
5
+ # Print human-friendly output. If a command requires a dedicated
6
+ # handler to format its output, define a method with the same name
7
+ # as that which fetches the data, in a WavefrontDisplay class,
8
+ # extending this one.
9
+ #
10
+ # We provide long_output() and terse_output() methods to solve
11
+ # standard formatting problems. To use them, define a do_() method
12
+ # but rather than printing the output, have it call the method.
13
+ #
14
+ class Base
15
+ include WavefrontCli::Constants
16
+
17
+ attr_reader :data, :options, :indent, :kw, :indent_str, :indent_step,
18
+ :hide_blank
19
+
20
+ # Display classes can provide a do_method_code() method, which
21
+ # handles <code> errors when running do_method()
22
+ #
23
+ def run_error(method)
24
+ return unless respond_to?(method)
25
+ send(method)
26
+ exit 1
27
+ end
28
+
29
+ def initialize(data, options = {})
30
+ @data = data
31
+ @options = options
32
+ @indent = 0
33
+ @indent_step = options[:indent_step] || 2
34
+ @hide_blank = options[:hide_blank] || true
35
+ end
36
+
37
+ def run(method)
38
+ if method == 'do_list'
39
+ if options[:long]
40
+ do_list
41
+ else
42
+ do_list_brief
43
+ end
44
+
45
+ return
46
+ end
47
+
48
+ if respond_to?("#{method}_brief")
49
+ send("#{method}_brief")
50
+ elsif respond_to?(method)
51
+ send(method)
52
+ else
53
+ long_output
54
+ end
55
+ end
56
+
57
+ def long_output(fields = nil, modified_data = nil)
58
+ _two_columns(modified_data || data, nil, fields)
59
+ end
60
+
61
+ # Extract two fields from a hash and print a list of them as
62
+ # pairs.
63
+ #
64
+ # @param col1 [String] the field to use in the first column
65
+ # @param col2 [String] the field to use in the second column
66
+ # @return [Nil]
67
+ #
68
+ def terse_output(col1 = :id, col2 = :name, modified_data = nil)
69
+ d = modified_data || data
70
+ want = d.each_with_object({}) { |r, a| a[r[col1]] = r[col2] }
71
+ @indent_str = ''
72
+ @kw = key_width(want)
73
+
74
+ want.each do |k, v|
75
+ v = v.join(', ') if v.is_a?(Array)
76
+ print_line(k, v)
77
+ end
78
+ end
79
+
80
+ # Print multiple column output. Currently this method does no
81
+ # word wrapping.
82
+ #
83
+ # @param keys [Symbol] the keys you want in the output. They
84
+ # will be printed in the order given.
85
+ #
86
+ def multicolumn(*keys)
87
+ len = Hash[*keys.map {|k| [k, 0]}.flatten]
88
+
89
+ keys.each do |k|
90
+ data.each do |obj|
91
+ val = obj[k]
92
+ val = val.join(', ') if val.is_a?(Array)
93
+ len[k] = val.size if val.size > len[k]
94
+ end
95
+ end
96
+
97
+ fmt = keys.each_with_object('') { |k, out| out.<< "%-#{len[k]}s " }
98
+
99
+ data.each do |obj|
100
+ args = keys.map do |k|
101
+ obj[k].is_a?(Array) ? obj[k].join(', ') : obj[k]
102
+ end
103
+
104
+ puts format(fmt, *args)
105
+ end
106
+ end
107
+
108
+ def set_indent(indent)
109
+ @indent_str = ' ' * indent
110
+ end
111
+
112
+ # A recursive function which displays a key-value hash in two
113
+ # columns. The key column width is automatically calculated.
114
+ # Multiple-value 'v's are printed one per line. Hashes are nested.
115
+ #
116
+ # @param data [Array] and array of objects to display. Each object
117
+ # should be a hash.
118
+ # @param indent [Integer] how many characters to indent the current
119
+ # data.
120
+ # @kw [Integer] the width of the first (key) column.
121
+ # @returns [Nil]
122
+ #
123
+ def _two_columns(data, kw = nil, fields = nil)
124
+ [data].flatten.each do |row|
125
+ row.keep_if { |k, _v| fields.include?(k) } unless fields.nil?
126
+ kw = key_width(row) unless kw
127
+ @kw = kw unless @kw
128
+ set_indent(indent)
129
+
130
+ row.each do |k, v|
131
+ next if v.respond_to?(:empty?) && v.empty? && hide_blank
132
+
133
+ if v.is_a?(String) && v.match(/<.*>/)
134
+ v = v.gsub(%r{<\/?[^>]*>}, '').delete("\n")
135
+ end
136
+
137
+ if v.is_a?(Hash)
138
+ print_line(k)
139
+ @indent += indent_step
140
+ @kw -= 2
141
+ _two_columns([v], kw - indent_step)
142
+ elsif v.is_a?(Array)
143
+ print_array(k, v)
144
+ else
145
+ print_line(k, v)
146
+ end
147
+ end
148
+ puts if indent.zero?
149
+ end
150
+
151
+ @indent -= indent_step if indent > 0
152
+ @kw += 2
153
+ set_indent(indent)
154
+ end
155
+
156
+ def print_array(k, v)
157
+ v.each_with_index do |w, i|
158
+ if w.is_a?(Hash)
159
+ print_line(k) if i.zero?
160
+ @indent += indent_step
161
+ @kw -= 2
162
+ _two_columns([w], kw - indent_step)
163
+ print_line('', '---') unless i == v.size - 1
164
+ else
165
+ if i.zero?
166
+ print_line(k, v.shift)
167
+ else
168
+ print_line('', w)
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ # Print a single line of output
175
+ # @param key [String] what to print in the first (key) column
176
+ # @param val [String, Numeric] what to print in the second column
177
+ # @param indent [Integer] number of leading spaces on line
178
+ #
179
+ def print_line(key, value = '')
180
+ if key.empty?
181
+ puts ' ' * kw + value
182
+ else
183
+ puts indent_str + format("%-#{kw}s%s", key, value).fold(TW, kw)
184
+ end
185
+ end
186
+
187
+ # Give it a key-value hash, and it will return the size of the first
188
+ # column to use when formatting that data.
189
+ #
190
+ # @param hash [Hash] the data for which you need a column width
191
+ # @param pad [Integer] the number of spaces you want between columns
192
+ # @return [Integer] length of longest key + pad
193
+ #
194
+ def key_width(hash, pad = 2)
195
+ return 0 if hash.keys.empty?
196
+ hash.keys.map(&:size).max + pad
197
+ end
198
+
199
+ def indent_wrap(line, cols = 78, offset = 22)
200
+ #
201
+ # hanging indent long lines to fit in an 80-column terminal
202
+ #
203
+ return unless line
204
+ line.gsub(/(.{1,#{cols - offset}})(\s+|\Z)/, "\\1\n#{' ' *
205
+ offset}").rstrip
206
+ end
207
+
208
+ def friendly_name
209
+ self.class.name.split('::').last.gsub(/([a-z])([A-Z])/, '\\1 \\2')
210
+ .downcase
211
+ end
212
+
213
+ def do_list
214
+ long_output
215
+ end
216
+
217
+ def do_list_brief
218
+ terse_output
219
+ end
220
+
221
+ def do_import
222
+ puts "Imported #{friendly_name}."
223
+ long_output
224
+ end
225
+
226
+ def do_delete
227
+ puts "Deleted #{friendly_name} '#{options[:'<id>']}'."
228
+ end
229
+
230
+ def do_undelete
231
+ puts "Undeleted #{friendly_name} '#{options[:'<id>']}'."
232
+ end
233
+
234
+ def do_tag_add
235
+ puts "Tagged #{friendly_name} '#{options[:'<id>']}'."
236
+ end
237
+
238
+ def do_tag_delete
239
+ puts "Deleted tag from #{friendly_name} '#{options[:'<id>']}'."
240
+ end
241
+
242
+ def do_tag_clear
243
+ puts "Cleared tags on #{friendly_name} '#{options[:'<id>']}'."
244
+ end
245
+
246
+ def do_tag_set
247
+ puts "Set tags on #{friendly_name} '#{options[:'<id>']}'."
248
+ end
249
+
250
+ def do_tags
251
+ if data.empty?
252
+ puts "No tags set on #{friendly_name} '#{options[:'<id>']}'."
253
+ else
254
+ data.sort.each { |t| puts t }
255
+ end
256
+ end
257
+
258
+ # Modify, in-place, the data structure to remove fields which
259
+ # we deem not of interest to the user.
260
+ #
261
+ # @param keys [Symbol] keys you do not wish to be shown.
262
+ #
263
+ def drop_fields(*keys)
264
+ data.delete_if { |k, _v| keys.include?(k.to_sym) }
265
+ end
266
+
267
+ # Modify, in-place, the data structure to make times
268
+ # human-readable. Automatically handles second and millisecond
269
+ # epoch times.
270
+ #
271
+ def readable_time(*keys)
272
+ keys.each do |k|
273
+ next unless data.key?(k)
274
+ data[k] = human_time(data[k])
275
+ end
276
+ end
277
+
278
+ def human_time(t)
279
+ str = t.to_s
280
+
281
+ if str.length == 13
282
+ fmt = '%Q'
283
+ out_fmt = HUMAN_TIME_FORMAT_MS
284
+ else
285
+ fmt = '%s'
286
+ out_fmt = HUMAN_TIME_FORMAT
287
+ end
288
+
289
+ DateTime.strptime(str, fmt).strftime(out_fmt)
290
+ end
291
+
292
+ end
293
+ end
294
+
295
+ # Extensions to the String class to help with formatting.
296
+ #
297
+ class String
298
+
299
+ # Fold long command lines and suitably indent
300
+ #
301
+ def fold(width = TW, indent = 10)
302
+ scan(/\S.{0,#{width - 2}}\S(?=\s|$)|\S+/).join("\n" + ' ' * indent)
303
+ end
304
+ end