wavefront-cli 5.1.1 → 7.2.0

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +37 -1
  3. data/HISTORY.md +34 -2
  4. data/README.md +2 -4
  5. data/lib/wavefront-cli/account.rb +119 -0
  6. data/lib/wavefront-cli/alert.rb +29 -0
  7. data/lib/wavefront-cli/base.rb +0 -2
  8. data/lib/wavefront-cli/cloudintegration.rb +12 -0
  9. data/lib/wavefront-cli/commands/.rubocop.yml +34 -0
  10. data/lib/wavefront-cli/commands/account.rb +61 -0
  11. data/lib/wavefront-cli/commands/alert.rb +1 -0
  12. data/lib/wavefront-cli/commands/base.rb +1 -1
  13. data/lib/wavefront-cli/commands/cloudintegration.rb +4 -1
  14. data/lib/wavefront-cli/commands/proxy.rb +2 -1
  15. data/lib/wavefront-cli/commands/query.rb +4 -1
  16. data/lib/wavefront-cli/commands/role.rb +44 -0
  17. data/lib/wavefront-cli/commands/spy.rb +0 -5
  18. data/lib/wavefront-cli/commands/usergroup.rb +7 -11
  19. data/lib/wavefront-cli/commands/write.rb +7 -2
  20. data/lib/wavefront-cli/controller.rb +5 -63
  21. data/lib/wavefront-cli/display/account.rb +122 -0
  22. data/lib/wavefront-cli/display/alert.rb +8 -0
  23. data/lib/wavefront-cli/display/base.rb +1 -1
  24. data/lib/wavefront-cli/display/cloudintegration.rb +15 -2
  25. data/lib/wavefront-cli/display/printer/long.rb +2 -1
  26. data/lib/wavefront-cli/display/proxy.rb +16 -0
  27. data/lib/wavefront-cli/display/role.rb +66 -0
  28. data/lib/wavefront-cli/display/settings.rb +1 -0
  29. data/lib/wavefront-cli/display/usergroup.rb +18 -14
  30. data/lib/wavefront-cli/exception_handler.rb +89 -0
  31. data/lib/wavefront-cli/output/hcl/base.rb +1 -1
  32. data/lib/wavefront-cli/output/hcl/dashboard.rb +1 -1
  33. data/lib/wavefront-cli/proxy.rb +5 -0
  34. data/lib/wavefront-cli/query.rb +13 -7
  35. data/lib/wavefront-cli/role.rb +54 -0
  36. data/lib/wavefront-cli/serviceaccount.rb +0 -6
  37. data/lib/wavefront-cli/spy.rb +0 -8
  38. data/lib/wavefront-cli/usergroup.rb +8 -8
  39. data/lib/wavefront-cli/version.rb +1 -1
  40. data/lib/wavefront-cli/write.rb +29 -5
  41. data/spec/.rubocop.yml +34 -0
  42. data/spec/test_mixins/delete.rb +1 -2
  43. data/spec/wavefront-cli/account_spec.rb +303 -0
  44. data/spec/wavefront-cli/alert_spec.rb +28 -0
  45. data/spec/wavefront-cli/cloudintegration_spec.rb +19 -6
  46. data/spec/wavefront-cli/commands/write_spec.rb +1 -1
  47. data/spec/wavefront-cli/event_spec.rb +1 -1
  48. data/spec/wavefront-cli/output/csv/query_spec.rb +1 -1
  49. data/spec/wavefront-cli/output/wavefront/query_spec.rb +2 -2
  50. data/spec/wavefront-cli/query_spec.rb +20 -3
  51. data/spec/wavefront-cli/role_spec.rb +187 -0
  52. data/spec/wavefront-cli/serviceaccount_spec.rb +3 -3
  53. data/spec/wavefront-cli/usergroup_spec.rb +48 -43
  54. data/spec/wavefront-cli/write_spec.rb +44 -0
  55. data/wavefront-cli.gemspec +3 -3
  56. metadata +28 -36
  57. data/lib/wavefront-cli/commands/cluster.rb +0 -44
  58. data/lib/wavefront-cli/commands/user.rb +0 -54
  59. data/lib/wavefront-cli/display/monitoredcluster.rb +0 -14
  60. data/lib/wavefront-cli/display/user.rb +0 -103
  61. data/lib/wavefront-cli/monitoredcluster.rb +0 -50
  62. data/lib/wavefront-cli/user.rb +0 -92
  63. data/spec/wavefront-cli/monitoredcluster_spec.rb +0 -85
  64. data/spec/wavefront-cli/resources/responses/user-list.json +0 -1
  65. data/spec/wavefront-cli/user_spec.rb +0 -311
@@ -26,7 +26,10 @@ class WavefrontCommandCloudintegration < WavefrontCommandBase
26
26
  "disable #{CMN} <id>",
27
27
  "dump #{CMN}",
28
28
  "import #{CMN} [-uU] <file>",
29
- "search #{CMN} [-al] [-o offset] [-L limit] [-O fields] <condition>..."]
29
+ "search #{CMN} [-al] [-o offset] [-L limit] [-O fields] <condition>...",
30
+ "awsid #{CMN} generate",
31
+ "awsid #{CMN} delete <external_id>",
32
+ "awsid #{CMN} confirm <external_id>"]
30
33
  end
31
34
 
32
35
  def _options
@@ -10,7 +10,7 @@ class WavefrontCommandProxy < WavefrontCommandBase
10
10
  end
11
11
 
12
12
  def _commands
13
- ["list #{CMN} [-al] [-O fields] [-o offset] [-L limit]",
13
+ ["list #{CMN} [-laA] [-O fields] [-o offset] [-L limit]",
14
14
  "describe #{CMN} <id>",
15
15
  "delete #{CMN} <id>",
16
16
  "undelete #{CMN} <id>",
@@ -23,6 +23,7 @@ class WavefrontCommandProxy < WavefrontCommandBase
23
23
  [common_options,
24
24
  "-l, --long list #{things} in detail",
25
25
  "-a, --all list all #{things}",
26
+ "-A, --active only show active #{things}",
26
27
  "-o, --offset=n start from nth #{thing}",
27
28
  '-O, --fields=F1,F2,... only show given fields',
28
29
  "-L, --limit=COUNT number of #{things} to list"]
@@ -12,7 +12,7 @@ class WavefrontCommandQuery < WavefrontCommandBase
12
12
  def _commands
13
13
  ['aliases [-DV] [-c file] [-P profile]',
14
14
  "#{CMN} [-g granularity] [-s time] [-e time] " \
15
- '[-WikvO] [-S mode] [-N name] [-p points] [-F options] <query>',
15
+ '[-ikvCGKOW] [-S mode] [-N name] [-p points] [-F options] <query>',
16
16
  "raw #{CMN} [-H host] [-s time] [-e time] " \
17
17
  '[-F options] <metric>',
18
18
  "run #{CMN} [-g granularity] [-s time] [-e time] " \
@@ -36,6 +36,9 @@ class WavefrontCommandQuery < WavefrontCommandBase
36
36
  '-F, --format-opts=STRING comma-separated options to pass to ' \
37
37
  'output formatter',
38
38
  '-k, --nospark do not show sparkline',
39
+ '-C, --nocache do not use the query cache',
40
+ '-K, --nostrict allow points outside the query window',
41
+ '-G, --histogram-view use histogram view rather than metric',
39
42
  '-W, --nowarn do not show API warning messages']
40
43
  end
41
44
 
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ # Define the 'role' command.
6
+ #
7
+ class WavefrontCommandRole < WavefrontCommandBase
8
+ def _commands
9
+ ["list #{CMN} [-al] [-O fields] [-o offset] [-L limit]",
10
+ "describe #{CMN} <id>",
11
+ "create #{CMN} [-d description] [-p permission...] <name>",
12
+ "delete #{CMN} <id>",
13
+ "dump #{CMN}",
14
+ "import #{CMN} [-uU] <file>",
15
+ "set #{CMN} <key=value> <id>",
16
+ "accounts #{CMN} <id>",
17
+ "groups #{CMN} <id>",
18
+ "permissions #{CMN} <id>",
19
+ "give #{CMN} <id> to <member>...",
20
+ "take #{CMN} <id> from <member>...",
21
+ "grant #{CMN} <permission> to <id>",
22
+ "revoke #{CMN} <permission> from <id>",
23
+ "search #{CMN} [-al] [-o offset] [-L limit] [-O fields] <condition>..."]
24
+ end
25
+
26
+ def _options
27
+ [common_options,
28
+ "-l, --long list #{things} in detail",
29
+ "-a, --all list all #{things}",
30
+ "-o, --offset=n start from nth #{thing}",
31
+ "-L, --limit=COUNT number of #{things} to list",
32
+ '-O, --fields=F1,F2,... only show given fields',
33
+ "-u, --update update an existing #{thing}",
34
+ "-U, --upsert import new or update existing #{thing}",
35
+ "-d, --description=STRING description of #{thing}",
36
+ '-p, --permission=STRING Wavefront permission']
37
+ end
38
+
39
+ def postscript
40
+ "A role 'member' can be an account ID or a usergroup ID. 'wf settings " \
41
+ "list permissions' will give you a list of all currently supported " \
42
+ 'permissions.'.fold(TW, 0)
43
+ end
44
+ end
@@ -34,9 +34,4 @@ class WavefrontCommandSpy < WavefrontCommandBase
34
34
  '-T, --tag-key=TAG only show metrics with the given point tag key',
35
35
  '-y, --type=STRING one of METRIC, SPAN, HOST, or STRING']
36
36
  end
37
-
38
- def postscript
39
- "\nNOTE: This command uses the unofficial 'spy' API endpoint, which " \
40
- 'is not guaranteed to remain stable.'.cmd_fold(TW, 0)
41
- end
42
37
  end
@@ -24,17 +24,18 @@ class WavefrontCommandUsergroup < WavefrontCommandBase
24
24
  def _commands
25
25
  ["list #{CMN} [-al] [-O fields] [-o offset] [-L limit]",
26
26
  "describe #{CMN} <id>",
27
- "create #{CMN} [-p permission...] <name>",
27
+ "create #{CMN} [-r role_id...] <name>",
28
28
  "delete #{CMN} <id>",
29
29
  "dump #{CMN}",
30
30
  "import #{CMN} [-uU] <file>",
31
31
  "set #{CMN} <key=value> <id>",
32
+ "add to #{CMN} <id> <user>...",
33
+ "remove from #{CMN} <id> <user>...",
32
34
  "users #{CMN} <id>",
35
+ "add role #{CMN} <id> <role>...",
36
+ "remove role #{CMN} <id> <role>...",
37
+ "roles #{CMN} <id>",
33
38
  "permissions #{CMN} <id>",
34
- "add user #{CMN} <id> <user>...",
35
- "remove user #{CMN} <id> <user>...",
36
- "grant #{CMN} <permission> to <id>",
37
- "revoke #{CMN} <permission> from <id>",
38
39
  "search #{CMN} [-al] [-o offset] [-L limit] [-O fields] <condition>..."]
39
40
  end
40
41
 
@@ -47,11 +48,6 @@ class WavefrontCommandUsergroup < WavefrontCommandBase
47
48
  '-O, --fields=F1,F2,... only show given fields',
48
49
  "-u, --update update an existing #{thing}",
49
50
  "-U, --upsert import new or update existing #{thing}",
50
- '-p, --permission=STRING Wavefront permission']
51
- end
52
-
53
- def postscript
54
- "'wf settings list permissions' will give you a list of all " \
55
- 'currently supported permissions.'.fold(TW, 0)
51
+ '-r, --role-id=STRING Wavefront role ID']
56
52
  end
57
53
  end
@@ -18,7 +18,9 @@ class WavefrontCommandWrite < WavefrontCommandBase
18
18
  '<metric> [--] <val>...',
19
19
  'file [-DnViq] [-c file] [-P profile] [-E proxy] [-H host] ' \
20
20
  '[-p port] [-F infileformat] [-m metric] [-T tag...] [-I interval] ' \
21
- '[-u method] [-S socket] <file>']
21
+ '[-u method] [-S socket] <file>',
22
+ 'noise [-DnViq] [-P profile] [-E proxy] [-H host] [-p port] ' \
23
+ '[-T tag...] [-I interval] [-x value] [-X value] <metric>']
22
24
  end
23
25
 
24
26
  def _options
@@ -33,9 +35,12 @@ class WavefrontCommandWrite < WavefrontCommandBase
33
35
  'a file will be assigned. If the file contains a metric name, ' \
34
36
  'the two will be dot-concatenated, with this value first',
35
37
  '-i, --delta increment metric by given value',
36
- "-I, --interval=INTERVAL interval of distribution (default 'm')",
38
+ "-I, --interval=INTERVAL interval of distribution (default 'm'), or " \
39
+ 'time in seconds between noise values (default 1)',
37
40
  '-u, --using=METHOD method by which to send points',
38
41
  '-S, --socket=FILE Unix datagram socket',
42
+ '-x, --min=NUMERIC lower bound of random values (default -10)',
43
+ '-X, --max=NUMERIC upper bound of random values (default 10)',
39
44
  "-q, --quiet don't report the points sent summary " \
40
45
  '(unless there were errors)']
41
46
  end
@@ -17,6 +17,7 @@ require_relative 'version'
17
17
  require_relative 'constants'
18
18
  require_relative 'exception'
19
19
  require_relative 'opt_handler'
20
+ require_relative 'exception_handler'
20
21
  require_relative 'stdlib/string'
21
22
 
22
23
  CMD_DIR = Pathname.new(__dir__) + 'commands'
@@ -28,6 +29,7 @@ class WavefrontCliController
28
29
  attr_reader :args, :usage, :opts, :cmds, :tw
29
30
 
30
31
  include WavefrontCli::Constants
32
+ include WavefrontCli::ExceptionMixins
31
33
 
32
34
  def initialize(args)
33
35
  @args = args
@@ -107,12 +109,8 @@ class WavefrontCliController
107
109
  #
108
110
  def cli_class(cmd, opts)
109
111
  load_cli_class(cmd, opts)
110
- rescue WavefrontCli::Exception::UnhandledCommand
111
- abort 'Fatal error. Unsupported command. Please open a Github issue.'
112
- rescue WavefrontCli::Exception::InvalidInput => e
113
- abort "Invalid input. #{e.message}"
114
- rescue RuntimeError => e
115
- abort "Unable to run command. #{e.message}."
112
+ rescue StandardError => e
113
+ exception_handler(e)
116
114
  end
117
115
 
118
116
  def load_cli_class(cmd, opts)
@@ -120,68 +118,12 @@ class WavefrontCliController
120
118
  Object.const_get('WavefrontCli').const_get(cmds[cmd].sdk_class).new(opts)
121
119
  end
122
120
 
123
- # rubocop:disable Metrics/MethodLength
124
- # rubocop:disable Metrics/AbcSize
125
121
  def run_command(cli_class_obj)
126
122
  cli_class_obj.validate_opts
127
123
  cli_class_obj.run
128
- rescue Interrupt
129
- abort "\nOperation aborted at user request."
130
- rescue WavefrontCli::Exception::ConfigFileNotFound => e
131
- abort "Configuration file #{e}' not found."
132
- rescue WavefrontCli::Exception::CredentialError => e
133
- handle_missing_credentials(e)
134
- rescue WavefrontCli::Exception::MandatoryValue
135
- abort 'A value must be supplied.'
136
- rescue Wavefront::Exception::NetworkTimeout
137
- abort 'Connection timed out.'
138
- rescue Wavefront::Exception::InvalidPermission => e
139
- abort "'#{e}' is not a valid privilege."
140
- rescue Wavefront::Exception::InvalidUserGroupId => e
141
- abort "'#{e}' is not a valid user group id."
142
- rescue WavefrontCli::Exception::InvalidValue => e
143
- abort "Invalid value for #{e}."
144
- rescue WavefrontCli::Exception::ProfileExists => e
145
- abort "Profile '#{e}' already exists."
146
- rescue WavefrontCli::Exception::ProfileNotFound => e
147
- abort "Profile '#{e}' not found."
148
- rescue WavefrontCli::Exception::FileNotFound
149
- abort 'File not found.'
150
- rescue WavefrontCli::Exception::InsufficientData => e
151
- abort "Insufficient data. #{e.message}"
152
- rescue WavefrontCli::Exception::InvalidQuery => e
153
- abort "Invalid query. API message: '#{e.message}'."
154
- rescue WavefrontCli::Exception::SystemError => e
155
- abort "Host system error. #{e.message}"
156
- rescue WavefrontCli::Exception::UnparseableInput => e
157
- abort "Cannot parse input. #{e.message}"
158
- rescue WavefrontCli::Exception::UnparseableSearchPattern
159
- abort 'Searches require a key, a value, and a match operator.'
160
- rescue WavefrontCli::Exception::UnsupportedFileFormat
161
- abort 'Unsupported file format.'
162
- rescue WavefrontCli::Exception::UnsupportedOperation => e
163
- abort "Unsupported operation.\n#{e.message}"
164
- rescue WavefrontCli::Exception::UnsupportedOutput => e
165
- abort e.message
166
- rescue WavefrontCli::Exception::UnsupportedNoop
167
- abort 'Multiple API call operations cannot be performed as no-ops.'
168
- rescue WavefrontCli::Exception::UserGroupNotFound => e
169
- abort "Cannot find user group '#{e.message}'."
170
- rescue Wavefront::Exception::UnsupportedWriter => e
171
- abort "Unsupported writer '#{e.message}'."
172
- rescue WavefrontCli::Exception::UserError => e
173
- abort "User error: #{e.message}."
174
- rescue WavefrontCli::Exception::ImpossibleSearch
175
- abort 'Search on non-existent key. Please use a top-level field.'
176
- rescue Wavefront::Exception::InvalidSamplingValue
177
- abort 'Sampling rates must be between 0 and 0.05.'
178
124
  rescue StandardError => e
179
- warn "general error: #{e}"
180
- backtrace_message(e)
181
- abort
125
+ exception_handler(e)
182
126
  end
183
- # rubocop:enable Metrics/MethodLength
184
- # rubocop:enable Metrics/AbcSize
185
127
 
186
128
  def backtrace_message(err)
187
129
  if opts[:debug]
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module WavefrontDisplay
6
+ #
7
+ # Format human-readable output for account management.
8
+ #
9
+ class Account < Base
10
+ def do_list_brief
11
+ filter_user_list
12
+ puts(data.map { |account| account[:identifier] })
13
+ end
14
+
15
+ def do_list
16
+ filter_user_list
17
+ super
18
+ end
19
+
20
+ def do_role_add_to
21
+ puts format("Gave %<quoted_roles>s to '%<id>s'.",
22
+ id: options[:'<id>'],
23
+ quoted_roles: quoted(options[:'<role>']))
24
+ end
25
+
26
+ def do_role_remove_from
27
+ puts format("Removed %<quoted_roles>s from '%<id>s'.",
28
+ id: options[:'<id>'],
29
+ quoted_roles: quoted(options[:'<role>']))
30
+ end
31
+
32
+ def do_roles
33
+ roles = data.fetch(:roles, [])
34
+ puts roles.empty? ? "'#{options[:'<id>']}' has no roles." : roles
35
+ end
36
+
37
+ def do_group_add_to
38
+ puts format("Added '%<id>s' to %<quoted_group>s.",
39
+ id: options[:'<id>'],
40
+ quoted_group: quoted(options[:'<group>']))
41
+ end
42
+
43
+ def do_group_remove_from
44
+ puts format("Removed '%<id>s' from %<quoted_group>s.",
45
+ id: options[:'<id>'],
46
+ quoted_group: quoted(options[:'<group>']))
47
+ end
48
+
49
+ def do_groups
50
+ groups = data.fetch(:userGroups, [])
51
+
52
+ if groups.empty?
53
+ puts "'#{options[:'<id>']}' does not belong to any groups."
54
+ else
55
+ puts groups.sort
56
+ end
57
+ end
58
+
59
+ def do_business_functions
60
+ puts data.sort
61
+ end
62
+
63
+ def do_grant_to
64
+ puts format("Granted '%<permission>s' to %<quoted_accounts>s.",
65
+ permission: options[:'<permission>'],
66
+ quoted_accounts: quoted(options[:'<account>']))
67
+ end
68
+
69
+ def do_revoke_from
70
+ puts format("Revoked '%<permission>s' from %<quoted_accounts>s.",
71
+ permission: options[:'<permission>'],
72
+ quoted_accounts: quoted(options[:'<account>']))
73
+ end
74
+
75
+ def do_permissions
76
+ perms = data.fetch(:groups, [])
77
+
78
+ if perms.empty?
79
+ puts "'#{options[:'<id>']}' does not have any permissions directly " \
80
+ 'attached.'
81
+ else
82
+ puts perms.sort
83
+ end
84
+ end
85
+
86
+ def do_ingestionpolicy_add_to
87
+ puts format("Added '%<policy>s' to '%<id>s'.",
88
+ id: options[:'<id>'],
89
+ policy: options[:'<policy>'])
90
+ end
91
+
92
+ def do_ingestionpolicy_remove_from
93
+ puts format("Removed '%<policy>s' from '%<id>s'.",
94
+ id: options[:'<id>'],
95
+ policy: options[:'<policy>'])
96
+ end
97
+
98
+ def do_ingestionpolicy
99
+ policy = data.fetch(:ingestionPolicyId, [])
100
+
101
+ if policy.empty?
102
+ puts "'#{options[:'<id>']}' has no ingestion policy."
103
+ else
104
+ puts policy
105
+ end
106
+ end
107
+
108
+ def do_invite_user
109
+ puts format("Sent invitation to '%<id>s'.", id: options[:'<id>'])
110
+ end
111
+
112
+ private
113
+
114
+ def filter_user_list
115
+ if options[:user]
116
+ data.delete_if { |a| a[:identifier].start_with?('sa::') }
117
+ elsif options[:service]
118
+ data.delete_if { |a| !a[:identifier].start_with?('sa::') }
119
+ end
120
+ end
121
+ end
122
+ end
@@ -74,5 +74,13 @@ module WavefrontDisplay
74
74
  def do_version
75
75
  puts data.max
76
76
  end
77
+
78
+ def do_affected_hosts
79
+ if data == [nil]
80
+ puts 'Alert event is not attached to any hosts.'
81
+ else
82
+ long_output
83
+ end
84
+ end
77
85
  end
78
86
  end
@@ -112,7 +112,7 @@ module WavefrontDisplay
112
112
  # long listing objects. Subclasses may define their own.
113
113
  #
114
114
  def priority_keys
115
- %i[id name]
115
+ %i[id name identifier]
116
116
  end
117
117
 
118
118
  def prioritize_keys(data, keys)
@@ -8,11 +8,12 @@ module WavefrontDisplay
8
8
  #
9
9
  class CloudIntegration < Base
10
10
  def do_list_brief
11
- multicolumn(:id, :service)
11
+ multicolumn(:id, :service, :name)
12
12
  end
13
13
 
14
14
  def do_describe
15
- readable_time(:lastReceivedDataPointMs, :lastProcessingTimestamp)
15
+ readable_time(:lastReceivedDataPointMs, :lastProcessingTimestamp,
16
+ :createdEpochMillis, :updatedEpochMillis)
16
17
  drop_fields(:forceSave, :inTrash, :deleted)
17
18
  long_output
18
19
  end
@@ -24,5 +25,17 @@ module WavefrontDisplay
24
25
  def do_disable
25
26
  puts "Disabled '#{options[:'<id>']}'."
26
27
  end
28
+
29
+ def do_awsid_generate
30
+ puts data
31
+ end
32
+
33
+ def do_awsid_delete
34
+ puts "Deleted external ID '#{options[:'<external_id>']}'."
35
+ end
36
+
37
+ def do_awsid_confirm
38
+ puts "'#{data}' is a registered external ID."
39
+ end
27
40
  end
28
41
  end
@@ -8,6 +8,7 @@ module WavefrontDisplayPrinter
8
8
  #
9
9
  class Long
10
10
  attr_reader :opts, :list, :kw
11
+
11
12
  #
12
13
  # @param data [Hash] of data to display
13
14
  # @param fields [Array[Symbol]] requred fields
@@ -55,7 +56,7 @@ module WavefrontDisplayPrinter
55
56
  def preened_value(value)
56
57
  return value unless value.is_a?(String) && value =~ /<.*>/
57
58
 
58
- value.gsub(%r{<\/?[^>]*>}, '').delete("\n")
59
+ value.gsub(%r{</?[^>]*>}, '').delete("\n")
59
60
  end
60
61
 
61
62
  # A recursive function which takes a structure, most likely a