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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +20 -0
- data/.gitignore +4 -0
- data/.travis.yml +16 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +65 -0
- data/README.md +221 -0
- data/Rakefile +18 -0
- data/bin/wavefront +14 -0
- data/lib/wavefront-cli/alert.rb +60 -0
- data/lib/wavefront-cli/base.rb +320 -0
- data/lib/wavefront-cli/cloudintegration.rb +12 -0
- data/lib/wavefront-cli/commands/alert.rb +38 -0
- data/lib/wavefront-cli/commands/base.rb +105 -0
- data/lib/wavefront-cli/commands/dashboard.rb +29 -0
- data/lib/wavefront-cli/commands/event.rb +44 -0
- data/lib/wavefront-cli/commands/integration.rb +33 -0
- data/lib/wavefront-cli/commands/link.rb +34 -0
- data/lib/wavefront-cli/commands/message.rb +23 -0
- data/lib/wavefront-cli/commands/metric.rb +20 -0
- data/lib/wavefront-cli/commands/proxy.rb +25 -0
- data/lib/wavefront-cli/commands/query.rb +32 -0
- data/lib/wavefront-cli/commands/savedsearch.rb +32 -0
- data/lib/wavefront-cli/commands/source.rb +27 -0
- data/lib/wavefront-cli/commands/user.rb +24 -0
- data/lib/wavefront-cli/commands/webhook.rb +25 -0
- data/lib/wavefront-cli/commands/window.rb +33 -0
- data/lib/wavefront-cli/commands/write.rb +35 -0
- data/lib/wavefront-cli/constants.rb +17 -0
- data/lib/wavefront-cli/controller.rb +134 -0
- data/lib/wavefront-cli/dashboard.rb +27 -0
- data/lib/wavefront-cli/display/alert.rb +44 -0
- data/lib/wavefront-cli/display/base.rb +304 -0
- data/lib/wavefront-cli/display/cloudintegration.rb +18 -0
- data/lib/wavefront-cli/display/dashboard.rb +21 -0
- data/lib/wavefront-cli/display/event.rb +19 -0
- data/lib/wavefront-cli/display/externallink.rb +13 -0
- data/lib/wavefront-cli/display/maintenancewindow.rb +19 -0
- data/lib/wavefront-cli/display/message.rb +8 -0
- data/lib/wavefront-cli/display/metric.rb +22 -0
- data/lib/wavefront-cli/display/proxy.rb +13 -0
- data/lib/wavefront-cli/display/query.rb +69 -0
- data/lib/wavefront-cli/display/savedsearch.rb +17 -0
- data/lib/wavefront-cli/display/source.rb +26 -0
- data/lib/wavefront-cli/display/user.rb +16 -0
- data/lib/wavefront-cli/display/webhook.rb +24 -0
- data/lib/wavefront-cli/display/write.rb +19 -0
- data/lib/wavefront-cli/event.rb +162 -0
- data/lib/wavefront-cli/exception.rb +5 -0
- data/lib/wavefront-cli/externallink.rb +16 -0
- data/lib/wavefront-cli/maintenancewindow.rb +16 -0
- data/lib/wavefront-cli/message.rb +19 -0
- data/lib/wavefront-cli/metric.rb +24 -0
- data/lib/wavefront-cli/opt_handler.rb +62 -0
- data/lib/wavefront-cli/proxy.rb +22 -0
- data/lib/wavefront-cli/query.rb +74 -0
- data/lib/wavefront-cli/savedsearch.rb +24 -0
- data/lib/wavefront-cli/source.rb +20 -0
- data/lib/wavefront-cli/user.rb +25 -0
- data/lib/wavefront-cli/version.rb +1 -0
- data/lib/wavefront-cli/webhook.rb +8 -0
- data/lib/wavefront-cli/write.rb +244 -0
- data/spec/spec_helper.rb +197 -0
- data/spec/wavefront-cli/alert_spec.rb +44 -0
- data/spec/wavefront-cli/base_spec.rb +47 -0
- data/spec/wavefront-cli/cli_help_spec.rb +47 -0
- data/spec/wavefront-cli/cloudintegration_spec.rb +24 -0
- data/spec/wavefront-cli/dashboard_spec.rb +37 -0
- data/spec/wavefront-cli/event_spec.rb +19 -0
- data/spec/wavefront-cli/externallink_spec.rb +18 -0
- data/spec/wavefront-cli/maintanancewindow_spec.rb +19 -0
- data/spec/wavefront-cli/message_spec.rb +28 -0
- data/spec/wavefront-cli/metric_spec.rb +22 -0
- data/spec/wavefront-cli/proxy_spec.rb +26 -0
- data/spec/wavefront-cli/query_spec.rb +63 -0
- data/spec/wavefront-cli/resources/conf.yaml +10 -0
- data/spec/wavefront-cli/savedsearch_spec.rb +18 -0
- data/spec/wavefront-cli/source_spec.rb +18 -0
- data/spec/wavefront-cli/user_spec.rb +31 -0
- data/spec/wavefront-cli/webhook_spec.rb +17 -0
- data/wavefront-cli.gemspec +36 -0
- 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
|