shell_helpers 0.1.0 → 0.6.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,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