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.
- checksums.yaml +7 -0
- data/NEWS.md +3 -0
- data/README.md +3 -0
- data/lib/nanoc-cli.rb +3 -0
- data/lib/nanoc/cli.rb +237 -0
- data/lib/nanoc/cli/ansi_string_colorizer.rb +30 -0
- data/lib/nanoc/cli/cleaning_stream.rb +160 -0
- data/lib/nanoc/cli/command_runner.rb +74 -0
- data/lib/nanoc/cli/commands/compile.rb +57 -0
- data/lib/nanoc/cli/commands/create-site.rb +257 -0
- data/lib/nanoc/cli/commands/nanoc.rb +42 -0
- data/lib/nanoc/cli/commands/prune.rb +49 -0
- data/lib/nanoc/cli/commands/shell.rb +57 -0
- data/lib/nanoc/cli/commands/show-data.rb +185 -0
- data/lib/nanoc/cli/commands/show-plugins.rb +97 -0
- data/lib/nanoc/cli/commands/view.rb +68 -0
- data/lib/nanoc/cli/compile_listeners/abstract.rb +58 -0
- data/lib/nanoc/cli/compile_listeners/aggregate.rb +50 -0
- data/lib/nanoc/cli/compile_listeners/debug_printer.rb +100 -0
- data/lib/nanoc/cli/compile_listeners/diff_generator.rb +101 -0
- data/lib/nanoc/cli/compile_listeners/file_action_printer.rb +80 -0
- data/lib/nanoc/cli/compile_listeners/timing_recorder.rb +170 -0
- data/lib/nanoc/cli/error_handler.rb +365 -0
- data/lib/nanoc/cli/logger.rb +77 -0
- data/lib/nanoc/cli/stack_trace_writer.rb +51 -0
- data/lib/nanoc/cli/stream_cleaners/abstract.rb +23 -0
- data/lib/nanoc/cli/stream_cleaners/ansi_colors.rb +15 -0
- data/lib/nanoc/cli/stream_cleaners/utf8.rb +20 -0
- data/lib/nanoc/cli/transform.rb +18 -0
- data/lib/nanoc/cli/version.rb +7 -0
- metadata +127 -0
@@ -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
|