wavefront-cli 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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