console 1.29.2 → 1.30.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 (42) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/console/capture.rb +26 -1
  4. data/lib/console/clock.rb +6 -1
  5. data/lib/console/compatible/logger.rb +30 -1
  6. data/lib/console/config.rb +93 -0
  7. data/lib/console/event/failure.rb +32 -3
  8. data/lib/console/event/generic.rb +14 -0
  9. data/lib/console/event/spawn.rb +38 -6
  10. data/lib/console/event.rb +6 -0
  11. data/lib/console/filter.rb +62 -2
  12. data/lib/console/format/safe.rb +35 -7
  13. data/lib/console/format.rb +3 -0
  14. data/lib/console/interface.rb +18 -10
  15. data/lib/console/logger.rb +22 -37
  16. data/lib/console/output/default.rb +11 -5
  17. data/lib/console/output/failure.rb +9 -2
  18. data/lib/console/output/null.rb +4 -1
  19. data/lib/console/output/sensitive.rb +41 -0
  20. data/lib/console/output/serialized.rb +26 -5
  21. data/lib/console/output/split.rb +11 -0
  22. data/lib/console/output/terminal.rb +66 -14
  23. data/lib/console/output/wrapper.rb +11 -0
  24. data/lib/console/output.rb +11 -0
  25. data/lib/console/progress.rb +55 -8
  26. data/lib/console/resolver.rb +18 -1
  27. data/lib/console/terminal/formatter/failure.rb +17 -5
  28. data/lib/console/terminal/formatter/progress.rb +15 -2
  29. data/lib/console/terminal/formatter/spawn.rb +15 -4
  30. data/lib/console/terminal/formatter.rb +16 -0
  31. data/lib/console/terminal/text.rb +60 -19
  32. data/lib/console/terminal/xterm.rb +16 -1
  33. data/lib/console/terminal.rb +6 -8
  34. data/lib/console/version.rb +1 -1
  35. data/lib/console/warn.rb +1 -1
  36. data/lib/console.rb +1 -0
  37. data/license.md +1 -1
  38. data/readme.md +19 -1
  39. data/releases.md +36 -1
  40. data.tar.gz.sig +0 -0
  41. metadata +5 -8
  42. metadata.gz.sig +0 -0
@@ -6,16 +6,27 @@
6
6
  require "json"
7
7
 
8
8
  module Console
9
+ # @namespace
9
10
  module Format
10
- # This class is used to safely dump objects.
11
- # It will attempt to dump the object using the given format, but if it fails, it will generate a safe version of the object.
11
+ # A safe format for converting objects to strings.
12
+ #
13
+ # Handles issues like circular references and encoding errors.
12
14
  class Safe
15
+ # Create a new safe format.
16
+ #
17
+ # @parameter format [JSON] The format to use for serialization.
18
+ # @parameter limit [Integer] The maximum depth to recurse into objects.
19
+ # @parameter encoding [Encoding] The encoding to use for strings.
13
20
  def initialize(format: ::JSON, limit: 8, encoding: ::Encoding::UTF_8)
14
21
  @format = format
15
22
  @limit = limit
16
23
  @encoding = encoding
17
24
  end
18
25
 
26
+ # Dump the given object to a string.
27
+ #
28
+ # @parameter object [Object] The object to dump.
29
+ # @returns [String] The dumped object.
19
30
  def dump(object)
20
31
  @format.dump(object, @limit)
21
32
  rescue SystemStackError, StandardError => error
@@ -24,6 +35,10 @@ module Console
24
35
 
25
36
  private
26
37
 
38
+ # Filter the backtrace to remove duplicate frames and reduce verbosity.
39
+ #
40
+ # @parameter error [Exception] The exception to filter.
41
+ # @returns [Array(String)] The filtered backtrace.
27
42
  def filter_backtrace(error)
28
43
  frames = error.backtrace
29
44
  filtered = {}
@@ -61,6 +76,13 @@ module Console
61
76
  return frames
62
77
  end
63
78
 
79
+ # Dump the given object to a string, replacing it with a safe representation if there is an error.
80
+ #
81
+ # This is a slow path so we try to avoid it.
82
+ #
83
+ # @parameter object [Object] The object to dump.
84
+ # @parameter error [Exception] The error that occurred while dumping the object.
85
+ # @returns [Hash] The dumped (truncated) object including error details.
64
86
  def safe_dump(object, error)
65
87
  object = safe_dump_recurse(object)
66
88
 
@@ -74,6 +96,10 @@ module Console
74
96
  return object
75
97
  end
76
98
 
99
+ # Replace the given object with a safe truncated representation.
100
+ #
101
+ # @parameter object [Object] The object to replace.
102
+ # @returns [String] The replacement string.
77
103
  def replacement_for(object)
78
104
  case object
79
105
  when Array
@@ -85,15 +111,17 @@ module Console
85
111
  end
86
112
  end
87
113
 
114
+ # Create a new hash with identity comparison.
88
115
  def default_objects
89
116
  Hash.new.compare_by_identity
90
117
  end
91
118
 
92
- # This will recursively generate a safe version of the object.
93
- # Nested hashes and arrays will be transformed recursively.
94
- # Strings will be encoded with the given encoding.
95
- # Primitive values will be returned as-is.
96
- # Other values will be converted using `as_json` if available, otherwise `to_s`.
119
+ # This will recursively generate a safe version of the object. Nested hashes and arrays will be transformed recursively. Strings will be encoded with the given encoding. Primitive values will be returned as-is. Other values will be converted using `as_json` if available, otherwise `to_s`.
120
+ #
121
+ # @parameter object [Object] The object to dump.
122
+ # @parameter limit [Integer] The maximum depth to recurse into objects.
123
+ # @parameter objects [Hash] The objects that have already been visited.
124
+ # @returns [Object] The dumped object as a primitive representation.
97
125
  def safe_dump_recurse(object, limit = @limit, objects = default_objects)
98
126
  if limit <= 0 || objects[object]
99
127
  return replacement_for(object)
@@ -7,6 +7,9 @@ require_relative "format/safe"
7
7
 
8
8
  module Console
9
9
  module Format
10
+ # A safe format for converting objects to strings.
11
+ #
12
+ # @returns [Console::Format::Safe]
10
13
  def self.default
11
14
  Safe.new(format: ::JSON)
12
15
  end
@@ -1,53 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright 2024, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
- require_relative "logger"
6
+ require "fiber/local"
7
+ require_relative "config"
7
8
 
8
9
  module Console
9
10
  # The public logger interface.
10
11
  module Interface
12
+ extend Fiber::Local
13
+
14
+ # Create a new (thread local) logger instance.
15
+ def self.local
16
+ Config::DEFAULT.make_logger
17
+ end
18
+
11
19
  # Get the current logger instance.
12
20
  def logger
13
- Logger.instance
21
+ Interface.instance
14
22
  end
15
23
 
16
24
  # Set the current logger instance.
17
25
  #
18
26
  # The current logger instance is assigned per-fiber.
19
27
  def logger= instance
20
- Logger.instance= instance
28
+ Interface.instance= instance
21
29
  end
22
30
 
23
31
  # Emit a debug log message.
24
32
  def debug(...)
25
- Logger.instance.debug(...)
33
+ Interface.instance.debug(...)
26
34
  end
27
35
 
28
36
  # Emit an informational log message.
29
37
  def info(...)
30
- Logger.instance.info(...)
38
+ Interface.instance.info(...)
31
39
  end
32
40
 
33
41
  # Emit a warning log message.
34
42
  def warn(...)
35
- Logger.instance.warn(...)
43
+ Interface.instance.warn(...)
36
44
  end
37
45
 
38
46
  # Emit an error log message.
39
47
  def error(...)
40
- Logger.instance.error(...)
48
+ Interface.instance.error(...)
41
49
  end
42
50
 
43
51
  # Emit a fatal log message.
44
52
  def fatal(...)
45
- Logger.instance.fatal(...)
53
+ Interface.instance.fatal(...)
46
54
  end
47
55
 
48
56
  # Emit a log message with arbitrary arguments and options.
49
57
  def call(...)
50
- Logger.instance.call(...)
58
+ Interface.instance.call(...)
51
59
  end
52
60
  end
53
61
  end
@@ -8,61 +8,40 @@
8
8
  require_relative "output"
9
9
  require_relative "output/failure"
10
10
 
11
- require_relative "filter"
12
- require_relative "event"
13
- require_relative "resolver"
14
11
  require_relative "progress"
15
-
16
- require "fiber/local"
12
+ require_relative "config"
17
13
 
18
14
  module Console
15
+ # The standard logger interface with support for log levels and verbosity.
16
+ #
17
+ # The log levels are: `debug`, `info`, `warn`, `error`, and `fatal`.
19
18
  class Logger < Filter[debug: 0, info: 1, warn: 2, error: 3, fatal: 4]
20
- extend Fiber::Local
21
-
22
19
  # Set the default log level based on `$DEBUG` and `$VERBOSE`.
23
20
  # You can also specify CONSOLE_LEVEL=debug or CONSOLE_LEVEL=info in environment.
24
21
  # https://mislav.net/2011/06/ruby-verbose-mode/ has more details about how it all fits together.
25
- def self.default_log_level(env = ENV)
22
+ #
23
+ # @parameter env [Hash] The environment to read the log level from.
24
+ # @parameter verbose [Boolean] The verbose flag.
25
+ # @parameter debug [Boolean] The debug flag.
26
+ # @returns [Integer] The default log level.
27
+ def self.default_log_level(env = ENV, verbose: $VERBOSE, debug: $DEBUG)
26
28
  if level = env["CONSOLE_LEVEL"]
27
29
  LEVELS[level.to_sym] || level.to_i
28
- elsif $DEBUG
30
+ elsif debug
29
31
  DEBUG
30
- elsif $VERBOSE.nil?
32
+ elsif verbose.nil?
31
33
  WARN
32
34
  else
33
35
  INFO
34
36
  end
35
37
  end
36
38
 
37
- # Controls verbose output using `$VERBOSE`.
38
- def self.verbose?(env = ENV)
39
- !$VERBOSE.nil? || env["CONSOLE_VERBOSE"]
40
- end
41
-
42
- def self.default_logger(output = $stderr, env = ENV, **options)
43
- if options[:verbose].nil?
44
- options[:verbose] = self.verbose?(env)
45
- end
46
-
47
- if options[:level].nil?
48
- options[:level] = self.default_log_level(env)
49
- end
50
-
51
- output = Output.new(output, env, **options)
52
-
53
- logger = self.new(output, **options)
54
-
55
- Resolver.default_resolver(logger)
56
-
57
- return logger
58
- end
59
-
60
- def self.local
61
- self.default_logger
62
- end
63
-
64
39
  DEFAULT_LEVEL = 1
65
40
 
41
+ # Create a new logger.
42
+ #
43
+ # @parameter output [Console::Output] The output destination.
44
+ # @parameter options [Hash] Additional options.
66
45
  def initialize(output, **options)
67
46
  # This is the expected default behaviour, but it may be nice to have a way to override it.
68
47
  output = Output::Failure.new(output, **options)
@@ -70,6 +49,12 @@ module Console
70
49
  super(output, **options)
71
50
  end
72
51
 
52
+ # Create a progress indicator for the given subject.
53
+ #
54
+ # @parameter subject [String] The subject of the progress indicator.
55
+ # @parameter total [Integer] The total number of items to process.
56
+ # @parameter options [Hash] Additional options passed to {Progress}.
57
+ # @returns [Progress] The progress indicator.
73
58
  def progress(subject, total, **options)
74
59
  options[:severity] ||= :info
75
60
 
@@ -9,14 +9,20 @@ require_relative "failure"
9
9
 
10
10
  module Console
11
11
  module Output
12
+ # Default output format selection.
12
13
  module Default
13
- def self.new(output, **options)
14
- output ||= $stderr
14
+ # Create a new output format based on the given stream.
15
+ #
16
+ # @parameter io [IO] The output stream.
17
+ # @parameter options [Hash] Additional options to customize the output.
18
+ # @returns [Console::Output::Terminal | Console::Output::Serialized] The output instance, depending on whether the `io` is a terminal or not.
19
+ def self.new(stream, **options)
20
+ stream ||= $stderr
15
21
 
16
- if output.tty?
17
- output = Terminal.new(output, **options)
22
+ if stream.tty?
23
+ output = Terminal.new(stream, **options)
18
24
  else
19
- output = Serialized.new(output, **options)
25
+ output = Serialized.new(stream, **options)
20
26
  end
21
27
 
22
28
  return output
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2024, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "wrapper"
7
7
  require_relative "../event/failure"
@@ -10,11 +10,18 @@ module Console
10
10
  module Output
11
11
  # A wrapper for outputting failure messages, which can include exceptions.
12
12
  class Failure < Wrapper
13
+ # Create a new failure output wrapper.
13
14
  def initialize(output, **options)
14
15
  super(output, **options)
15
16
  end
16
17
 
17
18
  # The exception must be either the last argument or passed as an option.
19
+ #
20
+ # @parameter subject [String] The subject of the message.
21
+ # @parameter arguments [Array] The arguments to output.
22
+ # @parameter exception [Exception] The exception to output.
23
+ # @parameter options [Hash] Additional options to pass to the output.
24
+ # @parameter block [Proc] An optional block to pass to the output.
18
25
  def call(subject = nil, *arguments, exception: nil, **options, &block)
19
26
  if exception.nil?
20
27
  last = arguments.last
@@ -28,7 +35,7 @@ module Console
28
35
  options[:exception] = exception
29
36
  end
30
37
 
31
- super(subject, *arguments, **options)
38
+ super(subject, *arguments, **options, &block)
32
39
  end
33
40
  end
34
41
  end
@@ -5,16 +5,19 @@
5
5
 
6
6
  module Console
7
7
  module Output
8
+ # A null output that does nothing.
8
9
  class Null
10
+ # Create a new null output.
9
11
  def initialize(...)
10
12
  end
11
13
 
14
+ # The last output is always self.
12
15
  def last_output
13
16
  self
14
17
  end
15
18
 
19
+ # Do nothing.
16
20
  def call(...)
17
- # Do nothing.
18
21
  end
19
22
  end
20
23
  end
@@ -7,7 +7,9 @@ require_relative "wrapper"
7
7
 
8
8
  module Console
9
9
  module Output
10
+ # Redact sensitive information from output.
10
11
  class Sensitive < Wrapper
12
+ # Default redaction pattern.
11
13
  REDACT = /
12
14
  phone
13
15
  | email
@@ -34,28 +36,52 @@ module Console
34
36
  | password
35
37
  /xi
36
38
 
39
+ # Create a new sensitive output wrapper.
40
+ #
41
+ # @parameter output [Console::Output] The output to wrap.
42
+ # @parameter redact [Regexp] The pattern to redact.
43
+ # @parameter options [Hash] Additional options to pass to the output.
37
44
  def initialize(output, redact: REDACT, **options)
38
45
  super(output, **options)
39
46
 
40
47
  @redact = redact
41
48
  end
42
49
 
50
+ # Check if the given text should be redacted.
51
+ #
52
+ # @parameter text [String] The text to check.
53
+ # @returns [Boolean] Whether the text should be redacted.
43
54
  def redact?(text)
44
55
  text.match?(@redact)
45
56
  end
46
57
 
58
+ # Redact sensitive information from a hash.
59
+ #
60
+ # @parameter arguments [Hash] The hash to redact.
61
+ # @parameter filter [Proc] An optional filter to apply to redacted text.
62
+ # @returns [Hash] The redacted hash.
47
63
  def redact_hash(arguments, filter)
48
64
  arguments.transform_values do |value|
49
65
  redact(value, filter)
50
66
  end
51
67
  end
52
68
 
69
+ # Redact sensitive information from an array.
70
+ #
71
+ # @parameter array [Array] The array to redact.
72
+ # @parameter filter [Proc] An optional filter to apply to redacted text.
73
+ # @returns [Array] The redacted array.
53
74
  def redact_array(array, filter)
54
75
  array.map do |value|
55
76
  redact(value, filter)
56
77
  end
57
78
  end
58
79
 
80
+ # Redact sensitive information from the given argument.
81
+ #
82
+ # @parameter argument [String | Array | Hash] The argument to redact.
83
+ # @parameter filter [Proc] An optional filter to apply to redacted text.
84
+ # @returns [String | Array | Hash] The redacted argument.
59
85
  def redact(argument, filter)
60
86
  case argument
61
87
  when String
@@ -75,17 +101,32 @@ module Console
75
101
  end
76
102
  end
77
103
 
104
+ # A simple filter for redacting sensitive information.
78
105
  class Filter
106
+ # Create a new filter.
107
+ #
108
+ # @parameter substitutions [Hash] The substitutions to apply.
79
109
  def initialize(substitutions)
80
110
  @substitutions = substitutions
81
111
  @pattern = Regexp.union(substitutions.keys)
82
112
  end
83
113
 
114
+ # Apply the filter to the given text. This will replace all occurrences of the pattern with the corresponding substitution.
115
+ #
116
+ # @parameter text [String] The text to filter.
117
+ # @returns [String] The filtered text.
84
118
  def call(text)
85
119
  text.gsub(@pattern, @substitutions)
86
120
  end
87
121
  end
88
122
 
123
+ # Write a message to the output, filtering sensitive information if necessary.
124
+ #
125
+ # @parameter subject [String] The subject of the message.
126
+ # @parameter arguments [Array] The arguments to output.
127
+ # @parameter sensitive [Boolean | Filter | Hash] Whether to filter sensitive information.
128
+ # @parameter options [Hash] Additional options to pass to the output.
129
+ # @parameter block [Proc] An optional block to pass to the output.
89
130
  def call(subject = nil, *arguments, sensitive: true, **options, &block)
90
131
  if sensitive
91
132
  if sensitive.respond_to?(:call)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "../format"
7
7
  require "time"
@@ -9,9 +9,15 @@ require "fiber/annotation"
9
9
 
10
10
  module Console
11
11
  module Output
12
+ # Serialize log messages in a structured format.
12
13
  class Serialized
13
- def initialize(io, format: Format.default, **options)
14
- @io = io
14
+ # Create a new serialized output.
15
+ #
16
+ # @parameter io [IO] The output stream.
17
+ # @parameter format [Console::Format] The format to use for serializing log messages.
18
+ # @parameter options [Hash] Additional options to customize the output.
19
+ def initialize(stream, format: Format.default, **options)
20
+ @stream = stream
15
21
  @format = format
16
22
  end
17
23
 
@@ -20,13 +26,27 @@ module Console
20
26
  self
21
27
  end
22
28
 
23
- attr :io
29
+ # @attribute [IO] The output stream.
30
+ attr :stream
31
+
32
+ # @attribute [Console::Format] The format to use for serializing log messages.
24
33
  attr :format
25
34
 
35
+ # Serialize the given record.
36
+ #
37
+ # @parameter record [Hash] The record to serialize.
38
+ # @returns [String] The serialized record.
26
39
  def dump(record)
27
40
  @format.dump(record)
28
41
  end
29
42
 
43
+ # Output the given log message.
44
+ #
45
+ # @parameter subject [String] The subject of the log message.
46
+ # @parameter arguments [Array] The arguments to log.
47
+ # @parameter severity [Symbol] The severity of the log message.
48
+ # @parameter options [Hash] Additional options.
49
+ # @parameter block [Proc] An optional block used to generate the log message.
30
50
  def call(subject = nil, *arguments, severity: UNKNOWN, **options, &block)
31
51
  record = {
32
52
  time: Time.now.iso8601,
@@ -68,10 +88,11 @@ module Console
68
88
 
69
89
  record.update(options)
70
90
 
71
- @io.puts(self.dump(record))
91
+ @stream.write(self.dump(record) << "\n")
72
92
  end
73
93
  end
74
94
 
95
+ # @deprecated This is a legacy constant, please use `Serialized` instead.
75
96
  JSON = Serialized
76
97
  end
77
98
  end
@@ -5,19 +5,30 @@
5
5
 
6
6
  module Console
7
7
  module Output
8
+ # Split output into multiple outputs.
8
9
  class Split
10
+ # Create a new split output.
11
+ #
12
+ # @parameter outputs [Array(Console::Output)] The outputs to split into.
9
13
  def self.[](*outputs)
10
14
  self.new(outputs)
11
15
  end
12
16
 
17
+ # Create a new split output.
18
+ #
19
+ # @parameter outputs [Array(Console::Output)] The outputs to split into.
13
20
  def initialize(outputs)
14
21
  @outputs = outputs
15
22
  end
16
23
 
24
+ # Set the verbose flag for all outputs.
25
+ #
26
+ # @parameter value [Boolean] The new value.
17
27
  def verbose!(value = true)
18
28
  @outputs.each{|output| output.verbose!(value)}
19
29
  end
20
30
 
31
+ # Invoke the outputs. If a block is used, it may be invoked multiple times.
21
32
  def call(...)
22
33
  @outputs.each do |output|
23
34
  output.call(...)