console 1.19.0 → 1.29.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 (47) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/bake/console.rb +3 -3
  4. data/lib/console/adapter.rb +2 -1
  5. data/lib/console/capture.rb +46 -17
  6. data/lib/console/compatible/logger.rb +4 -3
  7. data/lib/console/event/failure.rb +43 -39
  8. data/lib/console/event/generic.rb +9 -6
  9. data/lib/console/event/spawn.rb +35 -27
  10. data/lib/console/event.rb +3 -4
  11. data/lib/console/filter.rb +24 -10
  12. data/lib/console/format/safe.rb +136 -0
  13. data/lib/console/format.rb +14 -0
  14. data/lib/console/interface.rb +53 -0
  15. data/lib/console/logger.rb +17 -16
  16. data/lib/console/output/default.rb +8 -5
  17. data/lib/console/output/failure.rb +32 -0
  18. data/lib/console/output/null.rb +5 -1
  19. data/lib/console/output/sensitive.rb +12 -10
  20. data/lib/console/{serialized/logger.rb → output/serialized.rb} +14 -49
  21. data/lib/console/output/split.rb +3 -3
  22. data/lib/console/{terminal/logger.rb → output/terminal.rb} +98 -54
  23. data/lib/console/output/wrapper.rb +28 -0
  24. data/lib/console/output.rb +7 -8
  25. data/lib/console/progress.rb +20 -9
  26. data/lib/console/resolver.rb +5 -5
  27. data/lib/console/terminal/formatter/failure.rb +57 -0
  28. data/lib/console/terminal/formatter/progress.rb +58 -0
  29. data/lib/console/terminal/formatter/spawn.rb +42 -0
  30. data/lib/console/terminal/text.rb +6 -2
  31. data/lib/console/terminal/xterm.rb +10 -3
  32. data/lib/console/terminal.rb +19 -2
  33. data/lib/console/version.rb +2 -2
  34. data/lib/console/warn.rb +33 -0
  35. data/lib/console.rb +4 -22
  36. data/license.md +5 -1
  37. data/readme.md +39 -2
  38. data/releases.md +25 -0
  39. data.tar.gz.sig +0 -0
  40. metadata +31 -92
  41. metadata.gz.sig +0 -0
  42. data/lib/console/buffer.rb +0 -25
  43. data/lib/console/event/progress.rb +0 -60
  44. data/lib/console/output/json.rb +0 -16
  45. data/lib/console/output/text.rb +0 -16
  46. data/lib/console/output/xterm.rb +0 -16
  47. data/lib/console/split.rb +0 -10
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright 2024, by Samuel Williams.
5
+
6
+ require_relative "logger"
7
+
8
+ module Console
9
+ # The public logger interface.
10
+ module Interface
11
+ # Get the current logger instance.
12
+ def logger
13
+ Logger.instance
14
+ end
15
+
16
+ # Set the current logger instance.
17
+ #
18
+ # The current logger instance is assigned per-fiber.
19
+ def logger= instance
20
+ Logger.instance= instance
21
+ end
22
+
23
+ # Emit a debug log message.
24
+ def debug(...)
25
+ Logger.instance.debug(...)
26
+ end
27
+
28
+ # Emit an informational log message.
29
+ def info(...)
30
+ Logger.instance.info(...)
31
+ end
32
+
33
+ # Emit a warning log message.
34
+ def warn(...)
35
+ Logger.instance.warn(...)
36
+ end
37
+
38
+ # Emit an error log message.
39
+ def error(...)
40
+ Logger.instance.error(...)
41
+ end
42
+
43
+ # Emit a fatal log message.
44
+ def fatal(...)
45
+ Logger.instance.fatal(...)
46
+ end
47
+
48
+ # Emit a log message with arbitrary arguments and options.
49
+ def call(...)
50
+ Logger.instance.call(...)
51
+ end
52
+ end
53
+ end
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2022, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
  # Copyright, 2021, by Bryan Powell.
6
6
  # Copyright, 2021, by Robert Schulze.
7
7
 
8
- require_relative 'output'
9
- require_relative 'filter'
10
- require_relative 'progress'
8
+ require_relative "output"
9
+ require_relative "output/failure"
11
10
 
12
- require_relative 'resolver'
13
- require_relative 'terminal/logger'
14
- require_relative 'serialized/logger'
11
+ require_relative "filter"
12
+ require_relative "event"
13
+ require_relative "resolver"
14
+ require_relative "progress"
15
15
 
16
- require 'fiber/local'
16
+ require "fiber/local"
17
17
 
18
18
  module Console
19
19
  class Logger < Filter[debug: 0, info: 1, warn: 2, error: 3, fatal: 4]
@@ -23,7 +23,7 @@ module Console
23
23
  # You can also specify CONSOLE_LEVEL=debug or CONSOLE_LEVEL=info in environment.
24
24
  # https://mislav.net/2011/06/ruby-verbose-mode/ has more details about how it all fits together.
25
25
  def self.default_log_level(env = ENV)
26
- if level = env['CONSOLE_LEVEL']
26
+ if level = env["CONSOLE_LEVEL"]
27
27
  LEVELS[level.to_sym] || level.to_i
28
28
  elsif $DEBUG
29
29
  DEBUG
@@ -36,7 +36,7 @@ module Console
36
36
 
37
37
  # Controls verbose output using `$VERBOSE`.
38
38
  def self.verbose?(env = ENV)
39
- !$VERBOSE.nil? || env['CONSOLE_VERBOSE']
39
+ !$VERBOSE.nil? || env["CONSOLE_VERBOSE"]
40
40
  end
41
41
 
42
42
  def self.default_logger(output = $stderr, env = ENV, **options)
@@ -49,6 +49,7 @@ module Console
49
49
  end
50
50
 
51
51
  output = Output.new(output, env, **options)
52
+
52
53
  logger = self.new(output, **options)
53
54
 
54
55
  Resolver.default_resolver(logger)
@@ -63,16 +64,16 @@ module Console
63
64
  DEFAULT_LEVEL = 1
64
65
 
65
66
  def initialize(output, **options)
67
+ # This is the expected default behaviour, but it may be nice to have a way to override it.
68
+ output = Output::Failure.new(output, **options)
69
+
66
70
  super(output, **options)
67
71
  end
68
72
 
69
73
  def progress(subject, total, **options)
70
- Progress.new(self, subject, total, **options)
71
- end
72
-
73
- # @deprecated Use `fatal` instead.
74
- def failure(subject, exception, *arguments, &block)
75
- self.fatal(subject, exception, *arguments, &block)
74
+ options[:severity] ||= :info
75
+
76
+ Progress.new(subject, total, **options)
76
77
  end
77
78
  end
78
79
  end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2022, by Samuel Williams.
4
+ # Copyright, 2021-2024, by Samuel Williams.
5
5
 
6
- require_relative 'xterm'
7
- require_relative 'json'
6
+ require_relative "terminal"
7
+ require_relative "serialized"
8
+ require_relative "failure"
8
9
 
9
10
  module Console
10
11
  module Output
@@ -13,10 +14,12 @@ module Console
13
14
  output ||= $stderr
14
15
 
15
16
  if output.tty?
16
- XTerm.new(output, **options)
17
+ output = Terminal.new(output, **options)
17
18
  else
18
- JSON.new(output, **options)
19
+ output = Serialized.new(output, **options)
19
20
  end
21
+
22
+ return output
20
23
  end
21
24
  end
22
25
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2021-2024, by Samuel Williams.
5
+
6
+ require_relative "wrapper"
7
+ require_relative "../event/failure"
8
+
9
+ module Console
10
+ module Output
11
+ # A wrapper for outputting failure messages, which can include exceptions.
12
+ class Failure < Wrapper
13
+ def initialize(output, **options)
14
+ super(output, **options)
15
+ end
16
+
17
+ # The exception must be either the last argument or passed as an option.
18
+ def call(subject = nil, *arguments, exception: nil, **options, &block)
19
+ if exception.nil?
20
+ last = arguments.last
21
+ if last.is_a?(Exception)
22
+ options[:event] = Event::Failure.for(last)
23
+ end
24
+ else
25
+ options[:event] = Event::Failure.for(exception)
26
+ end
27
+
28
+ super(subject, *arguments, **options)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023, by Samuel Williams.
4
+ # Copyright, 2023-2024, by Samuel Williams.
5
5
 
6
6
  module Console
7
7
  module Output
@@ -9,6 +9,10 @@ module Console
9
9
  def initialize(...)
10
10
  end
11
11
 
12
+ def last_output
13
+ self
14
+ end
15
+
12
16
  def call(...)
13
17
  # Do nothing.
14
18
  end
@@ -1,19 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2022, by Samuel Williams.
4
+ # Copyright, 2021-2024, by Samuel Williams.
5
5
 
6
- require_relative '../serialized/logger'
6
+ require_relative "wrapper"
7
7
 
8
8
  module Console
9
9
  module Output
10
- class Sensitive
11
- def initialize(output, **options)
12
- @output = output
13
- end
14
-
10
+ class Sensitive < Wrapper
15
11
  REDACT = /
16
- phone
12
+ phone
17
13
  | email
18
14
  | full_?name
19
15
  | first_?name
@@ -38,8 +34,14 @@ module Console
38
34
  | password
39
35
  /xi
40
36
 
37
+ def initialize(output, redact: REDACT, **options)
38
+ super(output, **options)
39
+
40
+ @redact = redact
41
+ end
42
+
41
43
  def redact?(text)
42
- text.match?(REDACT)
44
+ text.match?(@redact)
43
45
  end
44
46
 
45
47
  def redact_hash(arguments, filter)
@@ -96,7 +98,7 @@ module Console
96
98
  arguments = redact_array(arguments, filter)
97
99
  end
98
100
 
99
- @output.call(subject, *arguments, **options)
101
+ super(subject, *arguments, **options)
100
102
  end
101
103
  end
102
104
  end
@@ -1,34 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2022, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
- require_relative '../buffer'
7
- require_relative '../filter'
8
-
9
- require 'time'
10
- require 'json'
11
-
12
- require 'fiber/annotation'
6
+ require_relative "../format"
7
+ require "time"
8
+ require "fiber/annotation"
13
9
 
14
10
  module Console
15
- module Serialized
16
- class Logger
17
- def initialize(io = $stderr, format: JSON, verbose: false, **options)
11
+ module Output
12
+ class Serialized
13
+ def initialize(io, format: Format.default, **options)
18
14
  @io = io
19
- @start = Time.now
20
15
  @format = format
21
- @verbose = verbose
16
+ end
17
+
18
+ # This a final output that then writes to an IO object.
19
+ def last_output
20
+ self
22
21
  end
23
22
 
24
23
  attr :io
25
- attr :start
26
24
  attr :format
27
25
 
28
- def verbose!(value = true)
29
- @verbose = true
30
- end
31
-
32
26
  def dump(record)
33
27
  @format.dump(record)
34
28
  end
@@ -72,41 +66,12 @@ module Console
72
66
  record[:message] = message
73
67
  end
74
68
 
75
- if exception = find_exception(message)
76
- record[:error] = {
77
- kind: exception.class,
78
- message: exception.message,
79
- stack: format_stack(exception)
80
- }
81
- end
82
-
83
69
  record.update(options)
84
70
 
85
71
  @io.puts(self.dump(record))
86
72
  end
87
-
88
- private
89
-
90
- def find_exception(message)
91
- message.find{|part| part.is_a?(Exception)}
92
- end
93
-
94
- def format_stack(exception)
95
- buffer = StringIO.new
96
- format_backtrace(exception, buffer)
97
- return buffer.string
98
- end
99
-
100
- def format_backtrace(exception, buffer)
101
- buffer.puts exception.backtrace
102
-
103
- if exception = exception.cause
104
- buffer.puts
105
- buffer.puts "Caused by: #{exception.class} #{exception.message}"
106
-
107
- format_backtrace(exception, buffer)
108
- end
109
- end
110
73
  end
74
+
75
+ JSON = Serialized
111
76
  end
112
77
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2022, by Samuel Williams.
4
+ # Copyright, 2022-2024, by Samuel Williams.
5
5
 
6
6
  module Console
7
7
  module Output
@@ -18,9 +18,9 @@ module Console
18
18
  @outputs.each{|output| output.verbose!(value)}
19
19
  end
20
20
 
21
- def call(level, subject = nil, *arguments, **options, &block)
21
+ def call(...)
22
22
  @outputs.each do |output|
23
- output.call(level, subject, *arguments, **options, &block)
23
+ output.call(...)
24
24
  end
25
25
  end
26
26
  end
@@ -1,53 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2022, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
  # Copyright, 2021, by Robert Schulze.
6
6
 
7
- require_relative '../buffer'
8
- require_relative '../event'
9
- require_relative '../clock'
7
+ require_relative "../clock"
8
+ require_relative "../terminal"
10
9
 
11
- require_relative 'text'
12
- require_relative 'xterm'
13
-
14
- require 'json'
15
- require 'fiber'
16
- require 'fiber/annotation'
10
+ require "json"
11
+ require "fiber"
12
+ require "fiber/annotation"
13
+ require "stringio"
17
14
 
18
15
  module Console
19
- module Terminal
20
- # This, and all related methods, is considered private.
21
- CONSOLE_START_AT = 'CONSOLE_START_AT'
22
-
23
- # Exports CONSOLE_START which can be used to synchronize the start times of all child processes when they log using delta time.
24
- def self.start_at!(environment = ENV)
25
- if time_string = environment[CONSOLE_START_AT]
26
- start_at = Time.parse(time_string) rescue nil
16
+ module Output
17
+ class Terminal
18
+ class Buffer < StringIO
19
+ def initialize(prefix = nil)
20
+ @prefix = prefix
21
+
22
+ super()
23
+ end
24
+
25
+ attr :prefix
26
+
27
+ def puts(*args, prefix: @prefix)
28
+ args.each do |arg|
29
+ self.write(prefix) if prefix
30
+ super(arg)
31
+ end
32
+ end
33
+
34
+ alias << puts
27
35
  end
28
36
 
29
- unless start_at
30
- start_at = Time.now
31
- environment[CONSOLE_START_AT] = start_at.to_s
32
- end
37
+ # This, and all related methods, is considered private.
38
+ CONSOLE_START_AT = "CONSOLE_START_AT"
33
39
 
34
- return start_at
35
- end
36
-
37
- def self.for(io)
38
- if io.isatty
39
- XTerm.new(io)
40
- else
41
- Text.new(io)
40
+ # Exports CONSOLE_START which can be used to synchronize the start times of all child processes when they log using delta time.
41
+ def self.start_at!(environment = ENV)
42
+ if time_string = environment[CONSOLE_START_AT]
43
+ start_at = Time.parse(time_string) rescue nil
44
+ end
45
+
46
+ unless start_at
47
+ start_at = Time.now
48
+ environment[CONSOLE_START_AT] = start_at.to_s
49
+ end
50
+
51
+ return start_at
42
52
  end
43
- end
44
-
45
- class Logger
46
- def initialize(io = $stderr, verbose: nil, start_at: Terminal.start_at!, format: nil, **options)
47
- @io = io
53
+
54
+ def initialize(output, verbose: nil, start_at: Terminal.start_at!, format: nil, **options)
55
+ @io = output
48
56
  @start_at = start_at
49
57
 
50
- @terminal = format.nil? ? Terminal.for(io) : format.new(io)
58
+ @terminal = format.nil? ? Console::Terminal.for(@io) : format.new(@io)
51
59
 
52
60
  if verbose.nil?
53
61
  @verbose = !@terminal.colors?
@@ -66,7 +74,13 @@ module Console
66
74
  @terminal[:annotation] = @terminal.reset
67
75
  @terminal[:value] = @terminal.style(:blue)
68
76
 
69
- self.register_defaults(@terminal)
77
+ @formatters = {}
78
+ self.register_formatters
79
+ end
80
+
81
+ # This a final output that then writes to an IO object.
82
+ def last_output
83
+ self
70
84
  end
71
85
 
72
86
  attr :io
@@ -80,20 +94,23 @@ module Console
80
94
  @verbose = value
81
95
  end
82
96
 
83
- def register_defaults(terminal)
84
- Event.constants.each do |constant|
85
- klass = Event.const_get(constant)
86
- klass.register(terminal)
97
+ def register_formatters(namespace = Console::Terminal::Formatter)
98
+ namespace.constants.each do |name|
99
+ formatter = namespace.const_get(name)
100
+ @formatters[formatter::KEY] = formatter.new(@terminal)
87
101
  end
88
102
  end
89
103
 
90
104
  UNKNOWN = :unknown
91
105
 
92
- def call(subject = nil, *arguments, name: nil, severity: UNKNOWN, **options, &block)
106
+ def call(subject = nil, *arguments, name: nil, severity: UNKNOWN, event: nil, **options, &block)
107
+ width = @terminal.width
108
+
93
109
  prefix = build_prefix(name || severity.to_s)
94
110
  indent = " " * prefix.size
95
111
 
96
112
  buffer = Buffer.new("#{indent}| ")
113
+ indent_size = buffer.prefix.size
97
114
 
98
115
  format_subject(severity, prefix, subject, buffer)
99
116
 
@@ -109,6 +126,10 @@ module Console
109
126
  end
110
127
  end
111
128
 
129
+ if event
130
+ format_event(event, buffer, width - indent_size)
131
+ end
132
+
112
133
  if options&.any?
113
134
  format_options(options, buffer)
114
135
  end
@@ -118,20 +139,24 @@ module Console
118
139
 
119
140
  protected
120
141
 
142
+ def format_event(event, buffer, width)
143
+ event = event.to_hash
144
+ type = event[:type]
145
+
146
+ if formatter = @formatters[type]
147
+ formatter.format(event, buffer, verbose: @verbose, width: width)
148
+ else
149
+ format_value(::JSON.pretty_generate(event), buffer)
150
+ end
151
+ end
152
+
121
153
  def format_options(options, output)
122
- format_value(options.to_json, output)
154
+ format_value(::JSON.pretty_generate(options), output)
123
155
  end
124
156
 
125
157
  def format_argument(argument, output)
126
- case argument
127
- when Exception
128
- Event::Failure.for(argument).format(output, @terminal, @verbose)
129
- when Event::Generic
130
- argument.format(output, @terminal, @verbose)
131
- else
132
- argument.to_s.each_line do |line|
133
- output.puts line
134
- end
158
+ argument.to_s.each_line do |line|
159
+ output.puts line
135
160
  end
136
161
  end
137
162
 
@@ -149,8 +174,14 @@ module Console
149
174
  buffer = +""
150
175
 
151
176
  if @verbose
152
- if annotation = Fiber.current.annotation and annotation.size > 0
153
- buffer << ": #{@terminal[:annotation]}#{annotation}#{@terminal.reset}"
177
+ if annotation = Fiber.current.annotation
178
+ # While typically annotations should be strings, that is not always the case.
179
+ annotation = annotation.to_s
180
+
181
+ # If the annotation is empty, we don't want to print it, as it will look like a formatting bug.
182
+ if annotation.size > 0
183
+ buffer << ": #{@terminal[:annotation]}#{annotation}#{@terminal.reset}"
184
+ end
154
185
  end
155
186
  end
156
187
 
@@ -193,6 +224,7 @@ module Console
193
224
  string = value.to_s
194
225
 
195
226
  string.each_line do |line|
227
+ line.chomp!
196
228
  output.puts "#{@terminal[:value]}#{line}#{@terminal.reset}"
197
229
  end
198
230
  end
@@ -209,5 +241,17 @@ module Console
209
241
  end
210
242
  end
211
243
  end
244
+
245
+ module Text
246
+ def self.new(output, **options)
247
+ Terminal.new(output, format: Console::Terminal::Text, **options)
248
+ end
249
+ end
250
+
251
+ module XTerm
252
+ def self.new(output, **options)
253
+ Terminal.new(output, format: Console::Terminal::XTerm, **options)
254
+ end
255
+ end
212
256
  end
213
257
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2021-2024, by Samuel Williams.
5
+
6
+ module Console
7
+ module Output
8
+ class Wrapper
9
+ def initialize(delegate, **options)
10
+ @delegate = delegate
11
+ end
12
+
13
+ attr :delegate
14
+
15
+ def last_output
16
+ @delegate.last_output
17
+ end
18
+
19
+ def verbose!(value = true)
20
+ @delegate.verbose!(value)
21
+ end
22
+
23
+ def call(...)
24
+ @delegate.call(...)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,19 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2022, by Samuel Williams.
4
+ # Copyright, 2021-2024, by Samuel Williams.
5
5
 
6
- require_relative 'output/default'
7
- require_relative 'output/json'
8
- require_relative 'output/text'
9
- require_relative 'output/xterm'
10
- require_relative 'output/null'
6
+ require_relative "output/default"
7
+ require_relative "output/serialized"
8
+ require_relative "output/terminal"
9
+ require_relative "output/null"
11
10
 
12
11
  module Console
13
12
  module Output
14
13
  def self.new(output = nil, env = ENV, **options)
15
- if names = env['CONSOLE_OUTPUT']
16
- names = names.split(',').reverse
14
+ if names = env["CONSOLE_OUTPUT"]
15
+ names = names.split(",").reverse
17
16
 
18
17
  names.inject(output) do |output, name|
19
18
  Output.const_get(name).new(output, **options)