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.
- 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
|