shell_helpers 0.6.0 → 0.7.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.
@@ -1,9 +1,234 @@
1
1
  # vim: foldmethod=marker
2
2
  #From methadone (cli_logger.rb, cli_logging.rb, last import: 4626a2bca9b6e54077a06a0f8e11a04fadc6e7ae; 2017-01-19)
3
3
  require 'logger'
4
+ require 'simplecolor'
4
5
 
5
6
  module ShellHelpers
6
- # CLILogger {{{
7
+ class ColorFormatter < Logger::Formatter #{{{1
8
+ CLI_COLORS={
9
+ mark: [:bold],
10
+ success: [:green, :bold],
11
+ important: [:blue, :bold],
12
+ warn: [:yellow, :bold],
13
+ error: [:red, :bold],
14
+ fatal: [:red, :bold]
15
+ }
16
+
17
+ def self.create(type=:default)
18
+ return type if type.respond_to?(:call)
19
+ logger=self.new
20
+ case type
21
+ when :blank
22
+ logger.format=BLANK_FORMAT
23
+ when :color
24
+ logger.cli_colors=CLI_COLORS
25
+ logger.format=BLANK_FORMAT
26
+ when :color_info
27
+ logger.cli_colors=CLI_COLORS
28
+ when :none
29
+ logger.format=""
30
+ end
31
+ logger
32
+ end
33
+
34
+ def format_severity(severity)
35
+ # sev_name = Logger::SEV_LABEL[severity] || 'ANY'
36
+ sev_name=severity.to_s.upcase
37
+ sev_short=sev_name[0..0]
38
+ [sev_name, sev_short]
39
+ end
40
+
41
+ attr_accessor :cli_colors
42
+
43
+ BLANK_FORMAT = "%{msg}\n"
44
+ # "%s, [%s#%d] %5s -- %s: %s\n"
45
+ DEFAULT_FORMAT = "%{severity_short}, [%{date}#%<pid>d] %<severity>s9 -- %{progname}: %{msg}\n"
46
+ attr_writer :format
47
+ def format
48
+ @format ||= DEFAULT_FORMAT
49
+ end
50
+
51
+ private def get_colors(severity, color: [], **_kwds)
52
+ if cli_colors.nil? #no colors at all
53
+ return []
54
+ end
55
+ colors=[*color]
56
+ unless severity.is_a?(Numeric)
57
+ colors=[*cli_colors[severity.to_sym]]+colors
58
+ end
59
+ colors
60
+ end
61
+
62
+ def format_msg(msg_infos, colors: [])
63
+ msg_infos[:msg]=SimpleColor[msg_infos[:msg], *colors]
64
+ format % msg_infos
65
+ end
66
+
67
+ def call(severity, time, progname, msg, **kwds)
68
+ colors=get_colors(severity, **kwds)
69
+ severity_short, severity_name=format_severity(severity)
70
+ format_msg( {severity_short: severity_short,
71
+ date: format_datetime(time),
72
+ pid: $$,
73
+ severity: severity_name,
74
+ progname: progname,
75
+ msg: msg2str(msg)}, colors: colors)
76
+ end
77
+ end
78
+
79
+ # like Logger but with more levels
80
+ class ColorLogger < ::Logger #{{{1
81
+ ColorLoggerError=Class.new(StandardError)
82
+ WrongLevel=Class.new(ColorLoggerError)
83
+ module Levels
84
+ #note Logger::Severity is included into Logger, so we can access the severity levels directly
85
+ DEBUG1=0 #=DEBUG
86
+ DEBUG2=-0.1
87
+ DEBUG3=-0.2
88
+ IMPORTANT=1.5 #between warning and info
89
+ SUCCESS=1.3 #between warning and info
90
+ VERBOSE=0.9
91
+ VERBOSE1=0.9
92
+ VERBOSE2=0.8
93
+ VERBOSE3=0.7
94
+ QUIET=-9
95
+ DEBUG = Logger::DEBUG # 0
96
+ INFO = Logger::INFO # 1
97
+ WARN = Logger::WARN # 2
98
+ ERROR = Logger::ERROR # 3
99
+ FATAL = Logger::FATAL # 4
100
+ UNKNOWN = Logger::UNKNOWN # 5
101
+ end
102
+
103
+ LOG_LEVELS=
104
+ {
105
+ quiet: Levels::QUIET,
106
+ debug3: Levels::DEBUG3,
107
+ debug2: Levels::DEBUG2,
108
+ debug1: Levels::DEBUG1,
109
+ debug: Levels::DEBUG, #0
110
+ verbose: Levels::VERBOSE,
111
+ verbose1: Levels::VERBOSE1,
112
+ verbose2: Levels::VERBOSE2,
113
+ verbose3: Levels::VERBOSE3,
114
+ info: Levels::INFO, #1
115
+ mark: :info,
116
+ success: Levels::SUCCESS,
117
+ important: Levels::IMPORTANT,
118
+ warn: Levels::WARN, #2
119
+ error: Levels::ERROR, #3
120
+ fatal: Levels::FATAL, #4
121
+ unknown: Levels::UNKNOWN, #5
122
+ }
123
+
124
+ def log_levels
125
+ @levels ||= LOG_LEVELS.dup
126
+ @levels
127
+ end
128
+
129
+ def severity(severity, default_lvl: @default_lvl, quiet_lvl: @quiet_lvl, **_opts)
130
+ severity ||= :unknown
131
+ severity=default_lvl if severity == true
132
+ severity=quiet_lvl if severity == false
133
+ severity
134
+ end
135
+
136
+ def severity_lvl(severity, **opts)
137
+ severity=severity(severity, **opts)
138
+ if severity.is_a?(Numeric)
139
+ return severity
140
+ else
141
+ sev=severity.to_s.downcase.to_sym
142
+ if log_levels.key?(sev)
143
+ return severity_lvl(log_levels[sev])
144
+ else
145
+ raise WrongLevel.new(severity)
146
+ end
147
+ end
148
+ end
149
+
150
+ attr_accessor :default_lvl, :verbose_lvl, :quiet_lvl, :default_formatter
151
+
152
+ def initialize(*args, levels: {}, default_lvl: :info, level: default_lvl, verbose_lvl: :verbose, quiet_lvl: :unknown, default_formatter: :color, **kwds)
153
+ @default_lvl=default_lvl
154
+ @verbose_lvl=verbose_lvl
155
+ @quiet_lvl=quiet_lvl
156
+ super(*args, level: severity_lvl(level), **kwds)
157
+ @default_formatter = ColorFormatter.create(default_formatter)
158
+ @level=severity_lvl(@default_lvl)
159
+ klass=self.singleton_class
160
+ levels=log_levels.merge!(levels)
161
+ levels.keys.each do |lvl|
162
+ klass.define_method(lvl.to_sym) do |msg=nil, **opts, &block|
163
+ add(lvl.to_sym, msg, **opts, &block)
164
+ end
165
+ klass.define_method("#{lvl}?".to_sym) do
166
+ @level <= severity_lvl(lvl)
167
+ end
168
+ end
169
+ yield self, @default_formatter if block_given?
170
+ end
171
+
172
+ def datetime_format=(datetime_format)
173
+ @default_formatter.datetime_format = datetime_format if @default_formatter.respond_to?(:datetime_format)
174
+ @formatter.datetime_format = datetime_format if defined? @formatter and @formatter.respond_to?(:datetime_format)
175
+ end
176
+
177
+ def datetime_format
178
+ @default_formatter.datetime_format if @default_formatter.respond_to?(:datetime_format)
179
+ end
180
+
181
+ def formatter=(form)
182
+ if form.nil?
183
+ super
184
+ else
185
+ @formatter=get_formatter(form)
186
+ end
187
+ end
188
+
189
+ def get_formatter(form=nil)
190
+ if form.nil?
191
+ @formatter || @default_formatter
192
+ else
193
+ formatter=ColorFormatter.create(form)
194
+ formatter.datetime_format = @default_formatter.datetime_format if formatter.respond_to?(:datetime_format) and @default_formatter.respond_to?(:datetime_format)
195
+ formatter
196
+ end
197
+ end
198
+
199
+ def format_message(severity, datetime, progname, msg, formatter: nil, **opts)
200
+ get_formatter(formatter).call(severity, datetime, progname, msg, **opts)
201
+ end
202
+
203
+ # log with given security. Also accepts 'true'
204
+ def add(severity, message = nil, progname: @progname, callback: nil, format: nil, **opts)
205
+ severity=severity(severity, **opts)
206
+ severity_lvl=severity_lvl(severity)
207
+ if @logdev.nil? or severity_lvl < @level
208
+ return true
209
+ end
210
+ if message.nil?
211
+ message = yield if block_given?
212
+ end
213
+ callback.call(message, progname, severity) if callback
214
+ @logdev.write(
215
+ format_message(severity, Time.now, progname, message, formatter: format, caller: self, **opts))
216
+ true
217
+ end
218
+
219
+ def level=(severity)
220
+ @level = severity_lvl(severity)
221
+ end
222
+
223
+ # like level= but for clis, so we can pass a default if level=true
224
+ def cli_level(level, active: @verbose_lvl, disactive: @quiet_lvl)
225
+ level=active if level==true #for cli
226
+ level=disactive if level==false #for cli
227
+ self.level=level
228
+ end
229
+ end
230
+
231
+ # CLILogger {{{1
7
232
  # A Logger instance that gives better control of messaging the user and
8
233
  # logging app activity. At it's most basic, you would use <tt>info</tt>
9
234
  # as a replacement for +puts+ and <tt>error</tt> as a replacement for
@@ -40,11 +265,7 @@ module ShellHelpers
40
265
  # logger = CLILogger.new('logfile.txt')
41
266
  # logger.debug("Starting up") #=> logfile.txt gets this
42
267
  # logger.error("Something went wrong!") # => BOTH logfile.txt AND the standard error get this
43
- class CLILogger < Logger
44
- BLANK_FORMAT = lambda { |severity,datetime,progname,msg|
45
- msg + "\n"
46
- }
47
-
268
+ class CLILogger < ColorLogger
48
269
  # Helper to proxy methods to the super class AND to the internal error logger
49
270
  # +symbol+:: Symbol for name of the method to proxy
50
271
  def self.proxy_method(symbol) #:nodoc:
@@ -60,23 +281,19 @@ module ShellHelpers
60
281
  proxy_method :'progname='
61
282
  proxy_method :'datetime_format='
62
283
 
63
- def add(severity, message = nil, progname = nil, &block) #:nodoc:
64
- return true if severity == QUIET
284
+ def add(severity, message = nil, **opts, &block) #:nodoc:
285
+ severity_lvl = severity_lvl(severity)
65
286
  if @split_logs
66
- unless severity >= @stderr_logger.level
67
- super(severity,message,progname,&block)
287
+ unless severity_lvl >= @stderr_logger.level
288
+ super(severity,message, **opts, &block)
68
289
  end
69
290
  else
70
- super(severity,message,progname,&block)
291
+ super(severity,message,**opts, &block)
71
292
  end
72
- @stderr_logger.add(severity,message,progname,&block)
293
+ severity = severity_lvl if severity == true
294
+ @stderr_logger.add(severity,message,**opts, &block)
73
295
  end
74
296
 
75
- def quiet(progname = nil, &block)
76
- add(QUIET, nil, progname, &block)
77
- end
78
-
79
-
80
297
  DEFAULT_ERROR_LEVEL = Logger::Severity::WARN
81
298
 
82
299
  # A logger that logs error-type messages to a second device; useful for
@@ -92,31 +309,44 @@ module ShellHelpers
92
309
  # By default, this is Logger::Severity::WARN
93
310
  # +error_device+:: device where all error messages should go.
94
311
  def initialize(log_device=$stdout,error_device=$stderr,
95
- split_log: :auto)
96
- @stderr_logger = Logger.new(error_device)
312
+ split_log: :auto, default_error_lvl: DEFAULT_ERROR_LEVEL, **kwds)
313
+ @stderr_logger = ColorLogger.new(error_device, default_lvl: default_error_lvl, **kwds)
97
314
 
98
- super(log_device)
315
+ super(log_device, **kwds)
99
316
 
100
317
  log_device_tty = tty?(log_device)
101
318
  error_device_tty = tty?(error_device)
102
319
 
103
320
  @split_logs = log_device_tty && error_device_tty if split_log==:auto
104
321
 
105
- self.level = Logger::Severity::INFO
106
- @stderr_logger.level = DEFAULT_ERROR_LEVEL
322
+ self.default_formatter = ColorFormatter.create(:color) if log_device_tty
323
+ @stderr_logger.default_formatter = ColorFormatter.create(:color) if error_device_tty
107
324
 
108
- self.formatter = BLANK_FORMAT if log_device_tty
109
- @stderr_logger.formatter = BLANK_FORMAT if error_device_tty
325
+ yield self, @stderr_logger if block_given?
110
326
  end
111
327
 
112
- def level=(level)
113
- super(level)
328
+ private def adjust_stderr_level
114
329
  #current_error_level = @stderr_logger.level
115
- if (level > DEFAULT_ERROR_LEVEL) && @split_logs
116
- @stderr_logger.level = level
330
+ if @split_logs
331
+ if (self.level > @stderr_logger.level)
332
+ @stderr_logger.level = self.level
333
+ end
334
+ if (self.level < @stderr_logger.level)
335
+ @stderr_logger.level = [self.level, @stderr_logger.severity_lvl(@stderr_logger.default_lvl)].min
336
+ end
117
337
  end
118
338
  end
119
339
 
340
+ def level=(level)
341
+ super
342
+ adjust_stderr_level
343
+ end
344
+
345
+ def cli_level(*args)
346
+ super
347
+ adjust_stderr_level
348
+ end
349
+
120
350
  # Set the threshold for what messages go to the error device. Note
121
351
  # that calling #level= will *not* affect the error logger *unless* both
122
352
  # devices are TTYs.
@@ -139,11 +369,11 @@ module ShellHelpers
139
369
 
140
370
  #log the action and execute it
141
371
  #Severity is Logger:: DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN
142
- def log_and_do(*args, severity: Logger::INFO, definee: self, **opts, &block)
372
+ def log_and_do(*args, severity: INFO, definee: self, **opts, &block)
143
373
  msg="log_and_do #{args} on #{self}"
144
374
  msg+=" with options #{opts}" unless opts.empty?
145
375
  msg+=" with block #{block}" if block
146
- logger.add(severity,msg)
376
+ add(severity,msg)
147
377
  if opts.empty?
148
378
  definee.send(*args, &block)
149
379
  else
@@ -151,25 +381,12 @@ module ShellHelpers
151
381
  end
152
382
  end
153
383
 
154
- QUIET=-1
155
-
156
- def log_levels
157
- {
158
- 'quiet' => QUIET,
159
- 'debug' => Logger::DEBUG,
160
- 'info' => Logger::INFO,
161
- 'warn' => Logger::WARN,
162
- 'error' => Logger::ERROR,
163
- 'fatal' => Logger::FATAL,
164
- }
165
- end
166
-
167
- private def toggle_log_level
384
+ private def toggle_log_level(toggle='debug')
168
385
  @log_level_original = self.level unless @log_level_toggled
169
386
  logger.level = if @log_level_toggled
170
387
  @log_level_original
171
388
  else
172
- log_levels.fetch('debug')
389
+ log_levels.fetch(toggle)
173
390
  end
174
391
  @log_level_toggled = !@log_level_toggled
175
392
  @log_level = logger.level
@@ -186,8 +403,8 @@ module ShellHelpers
186
403
  end
187
404
 
188
405
  end
189
- #}}}
190
- # CLILogging {{{
406
+
407
+ # CLILogging {{{1
191
408
  # Provides easier access to a shared DR::CLILogger instance.
192
409
  # Include this module into your class, and #logger provides access to a
193
410
  # shared logger. This is handy if you want all of your clases to have
@@ -253,21 +470,22 @@ module ShellHelpers
253
470
  logger.setup_toggle_trap(signal)
254
471
  end
255
472
 
256
- def log_and_do(*args)
257
- logger.log_and_do(*args)
473
+ def log_and_do(*args,**kws)
474
+ logger.log_and_do(*args,**kws)
258
475
  end
259
476
 
477
+ LOG_LEVELS=logger.log_levels
478
+
260
479
  #Include this in place of CLILogging if you prefer to use
261
480
  #info directly rather than logger.info
262
481
  module Shortcuts #{{{
263
- extend self
264
482
  include CLILogging
265
-
266
- def debug(progname = nil, &block); logger.debug(progname,&block); end
267
- def info(progname = nil, &block); logger.info(progname,&block); end
268
- def warns(progname = nil, &block); logger.warn(progname,&block); end
269
- def error(progname = nil, &block); logger.error(progname,&block); end
270
- def fatal(progname = nil, &block); logger.fatal(progname,&block); end
483
+ extend self
484
+ logger.log_levels.each_key do |lvl|
485
+ define_method(lvl.to_sym) do |*args, &block|
486
+ logger.send(lvl.to_sym, *args, &block)
487
+ end
488
+ end
271
489
  end
272
490
  #}}}
273
491
  end #}}}
@@ -1,4 +1,5 @@
1
1
  require 'pathname'
2
+ require 'forwardable'
2
3
 
3
4
  #backports from ruby 2.1
4
5
  class Pathname
@@ -97,6 +98,14 @@ module ShellHelpers
97
98
  exist? or symlink?
98
99
  end
99
100
 
101
+ #like read, but output nil rather than an exception if the file does
102
+ #not exist
103
+ def read!
104
+ read
105
+ rescue
106
+ nil
107
+ end
108
+
100
109
  def filewrite(*args,mode:"w",perm: nil,mkpath: false,backup: false)
101
110
  logger.debug("Write to #{self}"+ (perm ? " (#{perm})" : "")) if respond_to?(:logger)
102
111
  self.dirname.mkpath if mkpath
@@ -269,11 +278,15 @@ module ShellHelpers
269
278
  Utils.find(self,*args,&b)
270
279
  end
271
280
 
272
- def glob(pattern, expand: false)
281
+ # We now have Pathname#glob
282
+ def rel_glob(pattern, expand: false)
273
283
  g=[]
274
284
  self.cd { g=Dir.glob(pattern) }
275
- g=g.map {|f| self+f} if expand
276
- g
285
+ if expand
286
+ g.map {|f| self+f}
287
+ else
288
+ g.map {|f| Pathname.new(f)}
289
+ end
277
290
  end
278
291
 
279
292
  #follow a symlink