shell_helpers 0.1.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,8 @@
1
1
  # vim: foldmethod=marker
2
- #From methadone (cli_logger.rb, cli_logging.rb, last import: v1.3.1-2-g9be3b5a)
2
+ #From methadone (cli_logger.rb, cli_logging.rb, last import: 4626a2bca9b6e54077a06a0f8e11a04fadc6e7ae; 2017-01-19)
3
3
  require 'logger'
4
4
 
5
- module SH
5
+ module ShellHelpers
6
6
  # CLILogger {{{
7
7
  # A Logger instance that gives better control of messaging the user and
8
8
  # logging app activity. At it's most basic, you would use <tt>info</tt>
@@ -57,9 +57,11 @@ module SH
57
57
  end
58
58
 
59
59
  proxy_method :'formatter='
60
+ proxy_method :'progname='
60
61
  proxy_method :'datetime_format='
61
62
 
62
63
  def add(severity, message = nil, progname = nil, &block) #:nodoc:
64
+ return true if severity == QUIET
63
65
  if @split_logs
64
66
  unless severity >= @stderr_logger.level
65
67
  super(severity,message,progname,&block)
@@ -70,6 +72,11 @@ module SH
70
72
  @stderr_logger.add(severity,message,progname,&block)
71
73
  end
72
74
 
75
+ def quiet(progname = nil, &block)
76
+ add(QUIET, nil, progname, &block)
77
+ end
78
+
79
+
73
80
  DEFAULT_ERROR_LEVEL = Logger::Severity::WARN
74
81
 
75
82
  # A logger that logs error-type messages to a second device; useful for
@@ -85,16 +92,21 @@ module SH
85
92
  # By default, this is Logger::Severity::WARN
86
93
  # +error_device+:: device where all error messages should go.
87
94
  def initialize(log_device=$stdout,error_device=$stderr,
88
- split_log:log_device.tty? && error_device.tty?)
89
- super(log_device)
95
+ split_log: :auto)
90
96
  @stderr_logger = Logger.new(error_device)
91
97
 
92
- @split_logs = split_log
98
+ super(log_device)
99
+
100
+ log_device_tty = tty?(log_device)
101
+ error_device_tty = tty?(error_device)
102
+
103
+ @split_logs = log_device_tty && error_device_tty if split_log==:auto
104
+
93
105
  self.level = Logger::Severity::INFO
94
106
  @stderr_logger.level = DEFAULT_ERROR_LEVEL
95
107
 
96
- self.formatter = BLANK_FORMAT if log_device.tty?
97
- @stderr_logger.formatter = BLANK_FORMAT if error_device.tty?
108
+ self.formatter = BLANK_FORMAT if log_device_tty
109
+ @stderr_logger.formatter = BLANK_FORMAT if error_device_tty
98
110
  end
99
111
 
100
112
  def level=(level)
@@ -120,6 +132,59 @@ module SH
120
132
  @stderr_logger.formatter=formatter
121
133
  end
122
134
 
135
+ private def tty?(device_or_string)
136
+ return device_or_string.tty? if device_or_string.respond_to? :tty?
137
+ false
138
+ end
139
+
140
+ #log the action and execute it
141
+ #Severity is Logger:: DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN
142
+ def log_and_do(*args, severity: Logger::INFO, definee: self, **opts, &block)
143
+ msg="log_and_do #{args} on #{self}"
144
+ msg+=" with options #{opts}" unless opts.empty?
145
+ msg+=" with block #{block}" if block
146
+ logger.add(severity,msg)
147
+ if opts.empty?
148
+ definee.send(*args, &block)
149
+ else
150
+ definee.send(*args, **opts, &block)
151
+ end
152
+ end
153
+
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
168
+ @log_level_original = self.level unless @log_level_toggled
169
+ logger.level = if @log_level_toggled
170
+ @log_level_original
171
+ else
172
+ log_levels.fetch('debug')
173
+ end
174
+ @log_level_toggled = !@log_level_toggled
175
+ @log_level = logger.level
176
+ end
177
+
178
+ #call logger.setup_toggle_trap('USR1') to change the log level to
179
+ #:debug when USR1 is received
180
+ def setup_toggle_trap(signal)
181
+ if signal
182
+ Signal.trap(signal) do
183
+ toggle_log_level
184
+ end
185
+ end
186
+ end
187
+
123
188
  end
124
189
  #}}}
125
190
  # CLILogging {{{
@@ -156,13 +221,15 @@ module SH
156
221
  # Access the shared logger. All classes that include this module
157
222
  # will get the same logger via this method.
158
223
  def logger
159
- unless Module.class_variable_defined?(:@@logger)
224
+ unless CLILogging.class_variable_defined?(:@@logger)
160
225
  @@logger = CLILogger.new
161
226
  @@logger.progname=$0
162
227
  end
163
228
  @@logger
164
229
  end
165
230
 
231
+ self.logger.progname||=$0
232
+
166
233
  # Change the global logger that includers will use. Useful if you
167
234
  # don't want the default configured logger. Note that the
168
235
  # +change_logger+ version is preferred because Ruby will often parse
@@ -175,31 +242,19 @@ module SH
175
242
  def change_logger(new_logger)
176
243
  raise ArgumentError,"Logger may not be nil" if new_logger.nil?
177
244
  @@logger = new_logger
178
- @@logger.level = @log_level if @log_level
245
+ @@logger.level = @log_level if defined?(@log_level) && @log_level
179
246
  end
180
247
 
181
248
  alias logger= change_logger
182
249
 
183
- LOG_LEVELS = {
184
- 'debug' => Logger::DEBUG,
185
- 'info' => Logger::INFO,
186
- 'warn' => Logger::WARN,
187
- 'error' => Logger::ERROR,
188
- 'fatal' => Logger::FATAL,
189
- }
250
+ #call CLILogging.setup_toggle_trap('USR1') to change the log level to
251
+ #:debug when USR1 is received
252
+ def self.setup_toggle_trap(signal)
253
+ logger.setup_toggle_trap(signal)
254
+ end
190
255
 
191
- #log the action and execute it
192
- #Severity is Logger:: DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN
193
- def log_and_do(*args, severity: Logger::INFO, definee: self, **opts, &block)
194
- msg="log_and_do #{args} on #{self}"
195
- msg+=" with options #{opts}" unless opts.empty?
196
- msg+=" with block #{block}" if block
197
- logger.add(severity,msg)
198
- if opts.empty?
199
- definee.send(*args, &block)
200
- else
201
- definee.send(*args, **opts, &block)
202
- end
256
+ def log_and_do(*args)
257
+ logger.log_and_do(*args)
203
258
  end
204
259
 
205
260
  #Include this in place of CLILogging if you prefer to use
@@ -0,0 +1,28 @@
1
+ gem 'slop', '~> 4'
2
+ require 'slop'
3
+
4
+ module Slop
5
+ class SymbolOption < Option
6
+ def call(value)
7
+ value.to_sym
8
+ end
9
+ end
10
+
11
+ class PathOption < Option
12
+ def call(value)
13
+ ShellHelpers::Pathname.new(value)
14
+ end
15
+ def finish(opts)
16
+ if opts[:verbose] and opts[:test]
17
+ pathname=ShellHelpers::Pathname::DryRun
18
+ elsif opts[:verbose] and !opts[:test]
19
+ pathname=ShellHelpers::Pathname::Verbose
20
+ elsif opts[:test]
21
+ pathname=ShellHelpers::Pathname::NoWrite
22
+ else
23
+ pathname=ShellHelpers::Pathname
24
+ end
25
+ self.value=pathname.new(value)
26
+ end
27
+ end
28
+ end
@@ -14,146 +14,619 @@ end
14
14
 
15
15
  autoload :FileUtils, "fileutils"
16
16
 
17
- module SH
17
+ module ShellHelpers
18
18
  #SH::Pathname is Pathname with extra features
19
19
  #and methods from FileUtils rather than File when possible
20
20
  #to use this module rather than ::Pathname in a module or class,
21
21
  #simply define Pathname=SH::Pathname in an appropriate nesting level
22
- class Pathname < ::Pathname
23
- def hidden?
24
- return self.basename.to_s[0]=="."
25
- end
22
+ module PathnameExt
23
+ class Base < ::Pathname
24
+ #Some alias defined in FileUtils
25
+ alias_method :mkdir_p, :mkpath
26
+ alias_method :rm_r, :rmtree #rmtree should be rm_rf, not rm_r!
27
+ def rmtree
28
+ require 'fileutils'
29
+ FileUtils.rm_rf(@path)
30
+ nil
31
+ end
32
+ alias_method :rm_rf, :rmtree
33
+
34
+ def shellescape
35
+ require 'shellwords'
36
+ to_s.shellescape
37
+ end
38
+
39
+ def to_relative
40
+ return self if relative?
41
+ relative_path_from(Pathname.slash)
42
+ end
43
+
44
+ #use the low level FileUtils feature to copy the metadata
45
+ #if passed a dir just copy the dir metadata, not the directory recursively
46
+ #Note this differs from FileUtils.copy_entry who copy directories recursively
47
+ def copy_entry(dest, dereference: false, preserve: true)
48
+ require 'fileutils'
49
+ ent = FileUtils::Entry_.new(@path, nil, dereference)
50
+ ent.copy dest.to_s
51
+ ent.copy_metadata dest.to_s if preserve
52
+ end
53
+
54
+ class <<self
55
+ def home
56
+ return Pathname.new(Dir.home)
57
+ end
58
+ def hometilde
59
+ return Pathname.new('~')
60
+ end
61
+ def slash
62
+ return Pathname.new("/")
63
+ end
64
+ #differ from Pathname.pwd in that this returns a relative path
65
+ def current
66
+ return Pathname.new(".")
67
+ end
68
+ def null
69
+ return Pathname.new('/dev/null')
70
+ end
71
+
72
+ #Pathname / 'usr'
73
+ def /(path)
74
+ new(path)
75
+ end
76
+ #Pathname['/usr']
77
+ def [](path)
78
+ new(path)
79
+ end
80
+
81
+ def cd(dir,&b)
82
+ self.new(dir).cd(&b)
83
+ end
84
+ end
85
+
86
+ #these Pathname methods explicitly call Pathname.new so do not respect
87
+ #our subclass :-(
88
+ [:+,:join,:relative_path_from].each do |m|
89
+ define_method m do |*args,&b|
90
+ self.class.new(super(*args,&b))
91
+ end
92
+ end
93
+ alias_method :/, :+
94
+
95
+ #exist? returns false if called on a symlink pointing to a non existing file
96
+ def may_exist?
97
+ exist? or symlink?
98
+ end
99
+
100
+ def filewrite(*args,mode:"w",perm: nil,mkpath: false,backup: false)
101
+ logger.debug("Write to #{self}"+ (perm ? " (#{perm})" : "")) if respond_to?(:logger)
102
+ self.dirname.mkpath if mkpath
103
+ self.backup if backup and exist?
104
+ if !exist? && symlink?
105
+ logger.debug "Removing bad symlink #{self}" if respond_to?(:logger)
106
+ self.unlink
107
+ end
108
+ self.open(mode: mode) do |fh|
109
+ fh.chmod(perm) if perm
110
+ #hack to pass an array to write and do the right thing
111
+ if args.length == 1 && Array === args.first
112
+ fh.puts(args.first)
113
+ else
114
+ fh.write(*args)
115
+ end
116
+ yield fh if block_given?
117
+ end
118
+ end
119
+
120
+ def components
121
+ each_filename.to_a
122
+ end
123
+ def depth
124
+ each_filename.count
125
+ end
26
126
 
27
- def filewrite(*args,mode:"w",perm: nil,mkdir: false)
28
- logger.debug("Write to #{self}"+ (perm ? " (#{perm})" : "")) if respond_to?(:logger)
29
- self.dirname.mkpath if mkdir
30
- self.open(mode: mode) do |fh|
31
- fh.chmod(perm) if perm
32
- #hack to pass an array to write and do the right thing
33
- if args.length == 1 && Array === args.first
34
- fh.puts(args.first)
127
+ def entries(filter: true)
128
+ c=super()
129
+ if filter
130
+ c.reject {|c| c.to_s=="." or c.to_s==".."}
35
131
  else
36
- fh.write(*args)
132
+ c
37
133
  end
38
- yield fh if block_given?
39
134
  end
40
- end
41
- #write is not the same as filewrite
42
- #but if given only one argument we should be ok
43
- unless Pathname.method_defined?(:write)
44
- alias :write :filewrite
45
- end
46
135
 
47
- #Pathname.new("foo")+"bar" return "foo/bar"
48
- #Pathname.new("foo").append_name("bar") return "foobar"
49
- def append_name(*args,join:'')
50
- Pathname.new(self.to_s+args.join(join))
51
- end
136
+ #Pathname.new("foo")+"bar" return "foo/bar"
137
+ #Pathname.new("foo").append_name("bar") return "foobar"
138
+ def append_name(*args,join:'')
139
+ Pathname.new(self.to_s+args.join(join))
140
+ end
52
141
 
53
- def backup(suffix: '.old', overwrite: true)
54
- if self.exist?
55
- filebk=self.append_name(suffix)
56
- if filebk.exist? and !overwrite
57
- num=0
58
- begin
59
- filebknum=filebk.append_name("%02d" % num)
60
- num+=1
61
- end while filebknum.exist?
62
- filebk=filebknum
142
+ #loop until we get a name satisfying cond
143
+ def new_name(cond)
144
+ loop.with_index do |_,ind|
145
+ n=self.class.new(yield(self,ind))
146
+ return n if cond.call(n)
147
+ end
148
+ end
149
+ #find a non existing filename
150
+ def nonexisting_name
151
+ return self unless self.may_exist?
152
+ new_name(Proc.new {|f| !f.may_exist?}) do |old_name, ind|
153
+ old_name.append_name("%02d" % ind)
63
154
  end
64
- logger.debug "Backup #{self} -> #{filebk}" if respond_to?(:logger)
65
- FileUtils.mv(self,filebk)
66
155
  end
67
- end
68
156
 
69
- def abs_path(base: Pathname.pwd, mode: :clean)
70
- f=base+self
71
- case clean
72
- when :clean
73
- f.cleanpath
74
- when :clean_sym
75
- f.cleanpath(consider_symlink: true)
76
- when :real
77
- f.realpath
78
- when :realdir
79
- f.realdirpath
157
+ #stolen from ptools (https://github.com/djberg96/ptools/blob/master/lib/ptools.rb)
158
+ def binary?
159
+ return false if directory?
160
+ bytes = stat.blksize
161
+ bytes = 4096 if bytes > 4096
162
+ s = read(bytes, bytes) || ""
163
+ #s = s.encode('US-ASCII', :undef => :replace).split(//)
164
+ s=s.split(//)
165
+ ((s.size - s.grep(" ".."~").size) / s.size.to_f) > 0.30
80
166
  end
81
- end
82
167
 
83
- #wrapper around FileUtils
84
- #Pathname#rmdir uses Dir.rmdir, but the rmdir from FileUtils is a wrapper
85
- #around Dir.rmdir that accepts extra options
86
- #The same for mkdir
87
- [:chdir, :rmdir, :mkdir].each do |method|
88
- define_method method do |*args,&b|
89
- FileUtils.send(method,*args,&b)
168
+ #return true if the file is a text
169
+ def text?
170
+ #!! %x/file #{self.to_s}/.match(/text/)
171
+ return false if directory?
172
+ !binary?
173
+ end
174
+
175
+ def readbin(*args)
176
+ open("rb").read(*args)
177
+ end
178
+
179
+ #taken from facets/split_all
180
+ def split_all
181
+ head, tail = split
182
+ return [tail] if head.to_s == '.' || tail.to_s == '/'
183
+ return [head, tail] if head.to_s == '/'
184
+ return head.split_all + [tail]
185
+ end
186
+
187
+ def backup(suffix: '.old', overwrite: true)
188
+ if self.exist?
189
+ filebk=self.append_name(suffix)
190
+ filebk=nonexisting_name if filebk.exist? and !overwrite
191
+ logger.debug "Backup #{self} -> #{filebk}" if respond_to?(:logger)
192
+ FileUtils.mv(self,filebk)
193
+ end
194
+ end
195
+
196
+ def abs_path(base: self.class.pwd, mode: :clean)
197
+ f= absolute? ? self : base+self
198
+ case mode
199
+ when :clean
200
+ f.cleanpath
201
+ when :clean_sym
202
+ f.cleanpath(consider_symlink: true)
203
+ when :real
204
+ f.realpath
205
+ when :realdir
206
+ f.realdirpath
207
+ else
208
+ f
209
+ end
210
+ end
211
+
212
+ def rel_path(base: self.class.pwd, checkdir: false)
213
+ base=base.dirname unless base.directory? if checkdir
214
+ relative_path_from(base)
215
+ rescue ArgumentError => e
216
+ warn "#{self}.relative_path_from(#{base}): #{e}"
217
+ self
218
+ end
219
+
220
+ #call abs_path or rel_path according to :mode
221
+ def convert_path(base: self.class.pwd, mode: :clean, checkdir: false)
222
+ case mode
223
+ when :clean
224
+ cleanpath
225
+ when :clean_sym
226
+ cleanpath(consider_symlink: true)
227
+ when :rel
228
+ rel_path(base: base, checkdir: checkdir)
229
+ when :relative
230
+ rel_path(base: base, checkdir: checkdir) unless self.relative?
231
+ when :absolute,:abs
232
+ abs_path(base: base, mode: :abs)
233
+ when :abs_clean
234
+ abs_path(base: base, mode: :clean)
235
+ when :abs_cleansym
236
+ abs_path(base: base, mode: :cleansym)
237
+ when :abs_real
238
+ abs_path(base: base, mode: :real)
239
+ when :abs_realdir
240
+ abs_path(base: base, mode: :realdir)
241
+ else
242
+ self
243
+ end
244
+ end
245
+
246
+ #path from self to target (warning: we always assume that we are one
247
+ #level up self, except if inside is true)
248
+ # bar=SH::Pathname.new("foo/bar"); baz=SH::Pathname.new("foo/baz")
249
+ # bar.rel_path_to(baz) #<ShellHelpers::Pathname:baz>
250
+ # bar.rel_path_to(baz, inside: true) #<ShellHelpers::Pathname:../baz>
251
+ #note: there is no real sense to use mode: :rel here, but we don't
252
+ #prevent it
253
+ def rel_path_to(target=self.class.pwd, base: self.class.pwd, mode: :rel, clean_mode: :abs_clean, inside: false, **opts)
254
+ target=self.class.new(target) unless target.is_a?(self.class)
255
+ sbase=opts[:source_base]||base
256
+ smode=opts[:source_mode]||clean_mode
257
+ tbase=opts[:target_base]||base
258
+ tmode=opts[:target_mode]||clean_mode
259
+ source=self.convert_path(base: sbase, mode: smode)
260
+ target=target.convert_path(base: tbase, mode: tmode)
261
+ from=inside ? source : source.dirname
262
+ target.convert_path(base: from, mode: mode)
263
+ end
264
+
265
+ #overwrites Pathname#find
266
+ alias orig_find find
267
+ def find(*args,&b)
268
+ require 'shell_helpers/utils'
269
+ Utils.find(self,*args,&b)
270
+ end
271
+
272
+ def glob(pattern, expand: false)
273
+ g=[]
274
+ self.cd { g=Dir.glob(pattern) }
275
+ g=g.map {|f| self+f} if expand
276
+ g
277
+ end
278
+
279
+ #follow a symlink
280
+ def follow
281
+ return self unless symlink?
282
+ l=readlink
283
+ if l.relative?
284
+ self.dirname+l
285
+ else
286
+ l
287
+ end
288
+ end
289
+
290
+ def dereference(mode=true)
291
+ return self unless mode
292
+ case mode
293
+ when :simple
294
+ return follow if symlink?
295
+ else
296
+ return follow.dereference(mode) if symlink?
297
+ end
298
+ self
299
+ end
300
+
301
+ def bad_symlink?
302
+ symlink? and !dereference.exist?
303
+ end
304
+
305
+ def hidden?
306
+ #without abs_path '.' is considered as hidden
307
+ abs_path.basename.to_s[0]=="."
308
+ end
309
+
310
+ #remove all empty directories inside self
311
+ #this includes directories which only include empty directories
312
+ def rm_empty_dirs(rm:true)
313
+ r=[]
314
+ if directory?
315
+ find(depth:true) do |file|
316
+ if file.directory? and file.children(false).empty?
317
+ r<<file
318
+ file.rmdir if rm
319
+ end
320
+ end
321
+ end
322
+ r
323
+ end
324
+
325
+ def rm_bad_symlinks(rm:false,hidden:false)
326
+ r=[]
327
+ if directory?
328
+ filter=if hidden
329
+ ->(x,_) {x.hidden?}
330
+ else
331
+ ->(*x) {false}
332
+ end
333
+ find(filter:filter) do |file|
334
+ if file.bad_symlink?
335
+ r<<file
336
+ file.rm if rm
337
+ end
338
+ end
339
+ end
340
+ r
341
+ end
342
+
343
+ #calls an external program
344
+ def call(prog,*args,pos: :last,full:false,**opts)
345
+ name=to_s
346
+ name=(self.class.pwd+self).to_s if full and relative?
347
+ sh_args=args
348
+ pos=sh_args.length if pos==:last
349
+ sh_args[pos,0]=name
350
+ Sh.sh(prog,*sh_args,**opts)
351
+ end
352
+
353
+ def chattr(*args,**opts)
354
+ call("chattr",*args,**opts)
355
+ end
356
+
357
+ def sudo_mkdir
358
+ Sh.sh("mkdir -p #{shellescape}", sudo: true)
359
+ end
360
+ def sudo_mkpath
361
+ Sh.sh("mkdir -p #{shellescape}", sudo: true)
90
362
  end
91
- end
92
- #mkpath is already defined (use FileUtils), but not mkdir_p
93
- #alias mkdir_p mkpath
94
-
95
- #Options: verbose, noop, force
96
- def rm(recursive: false, mode: :file, verbose: true, noop: false, force: false)
97
- mode.to_s.match(/^recursive-(.*)$/) do |m|
98
- recursive=true
99
- mode=m[1].to_sym
100
- end
101
- case mode
102
- when :symlink
103
- return unless self.symlink?
104
- when :dangling_symlink
105
- return unless self.symlink? && ! self.exist?
106
- end
107
- if recursive
108
- FileUtils.rm_r(self, verbose: verbose, noop: noop, force: force)
109
- else
110
- FileUtils.rm(self, verbose: verbose, noop: noop, force: force)
111
- end
112
- rescue => e
113
- warn "Error in #{self}.clobber: #{e}"
114
363
  end
115
364
 
116
- #Options: preserve noop verbose
117
- def cp(*files,**opts)
118
- FileUtils.cp(files,self,**opts)
365
+ module FUClass
366
+ attr_writer :fu_class
367
+ def fu_class
368
+ @fu_class||=::FileUtils
369
+ end
119
370
  end
120
- #Options: force noop verbose
121
- def mv(*files,**opts)
122
- FileUtils.mv(files,self,**opts)
371
+ def self.included(base)
372
+ base.extend(FUClass)
123
373
  end
124
374
 
125
- #find already exists
126
- def dr_find(*args,&b)
127
- require 'dr/sh/utils'
128
- SH::ShellUtils.find(self,*args,&b)
129
- end
375
+ module FileUtilsWrapper
376
+ extend FUClass
377
+ #wrapper around FileUtils
378
+ #For instance Pathname#rmdir uses Dir.rmdir, but the rmdir from FileUtils is a wrapper around Dir.rmdir that accepts extra options
379
+ [:chdir, :rmdir, :mkdir, :cmp, :touch, :rm, :rm_r, :uptodate?, :cmp, :cp,:cp_r,:mv,:ln,:ln_s,:ln_sf].each do |method|
380
+ define_method method do |*args,&b|
381
+ self.class.fu_class.public_send(method,self,*args,&b)
382
+ end
383
+ end
384
+ # for these the path argument goes last
385
+ [:chown, :chown_R].each do |method|
386
+ define_method method do |*args,**opts,&b|
387
+ require 'pry'; binding.pry
388
+ self.class.fu_class.public_send(method,*args,self.to_path,&b)
389
+ end
390
+ end
391
+
392
+ # we rewrap chdir this way, so that the argument stays a SH::Pathname
393
+ def chdir(*args)
394
+ self.class.fu_class.public_send(:chdir, self, *args) do |dir|
395
+ yield self.class.new(dir)
396
+ end
397
+ end
130
398
 
131
- def self.home
132
- return Pathname.new(Dir.home)
399
+ #These methods are of the form FileUtils.chmod paramater, file
400
+ [:chmod, :chmod_R].each do |method|
401
+ define_method method do |*args,&b|
402
+ self.class.fu_class.public_send(method,*args,self,&b)
403
+ end
404
+ end
405
+ #Some alias defined in FileUtils
406
+ alias_method :cd, :chdir
407
+ alias_method :identical?, :cmp
408
+
409
+ #We need to inverse the way we call cp, since it is the only way we can
410
+ #mv/cp several files in a directory:
411
+ # self.on_cp("file1","file2")
412
+ #Options: preserve noop verbose force
413
+ #Note: if ActionHandler is included, this will overwrite these
414
+ #methods
415
+ [:cp,:cp_r,:mv,:ln,:ln_s,:ln_sf].each do |method|
416
+ define_method :"on_#{method}" do |*files,**opts,&b|
417
+ files.each do |file|
418
+ self.class.fu_class.send(method,file,self,**opts,&b)
419
+ end
420
+ end
421
+ end
422
+ alias_method :on_link, :on_ln
423
+ alias_method :on_symlink, :on_ln_s
133
424
  end
134
- def self.hometilde
135
- return Pathname.new('~')
425
+ include FileUtilsWrapper
426
+
427
+ module ActionHandler
428
+ extend FUClass
429
+ class PathnameError < StandardError
430
+ #encapsulate another exception
431
+ attr_accessor :ex
432
+ def initialize(ex=nil)
433
+ @ex=ex
434
+ end
435
+ def to_s
436
+ @ex.to_s
437
+ end
438
+ end
439
+
440
+ protected def do_action?(mode: :all, dereference: false, **others)
441
+ path=self.dereference(dereference)
442
+ case mode
443
+ when :none, false
444
+ return false
445
+ when :noclobber
446
+ return false if path.may_exist?
447
+ when :symlink
448
+ return false unless path.symlink?
449
+ when :dangling_symlink
450
+ return false unless path.symlink? && ! self.exist?
451
+ when :file
452
+ return false if path.directory?
453
+ when :dir
454
+ return false unless path.directory?
455
+ end
456
+ true
457
+ end
458
+
459
+ RemoveError = Class.new(PathnameError)
460
+ def on_rm(recursive: false, mode: :all, dereference: false, rescue_error: true, **others)
461
+ path=self.dereference(dereference)
462
+ return nil unless path.may_exist?
463
+ if path.do_action?(mode: mode)
464
+ fuopts=others.select {|k,v| [:verbose,:noop,:force].include?(k)}
465
+ if recursive
466
+ #this is only called if both recursive=true and mode=:all or :dir
467
+ logger.debug("rm_r #{self} (#{path}) #{fuopts}") if respond_to?(:logger)
468
+ self.class.fu_class.rm_r(path, **fuopts)
469
+ else
470
+ logger.debug("rm #{self} (#{path}) #{fuopts}") if respond_to?(:logger)
471
+ self.class.fu_class.rm(path, **fuopts)
472
+ end
473
+ else
474
+ puts "\# #{__method__}: Skip #{self} [mode=#{mode}]" if others[:verbose]
475
+ end
476
+ rescue => e
477
+ warn "Error in #{path}.#{__method__}: #{e}"
478
+ raise RemoveError.new(e) unless rescue_error
479
+ end
480
+ def on_rm_r(**opts)
481
+ on_rm(recursive:true,**opts)
482
+ end
483
+ def on_rm_rf(**opts)
484
+ on_rm(recursive:true,force:true,**opts)
485
+ end
486
+
487
+ FSError = Class.new(PathnameError)
488
+ [:cp,:cp_r,:mv,:ln,:ln_s,:ln_sf].each do |method|
489
+ define_method :"on_#{method}" do |*files, rescue_error: true,
490
+ dereference: false, mode: :all, rm: nil, mkpath: false, **opts,&b|
491
+ #FileUtils.{cp,mv,ln_s} dereference a target symlink if it points to a
492
+ #directory; the only solution to not dereference it is to remove it
493
+ #before hand
494
+ if dereference==:none and rm.nil?
495
+ dereference=false
496
+ rm=:symlink
497
+ end
498
+ path=self.dereference(dereference)
499
+ if path.do_action?(mode: mode)
500
+ begin
501
+ path.on_rm(mode: rm, rescue_error: false, **opts) if rm
502
+ if mkpath
503
+ path.to_s[-1]=="/" ? path.mkpath : path.dirname.mkpath
504
+ end
505
+ fuopts=opts.reject {|k,v| [:recursive].include?(k)}
506
+ logger.debug("#{method} #{self} -> #{files.join(' ')} #{fuopts}") if respond_to?(:logger)
507
+ files.each do |file|
508
+ self.class.fu_class.send(method,file,path,**fuopts,&b)
509
+ end
510
+ rescue RemoveError
511
+ raise unless rescue_error
512
+ rescue => e
513
+ warn "Error in #{self}.#{__method__}(#{files}): #{e}"
514
+ raise FSError.new(e) unless rescue_error
515
+ end
516
+ else
517
+ puts "\# #{__method__}: Skip #{path} [mode=#{mode}]" if opts[:verbose]
518
+ end
519
+ end
520
+ end
521
+ alias_method :on_link, :on_ln
522
+ alias_method :on_symlink, :on_ln_s
523
+
524
+ #Pathname.new("foo").squel("bar/baz", action: :on_ln_s)
525
+ #will create a symlink foo/bar/baz -> ../../bar/baz
526
+ def squel(target, base: self.class.pwd, action: nil, rel_path_opts: {}, mkpath: false, **opts)
527
+ target=self.class.new(target)
528
+ out=self+base.rel_path_to(target, inside: true)
529
+ out.dirname.mkpath if mkpath
530
+ rel_path=out.rel_path_to(target, **rel_path_opts)
531
+ #rel_path is the path from out to target
532
+ out.public_send(action, rel_path,**opts) if action
533
+ yield(out,rel_path, target: target, orig: self, **opts) if block_given?
534
+ end
535
+
536
+ def squel_dir(target, action: nil, **opts)
537
+ target=self.class.new(target)
538
+ target.find do |file|
539
+ squel(file,mkpath: opts.fetch(:mkpath,!!action), **opts) do |out,rel_path|
540
+ out.public_send(action, rel_path,**opts) if action and !file.directory?
541
+ yield(out,rel_path, target: file, squel_target: target, orig: self, **opts) if block_given?
542
+ end
543
+ end
544
+ end
545
+ #Example: symlink all files in a directory into another, while
546
+ #preserving the structure
547
+ #Pathname.new("foo").squel_dir("bar',action: :on_ln_s)
548
+ #Remove these symlinks:
549
+ #SH::Pathname.new("foo").squel_dir("bar") {|o,t| o.on_rm(mode: :symlink)}
550
+
551
+ #add the relative path to target in the symlink
552
+ #Pathname.new("foo/bar").rel_ln_s(Pathname.new("baz/toto"))
553
+ #makes a symlink foo/bar -> ../baz/toto
554
+ #this is similar to 'ln -rs ...' from coreutils
555
+ def rel_ln_s(target)
556
+ on_ln_s(rel_path_to(target))
557
+ end
136
558
  end
137
- def self.slash
138
- return Pathname.new("/")
559
+ include ActionHandler
560
+ end
561
+
562
+ #to affect the original ::Pathname, just include PathnameExt there
563
+ class Pathname < PathnameExt::Base
564
+ include PathnameExt
565
+ end
566
+ #an alternative to use Pathname::Verbose explicitly is to use
567
+ #Pathname.fu_class=FileUtils::Verbose
568
+ class Pathname::Verbose < Pathname
569
+ @fu_class=FileUtils::Verbose
570
+ end
571
+ class Pathname::NoWrite < Pathname
572
+ @fu_class=FileUtils::NoWrite
573
+ end
574
+ class Pathname::DryRun < Pathname
575
+ @fu_class=FileUtils::DryRun
576
+ end
577
+
578
+ class VirtualFile
579
+ extend Forwardable
580
+ def_delegators :@tmpfile, :open, :close, :close!, :unlink, :path
581
+ attr_accessor :content, :name, :tmpfile
582
+
583
+ def initialize(name, content)
584
+ @content=content
585
+ @tmpfile=nil
586
+ @name=name
139
587
  end
140
- #differ from Pathname.pwd in that this returns a relative path
141
- def self.current
142
- return Pathname.new(".")
588
+
589
+ def path
590
+ @tmpfile&.path && Pathname.new(@tmpfile.path)
143
591
  end
144
- def self.null
145
- return Pathname.new('/dev/null')
592
+
593
+ def file
594
+ create
595
+ path
146
596
  end
147
597
 
148
- #Pathname / 'usr'
149
- def self./(path)
150
- new(path)
598
+ def create(unlink=false)
599
+ require 'tempfile'
600
+ unless @tmpfile&.path
601
+ @tmpfile = Tempfile.new(@name)
602
+ @tmpfile.write(@content)
603
+ @tmpfile.flush
604
+ end
605
+ if block_given?
606
+ yield path
607
+ @tmpfile.close(unlink)
608
+ end
609
+ path
151
610
  end
152
- #Pathname['/usr']
153
- def self.[](path)
154
- new(path)
611
+
612
+ def to_s
613
+ @tmpfile&.path || "VirtualFile:#{@name}"
155
614
  end
156
615
 
616
+ def shellescape
617
+ create
618
+ to_s&.shellescape
619
+ end
157
620
  end
158
-
159
621
  end
622
+
623
+ =begin
624
+ pry
625
+ load "dr/sh.rb"
626
+ ploum=SH::Pathname.new("ploum")
627
+ plim=SH::Pathname.new("plim")
628
+ plam=SH::Pathname.new("plam")
629
+ plim.on_cp_r(ploum, mode: :symlink, verbose: true)
630
+ plim.on_cp_r(ploum, mode: :file, verbose: true)
631
+ plim.on_cp_r(ploum, mode: :file, rm: :file, verbose: true)
632
+ =end