logstash-cli 0.0.2 → 0.0.3
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.
- data/README.md +42 -1
- data/lib/logstash-cli/cli.rb +29 -4
- data/lib/logstash-cli/command/grep.rb +2 -1
- data/lib/logstash-cli/command/tail.rb +67 -0
- data/lib/logstash-cli/command.rb +7 -0
- data/lib/logstash-cli/version.rb +1 -1
- data/logstashcli.gemspec +2 -2
- metadata +26 -11
- data/lib/logstash-cli/command/live.rb +0 -139
data/README.md
CHANGED
@@ -33,6 +33,8 @@ Mucho inspired by a gist of the eminent @lusis - <https://gist.github.com/138807
|
|
33
33
|
|
34
34
|
## Commandline Options
|
35
35
|
|
36
|
+
### Grep
|
37
|
+
|
36
38
|
Usage:
|
37
39
|
logstash-cli grep PATTERN
|
38
40
|
|
@@ -59,15 +61,54 @@ Mucho inspired by a gist of the eminent @lusis - <https://gist.github.com/138807
|
|
59
61
|
|
60
62
|
Search logstash for a pattern
|
61
63
|
|
64
|
+
### Tail
|
65
|
+
|
66
|
+
Usage:
|
67
|
+
logstash-cli tail
|
68
|
+
|
69
|
+
Options:
|
70
|
+
[--host=HOST] # Host to connect to AMQP
|
71
|
+
# Default: localhost
|
72
|
+
--amqpurl, [--url=URL] # Alternate way to specify settings via an AMQP Url f.i. amqp://logstash:foopass@localhost:5672.
|
73
|
+
This takes precendence over other settings. Note that username and password need to be percentage encoded(URL encoded) in case of special characters
|
74
|
+
[--auto-delete] # Autodelete Exchange or not
|
75
|
+
[--vhost=VHOST] # VHost to connect to AMQP
|
76
|
+
# Default: /
|
77
|
+
[--persistent] # Persistent Exchange or not
|
78
|
+
[--ssl] # Enable SSL to connect to AMQP
|
79
|
+
[--user=USER] # User to connect to AMQP
|
80
|
+
# Default: logstash
|
81
|
+
[--meta=META] # Meta Logstash fields to show
|
82
|
+
# Default: timestamp,type,message
|
83
|
+
[--format=FORMAT] # Format to use for exporting (plain,csv,json)
|
84
|
+
# Default: csv
|
85
|
+
[--key=KEY] # Routing key
|
86
|
+
# Default: #
|
87
|
+
[--port=PORT] # Port to connect to AMQP
|
88
|
+
# Default: 5672
|
89
|
+
[--exchange=EXCHANGE] # Exchange name
|
90
|
+
# Default: rawlogs
|
91
|
+
[--password=PASSWORD] # Password to connect to AMQP
|
92
|
+
# Default: foo
|
93
|
+
[--delim=DELIM] # csv delimiter
|
94
|
+
# Default: |
|
95
|
+
[--exchange-type=EXCHANGE_TYPE] # Exchange Type
|
96
|
+
# Default: direct
|
97
|
+
[--durable] # Durable Exchange or not
|
98
|
+
|
99
|
+
Stream a live feed via AMQP
|
100
|
+
|
101
|
+
|
62
102
|
## Examples
|
63
103
|
|
64
104
|
$ logstash-cli grep --esurl="http://logger-1.jedi.be:9200" '@message:jedi4ever AND program:sshd' --last 5d --format csv --delim ':'
|
65
105
|
|
106
|
+
$ logstash-cli tail --amqpurl="amqp://logger-1.jedi.be:5672" --key="program.sshd"
|
107
|
+
|
66
108
|
## TODO
|
67
109
|
|
68
110
|
- find a way to query existing instances
|
69
111
|
- specify last 15m
|
70
112
|
- find a way to get the results by streaming instead of loading all in memory (maybe pagination will help here)
|
71
|
-
- live tailing the output
|
72
113
|
- produce ascii histograms
|
73
114
|
- or sparklines
|
data/lib/logstash-cli/cli.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'bundler/setup'
|
3
3
|
require 'rack'
|
4
|
+
require 'amqp'
|
4
5
|
require 'tire'
|
5
6
|
require 'time'
|
6
7
|
require 'fastercsv'
|
7
|
-
require 'logstash-cli/command
|
8
|
+
require 'logstash-cli/command'
|
8
9
|
|
9
10
|
module LogstashCli
|
10
11
|
class CLI < Thor
|
@@ -16,15 +17,39 @@ module LogstashCli
|
|
16
17
|
method_option :index_prefix , :default => "logstash-", :desc => "Logstash index prefix"
|
17
18
|
method_option :from , :default => "#{Time.now.strftime('%Y-%m-%d')}", :desc => "Begin date"
|
18
19
|
method_option :to, :default => "#{Time.now.strftime('%Y-%m-%d')}", :desc => "End date"
|
19
|
-
method_option :format , :default => 'csv', :desc => "Format to use for exporting"
|
20
|
+
method_option :format , :default => 'csv', :desc => "Format to use for exporting (plain,csv,json)"
|
20
21
|
method_option :size , :default => 500, :desc => "Number of results to return"
|
21
22
|
method_option :last , :default => nil, :desc => "Specify period since now f.i. 1d"
|
22
23
|
method_option :meta , :default => "type,message", :desc => "Meta Logstash fields to show"
|
23
|
-
method_option :fields , :default => "
|
24
|
-
method_option :delim , :default => "|", :desc => "csv delimiter"
|
24
|
+
method_option :fields , :default => "", :desc => "Logstash Fields to show"
|
25
|
+
method_option :delim , :default => "|", :desc => "plain or csv delimiter"
|
26
|
+
|
25
27
|
def grep(pattern)
|
26
28
|
_grep(pattern,options)
|
27
29
|
end
|
28
30
|
|
31
|
+
desc "tail", "Stream a live feed via AMQP"
|
32
|
+
method_option :url, :desc => "Alternate way to specify settings via an AMQP Url f.i. amqp://logstash:foopass@localhost:5672. \n This takes precendence over other settings. Note that username and password need to be percentage encoded(URL encoded) in case of special characters",:aliases => "\--amqpurl"
|
33
|
+
method_option :user, :default => 'logstash', :desc => "User to connect to AMQP"
|
34
|
+
method_option :password, :default => 'foo', :desc => "Password to connect to AMQP"
|
35
|
+
method_option :vhost, :default => '/', :desc => "VHost to connect to AMQP"
|
36
|
+
method_option :port, :default => 5672, :desc => "Port to connect to AMQP"
|
37
|
+
method_option :host, :default => 'localhost' , :desc => "Host to connect to AMQP"
|
38
|
+
method_option :ssl, :default => false , :desc => "Enable SSL to connect to AMQP", :type => :boolean
|
39
|
+
|
40
|
+
method_option :exchange, :default => 'rawlogs', :desc => "Exchange name"
|
41
|
+
method_option :exchange_type, :default => 'direct', :desc => "Exchange Type"
|
42
|
+
method_option :durable, :default => false, :desc => "Durable Exchange or not", :type => :boolean
|
43
|
+
method_option :auto_delete, :default => false, :desc => "Autodelete Exchange or not" , :type => :boolean
|
44
|
+
method_option :persistent, :default => false, :desc => "Persistent Exchange or not", :type => :boolean
|
45
|
+
method_option :key, :default => '#', :desc => "Routing key"
|
46
|
+
method_option :format , :default => 'csv', :desc => "Format to use for exporting (plain,csv,json)"
|
47
|
+
method_option :meta, :default => "timestamp,type,message", :desc => "Meta Logstash fields to show"
|
48
|
+
method_option :delim, :default => "|", :desc => "csv delimiter"
|
49
|
+
|
50
|
+
def tail()
|
51
|
+
_tail(options)
|
52
|
+
end
|
53
|
+
|
29
54
|
end
|
30
55
|
end
|
@@ -2,7 +2,7 @@ require 'date'
|
|
2
2
|
|
3
3
|
require 'yajl/json_gem'
|
4
4
|
|
5
|
-
module
|
5
|
+
module Grep
|
6
6
|
|
7
7
|
def _grep(pattern,options)
|
8
8
|
es_url = options[:esurl]
|
@@ -80,6 +80,7 @@ module LogstashCli::Command
|
|
80
80
|
output = case options[:format]
|
81
81
|
when 'csv' then result.to_csv({:col_sep => options[:delim]})
|
82
82
|
when 'json' then result.to_json
|
83
|
+
when 'plain' then result.join(options[:delim])
|
83
84
|
end
|
84
85
|
#tstamp = Time.iso8601(res[:@timestamp]).localtime.iso8601
|
85
86
|
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'yajl/json_gem'
|
2
|
+
|
3
|
+
module Tail
|
4
|
+
|
5
|
+
def _tail(options)
|
6
|
+
amqp_url = options[:url]
|
7
|
+
|
8
|
+
amqp_user = options[:user]
|
9
|
+
amqp_password = options[:password]
|
10
|
+
amqp_vhost = options[:vhost]
|
11
|
+
amqp_port = options[:port]
|
12
|
+
amqp_host = options[:host]
|
13
|
+
amqp_ssl = options[:ssl]
|
14
|
+
|
15
|
+
exchange_name = options[:exchange]
|
16
|
+
exchange_type = options[:exchange_type]
|
17
|
+
persistent = options[:persistent]
|
18
|
+
durable = options[:durable]
|
19
|
+
auto_delete = options[:autodelete]
|
20
|
+
routing_key = options[:key]
|
21
|
+
metafields = options[:meta].split(',')
|
22
|
+
|
23
|
+
begin
|
24
|
+
#connection = AMQP.connect(AMQP_OPTS.merge(:username => "amqp_gem", :password => "amqp_gem_password", :vhost => "amqp_gem_testbed"))
|
25
|
+
settings= { :host => amqp_host, :vhost => amqp_vhost, :port => amqp_port,
|
26
|
+
:user => amqp_user, :password => amqp_password ,
|
27
|
+
:ssl => amqp_ssl }
|
28
|
+
|
29
|
+
# Aqmp url can override settings
|
30
|
+
unless amqp_url.nil?
|
31
|
+
settings = aqmp_url
|
32
|
+
end
|
33
|
+
|
34
|
+
AMQP.start(settings) do |connection, open_ok|
|
35
|
+
channel = AMQP::Channel.new(connection, :auto_recovery => true)
|
36
|
+
|
37
|
+
channel.queue("", :auto_delete => auto_delete, :peristent => persistent , :durable => durable) do |queue, declare_ok|
|
38
|
+
queue.bind(exchange_name, :routing_key => routing_key)
|
39
|
+
queue.subscribe do |payload|
|
40
|
+
parsed_message = JSON.parse(payload)
|
41
|
+
result = Array.new
|
42
|
+
|
43
|
+
metafields.each do |metafield|
|
44
|
+
result << parsed_message["@#{metafield}"]
|
45
|
+
end
|
46
|
+
|
47
|
+
output = case options[:format]
|
48
|
+
when 'csv' then result.to_csv({:col_sep => options[:delim]})
|
49
|
+
when 'json' then result.to_json
|
50
|
+
when 'plain' then result.join(options[:delim])
|
51
|
+
end
|
52
|
+
|
53
|
+
puts output
|
54
|
+
result = []
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
rescue AMQP::PossibleAuthenticationFailureError => ex
|
59
|
+
puts "Possible Authentication error:\nthe AMQP connection URL used is #{amqp_url}\n\nDetail Info:\n#{ex}"
|
60
|
+
exit -1
|
61
|
+
rescue Exception => ex
|
62
|
+
puts "Error occured: #{ex}"
|
63
|
+
exit -1
|
64
|
+
end
|
65
|
+
trap("INT") { puts "Shutting down..."; connection.close { EM.stop };exit }
|
66
|
+
end
|
67
|
+
end
|
data/lib/logstash-cli/version.rb
CHANGED
data/logstashcli.gemspec
CHANGED
@@ -9,14 +9,14 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.email = ["patrick.debois@jedi.be"]
|
10
10
|
s.homepage = "http://github.com/jedi4ever/logstash-cli/"
|
11
11
|
s.summary = %q{CLI interface to logstash}
|
12
|
-
s.description = %q{CLI
|
12
|
+
s.description = %q{CLI interface to logstash}
|
13
13
|
|
14
14
|
s.required_rubygems_version = ">= 1.3.6"
|
15
15
|
s.rubyforge_project = "logstash-cli"
|
16
16
|
|
17
17
|
s.add_dependency "tire"
|
18
18
|
s.add_dependency "thor"
|
19
|
-
|
19
|
+
s.add_dependency "amqp"
|
20
20
|
s.add_dependency "rack"
|
21
21
|
s.add_dependency "yajl-ruby"
|
22
22
|
s.add_dependency "fastercsv"
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 25
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 3
|
10
|
+
version: 0.0.3
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Patrick Debois
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-
|
18
|
+
date: 2012-06-24 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
requirement: &id001 !ruby/object:Gem::Requirement
|
@@ -56,7 +56,7 @@ dependencies:
|
|
56
56
|
- 0
|
57
57
|
version: "0"
|
58
58
|
type: :runtime
|
59
|
-
name:
|
59
|
+
name: amqp
|
60
60
|
version_requirements: *id003
|
61
61
|
prerelease: false
|
62
62
|
- !ruby/object:Gem::Dependency
|
@@ -70,7 +70,7 @@ dependencies:
|
|
70
70
|
- 0
|
71
71
|
version: "0"
|
72
72
|
type: :runtime
|
73
|
-
name:
|
73
|
+
name: rack
|
74
74
|
version_requirements: *id004
|
75
75
|
prerelease: false
|
76
76
|
- !ruby/object:Gem::Dependency
|
@@ -84,7 +84,7 @@ dependencies:
|
|
84
84
|
- 0
|
85
85
|
version: "0"
|
86
86
|
type: :runtime
|
87
|
-
name:
|
87
|
+
name: yajl-ruby
|
88
88
|
version_requirements: *id005
|
89
89
|
prerelease: false
|
90
90
|
- !ruby/object:Gem::Dependency
|
@@ -98,11 +98,25 @@ dependencies:
|
|
98
98
|
- 0
|
99
99
|
version: "0"
|
100
100
|
type: :runtime
|
101
|
-
name:
|
101
|
+
name: fastercsv
|
102
102
|
version_requirements: *id006
|
103
103
|
prerelease: false
|
104
104
|
- !ruby/object:Gem::Dependency
|
105
105
|
requirement: &id007 !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
hash: 3
|
111
|
+
segments:
|
112
|
+
- 0
|
113
|
+
version: "0"
|
114
|
+
type: :runtime
|
115
|
+
name: json
|
116
|
+
version_requirements: *id007
|
117
|
+
prerelease: false
|
118
|
+
- !ruby/object:Gem::Dependency
|
119
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
106
120
|
none: false
|
107
121
|
requirements:
|
108
122
|
- - ">="
|
@@ -115,9 +129,9 @@ dependencies:
|
|
115
129
|
version: 1.0.0
|
116
130
|
type: :development
|
117
131
|
name: bundler
|
118
|
-
version_requirements: *
|
132
|
+
version_requirements: *id008
|
119
133
|
prerelease: false
|
120
|
-
description: CLI
|
134
|
+
description: CLI interface to logstash
|
121
135
|
email:
|
122
136
|
- patrick.debois@jedi.be
|
123
137
|
executables:
|
@@ -135,8 +149,9 @@ files:
|
|
135
149
|
- bin/logstash-cli
|
136
150
|
- lib/logstash-cli.rb
|
137
151
|
- lib/logstash-cli/cli.rb
|
152
|
+
- lib/logstash-cli/command.rb
|
138
153
|
- lib/logstash-cli/command/grep.rb
|
139
|
-
- lib/logstash-cli/command/
|
154
|
+
- lib/logstash-cli/command/tail.rb
|
140
155
|
- lib/logstash-cli/version.rb
|
141
156
|
- logstashcli.gemspec
|
142
157
|
homepage: http://github.com/jedi4ever/logstash-cli/
|
@@ -1,139 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# vim:filetype=ruby
|
3
|
-
require 'rubygems'
|
4
|
-
require 'bundler/setup'
|
5
|
-
begin
|
6
|
-
require 'tire'
|
7
|
-
require 'slop'
|
8
|
-
require 'time'
|
9
|
-
rescue LoadError
|
10
|
-
puts "You seem to be missing some key libraries"
|
11
|
-
puts "please run `gem install bundler --no-ri --no-rdoc; bundle install`"
|
12
|
-
end
|
13
|
-
|
14
|
-
begin
|
15
|
-
require 'yajl/json_gem'
|
16
|
-
rescue LoadError
|
17
|
-
puts "`gem install yajl-ruby for better performance"
|
18
|
-
end
|
19
|
-
|
20
|
-
opts = Slop.parse do
|
21
|
-
banner "Usage: search.rb -i <index> -f <facility>"
|
22
|
-
on :i, :index=, "index to search (default: logstash-#{Time.now.strftime('%Y.%m.%d')})", :default => "logstash-#{Time.now.strftime('%Y.%m.%d')}"
|
23
|
-
on :f, :facility=, "REQUIRED: Facility(application name) to use", nil
|
24
|
-
on :s, :size=, "number of results to return (default: 500)", true, :default => 500
|
25
|
-
on :c, :class_name=, "optional class name to narrow results by", nil
|
26
|
-
on :g, :grep=, "optional search on message content. Warning!!! This is slow", true
|
27
|
-
on :exceptions, "toggle exception search. Warning!!! This is slow", :default => false
|
28
|
-
on :live, "runs in live logging mode. This is A LOT of output. Please use non-wildcard facilities", :default => false
|
29
|
-
on :fields=, Array, "optional comma-separated list of fields to display (tstamp, msg, file, class_name, service) in order (default: tstamp,service,msg)"
|
30
|
-
on :h, :help, 'Print this help message', :tail => true do
|
31
|
-
#puts help
|
32
|
-
puts <<-EOF
|
33
|
-
|
34
|
-
--------
|
35
|
-
Examples
|
36
|
-
--------
|
37
|
-
- last 10 results log entries from curation including timestamp, classname and message:
|
38
|
-
|
39
|
-
search.rb -f curation* -s 10 --fields tstamp,class_name,msg
|
40
|
-
|
41
|
-
- last 5 entries for class name com.va (note the quote around * in the -f option):
|
42
|
-
|
43
|
-
search.rb -f "*" -s 5 -c com.va.*
|
44
|
-
|
45
|
-
- last 20 entries everywhere with timestamp, service name and message
|
46
|
-
|
47
|
-
search.rb -f "*" -s 20 --fields tstamp,service,msg
|
48
|
-
|
49
|
-
- last 5 exceptions everywhere
|
50
|
-
|
51
|
-
search.rb -f "*" -s 5 --exceptions
|
52
|
-
|
53
|
-
- live tail tracker_web
|
54
|
-
|
55
|
-
search.rb -f tracker_web --live
|
56
|
-
|
57
|
-
- live tail foo with custom display
|
58
|
-
|
59
|
-
search.rb -f foo --live --fields tstamp,service,class_name,msg
|
60
|
-
|
61
|
-
EOF
|
62
|
-
exit
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
if opts[:live]
|
67
|
-
require 'amqp'
|
68
|
-
require 'yajl/json_gem'
|
69
|
-
#https://github.com/ruby-amqp/amqp/pull/74
|
70
|
-
#URL naming scheme
|
71
|
-
AMQP.start("amqp://logstash:bar@localhost:7777") do |connection, open_ok|
|
72
|
-
channel = AMQP::Channel.new(connection, :auto_recovery => true)
|
73
|
-
exchange_name = "rawlogs"
|
74
|
-
|
75
|
-
channel.queue("", :auto_delete => true, :durable => false) do |queue, declare_ok|
|
76
|
-
queue.bind(exchange_name, :routing_key => opts[:facility])
|
77
|
-
queue.subscribe do |payload|
|
78
|
-
parsed_message = JSON.parse(payload)
|
79
|
-
require 'pp'
|
80
|
-
pp parsed_message
|
81
|
-
service = parsed_message["@fields"]["facility"]
|
82
|
-
class_name = parsed_message["@fields"]["_logger"]
|
83
|
-
file = parsed_message["@fields"]["file"]
|
84
|
-
msg = parsed_message["@message"]
|
85
|
-
#msg = parsed_message["@fields"]["full_message"]
|
86
|
-
tstamp = Time.iso8601(parsed_message["@timestamp"]).localtime.iso8601
|
87
|
-
fields = opts[:fields] || ["tstamp", "service", "msg"]
|
88
|
-
vals = fields.map {|x| x == fields[0] ? "\e[1m[#{eval(x)}]\e[0m" : eval(x)}
|
89
|
-
display = vals.join(" - ")
|
90
|
-
puts display
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
trap("INT") { puts "Shutting down..."; connection.close { EM.stop };exit }
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
if opts[:facility].nil?
|
99
|
-
puts "Facility (matches name of service) CANNOT be empty!"
|
100
|
-
require 'pp'
|
101
|
-
pp opts
|
102
|
-
exit
|
103
|
-
end
|
104
|
-
|
105
|
-
begin
|
106
|
-
Tire.configure {url "http://localhost:9200"}
|
107
|
-
puts opts[:index]
|
108
|
-
search = Tire.search(opts[:index]) do
|
109
|
-
query do
|
110
|
-
boolean do
|
111
|
-
must { string 'HOSTNAME:shipper'}
|
112
|
-
#must { string "facility:#{opts[:facility]}" } unless opts[:facility].nil?
|
113
|
-
#must { string "_logger:#{opts[:class_name]}" } unless opts[:class_name].nil?
|
114
|
-
#must { string "full_message:Exception*" } if opts[:exceptions]
|
115
|
-
must { string "message:#{opts[:grep]}*" } if opts[:grep]
|
116
|
-
end
|
117
|
-
end
|
118
|
-
sort do
|
119
|
-
by :@timestamp, 'desc'
|
120
|
-
end
|
121
|
-
size opts[:size]
|
122
|
-
end
|
123
|
-
rescue Exception => e
|
124
|
-
puts "\nSomething went wrong with the search. This is usually do to lucene query parsing of the 'grep' option"
|
125
|
-
exit
|
126
|
-
end
|
127
|
-
|
128
|
-
search.results.sort {|a,b| a[:@timestamp] <=> b[:@timestamp] }.each do |res|
|
129
|
-
service = res[:@fields][:facility]
|
130
|
-
class_name = res[:@fields][:_logger]
|
131
|
-
file = res[:@fields][:file]
|
132
|
-
#msg = res[:@fields][:full_message]
|
133
|
-
msg = res[:@fields][:message]
|
134
|
-
tstamp = Time.iso8601(res[:@timestamp]).localtime.iso8601
|
135
|
-
fields = opts[:fields] || ["tstamp", "service", "msg"]
|
136
|
-
vals = fields.map {|x| x == fields[0] ? "\e[1m[#{eval(x)}]\e[0m" : eval(x)}
|
137
|
-
display = vals.join(" - ")
|
138
|
-
puts display
|
139
|
-
end
|