nanoc-cli 4.11.13

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.
@@ -0,0 +1,365 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc::CLI
4
+ # Catches errors and prints nice diagnostic messages, then exits.
5
+ #
6
+ # @api private
7
+ class ErrorHandler
8
+ # Enables error handling in the given block.
9
+ #
10
+ # @return [void]
11
+ def self.handle_while(exit_on_error: true)
12
+ if @disabled
13
+ yield
14
+ else
15
+ new.handle_while(exit_on_error: exit_on_error) { yield }
16
+ end
17
+ end
18
+
19
+ # Disables error handling. This is used by the test cases to prevent error
20
+ # from being handled by the CLI while tests are running.
21
+ def self.disable
22
+ @disabled = true
23
+ end
24
+
25
+ # Re-enables error handling after it was disabled. This is used by the test
26
+ # cases to prevent error from being handled by the CLI while tests are
27
+ # running.
28
+ def self.enable
29
+ @disabled = false
30
+ end
31
+
32
+ # Enables error handling in the given block. This method should not be
33
+ # called directly; use {Nanoc::CLI::ErrorHandler.handle_while} instead.
34
+ #
35
+ # @return [void]
36
+ def handle_while(exit_on_error:)
37
+ # Set exit handler
38
+ %w[INT TERM].each do |signal|
39
+ Signal.trap(signal) do
40
+ puts
41
+ exit!(0)
42
+ end
43
+ end
44
+
45
+ # Set stack trace dump handler
46
+ if !defined?(RUBY_ENGINE) || RUBY_ENGINE != 'jruby'
47
+ begin
48
+ Signal.trap('USR1') do
49
+ puts 'Caught USR1; dumping a stack trace'
50
+ puts caller.map { |i| " #{i}" }.join("\n")
51
+ end
52
+ rescue ArgumentError
53
+ end
54
+ end
55
+
56
+ # Run
57
+ yield
58
+ rescue Interrupt
59
+ exit(1)
60
+ rescue StandardError, ScriptError => e
61
+ handle_error(e, exit_on_error: exit_on_error)
62
+ end
63
+
64
+ def handle_error(error, exit_on_error:)
65
+ if trivial?(error)
66
+ $stderr.puts
67
+ $stderr.puts "Error: #{error.message}"
68
+ resolution = resolution_for(error)
69
+ if resolution
70
+ $stderr.puts
71
+ $stderr.puts resolution
72
+ end
73
+ else
74
+ print_error(error)
75
+ end
76
+ exit(1) if exit_on_error
77
+ end
78
+
79
+ # Prints the given error to stderr. Includes message, possible resolution
80
+ # (see {#resolution_for}), compilation stack, backtrace, etc.
81
+ #
82
+ # @param [Error] error The error that should be described
83
+ #
84
+ # @return [void]
85
+ def self.print_error(error)
86
+ new.print_error(error)
87
+ end
88
+
89
+ # Prints the given error to stderr. Includes message, possible resolution
90
+ # (see {#resolution_for}), compilation stack, backtrace, etc.
91
+ #
92
+ # @param [Error] error The error that should be described
93
+ #
94
+ # @return [void]
95
+ def print_error(error)
96
+ write_compact_error(error, $stderr)
97
+
98
+ File.open('crash.log', 'w') do |io|
99
+ cio = Nanoc::CLI.wrap_in_cleaning_stream(io)
100
+ cio.add_stream_cleaner(::Nanoc::CLI::StreamCleaners::ANSIColors)
101
+ write_verbose_error(error, cio)
102
+ end
103
+ end
104
+
105
+ # Writes a compact representation of the error, suitable for a terminal, on
106
+ # the given stream (probably stderr).
107
+ #
108
+ # @param [Error] error The error that should be described
109
+ #
110
+ # @param [IO] stream The stream to write the description too
111
+ #
112
+ # @return [void]
113
+ def write_compact_error(error, stream)
114
+ stream.puts
115
+ stream.puts 'Captain! We’ve been hit!'
116
+
117
+ if forwards_stack_trace?
118
+ write_stack_trace(stream, error)
119
+ write_error_message(stream, error)
120
+ write_item_rep(stream, error)
121
+ else
122
+ write_error_message(stream, error)
123
+ write_item_rep(stream, error)
124
+ write_stack_trace(stream, error)
125
+ end
126
+
127
+ stream.puts
128
+ stream.puts 'A detailed crash log has been written to ./crash.log.'
129
+ end
130
+
131
+ # Writes a verbose representation of the error on the given stream.
132
+ #
133
+ # @param [Error] error The error that should be described
134
+ #
135
+ # @param [IO] stream The stream to write the description too
136
+ #
137
+ # @return [void]
138
+ def write_verbose_error(error, stream)
139
+ stream.puts "Crashlog created at #{Time.now}"
140
+
141
+ write_error_message(stream, error, verbose: true)
142
+ write_item_rep(stream, error, verbose: true)
143
+ write_stack_trace(stream, error, verbose: true)
144
+ write_version_information(stream, verbose: true)
145
+ write_system_information(stream, verbose: true)
146
+ write_installed_gems(stream, verbose: true)
147
+ write_gemfile_lock(stream, verbose: true)
148
+ write_load_paths(stream, verbose: true)
149
+ end
150
+
151
+ # @api private
152
+ def forwards_stack_trace?
153
+ ruby_version.start_with?('2.5')
154
+ end
155
+
156
+ # @api private
157
+ def trivial?(error)
158
+ case error
159
+ when Nanoc::Core::TrivialError, Errno::EADDRINUSE
160
+ true
161
+ when LoadError
162
+ GEM_NAMES.key?(gem_name_from_load_error(error))
163
+ else
164
+ false
165
+ end
166
+ end
167
+
168
+ protected
169
+
170
+ # @return [Hash<String, Array>] A hash containing the gem names as keys and gem versions as value
171
+ def gems_and_versions
172
+ gems = {}
173
+ Gem::Specification.find_all.sort_by { |s| [s.name, s.version] }.each do |spec|
174
+ gems[spec.name] ||= []
175
+ gems[spec.name] << spec.version.to_s
176
+ end
177
+ gems
178
+ end
179
+
180
+ # A hash that contains the name of the gem for a given required file. If a
181
+ # `#require` fails, the gem name is looked up in this hash.
182
+ GEM_NAMES = {
183
+ 'adsf' => 'adsf',
184
+ 'asciidoctor' => 'asciidoctor',
185
+ 'bluecloth' => 'bluecloth',
186
+ 'builder' => 'builder',
187
+ 'coderay' => 'coderay',
188
+ 'coffee-script' => 'coffee-script',
189
+ 'cri' => 'cri',
190
+ 'erubi' => 'erubi',
191
+ 'erubis' => 'erubis',
192
+ 'escape' => 'escape',
193
+ 'fog' => 'fog',
194
+ 'haml' => 'haml',
195
+ 'json' => 'json',
196
+ 'kramdown' => 'kramdown',
197
+ 'less' => 'less',
198
+ 'listen' => 'listen',
199
+ 'markaby' => 'markaby',
200
+ 'maruku' => 'maruku',
201
+ 'mime/types' => 'mime-types',
202
+ 'mustache' => 'mustache',
203
+ 'nanoc/live' => 'nanoc-live',
204
+ 'nokogiri' => 'nokogiri',
205
+ 'nokogumbo' => 'nokogumbo',
206
+ 'pandoc-ruby' => 'pandoc-ruby',
207
+ 'pry' => 'pry',
208
+ 'rack' => 'rack',
209
+ 'rack/cache' => 'rack-cache',
210
+ 'rainpress' => 'rainpress',
211
+ 'rdiscount' => 'rdiscount',
212
+ 'redcarpet' => 'redcarpet',
213
+ 'redcloth' => 'RedCloth',
214
+ 'ruby-handlebars' => 'hbs',
215
+ 'rubypants' => 'rubypants',
216
+ 'sass' => 'sass',
217
+ 'slim' => 'slim',
218
+ 'typogruby' => 'typogruby',
219
+ 'uglifier' => 'uglifier',
220
+ 'w3c_validators' => 'w3c_validators',
221
+ 'yuicompressor' => 'yuicompressor',
222
+ }.freeze
223
+
224
+ # Attempts to find a resolution for the given error, or nil if no
225
+ # resolution can be automatically obtained.
226
+ #
227
+ # @param [Error] error The error to find a resolution for
228
+ #
229
+ # @return [String] The resolution for the given error
230
+ def resolution_for(error)
231
+ error = unwrap_error(error)
232
+
233
+ case error
234
+ when LoadError
235
+ gem_name = gem_name_from_load_error(error)
236
+
237
+ if gem_name
238
+ if using_bundler?
239
+ <<~RES
240
+ 1. Add `gem '#{gem_name}'` to your Gemfile
241
+ 2. Run `bundle install`
242
+ 3. Re-run this command
243
+ RES
244
+ else
245
+ "Install the '#{gem_name}' gem using `gem install #{gem_name}`."
246
+ end
247
+ end
248
+ when RuntimeError
249
+ if /^can't modify frozen/.match?(error.message)
250
+ 'You attempted to modify immutable data. Some data cannot ' \
251
+ 'be modified once compilation has started. Such data includes ' \
252
+ 'content and attributes of items and layouts, and filter arguments.'
253
+ end
254
+ when Errno::EADDRINUSE
255
+ 'There already is a server running. Either shut down that one, or ' \
256
+ 'specify a different port to run this server on.'
257
+ end
258
+ end
259
+
260
+ def gem_name_from_load_error(error)
261
+ matches = error.message.match(/(no such file to load|cannot load such file) -- ([^\s]+)/)
262
+ return nil if matches.nil?
263
+
264
+ GEM_NAMES[matches[2]]
265
+ end
266
+
267
+ def using_bundler?
268
+ defined?(Bundler) && Bundler::SharedHelpers.in_bundle?
269
+ end
270
+
271
+ def ruby_version
272
+ RUBY_VERSION
273
+ end
274
+
275
+ def write_section_header(stream, title, verbose: false)
276
+ stream.puts
277
+
278
+ if verbose
279
+ stream.puts '===== ' + title.upcase + ':'
280
+ stream.puts
281
+ end
282
+ end
283
+
284
+ def write_error_message(stream, error, verbose: false)
285
+ write_section_header(stream, 'Message', verbose: verbose)
286
+
287
+ error = unwrap_error(error)
288
+
289
+ message = "#{error.class}: #{message_for_error(error)}"
290
+ unless verbose
291
+ message = "\e[1m\e[31m" + message + "\e[0m"
292
+ end
293
+ stream.puts message
294
+ resolution = resolution_for(error)
295
+ stream.puts resolution.to_s if resolution
296
+ end
297
+
298
+ def message_for_error(error)
299
+ case error
300
+ when JsonSchema::AggregateError
301
+ "\n" + error.errors.map { |e| " * #{e.pointer}: #{e.message}" }.join("\n")
302
+ else
303
+ error.message
304
+ end
305
+ end
306
+
307
+ def write_item_rep(stream, error, verbose: false)
308
+ return unless error.is_a?(Nanoc::Core::Errors::CompilationError)
309
+
310
+ write_section_header(stream, 'Item being compiled', verbose: verbose)
311
+
312
+ item_rep = error.item_rep
313
+ stream.puts "Current item: #{item_rep.item.identifier} (#{item_rep.name.inspect} representation)"
314
+ end
315
+
316
+ def write_stack_trace(stream, error, verbose: false)
317
+ write_section_header(stream, 'Stack trace', verbose: verbose)
318
+
319
+ writer = Nanoc::CLI::StackTraceWriter.new(stream, forwards: forwards_stack_trace?)
320
+ writer.write(unwrap_error(error), verbose: verbose)
321
+ end
322
+
323
+ def write_version_information(stream, verbose: false)
324
+ write_section_header(stream, 'Version information', verbose: verbose)
325
+ stream.puts Nanoc::Core.version_information
326
+ end
327
+
328
+ def write_system_information(stream, verbose: false)
329
+ uname = `uname -a`
330
+ write_section_header(stream, 'System information', verbose: verbose)
331
+ stream.puts uname
332
+ rescue Errno::ENOENT
333
+ end
334
+
335
+ def write_installed_gems(stream, verbose: false)
336
+ write_section_header(stream, 'Installed gems', verbose: verbose)
337
+ gems_and_versions.each do |g|
338
+ stream.puts " #{g.first} #{g.last.join(', ')}"
339
+ end
340
+ end
341
+
342
+ def write_gemfile_lock(stream, verbose: false)
343
+ if File.exist?('Gemfile.lock')
344
+ write_section_header(stream, 'Gemfile.lock', verbose: verbose)
345
+ stream.puts File.read('Gemfile.lock')
346
+ end
347
+ end
348
+
349
+ def write_load_paths(stream, verbose: false)
350
+ write_section_header(stream, 'Load paths', verbose: verbose)
351
+ $LOAD_PATH.each_with_index do |i, index|
352
+ stream.puts " #{index}. #{i}"
353
+ end
354
+ end
355
+
356
+ def unwrap_error(e)
357
+ case e
358
+ when Nanoc::Core::Errors::CompilationError
359
+ e.unwrap
360
+ else
361
+ e
362
+ end
363
+ end
364
+ end
365
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module CLI
5
+ # Nanoc::CLI::Logger is a singleton class responsible for generating
6
+ # feedback in the terminal.
7
+ #
8
+ # @api private
9
+ class Logger
10
+ # Maps actions (`:create`, `:update`, `:identical`, `:cached`, `:skip` and `:delete`)
11
+ # onto their ANSI color codes.
12
+ ACTION_COLORS = {
13
+ create: "\e[32m", # green
14
+ update: "\e[33m", # yellow
15
+ identical: '', # (nothing)
16
+ cached: '', # (nothing)
17
+ skip: '', # (nothing)
18
+ delete: "\e[31m", # red
19
+ }.freeze
20
+
21
+ include Singleton
22
+
23
+ # Returns the log level, which can be :high, :low or :off (which will log
24
+ # all messages, only high-priority messages, or no messages at all,
25
+ # respectively).
26
+ #
27
+ # @return [Symbol] The log level
28
+ attr_accessor :level
29
+
30
+ def initialize
31
+ @level = :high
32
+ @mutex = Mutex.new
33
+ end
34
+
35
+ # Logs a file-related action.
36
+ #
37
+ # @param [:high, :low] level The importance of this action
38
+ #
39
+ # @param [:create, :update, :identical, :cached, :skip, :delete] action The kind of file action
40
+ #
41
+ # @param [String] name The name of the file the action was performed on
42
+ #
43
+ # @return [void]
44
+ def file(level, action, name, duration = nil)
45
+ log(
46
+ level,
47
+ format(
48
+ '%s%12s%s %s%s',
49
+ ACTION_COLORS[action.to_sym],
50
+ action,
51
+ "\e[0m",
52
+ duration.nil? ? '' : format('[%2.2fs] ', duration),
53
+ name,
54
+ ),
55
+ )
56
+ end
57
+
58
+ # Logs a message.
59
+ #
60
+ # @param [:high, :low] level The importance of this message
61
+ #
62
+ # @param [String] message The message to be logged
63
+ #
64
+ # @param [#puts] io The stream to which the message should be written
65
+ #
66
+ # @return [void]
67
+ def log(level, message, io = $stdout)
68
+ return if @level == :off
69
+ return if @level != :low && @level != level
70
+
71
+ @mutex.synchronize do
72
+ io.puts(message)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module CLI
5
+ class StackTraceWriter
6
+ def initialize(stream, forwards:)
7
+ @stream = stream
8
+ @forwards = forwards
9
+ end
10
+
11
+ def write(error, verbose:)
12
+ if @forwards
13
+ write_forwards(error, verbose: verbose)
14
+ else
15
+ write_backwards(error, verbose: verbose)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def write_backwards(error, verbose:)
22
+ count = verbose ? -1 : 10
23
+
24
+ error.backtrace[0...count].each_with_index do |item, index|
25
+ @stream.puts " #{index}. #{item}"
26
+ end
27
+
28
+ if !verbose && error.backtrace.size > count
29
+ @stream.puts " ... #{error.backtrace.size - count} lines omitted (see crash.log for details)"
30
+ end
31
+ end
32
+
33
+ def write_forwards(error, verbose:)
34
+ count = 10
35
+ backtrace = verbose ? error.backtrace : error.backtrace.take(count)
36
+
37
+ if !verbose && error.backtrace.size > count
38
+ @stream.puts " ... #{error.backtrace.size - count} lines omitted (see crash.log for details)"
39
+ end
40
+
41
+ backtrace.each_with_index.to_a.reverse_each do |(item, index)|
42
+ if index.zero?
43
+ @stream.puts " #{item}"
44
+ else
45
+ @stream.puts " #{index}. from #{item}"
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end