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.
- checksums.yaml +5 -5
- data/.gitignore +6 -2
- data/.travis.yml +10 -0
- data/.yardopts +6 -1
- data/Gemfile +8 -0
- data/LICENSE.txt +1 -1
- data/README.md +27 -3
- data/Rakefile +7 -12
- data/TODO +2 -0
- data/bin/abs_to_rel.rb +42 -0
- data/bin/mv_and_ln.rb +54 -0
- data/gemspec.yml +5 -4
- data/lib/shell_helpers.rb +38 -11
- data/lib/shell_helpers/export.rb +169 -0
- data/lib/shell_helpers/logger.rb +83 -28
- data/lib/shell_helpers/options.rb +28 -0
- data/lib/shell_helpers/pathname.rb +583 -110
- data/lib/shell_helpers/run.rb +115 -29
- data/lib/shell_helpers/sh.rb +188 -39
- data/lib/shell_helpers/sysutils.rb +427 -0
- data/lib/shell_helpers/utils.rb +216 -119
- data/lib/shell_helpers/version.rb +1 -1
- data/shell_helpers.gemspec +13 -1
- data/test/helper.rb +12 -1
- data/test/test_export.rb +77 -0
- metadata +33 -8
- data/.document +0 -3
data/lib/shell_helpers/logger.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# vim: foldmethod=marker
|
2
|
-
#From methadone (cli_logger.rb, cli_logging.rb, last import:
|
2
|
+
#From methadone (cli_logger.rb, cli_logging.rb, last import: 4626a2bca9b6e54077a06a0f8e11a04fadc6e7ae; 2017-01-19)
|
3
3
|
require 'logger'
|
4
4
|
|
5
|
-
module
|
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:
|
89
|
-
super(log_device)
|
95
|
+
split_log: :auto)
|
90
96
|
@stderr_logger = Logger.new(error_device)
|
91
97
|
|
92
|
-
|
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
|
97
|
-
@stderr_logger.formatter = BLANK_FORMAT if
|
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
|
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
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
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
|
-
|
192
|
-
|
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
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
365
|
+
module FUClass
|
366
|
+
attr_writer :fu_class
|
367
|
+
def fu_class
|
368
|
+
@fu_class||=::FileUtils
|
369
|
+
end
|
119
370
|
end
|
120
|
-
|
121
|
-
|
122
|
-
FileUtils.mv(files,self,**opts)
|
371
|
+
def self.included(base)
|
372
|
+
base.extend(FUClass)
|
123
373
|
end
|
124
374
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
132
|
-
|
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
|
-
|
135
|
-
|
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
|
-
|
138
|
-
|
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
|
-
|
141
|
-
def
|
142
|
-
|
588
|
+
|
589
|
+
def path
|
590
|
+
@tmpfile&.path && Pathname.new(@tmpfile.path)
|
143
591
|
end
|
144
|
-
|
145
|
-
|
592
|
+
|
593
|
+
def file
|
594
|
+
create
|
595
|
+
path
|
146
596
|
end
|
147
597
|
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
153
|
-
def
|
154
|
-
|
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
|