benry-unixcommand 1.0.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 +7 -0
- data/CHANGES.md +20 -0
- data/MIT-LICENSE +21 -0
- data/README.md +1063 -0
- data/benry-unixcommand.gemspec +31 -0
- data/doc/benry-unixcommand.html +932 -0
- data/doc/css/style.css +168 -0
- data/lib/benry/unixcommand.rb +1322 -0
- data/test/run_all.rb +14 -0
- data/test/unixcommand_test.rb +2517 -0
- metadata +69 -0
@@ -0,0 +1,1322 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
###
|
4
|
+
### File commands like FileUtils module
|
5
|
+
###
|
6
|
+
### $Release: 1.0.0 $
|
7
|
+
### $Copyright: copyright(c) 2021 kwatch@gmail.com $
|
8
|
+
### $License: MIT License $
|
9
|
+
###
|
10
|
+
|
11
|
+
|
12
|
+
#require 'fileutils'
|
13
|
+
|
14
|
+
|
15
|
+
module Benry
|
16
|
+
|
17
|
+
|
18
|
+
module UnixCommand
|
19
|
+
|
20
|
+
class Error < StandardError
|
21
|
+
end
|
22
|
+
|
23
|
+
module_function
|
24
|
+
|
25
|
+
def __err(msg)
|
26
|
+
raise ArgumentError.new(msg)
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def prompt()
|
31
|
+
#; [!uilyk] returns prompt string.
|
32
|
+
return "$ "
|
33
|
+
end
|
34
|
+
|
35
|
+
def prompt!(depth)
|
36
|
+
#; [!q992e] adds indentation after prompt.
|
37
|
+
return prompt() + ' ' * depth
|
38
|
+
end
|
39
|
+
|
40
|
+
def echoback(cmd)
|
41
|
+
#; [!x7atu] prints argument string into $stdout with prompt.
|
42
|
+
puts "#{prompt!(@__depth ||= 0)}#{cmd}"
|
43
|
+
end
|
44
|
+
#alias fu_output_message echoback
|
45
|
+
#private :fu_output_message
|
46
|
+
|
47
|
+
def __echoback?()
|
48
|
+
#; [!ik00u] returns value of `@__BENRY_ECHOBACK` or `$BENRY_ECHOBACK`.
|
49
|
+
#; [!1hp69] instance var `@__BENRY_ECHOBACK` is prior than `$BENRY_ECHOBACK`.
|
50
|
+
return @__BENRY_ECHOBACK != nil ? @__BENRY_ECHOBACK : $BENRY_ECHOBACK
|
51
|
+
end
|
52
|
+
|
53
|
+
$BENRY_ECHOBACK = true unless defined?($BENRY_ECHOBACK) && $BENRY_ECHOBACK != nil
|
54
|
+
|
55
|
+
def echoback_on(&block)
|
56
|
+
#; [!9x2lh] enables echoback temporarily.
|
57
|
+
echoback_switch(true, &block)
|
58
|
+
end
|
59
|
+
|
60
|
+
def echoback_off(&block)
|
61
|
+
#; [!prkfg] disables echoback temporarily.
|
62
|
+
echoback_switch(false, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
def echoback_switch(val, &block)
|
66
|
+
#; [!aw9b2] switches on/off of echoback temporarily.
|
67
|
+
defined = instance_variable_defined?(:@__BENRY_ECHOBACK)
|
68
|
+
prev = @__BENRY_ECHOBACK
|
69
|
+
@__BENRY_ECHOBACK = val
|
70
|
+
yield
|
71
|
+
nil
|
72
|
+
ensure
|
73
|
+
defined ? (@__BENRY_ECHOBACK = prev) \
|
74
|
+
: remove_instance_variable(:@__BENRY_ECHOBACK)
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
def echo(*args)
|
79
|
+
__echo('echo', args)
|
80
|
+
end
|
81
|
+
|
82
|
+
def __echo(cmd, args)
|
83
|
+
#; [!mzbdj] echoback command arguments.
|
84
|
+
optchars = __prepare(cmd, args, "n", nil)
|
85
|
+
not_nl = optchars.include?('n')
|
86
|
+
#; [!cjggd] prints arguments.
|
87
|
+
#; [!vhpw3] not print newline at end if '-n' option specified.
|
88
|
+
print args.join(" ")
|
89
|
+
puts "" unless not_nl
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
def sys(*args, &b)
|
94
|
+
__sys('sys', args, false, &b)
|
95
|
+
end
|
96
|
+
|
97
|
+
def sys!(*args, &b)
|
98
|
+
__sys('sys!', args, true, &b)
|
99
|
+
end
|
100
|
+
|
101
|
+
def __sys(cmd, args, ignore_error, &b)
|
102
|
+
optchars = __prepare(cmd, args, "q", nil) { nil }
|
103
|
+
quiet_p = optchars.include?("q")
|
104
|
+
#; [!fb1ji] error if both array and string are specified at the same time.
|
105
|
+
if args[0].is_a?(Array)
|
106
|
+
args.length == 1 or
|
107
|
+
__err "#{cmd}: Invalid argument (if arg is specified as an array, other args should not be specified)."
|
108
|
+
end
|
109
|
+
#; [!rqe7a] echoback command and arguments when `:p` not specified.
|
110
|
+
#; [!ptipz] not echoback command and arguments when `:p` specified.
|
111
|
+
#; [!4u9lj] arguments in echoback string should be quoted or escaped.
|
112
|
+
echoback_str = __build_echoback_str(args)
|
113
|
+
echoback(echoback_str) if ! quiet_p && __echoback?()
|
114
|
+
#; [!dccme] accepts one string, one array, or multiple strings.
|
115
|
+
#; [!r9ne3] shell is not invoked if arg is one array or multiple string.
|
116
|
+
#; [!w6ol7] globbing is enabled when arg is multiple string.
|
117
|
+
#; [!ifgkd] globbing is disabled when arg is one array.
|
118
|
+
if args[0].is_a?(Array)
|
119
|
+
result = __system(*args[0], shell: false) # shell: no, glob: no
|
120
|
+
elsif args.length == 1
|
121
|
+
result = __system(args[0]) # shell: yes (if necessary)
|
122
|
+
else
|
123
|
+
args2 = glob_if_possible(*args) # glob: yes
|
124
|
+
result = __system(*args2, shell: false) # shell: no
|
125
|
+
end
|
126
|
+
#; [!agntr] returns process status if command succeeded.
|
127
|
+
#; [!clfig] yields block if command failed.
|
128
|
+
#; [!deu3e] not yield block if command succeeded.
|
129
|
+
#; [!chko8] block argument is process status.
|
130
|
+
#; [!0yy6r] (sys) not raise error if block result is truthy
|
131
|
+
#; [!xsspi] (sys) raises error if command failed.
|
132
|
+
#; [!tbfii] (sys!) returns process status if command failed.
|
133
|
+
stat = $?
|
134
|
+
return stat if result
|
135
|
+
if block_given?()
|
136
|
+
result = yield stat
|
137
|
+
return stat if result
|
138
|
+
end
|
139
|
+
return stat if ignore_error
|
140
|
+
raise "Command failed with status (#{$?.exitstatus}): #{echoback_str}"
|
141
|
+
end
|
142
|
+
|
143
|
+
def __system(*args, shell: true)
|
144
|
+
#; [!9xarc] invokes command without shell when `shell:` is falty.
|
145
|
+
#; [!0z33p] invokes command with shell (if necessary) when `shell:` is truthy.
|
146
|
+
if shell
|
147
|
+
return system(*args) # with shell (if necessary)
|
148
|
+
else
|
149
|
+
return system([args[0], args[0]], *args[1..-1]) # without shell
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def __build_echoback_str(args)
|
154
|
+
#; [!4dcra] if arg is one array, quotes or escapes arguments.
|
155
|
+
#; [!ueoov] if arg is multiple string, quotes or escapes arguments.
|
156
|
+
#; [!hnp41] if arg is one string, not quote nor escape argument.
|
157
|
+
echoback_str = (
|
158
|
+
if args[0].is_a?(Array) ; args[0].collect {|x| __qq(x) }.join(" ")
|
159
|
+
elsif args.length == 1 ; args[0]
|
160
|
+
else ; args.collect {|x| __qq(x) }.join(" ")
|
161
|
+
end
|
162
|
+
)
|
163
|
+
end
|
164
|
+
|
165
|
+
def __qq(str)
|
166
|
+
if str =~ /\s/
|
167
|
+
return "\"#{str.gsub(/"/, '\\"')}\""
|
168
|
+
else
|
169
|
+
return str.gsub(/(['"\\])/, '\\\\\1')
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def glob_if_possible(*strs)
|
174
|
+
#; [!xvr32] expands file pattern matching.
|
175
|
+
#; [!z38re] if pattern not matched to any files, just returns pattern as is.
|
176
|
+
arr = []
|
177
|
+
strs.each do |s|
|
178
|
+
globbed = Dir.glob(s)
|
179
|
+
if globbed.empty?
|
180
|
+
arr << s
|
181
|
+
else
|
182
|
+
arr.concat(globbed)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
return arr
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
def ruby(*args, &b)
|
190
|
+
__ruby('ruby', args, false, &b)
|
191
|
+
end
|
192
|
+
|
193
|
+
def ruby!(*args, &b)
|
194
|
+
__ruby('ruby!', args, true, &b)
|
195
|
+
end
|
196
|
+
|
197
|
+
def __ruby(cmd, args, ignore_error, &b)
|
198
|
+
#; [!98qro] echoback command and args.
|
199
|
+
#; [!u5f5l] run ruby command.
|
200
|
+
#; [!2jano] returns process status object if ruby command succeeded.
|
201
|
+
#; [!69clt] (ruby) error when ruby command failed.
|
202
|
+
#; [!z1f03] (ruby!) ignores error even when ruby command failed.
|
203
|
+
ruby = RbConfig.ruby
|
204
|
+
if args.length == 1
|
205
|
+
__sys(cmd, ["#{ruby} #{args[0]}"], ignore_error, &b)
|
206
|
+
else
|
207
|
+
__sys(cmd, [ruby]+args, ignore_error, &b)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
def popen2( *args, **kws, &b); __popen(:popen2 , args, kws, &b); end # :nodoc:
|
213
|
+
def popen2e(*args, **kws, &b); __popen(:popen2e, args, kws, &b); end # :nodoc:
|
214
|
+
def popen3( *args, **kws, &b); __popen(:popen3 , args, kws, &b); end # :nodoc:
|
215
|
+
|
216
|
+
def __popen(cmd, args, kws, &b) # :nodoc:
|
217
|
+
#; [!8que2] calls 'Open3.popen2()'.
|
218
|
+
#; [!s6g1r] calls 'Open3.popen2e()'.
|
219
|
+
#; [!evlx7] calls 'Open3.popen3()'.
|
220
|
+
require 'open3' unless defined?(::Open3)
|
221
|
+
echoback(args.join(" ")) if __echoback?()
|
222
|
+
return ::Open3.__send__(cmd, *args, **kws, &b)
|
223
|
+
end
|
224
|
+
|
225
|
+
def capture2( *args, **kws); __capture(:capture2 , args, kws, false); end
|
226
|
+
def capture2e( *args, **kws); __capture(:capture2e, args, kws, false); end
|
227
|
+
def capture3( *args, **kws); __capture(:capture3 , args, kws, false); end
|
228
|
+
def capture2!( *args, **kws); __capture(:capture2 , args, kws, true ); end
|
229
|
+
def capture2e!(*args, **kws); __capture(:capture2e, args, kws, true ); end
|
230
|
+
def capture3!( *args, **kws); __capture(:capture3 , args, kws, true ); end
|
231
|
+
|
232
|
+
def __capture(cmd, args, kws, ignore_error) # :nodoc:
|
233
|
+
#; [!5p4dw] calls 'Open3.capture2()'.
|
234
|
+
#; [!jgn71] calls 'Open3.capture2e()'.
|
235
|
+
#; [!n91rh] calls 'Open3.capture3()'.
|
236
|
+
#; [!2s1by] error when command failed.
|
237
|
+
#; [!qr3ka] error when command failed.
|
238
|
+
#; [!thnyv] error when command failed.
|
239
|
+
#; [!357e1] ignore errors even if command failed.
|
240
|
+
#; [!o0b7c] ignore errors even if command failed.
|
241
|
+
#; [!rwfiu] ignore errors even if command failed.
|
242
|
+
require 'open3' unless defined?(::Open3)
|
243
|
+
echoback(args.join(" ")) if __echoback?()
|
244
|
+
arr = ::Open3.__send__(cmd, *args, **kws)
|
245
|
+
ignore_error || arr[-1].exitstatus == 0 or
|
246
|
+
raise "Command failed with status (#{arr[-1].exitstatus}): #{args.join(' ')}"
|
247
|
+
return arr if ignore_error
|
248
|
+
arr.pop()
|
249
|
+
return arr.length == 1 ? arr[0] : arr
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
def cd(arg, &b)
|
254
|
+
cmd = 'cd'
|
255
|
+
#; [!gnmdg] expands file pattern.
|
256
|
+
#; [!v7bn7] error when pattern not matched to any file.
|
257
|
+
#; [!08wuv] error when pattern matched to multiple files.
|
258
|
+
#; [!hs7u8] error when argument is not a directory name.
|
259
|
+
dir = __glob_onedir(cmd, arg)
|
260
|
+
#; [!cg5ns] changes current directory.
|
261
|
+
here = Dir.pwd
|
262
|
+
echoback("cd #{dir}") if __echoback?()
|
263
|
+
Dir.chdir(dir)
|
264
|
+
#; [!uit6q] if block given, then back to current dir.
|
265
|
+
if block_given?()
|
266
|
+
@__depth ||= 0
|
267
|
+
@__depth += 1
|
268
|
+
begin
|
269
|
+
yield
|
270
|
+
ensure
|
271
|
+
@__depth -= 1
|
272
|
+
echoback("cd -") if __echoback?()
|
273
|
+
Dir.chdir(here)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
#; [!cg298] returns path before changing directory.
|
277
|
+
return here
|
278
|
+
end
|
279
|
+
alias chdir cd
|
280
|
+
|
281
|
+
def pushd(arg, &b)
|
282
|
+
cmd = 'pushd'
|
283
|
+
#; [!xl6lg] raises error when block not given.
|
284
|
+
block_given?() or
|
285
|
+
__err "pushd: requires block argument."
|
286
|
+
#; [!nvkha] expands file pattern.
|
287
|
+
#; [!q3itn] error when pattern not matched to any file.
|
288
|
+
#; [!hveaj] error when pattern matched to multiple files.
|
289
|
+
#; [!y6cq9] error when argument is not a directory name.
|
290
|
+
dir = __glob_onedir(cmd, arg)
|
291
|
+
#; [!7ksfd] replaces home path with '~'.
|
292
|
+
here = Dir.pwd
|
293
|
+
home = File.expand_path("~")
|
294
|
+
here2 = here.start_with?(home) ? here.sub(home, "~") : here
|
295
|
+
#; [!rxtd0] changes directory and yields block.
|
296
|
+
echoback("pushd #{dir}") if __echoback?()
|
297
|
+
@__depth ||= 0
|
298
|
+
@__depth += 1
|
299
|
+
Dir.chdir(dir)
|
300
|
+
yield
|
301
|
+
@__depth -= 1
|
302
|
+
#; [!9jszw] back to origin directory after yielding block.
|
303
|
+
echoback("popd # back to #{here2}") if __echoback?()
|
304
|
+
Dir.chdir(here)
|
305
|
+
here
|
306
|
+
end
|
307
|
+
|
308
|
+
|
309
|
+
def __prepare(cmd, args, short_opts, to=nil) # :nodoc:
|
310
|
+
optchars = ""
|
311
|
+
errmsg = nil
|
312
|
+
while args[0].is_a?(Symbol)
|
313
|
+
optstr = args.shift().to_s.sub(/^-/, '')
|
314
|
+
optstr.each_char do |c|
|
315
|
+
if short_opts.include?(c)
|
316
|
+
optchars << c
|
317
|
+
else
|
318
|
+
errmsg ||= "#{cmd}: -#{c}: unknown option."
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
#
|
323
|
+
if block_given?()
|
324
|
+
yield optchars, args, to
|
325
|
+
elsif __echoback?()
|
326
|
+
buf = [cmd]
|
327
|
+
buf << "-#{optchars}" unless optchars.empty?
|
328
|
+
buf.concat(args)
|
329
|
+
buf << to if to
|
330
|
+
echoback(buf.join(" "))
|
331
|
+
else
|
332
|
+
nil
|
333
|
+
end
|
334
|
+
#
|
335
|
+
__err errmsg if errmsg
|
336
|
+
return optchars
|
337
|
+
end
|
338
|
+
|
339
|
+
def __filecheck1(cmd, args) # :nodoc:
|
340
|
+
n = args.length
|
341
|
+
if n < 2 ; __err "#{cmd}: requires two arguments."
|
342
|
+
elsif n > 2 ; __err "#{cmd}: too much arguments."
|
343
|
+
end
|
344
|
+
#
|
345
|
+
arr = Dir.glob(args[0]); n = arr.length
|
346
|
+
if n < 1 ; src = args[0]
|
347
|
+
elsif n > 1 ; __err "#{cmd}: #{args[0]}: unexpectedly matched to multiple files (#{arr.sort.join(', ')})."
|
348
|
+
else ; src = arr[0]
|
349
|
+
end
|
350
|
+
#
|
351
|
+
arr = Dir.glob(args[1]); n = arr.length
|
352
|
+
if n < 1 ; dst = args[1]
|
353
|
+
elsif n > 1 ; __err "#{cmd}: #{args[1]}: unexpectedly matched to multiple files (#{arr.sort.join(', ')})."
|
354
|
+
else ; dst = arr[0]
|
355
|
+
end
|
356
|
+
#
|
357
|
+
return src, dst
|
358
|
+
end
|
359
|
+
|
360
|
+
def __glob_onedir(cmd, to) # :nodoc:
|
361
|
+
arr = Dir.glob(to); n = arr.length
|
362
|
+
if n < 1 ; __err "#{cmd}: #{to}: directory not found."
|
363
|
+
elsif n > 1 ; __err "#{cmd}: #{to}: unexpectedly matched to multiple filenames (#{arr.sort.join(', ')})."
|
364
|
+
end
|
365
|
+
dir = arr[0]
|
366
|
+
File.directory?(dir) or
|
367
|
+
__err "#{cmd}: #{dir}: Not a directory."
|
368
|
+
return dir
|
369
|
+
end
|
370
|
+
|
371
|
+
def __filecheck2(cmd, filenames, dir, overwrite) # :nodoc:
|
372
|
+
if ! overwrite
|
373
|
+
filenames.each do |fname|
|
374
|
+
newfile = File.join(dir, File.basename(fname))
|
375
|
+
! File.exist?(newfile) or
|
376
|
+
__err "#{cmd}: #{newfile}: file or directory already exists (to overwrite it, call '#{cmd}!' instead of '#{cmd}')."
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def __glob_filenames(cmd, args, ignore) # :nodoc:
|
382
|
+
filenames = []
|
383
|
+
block_p = block_given?()
|
384
|
+
args.each do |arg|
|
385
|
+
arr = Dir.glob(arg)
|
386
|
+
if ! arr.empty?
|
387
|
+
filenames.concat(arr)
|
388
|
+
elsif block_p
|
389
|
+
yield arg, filenames
|
390
|
+
else
|
391
|
+
ignore or
|
392
|
+
__err "#{cmd}: #{arg}: file or directory not found (add '-f' option to ignore missing files)."
|
393
|
+
end
|
394
|
+
end
|
395
|
+
return filenames
|
396
|
+
end
|
397
|
+
|
398
|
+
|
399
|
+
def cp(*args, to: nil)
|
400
|
+
__cp('cp', args, to: to, overwrite: false)
|
401
|
+
end
|
402
|
+
|
403
|
+
def cp!(*args, to: nil)
|
404
|
+
__cp('cp!', args, to: to, overwrite: true)
|
405
|
+
end
|
406
|
+
|
407
|
+
def __cp(cmd, args, to: nil, overwrite: nil) # :nodoc:
|
408
|
+
#; [!mtuec] echoback copy command and arguments.
|
409
|
+
optchars = __prepare(cmd, args, "prfl", to)
|
410
|
+
recursive = optchars.include?("r")
|
411
|
+
preserve = optchars.include?("p")
|
412
|
+
ignore = optchars.include?("f")
|
413
|
+
hardlink = optchars.include?("l")
|
414
|
+
#; [!u98f8] when `to:` keyword arg not specified...
|
415
|
+
if ! to
|
416
|
+
#; [!u39p0] error when number of arguments is not 2.
|
417
|
+
#; [!fux6x] error when source pattern matched to multiple files.
|
418
|
+
#; [!y74ux] error when destination pattern matched to multiple files.
|
419
|
+
src, dst = __filecheck1(cmd, args)
|
420
|
+
#
|
421
|
+
if File.file?(src)
|
422
|
+
#; [!qfidz] error when destination is a directory.
|
423
|
+
! File.directory?(dst) or
|
424
|
+
__err "#{cmd}: #{dst}: cannot copy into directory (requires `to: '#{dst}'` keyword option)."
|
425
|
+
#; [!073so] (cp) error when destination already exists to avoid overwriting it.
|
426
|
+
#; [!cpr7l] (cp!) overwrites existing destination file.
|
427
|
+
! File.exist?(dst) || overwrite or
|
428
|
+
__err "#{cmd}: #{dst}: file already exists (to overwrite it, call `#{cmd}!` instead of `#{cmd}`)."
|
429
|
+
elsif File.directory?(src)
|
430
|
+
#; [!0tw8r] error when source is a directory but '-r' not specified.
|
431
|
+
recursive or
|
432
|
+
__err "#{cmd}: #{src}: is a directory (requires `:-r` option)."
|
433
|
+
#; [!lf6qi] error when target already exists.
|
434
|
+
! File.exist?(dst) or
|
435
|
+
__err "#{cmd}: #{dst}: already exists."
|
436
|
+
elsif File.exist?(src)
|
437
|
+
#; [!4xxpe] error when source is a special file.
|
438
|
+
__err "#{cmd}: #{src}: cannot copy special file."
|
439
|
+
else
|
440
|
+
#; [!urh40] do nothing if source file not found and '-f' option specified.
|
441
|
+
return if ignore
|
442
|
+
#; [!lr2bj] error when source file not found and '-f' option not specified.
|
443
|
+
__err "#{cmd}: #{src}: not found."
|
444
|
+
end
|
445
|
+
#; [!lac46] keeps file mtime if '-p' option specified.
|
446
|
+
#; [!d49vw] not keep file mtime if '-p' option not specified.
|
447
|
+
#; [!kqgdl] copy a directory recursively if '-r' option specified.
|
448
|
+
#; [!ko4he] copy a file into new file if '-r' option not specifieid.
|
449
|
+
#; [!ubthp] creates hard link instead of copy if '-l' option specified.
|
450
|
+
#; [!yu51t] error when copying supecial files such as character device.
|
451
|
+
#FileUtils.cp_r src, dst, preserve: preserve, verbose: false if recursive
|
452
|
+
#FileUtils.cp src, dst, preserve: preserve, verbose: false unless recursive
|
453
|
+
__cp_file(cmd, src, dst, preserve, hardlink)
|
454
|
+
#; [!z8xce] when `to:` keyword arg specified...
|
455
|
+
else
|
456
|
+
#; [!ms2sv] error when destination directory not exist.
|
457
|
+
#; [!q9da3] error when destination pattern matched to multiple filenames.
|
458
|
+
#; [!lg3uz] error when destination is not a directory.
|
459
|
+
dir = __glob_onedir(cmd, to)
|
460
|
+
#; [!slavo] error when file not exist but '-f' option not specified.
|
461
|
+
filenames = __glob_filenames(cmd, args, ignore)
|
462
|
+
#; [!1ceaf] (cp) error when target file or directory already exists.
|
463
|
+
#; [!melhx] (cp!) overwrites existing files.
|
464
|
+
__filecheck2(cmd, filenames, dir, overwrite)
|
465
|
+
#; [!bi897] error when copying directory but '-r' option not specified.
|
466
|
+
if ! recursive
|
467
|
+
filenames.each do |fname|
|
468
|
+
! File.directory?(fname) or
|
469
|
+
__err "#{cmd}: #{fname}: cannot copy directory (add '-r' option to copy it)."
|
470
|
+
end
|
471
|
+
end
|
472
|
+
#; [!k8gyx] keeps file timestamp (mtime) if '-p' option specified.
|
473
|
+
#; [!zoun9] not keep file timestamp (mtime) if '-p' option not specified.
|
474
|
+
#; [!654d2] copy files recursively if '-r' option specified.
|
475
|
+
#; [!i5g8r] copy files non-recursively if '-r' option not specified.
|
476
|
+
#; [!p7ah8] creates hard link instead of copy if '-l' option specified.
|
477
|
+
#; [!e90ii] error when copying supecial files such as character device.
|
478
|
+
#FileUtils.cp_r filenames, dir, preserve: preserve, verbose: false if recursive
|
479
|
+
#FileUtils.cp filenames, dir, preserve: preserve, verbose: false unless recursive
|
480
|
+
filenames.each do |fname|
|
481
|
+
newfile = File.join(dir, File.basename(fname))
|
482
|
+
__cp_file(cmd, fname, newfile, preserve, hardlink)
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
def __cp_file(cmd, srcpath, dstpath, preserve, hardlink, bufsize=4096) # :nodoc:
|
488
|
+
ftype = File.ftype(srcpath)
|
489
|
+
case ftype
|
490
|
+
when 'link'
|
491
|
+
File.symlink(File.readlink(srcpath), dstpath)
|
492
|
+
when 'file'
|
493
|
+
if hardlink
|
494
|
+
File.link(srcpath, dstpath)
|
495
|
+
else
|
496
|
+
File.open(srcpath, 'rb') do |sf|
|
497
|
+
File.open(dstpath, 'wb') do |df; bytes|
|
498
|
+
df.write(bytes) while (bytes = sf.read(bufsize))
|
499
|
+
end
|
500
|
+
end
|
501
|
+
__cp_meta(srcpath, dstpath) if preserve
|
502
|
+
end
|
503
|
+
when 'directory'
|
504
|
+
Dir.mkdir(dstpath)
|
505
|
+
Dir.open(srcpath) do |d|
|
506
|
+
d.each do |x|
|
507
|
+
next if x == '.' || x == '..'
|
508
|
+
__cp_file(cmd, File.join(srcpath, x), File.join(dstpath, x), preserve, hardlink, bufsize)
|
509
|
+
end
|
510
|
+
end
|
511
|
+
__cp_meta(srcpath, dstpath) if preserve
|
512
|
+
else # characterSpecial, blockSpecial, fifo, socket, unknown
|
513
|
+
__err "#{cmd}: #{srcpath}: cannot copy #{ftype} file."
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
def __cp_meta(src, dst) # :nodoc:
|
518
|
+
stat = File.stat(src)
|
519
|
+
File.chmod(stat.mode, dst)
|
520
|
+
File.chown(stat.uid, stat.gid, dst)
|
521
|
+
File.utime(stat.atime, stat.mtime, dst)
|
522
|
+
end
|
523
|
+
|
524
|
+
|
525
|
+
def mv(*args, to: nil)
|
526
|
+
__mv('mv', args, to: to, overwrite: false)
|
527
|
+
end
|
528
|
+
|
529
|
+
def mv!(*args, to: nil)
|
530
|
+
__mv('mv!', args, to: to, overwrite: true)
|
531
|
+
end
|
532
|
+
|
533
|
+
def __mv(cmd, args, to: nil, overwrite: nil) # :nodoc:
|
534
|
+
#; [!ajm59] echoback command and arguments.
|
535
|
+
optchars = __prepare(cmd, args, "f", to)
|
536
|
+
ignore = optchars.include?("f")
|
537
|
+
#; [!g732t] when `to:` keyword argument not specified...
|
538
|
+
if !to
|
539
|
+
#; [!0f106] error when number of arguments is not 2.
|
540
|
+
#; [!xsti2] error when source pattern matched to multiple files.
|
541
|
+
#; [!4wam3] error when destination pattern matched to multiple files.
|
542
|
+
src, dst = __filecheck1(cmd, args)
|
543
|
+
#
|
544
|
+
if !File.exist?(src)
|
545
|
+
#; [!397kn] do nothing when file or directory not found but '-f' option specified.
|
546
|
+
return if ignore
|
547
|
+
#; [!1z89i] error when source file or directory not found.
|
548
|
+
__err "#{cmd}: #{src}: not found."
|
549
|
+
end
|
550
|
+
#
|
551
|
+
if File.exist?(dst)
|
552
|
+
#; [!ude1j] cannot move file into existing directory.
|
553
|
+
if File.file?(src) && File.directory?(dst)
|
554
|
+
__err "#{cmd}: cannot move file '#{src}' into directory '#{dst}' without 'to:' keyword option."
|
555
|
+
end
|
556
|
+
#; [!2aws0] cannt rename directory into existing file or directory.
|
557
|
+
if File.directory?(src)
|
558
|
+
__err "#{cmd}: cannot rename directory '#{src}' to existing file or directory."
|
559
|
+
end
|
560
|
+
#; [!3fbpu] (mv) error when destination file already exists.
|
561
|
+
#; [!zpojx] (mv!) overwrites existing files.
|
562
|
+
overwrite or
|
563
|
+
__err "#{cmd}: #{dst}: already exists (to overwrite it, call `#{cmd}!` instead of `#{cmd}`)."
|
564
|
+
end
|
565
|
+
#; [!9eqt3] rename file or directory.
|
566
|
+
#FileUtils.mv src, dst, verbose: false
|
567
|
+
File.rename(src, dst)
|
568
|
+
#; [!iu87y] when `to:` keyword argument specified...
|
569
|
+
else
|
570
|
+
#; [!wf6pc] error when destination directory not exist.
|
571
|
+
#; [!8v4dn] error when destination pattern matched to multiple filenames.
|
572
|
+
#; [!ppr6n] error when destination is not a directory.
|
573
|
+
dir = __glob_onedir(cmd, to)
|
574
|
+
#; [!bjqwi] error when file not exist but '-f' option not specified.
|
575
|
+
filenames = __glob_filenames(cmd, args, ignore)
|
576
|
+
#; [!k21ns] (mv) error when target file or directory already exists.
|
577
|
+
#; [!vcaf5] (mv!) overwrites existing files.
|
578
|
+
__filecheck2(cmd, filenames, dir, overwrite)
|
579
|
+
#; [!ri2ia] move files into existing directory.
|
580
|
+
#FileUtils.mv filenames, dir, verbose: false
|
581
|
+
filenames.each do |fname|
|
582
|
+
newfile = File.join(dir, File.basename(fname))
|
583
|
+
File.rename(fname, newfile)
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
|
589
|
+
def rm(*args)
|
590
|
+
__rm('rm', args)
|
591
|
+
end
|
592
|
+
|
593
|
+
def __rm(cmd, args) # :nodoc:
|
594
|
+
#; [!bikrs] echoback command and arguments.
|
595
|
+
optchars = __prepare(cmd, args, "rf", nil)
|
596
|
+
recursive = optchars.include?("r")
|
597
|
+
ignore = optchars.include?("f")
|
598
|
+
#; [!va1j0] error when file not exist but '-f' option not specified.
|
599
|
+
#; [!t6vhx] ignores missing files if '-f' option specified.
|
600
|
+
filenames = __glob_filenames(cmd, args, ignore)
|
601
|
+
#; [!o92yi] cannot remove directory unless '-r' option specified.
|
602
|
+
if ! recursive
|
603
|
+
filenames.each do |fname|
|
604
|
+
! File.directory?(fname) or
|
605
|
+
__err "#{cmd}: #{fname}: cannot remove directory (add '-r' option to remove it)."
|
606
|
+
end
|
607
|
+
end
|
608
|
+
#; [!srx8w] remove directories recursively if '-r' option specified.
|
609
|
+
#; [!mdgjc] remove files if '-r' option not specified.
|
610
|
+
#FileUtils.rm_r filenames, verbose: false, secure: true if recursive
|
611
|
+
#FileUtils.rm filenames, verbose: false unless recursive
|
612
|
+
__each_file(filenames, recursive) do |type, fpath|
|
613
|
+
case type
|
614
|
+
when :sym ; File.unlink(fpath)
|
615
|
+
when :dir ; Dir.rmdir(fpath)
|
616
|
+
when :file ; File.unlink(fpath)
|
617
|
+
end
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
|
622
|
+
def mkdir(*args)
|
623
|
+
__mkdir('mkdir', args)
|
624
|
+
end
|
625
|
+
|
626
|
+
def __mkdir(cmd, args) # :nodoc:
|
627
|
+
optchars = __prepare(cmd, args, "pm", nil)
|
628
|
+
mkpath = optchars.include?("p")
|
629
|
+
mode = optchars.include?("m") ? args.shift() : nil
|
630
|
+
#; [!wd7rm] error when mode is invalid.
|
631
|
+
case mode
|
632
|
+
when nil ; # pass
|
633
|
+
when Integer ; # pass
|
634
|
+
when /\A\d+\z/ ; mode = mode.to_i(8)
|
635
|
+
when /\A\w+[-+]\w+\z/ ; __err "#{cmd}: #{mode}: '-m' option doesn't support this style mode (use '0755' tyle instead)."
|
636
|
+
else ; __err "#{cmd}: #{mode}: invalid mode."
|
637
|
+
end
|
638
|
+
#; [!xusor] raises error when argument not specified.
|
639
|
+
! args.empty? or
|
640
|
+
__err "#{cmd}: argument required."
|
641
|
+
#
|
642
|
+
filenames = []
|
643
|
+
args.each do |arg|
|
644
|
+
arr = Dir.glob(arg)
|
645
|
+
if arr.empty?
|
646
|
+
#; [!xx7mv] error when parent directory not exist but '-p' option not specified.
|
647
|
+
if ! File.directory?(File.dirname(arg))
|
648
|
+
mkpath or
|
649
|
+
__err "#{cmd}: #{arg}: parent directory not exists (add '-p' to create it)."
|
650
|
+
end
|
651
|
+
filenames << arg
|
652
|
+
#; [!51pmg] error when directory already exists but '-p' option not specified.
|
653
|
+
#; [!pydy1] ignores existing directories if '-p' option specified.
|
654
|
+
elsif File.directory?(arr[0])
|
655
|
+
mkpath or
|
656
|
+
__err "#{cmd}: #{arr[0]}: directory already exists."
|
657
|
+
#; [!om8a6] error when file already exists.
|
658
|
+
else
|
659
|
+
__err "#{cmd}: #{arr[0]}: file exists."
|
660
|
+
end
|
661
|
+
end
|
662
|
+
#; [!jc8hm] '-m' option specifies mode of new directories.
|
663
|
+
if mkpath
|
664
|
+
#; [!0zeu3] create intermediate path if '-p' option specified.
|
665
|
+
#FileUtils.mkdir_p args, mode: mode, verbose: false
|
666
|
+
pr = proc do |fname|
|
667
|
+
parent = File.dirname(fname)
|
668
|
+
parent != fname or
|
669
|
+
raise "** assertion failed: fname=#{fname.inspect}, parent=#{parent.inspect}"
|
670
|
+
pr.call(parent) unless File.directory?(parent)
|
671
|
+
Dir.mkdir(fname)
|
672
|
+
File.chmod(mode, fname) if mode
|
673
|
+
end
|
674
|
+
filenames.each {|fname| pr.call(fname) }
|
675
|
+
else
|
676
|
+
#; [!l0pr8] create directories if '-p' option not specified.
|
677
|
+
#FileUtils.mkdir args, mode: mode, verbose: false
|
678
|
+
filenames.each {|fname|
|
679
|
+
Dir.mkdir(fname)
|
680
|
+
File.chmod(mode, fname) if mode
|
681
|
+
}
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
|
686
|
+
def rmdir(*args)
|
687
|
+
__rmdir('rmdir', args)
|
688
|
+
end
|
689
|
+
|
690
|
+
def __rmdir(cmd, args) # :nodoc:
|
691
|
+
optchars = __prepare(cmd, args, "", nil)
|
692
|
+
_ = optchars # avoid waring of `ruby -wc`
|
693
|
+
#; [!bqhdd] error when argument not specified.
|
694
|
+
! args.empty? or
|
695
|
+
__err "#{cmd}: argument required."
|
696
|
+
#; [!o1k3g] error when directory not exist.
|
697
|
+
dirnames = __glob_filenames(cmd, args, false) do |arg, filenames|
|
698
|
+
__err "#{cmd}: #{arg}: No such file or directory."
|
699
|
+
end
|
700
|
+
#
|
701
|
+
dirnames.each do |dname|
|
702
|
+
#; [!ch5rq] error when directory is a symbolic link.
|
703
|
+
if File.symlink?(dname)
|
704
|
+
__err "#{cmd}: #{dname}: Not a directory."
|
705
|
+
#; [!igfti] error when directory is not empty.
|
706
|
+
elsif File.directory?(dname)
|
707
|
+
found = Dir.open(dname) {|d|
|
708
|
+
d.any? {|x| x != '.' && x != '..' }
|
709
|
+
}
|
710
|
+
! found or
|
711
|
+
__err "#{cmd}: #{dname}: Directory not empty."
|
712
|
+
#; [!qnnqy] error when argument is not a directory.
|
713
|
+
elsif File.exist?(dname)
|
714
|
+
__err "#{cmd}: #{dname}: Not a directory."
|
715
|
+
else
|
716
|
+
raise "** assertion failed: dname=#{dname.inspect}"
|
717
|
+
end
|
718
|
+
end
|
719
|
+
#; [!jgmw7] remove empty directories.
|
720
|
+
#FileUtils.rmdir dirnames, verbose: false
|
721
|
+
dirnames.each do |dname|
|
722
|
+
Dir.rmdir(dname)
|
723
|
+
end
|
724
|
+
end
|
725
|
+
|
726
|
+
|
727
|
+
def ln(*args, to: nil)
|
728
|
+
__ln('ln', args, to: to, overwrite: false)
|
729
|
+
end
|
730
|
+
|
731
|
+
def ln!(*args, to: nil)
|
732
|
+
__ln('ln!', args, to: to, overwrite: true)
|
733
|
+
end
|
734
|
+
|
735
|
+
def __ln(cmd, args, to: nil, overwrite: nil) # :nodoc:
|
736
|
+
#; [!ycp6e] echobacks command and arguments.
|
737
|
+
#; [!umk6m] keyword arg `to: xx` is echobacked as `-t xx`.
|
738
|
+
optchars = __prepare(cmd, args, "s", to) do |optchars, args_, to_|
|
739
|
+
buf = [cmd]
|
740
|
+
buf << "-t #{to_}" if to_
|
741
|
+
buf << "-#{optchars}n" # `-n` means "don't follow symbolic link"
|
742
|
+
echoback(buf.concat(args).join(" ")) if __echoback?()
|
743
|
+
end
|
744
|
+
symbolic = optchars.include?("s")
|
745
|
+
#; [!qtbp4] when `to:` keyword argument not specified...
|
746
|
+
if !to
|
747
|
+
#; [!n1zpi] error when number of arguments is not 2.
|
748
|
+
#; [!2rxqo] error when source pattern matched to multiple files.
|
749
|
+
#; [!ysxdq] error when destination pattern matched to multiple files.
|
750
|
+
src, dst = __filecheck1(cmd, args)
|
751
|
+
#
|
752
|
+
if ! symbolic
|
753
|
+
#; [!4ry8j] (hard link) error when source file not exists.
|
754
|
+
File.exist?(src) or
|
755
|
+
__err "#{cmd}: #{src}: No such file or directory."
|
756
|
+
#; [!tf29w] (hard link) error when source is a directory.
|
757
|
+
! File.directory?(src) or
|
758
|
+
__err "#{cmd}: #{src}: Is a directory."
|
759
|
+
end
|
760
|
+
#; [!zmijh] error when destination is a directory without `to:` keyword argument.
|
761
|
+
if File.directory?(dst)
|
762
|
+
__err "#{cmd}: #{dst}: cannot create link under directory without `to:` keyword option."
|
763
|
+
end
|
764
|
+
#; [!nzci0] (ln) error when destination already exists.
|
765
|
+
if ! overwrite
|
766
|
+
! File.exist?(dst) or
|
767
|
+
__err "#{cmd}: #{dst}: File exists (to overwrite it, call `#{cmd}!` instead of `#{cmd}`)."
|
768
|
+
#; [!dkqgq] (ln!) overwrites existing destination file.
|
769
|
+
else
|
770
|
+
File.unlink(dst) if File.symlink?(dst) || File.file?(dst)
|
771
|
+
end
|
772
|
+
#; [!oxjqv] create symbolic link if '-s' option specified.
|
773
|
+
#; [!awig1] (symlink) can create symbolic link to non-existing file.
|
774
|
+
#; [!5kl3w] (symlink) can create symbolic link to directory.
|
775
|
+
if symbolic
|
776
|
+
File.unlink(dst) if overwrite && File.symlink?(dst)
|
777
|
+
File.symlink(src, dst)
|
778
|
+
#; [!sb29p] create hard link if '-s' option not specified.
|
779
|
+
else
|
780
|
+
File.link(src, dst) unless symbolic
|
781
|
+
end
|
782
|
+
#; [!5x2wr] when `to:` keyword argument specified...
|
783
|
+
else
|
784
|
+
#; [!5gfxk] error when destination directory not exist.
|
785
|
+
#; [!euu5d] error when destination pattern matched to multiple filenames.
|
786
|
+
#; [!42nb7] error when destination is not a directory.
|
787
|
+
dir = __glob_onedir(cmd, to)
|
788
|
+
#; [!x7wh5] (symlink) can create symlink to unexisting file.
|
789
|
+
#; [!ml1vm] (hard link) error when source file not exist.
|
790
|
+
filenames = __glob_filenames(cmd, args, false) do |arg, filenames|
|
791
|
+
if symbolic
|
792
|
+
filenames << arg
|
793
|
+
else
|
794
|
+
__err "#{cmd}: #{arg}: No such file or directory."
|
795
|
+
end
|
796
|
+
end
|
797
|
+
#; [!mwukw] (ln) error when target file or directory already exists.
|
798
|
+
#; [!c3vwn] (ln!) error when target file is a directory.
|
799
|
+
#__filecheck2(cmd, filenames, dir, overwrite)
|
800
|
+
filenames.each do |fname|
|
801
|
+
newfile = File.join(dir, fname)
|
802
|
+
if File.symlink?(newfile)
|
803
|
+
overwrite or
|
804
|
+
__err "#{cmd}: #{newfile}: symbolic link already exists (to overwrite it, call `#{cmd}!` instead of `#{cmd}`)."
|
805
|
+
elsif File.file?(newfile)
|
806
|
+
overwrite or
|
807
|
+
__err "#{cmd}: #{newfile}: File exists (to overwrite it, call `#{cmd}!` instead of `#{cmd}`)."
|
808
|
+
elsif File.directory?(newfile)
|
809
|
+
__err "#{cmd}: #{newfile}: directory already exists."
|
810
|
+
end
|
811
|
+
end
|
812
|
+
#
|
813
|
+
filenames.each do |fname|
|
814
|
+
newfile = File.join(dir, File.basename(fname))
|
815
|
+
#; [!bfcki] (ln!) overwrites existing symbolic links.
|
816
|
+
#; [!ipy2c] (ln!) overwrites existing files.
|
817
|
+
if File.symlink?(newfile) || File.file?(newfile)
|
818
|
+
File.unlink(newfile) if overwrite
|
819
|
+
end
|
820
|
+
#; [!c8hpp] (hard link) create hard link under directory if '-s' option not specified.
|
821
|
+
#; [!9tv9g] (symlik) create symbolic link under directory if '-s' option specified.
|
822
|
+
if symbolic
|
823
|
+
File.symlink(fname, newfile)
|
824
|
+
else
|
825
|
+
File.link(fname, newfile)
|
826
|
+
end
|
827
|
+
end
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
|
832
|
+
def atomic_symlink!(src, dst)
|
833
|
+
cmd = 'atomic_symlink!'
|
834
|
+
#; [!gzp4a] creates temporal symlink and rename it when symlink already exists.
|
835
|
+
#; [!lhomw] creates temporal symlink and rename it when symlink not exist.
|
836
|
+
if File.symlink?(dst) || ! File.exist?(dst)
|
837
|
+
tmp = "#{dst}.#{rand().to_s[2..5]}"
|
838
|
+
echoback("ln -s #{src} #{tmp} && mv -Tf #{tmp} #{dst}") if __echoback?()
|
839
|
+
File.symlink(src, tmp)
|
840
|
+
File.rename(tmp, dst)
|
841
|
+
#; [!h75kp] error when destination is normal file or directory.
|
842
|
+
else
|
843
|
+
__err "#{cmd}: #{dst}: not a symbolic link."
|
844
|
+
end
|
845
|
+
end
|
846
|
+
|
847
|
+
|
848
|
+
def pwd()
|
849
|
+
#; [!aelx6] echoback command and arguments.
|
850
|
+
echoback("pwd") if __echoback?()
|
851
|
+
#; [!kh3l2] prints current directory path.
|
852
|
+
puts Dir.pwd
|
853
|
+
end
|
854
|
+
|
855
|
+
|
856
|
+
def touch(*args)
|
857
|
+
__touch('touch', *args)
|
858
|
+
end
|
859
|
+
|
860
|
+
def __touch(cmd, *args) # :nodoc:
|
861
|
+
#; [!ifxob] echobacks command and arguments.
|
862
|
+
optchars = __prepare(cmd, args, "amrc", nil)
|
863
|
+
access_time = optchars.include?("a")
|
864
|
+
modify_time = optchars.include?("m")
|
865
|
+
not_create = optchars.include?("c")
|
866
|
+
ref_file = optchars.include?("r") ? args.shift() : nil
|
867
|
+
#; [!c7e51] error when reference file not exist.
|
868
|
+
ref_file.nil? || File.exist?(ref_file) or
|
869
|
+
__err "#{cmd}: #{ref_file}: not exist."
|
870
|
+
#; [!pggnv] changes both access time and modification time in default.
|
871
|
+
if access_time == false && modify_time == false
|
872
|
+
access_time = true
|
873
|
+
modify_time = true
|
874
|
+
end
|
875
|
+
#; [!o9h74] expands file name pattern.
|
876
|
+
filenames = []
|
877
|
+
args.each do |arg|
|
878
|
+
arr = Dir.glob(arg)
|
879
|
+
if arr.empty?
|
880
|
+
filenames << arg
|
881
|
+
else
|
882
|
+
filenames.concat(arr)
|
883
|
+
end
|
884
|
+
end
|
885
|
+
#; [!9ahsu] changes timestamp of files to current datetime.
|
886
|
+
now = Time.now
|
887
|
+
filenames.each do |fname|
|
888
|
+
atime = mtime = now
|
889
|
+
#; [!wo080] if reference file specified, use it's timestamp.
|
890
|
+
if ref_file
|
891
|
+
atime = File.atime(ref_file)
|
892
|
+
mtime = File.mtime(ref_file)
|
893
|
+
end
|
894
|
+
#; [!726rq] creates empty file if file not found and '-c' option not specified.
|
895
|
+
#; [!cfc40] skips non-existing files if '-c' option specified.
|
896
|
+
if ! File.exist?(fname)
|
897
|
+
next if not_create
|
898
|
+
File.open(fname, 'w') {|f| f.write("") }
|
899
|
+
end
|
900
|
+
#; [!s50bp] changes only access timestamp if '-a' option specified.
|
901
|
+
#; [!k7zap] changes only modification timestamp if '-m' option specified.
|
902
|
+
#; [!b5c1n] changes both access and modification timestamps in default.
|
903
|
+
if false
|
904
|
+
elsif access_time && modify_time
|
905
|
+
File.utime(atime, mtime, fname)
|
906
|
+
elsif access_time
|
907
|
+
File.utime(atime, File.mtime(fname), fname)
|
908
|
+
elsif modify_time
|
909
|
+
File.utime(File.atime(fname), mtime, fname)
|
910
|
+
end
|
911
|
+
end
|
912
|
+
end
|
913
|
+
|
914
|
+
|
915
|
+
def chmod(*args)
|
916
|
+
__chmod("chmod", args)
|
917
|
+
end
|
918
|
+
|
919
|
+
def __chmod(cmd, args, _debug=false) # :nodoc:
|
920
|
+
#; [!pmmvj] echobacks command and arguments.
|
921
|
+
optchars = __prepare(cmd, args, "R", nil)
|
922
|
+
recursive = optchars.include?("R")
|
923
|
+
#; [!94hl9] error when mode not specified.
|
924
|
+
mode_s = args.shift() or
|
925
|
+
__err "#{cmd}: argument required."
|
926
|
+
#; [!c8zhu] mode can be integer or octal string.
|
927
|
+
mode_i = nil; mask = op = nil
|
928
|
+
case mode_s
|
929
|
+
when Integer
|
930
|
+
mode_i = mode_s
|
931
|
+
#; [!j3nqp] error when integer mode is invalid.
|
932
|
+
(0..0777).include?(mode_i) or
|
933
|
+
__err "#{cmd}: #{mode_i}: Invalid file mode."
|
934
|
+
when /\A[0-7][0-7][0-7][0-7]?\z/
|
935
|
+
mode_i = mode_s.to_i(8) # octal -> decimal
|
936
|
+
#; [!ox3le] converts 'u+r' style mode into mask.
|
937
|
+
when /\A([ugoa])([-+])([rwxst])\z/
|
938
|
+
who = $1; op = $2; perm = $3
|
939
|
+
i = "ugoa".index(who) or raise "** assertion failed: who=#{who.inspect}"
|
940
|
+
mask = CHMOD_MODES[perm][i]
|
941
|
+
#; [!axqed] error when mode is invalid.
|
942
|
+
else
|
943
|
+
__err "#{cmd}: #{mode_s}: Invalid file mode."
|
944
|
+
end
|
945
|
+
return mode_i, mask if _debug
|
946
|
+
#; [!ru371] expands file pattern.
|
947
|
+
#; [!ou3ih] error when file not exist.
|
948
|
+
#; [!8sd4b] error when file pattern not matched to anything.
|
949
|
+
filenames = __glob_filenames(cmd, args, false) do |arg, filenames|
|
950
|
+
__err "#{cmd}: #{arg}: No such file or directory."
|
951
|
+
end
|
952
|
+
#; [!q1psx] changes file mode.
|
953
|
+
#; [!4en6n] skips symbolic links.
|
954
|
+
#; [!4e7ve] changes mode recursively if '-R' option specified.
|
955
|
+
__each_file(filenames, recursive) do |type, fpath|
|
956
|
+
next if type == :sym
|
957
|
+
if mode_i
|
958
|
+
mode = mode_i
|
959
|
+
else
|
960
|
+
mode = File.stat(fpath).mode
|
961
|
+
mode = case op
|
962
|
+
when '+' ; mode | mask
|
963
|
+
when '-' ; mode & ~mask
|
964
|
+
end
|
965
|
+
end
|
966
|
+
File.chmod(mode, fpath)
|
967
|
+
end
|
968
|
+
end
|
969
|
+
|
970
|
+
def __each_file(filenames, recursive, &b) # :nodoc:
|
971
|
+
filenames.each do |fname|
|
972
|
+
__each_path(fname, recursive, &b)
|
973
|
+
end
|
974
|
+
end
|
975
|
+
|
976
|
+
def __each_path(fpath, recursive, &b) # :nodoc:
|
977
|
+
if File.symlink?(fpath)
|
978
|
+
yield :sym, fpath
|
979
|
+
elsif File.directory?(fpath) && recursive
|
980
|
+
Dir.open(fpath) do |d|
|
981
|
+
d.each do |x|
|
982
|
+
next if x == '.' || x == '..'
|
983
|
+
__each_path(File.join(fpath, x), recursive, &b)
|
984
|
+
end
|
985
|
+
end
|
986
|
+
yield :dir, fpath
|
987
|
+
else
|
988
|
+
yield :file, fpath
|
989
|
+
end
|
990
|
+
end
|
991
|
+
|
992
|
+
CHMOD_MODES = {
|
993
|
+
## perm => [user, group, other, all]
|
994
|
+
'r' => [ 0400, 0040, 0004, 0444],
|
995
|
+
'w' => [ 0200, 0020, 0002, 0222],
|
996
|
+
'x' => [ 0100, 0010, 0001, 0111],
|
997
|
+
's' => [04000, 02000, 0, 06000],
|
998
|
+
't' => [ 0, 0, 0, 01000],
|
999
|
+
}.freeze
|
1000
|
+
|
1001
|
+
|
1002
|
+
def chown(*args)
|
1003
|
+
__chown("chown", args)
|
1004
|
+
end
|
1005
|
+
|
1006
|
+
def __chown(cmd, args, _debug=false) # :nodoc:
|
1007
|
+
#; [!5jqqv] echobacks command and arguments.
|
1008
|
+
optchars = __prepare(cmd, args, "R", nil)
|
1009
|
+
recursive = optchars.include?("R")
|
1010
|
+
#; [!hkxgu] error when owner not specified.
|
1011
|
+
owner = args.shift() or
|
1012
|
+
__err "#{cmd}: argument required."
|
1013
|
+
#; [!0a35v] accepts integer as user id.
|
1014
|
+
owner = owner.to_s if owner.is_a?(Integer)
|
1015
|
+
#; [!b5qud] accepts 'user:group' argument.
|
1016
|
+
#; [!18gf0] accepts 'user' argument.
|
1017
|
+
#; [!mw5tg] accepts ':group' argument.
|
1018
|
+
case owner
|
1019
|
+
when /\A(\w+):?\z/ ; user = $1 ; group = nil
|
1020
|
+
when /\A(\w+):(\w+)\z/ ; user = $1 ; group = $2
|
1021
|
+
when /\A:(\w+)\z/ ; user = nil; group = $1
|
1022
|
+
else
|
1023
|
+
__err "#{cmd}: #{owner}: invalid owner."
|
1024
|
+
end
|
1025
|
+
#; [!jyecc] converts user name into user id.
|
1026
|
+
#; [!kt7mp] error when invalid user name specified.
|
1027
|
+
begin
|
1028
|
+
user_id = user ? __chown_uid(user) : nil
|
1029
|
+
rescue ArgumentError
|
1030
|
+
__err "#{cmd}: #{user}: unknown user name."
|
1031
|
+
end
|
1032
|
+
#; [!f7ye0] converts group name into group id.
|
1033
|
+
#; [!szlsb] error when invalid group name specified.
|
1034
|
+
begin
|
1035
|
+
group_id = group ? __chown_gid(group) : nil
|
1036
|
+
rescue ArgumentError
|
1037
|
+
__err "#{cmd}: #{group}: unknown group name."
|
1038
|
+
end
|
1039
|
+
return user_id, group_id if _debug
|
1040
|
+
#; [!138eh] expands file pattern.
|
1041
|
+
#; [!tvpey] error when file not exist.
|
1042
|
+
#; [!ovkk8] error when file pattern not matched to anything.
|
1043
|
+
filenames = __glob_filenames(cmd, args, false) do |arg, filenames|
|
1044
|
+
__err "#{cmd}: #{arg}: No such file or directory."
|
1045
|
+
end
|
1046
|
+
#
|
1047
|
+
#; [!7tf3k] changes file mode.
|
1048
|
+
#; [!m6mrg] skips symbolic links.
|
1049
|
+
#; [!b07ff] changes file mode recursively if '-R' option specified.
|
1050
|
+
__each_file(filenames, recursive) do |type, fpath|
|
1051
|
+
next if type == :sym
|
1052
|
+
File.chown(user_id, group_id, fpath)
|
1053
|
+
end
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
def __chown_uid(user) # :nodoc:
|
1057
|
+
require 'etc' unless defined?(::Etc)
|
1058
|
+
case user
|
1059
|
+
when nil ; return nil
|
1060
|
+
when /\A\d+\z/ ; return user.to_i
|
1061
|
+
else ; return (x = Etc.getpwnam(user)) ? x.uid : nil # ArgumentError
|
1062
|
+
end
|
1063
|
+
end
|
1064
|
+
|
1065
|
+
def __chown_gid(group) # :nodoc:
|
1066
|
+
require 'etc' unless defined?(::Etc)
|
1067
|
+
case group
|
1068
|
+
when nil ; return nil
|
1069
|
+
when /\A\d+\z/ ; return group.to_i
|
1070
|
+
else ; return (x = Etc.getgrnam(group)) ? x.gid : nil # ArgumentError
|
1071
|
+
end
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
|
1075
|
+
def store(*args, to:)
|
1076
|
+
__store('store', args, false, to: to)
|
1077
|
+
end
|
1078
|
+
|
1079
|
+
def store!(*args, to:)
|
1080
|
+
__store('store!', args, true, to: to)
|
1081
|
+
end
|
1082
|
+
|
1083
|
+
def __store(cmd, args, overwrite, to:)
|
1084
|
+
#; [!9wr1o] error when `to:` keyword argument not specified.
|
1085
|
+
! to.nil? or
|
1086
|
+
__err "#{cmd}: 'to:' keyword argument required."
|
1087
|
+
#; [!n43u2] echoback command and arguments.
|
1088
|
+
optchars = __prepare(cmd, args, "pfl", to)
|
1089
|
+
preserve = optchars.include?("p")
|
1090
|
+
ignore = optchars.include?("f")
|
1091
|
+
hardlink = optchars.include?("l")
|
1092
|
+
#; [!588e5] error when destination directory not exist.
|
1093
|
+
#; [!lm43y] error when destination pattern matched to multiple filenames.
|
1094
|
+
#; [!u5zoy] error when destination is not a directory.
|
1095
|
+
dir = __glob_onedir(cmd, to)
|
1096
|
+
#; [!g1duw] error when absolute path specified.
|
1097
|
+
args.each do |arg|
|
1098
|
+
#! File.absolute_path?(arg) or # Ruby >= 2.7
|
1099
|
+
File.absolute_path(arg) != arg or
|
1100
|
+
__err "#{cmd}: #{arg}: absolute path not expected (only relative path expected)."
|
1101
|
+
end
|
1102
|
+
#; [!je1i2] error when file not exist but '-f' option not specified.
|
1103
|
+
filenames = __glob_filenames(cmd, args, ignore)
|
1104
|
+
#; [!5619q] (store) error when target file or directory already exists.
|
1105
|
+
#; [!cw08t] (store!) overwrites existing files.
|
1106
|
+
if ! overwrite
|
1107
|
+
filenames.each do |fpath|
|
1108
|
+
newpath = File.join(dir, fpath)
|
1109
|
+
! File.exist?(newpath) or
|
1110
|
+
__err "#{cmd}: #{newpath}: destination file or directory already exists."
|
1111
|
+
end
|
1112
|
+
end
|
1113
|
+
#; [!4y4zy] copy files with keeping filepath.
|
1114
|
+
#; [!f0n0y] copy timestamps if '-p' option specified.
|
1115
|
+
#; [!w8oq6] creates hard links if '-l' option specified.
|
1116
|
+
#; [!7n869] error when copying supecial files such as character device.
|
1117
|
+
pathcache = {}
|
1118
|
+
filenames.each do |fpath|
|
1119
|
+
newpath = File.join(dir, fpath)
|
1120
|
+
__mkpath(File.dirname(newpath), pathcache)
|
1121
|
+
__cp_file(cmd, fpath, newpath, preserve, hardlink, bufsize=4096)
|
1122
|
+
end
|
1123
|
+
end
|
1124
|
+
|
1125
|
+
def __mkpath(dirpath, pathcache={})
|
1126
|
+
if ! pathcache.include?(dirpath)
|
1127
|
+
parent = File.dirname(dirpath)
|
1128
|
+
__mkpath(parent, pathcache) unless parent == dirpath
|
1129
|
+
Dir.mkdir(dirpath) unless File.exist?(dirpath)
|
1130
|
+
pathcache[dirpath] = true
|
1131
|
+
end
|
1132
|
+
end
|
1133
|
+
|
1134
|
+
|
1135
|
+
def zip(*args)
|
1136
|
+
__zip('zip', args, false)
|
1137
|
+
end
|
1138
|
+
|
1139
|
+
def zip!(*args)
|
1140
|
+
__zip('zip', args, true)
|
1141
|
+
end
|
1142
|
+
|
1143
|
+
def __zip(cmd, args, overwrite)
|
1144
|
+
#; [!zzvuk] requires 'zip' gem automatically.
|
1145
|
+
require 'zip' unless defined?(::Zip)
|
1146
|
+
#; [!zk1qt] echoback command and arguments.
|
1147
|
+
optchars = __prepare(cmd, args, "r0123456789", nil)
|
1148
|
+
recursive = optchars.include?('r')
|
1149
|
+
complevel = (optchars =~ /(\d)/ ? $1.to_i : nil)
|
1150
|
+
#; [!lrnj7] zip filename required.
|
1151
|
+
zip_filename = args.shift() or
|
1152
|
+
__err "#{cmd}: zip filename required."
|
1153
|
+
#; [!khbiq] zip filename can be glob pattern.
|
1154
|
+
#; [!umbal] error when zip file glob pattern matched to mutilple filenames.
|
1155
|
+
arr = Dir.glob(zip_filename); n = arr.length
|
1156
|
+
if n < 1 ; nil
|
1157
|
+
elsif n > 1 ; __err "#{cmd}: #{zip_filename}: matched to multiple filenames (#{arr.sort.join(', ')})."
|
1158
|
+
else ; zip_filename = arr[0]
|
1159
|
+
end
|
1160
|
+
#; [!oqzna] (zip) raises error if zip file already exists.
|
1161
|
+
! File.exist?(zip_filename) || overwrite or
|
1162
|
+
__err "#{cmd}: #{zip_filename}: already exists (to overwrite it, call `#{cmd}!` command instead of `#{cmd}` command)."
|
1163
|
+
#; [!uu8uz] expands glob pattern.
|
1164
|
+
#; [!nahxa] error if file not exist.
|
1165
|
+
filenames = __glob_filenames(cmd, args, false) do |arg, _|
|
1166
|
+
__err "#{cmd}: #{arg}: file or directory not found."
|
1167
|
+
end
|
1168
|
+
#; [!qsp7c] cannot specify absolute path.
|
1169
|
+
filenames.each do |fname|
|
1170
|
+
if File.absolute_path(fname) == fname # Ruby >= 2.7: File.absolute_path?()
|
1171
|
+
__err "#{cmd}: #{fname}: not support absolute path."
|
1172
|
+
end
|
1173
|
+
end
|
1174
|
+
#; [!e995z] (zip!) removes zip file if exists.
|
1175
|
+
File.unlink(zip_filename) if File.exist?(zip_filename)
|
1176
|
+
#; [!3sxmg] supports complession level (0~9).
|
1177
|
+
orig = Zip.default_compression
|
1178
|
+
Zip.default_compression = complevel if complevel
|
1179
|
+
#; [!p8alf] creates zip file.
|
1180
|
+
begin
|
1181
|
+
zipf = ::Zip::File.open(zip_filename, create: true) do |zf| # `compression_level: n` doesn't work. why?
|
1182
|
+
filenames.each do |fname|
|
1183
|
+
__zip_add(cmd, zf, fname, recursive)
|
1184
|
+
end
|
1185
|
+
zf
|
1186
|
+
end
|
1187
|
+
ensure
|
1188
|
+
#; [!h7yxl] restores value of `Zip.default_compression`.
|
1189
|
+
Zip.default_compression = orig if complevel
|
1190
|
+
end
|
1191
|
+
#; [!fvvn8] returns zip file object.
|
1192
|
+
return zipf
|
1193
|
+
end
|
1194
|
+
|
1195
|
+
def __zip_add(cmd, zf, fpath, recursive)
|
1196
|
+
ftype = File.ftype(fpath)
|
1197
|
+
case ftype
|
1198
|
+
when 'link'; zf.add(fpath, fpath)
|
1199
|
+
when 'file'; zf.add(fpath, fpath)
|
1200
|
+
when 'directory'
|
1201
|
+
zf.add(fpath, fpath)
|
1202
|
+
#; [!bgdg7] adds files recursively into zip file if '-r' option specified.
|
1203
|
+
Dir.open(fpath) do |dir|
|
1204
|
+
dir.each do |x|
|
1205
|
+
next if x == '.' || x == '..'
|
1206
|
+
__zip_add(cmd, zf, File.join(fpath, x), recursive)
|
1207
|
+
end
|
1208
|
+
end if recursive
|
1209
|
+
else
|
1210
|
+
#; [!jgt96] error when special file specified.
|
1211
|
+
__err "#{cmd}: #{fpath}: #{ftype} file not supported."
|
1212
|
+
end
|
1213
|
+
end
|
1214
|
+
|
1215
|
+
|
1216
|
+
def unzip(*args)
|
1217
|
+
__unzip('unzip', args, false)
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
def unzip!(*args)
|
1221
|
+
__unzip('unzip!', args, true)
|
1222
|
+
end
|
1223
|
+
|
1224
|
+
def __unzip(cmd, args, overwrite)
|
1225
|
+
#; [!eqx48] requires 'zip' gem automatically.
|
1226
|
+
require 'zip' unless defined?(::Zip)
|
1227
|
+
#; [!ednxk] echoback command and arguments.
|
1228
|
+
optchars = __prepare(cmd, args, "d", nil)
|
1229
|
+
outdir = optchars.include?('d') ? args.shift() : nil
|
1230
|
+
#; [!1lul7] error if zip file not specified.
|
1231
|
+
zip_filename = args.shift() or
|
1232
|
+
__err "#{cmd}: zip filename required."
|
1233
|
+
#; [!0yyg8] target directory should not exist, or be empty.
|
1234
|
+
if outdir
|
1235
|
+
if ! File.exist?(outdir)
|
1236
|
+
# pass
|
1237
|
+
elsif File.directory?(outdir)
|
1238
|
+
#; [!1ls2h] error if target directory not empty.
|
1239
|
+
found = Dir.open(outdir) {|dir|
|
1240
|
+
dir.find {|x| x != '.' && x != '..' }
|
1241
|
+
}
|
1242
|
+
! found or
|
1243
|
+
__err "#{cmd}: #{outdir}: directory not empty."
|
1244
|
+
else
|
1245
|
+
#; [!lb6r5] error if target directory is not a directory.
|
1246
|
+
__err "#{cmd}: #{outdir}: not a directory."
|
1247
|
+
end
|
1248
|
+
end
|
1249
|
+
#; [!o1ot5] expands glob pattern.
|
1250
|
+
#; [!92bh4] error if glob pattern matched to multiple filenames.
|
1251
|
+
#; [!esnke] error if zip file not found.
|
1252
|
+
arr = Dir.glob(zip_filename); n = arr.length
|
1253
|
+
if n < 1 ; __err "#{cmd}: #{zip_filename}: zip file not found."
|
1254
|
+
elsif n > 1 ; __err "#{cmd}: #{zip_filename}: matched to multiple filenames (#{arr.sort.join(' ')})."
|
1255
|
+
else ; zip_filename = arr[0]
|
1256
|
+
end
|
1257
|
+
#
|
1258
|
+
filenames = args
|
1259
|
+
filenames = nil if filenames.empty?
|
1260
|
+
#; [!dzk7c] creates target directory if not exists.
|
1261
|
+
__mkpath(outdir, {}) if outdir && ! File.exist?(outdir)
|
1262
|
+
#
|
1263
|
+
orig = ::Zip.on_exists_proc
|
1264
|
+
begin
|
1265
|
+
#; [!06nyv] (unzip!) overwrites existing files.
|
1266
|
+
::Zip.on_exists_proc = overwrite
|
1267
|
+
extglob = File::FNM_EXTGLOB
|
1268
|
+
#; [!ekllx] (unzip) error when file already exists.
|
1269
|
+
::Zip::File.open(zip_filename) do |zf|
|
1270
|
+
zf.each do |x|
|
1271
|
+
next if filenames && ! filenames.find {|pat| File.fnmatch?(pat, x.name, extglob) }
|
1272
|
+
#; [!zg60i] error if file has absolute path.
|
1273
|
+
outdir || File.absolute_path(x.name) != x.name or
|
1274
|
+
__err "#{cmd}: #{x.name}: cannot extract absolute path."
|
1275
|
+
#
|
1276
|
+
next if x.directory?
|
1277
|
+
fpath = outdir ? File.join(outdir, x.name) : x.name
|
1278
|
+
overwrite || ! File.exist?(fpath) or
|
1279
|
+
__err "#{cmd}: #{fpath}: file already exists (to overwrite it, call `#{cmd}!` command instead of `#{cmd}` command)."
|
1280
|
+
end
|
1281
|
+
end
|
1282
|
+
#; [!0tedi] extract zip file.
|
1283
|
+
::Zip::File.open(zip_filename) do |zf|
|
1284
|
+
zf.each do |x|
|
1285
|
+
#; [!ikq5w] if filenames are specified, extracts files matched to them.
|
1286
|
+
next if filenames && ! filenames.find {|pat| File.fnmatch?(pat, x.name, extglob) }
|
1287
|
+
#; [!dy4r4] if '-d' option specified, extracts files under target directory.
|
1288
|
+
if outdir
|
1289
|
+
x.extract(File.join(outdir, x.name))
|
1290
|
+
#; [!5u645] if '-d' option not specified, extracts files under current directory.
|
1291
|
+
else
|
1292
|
+
x.extract()
|
1293
|
+
end
|
1294
|
+
end
|
1295
|
+
end
|
1296
|
+
ensure
|
1297
|
+
#; [!sjf80] (unzip!) `Zip.on_exists_proc` should be recovered.
|
1298
|
+
::Zip.on_exists_proc = orig
|
1299
|
+
end
|
1300
|
+
end
|
1301
|
+
|
1302
|
+
|
1303
|
+
def time(format=nil, &b)
|
1304
|
+
#; [!ddl3a] measures elapsed time of block and reports into stderr.
|
1305
|
+
pt1 = Process.times()
|
1306
|
+
t1 = Time.new
|
1307
|
+
yield
|
1308
|
+
t2 = Time.new
|
1309
|
+
pt2 = Process.times()
|
1310
|
+
user = pt2.cutime - pt1.cutime
|
1311
|
+
sys = pt2.cstime - pt1.cstime
|
1312
|
+
real = t2 - t1
|
1313
|
+
format ||= " %.3fs real %.3fs user %.3fs sys"
|
1314
|
+
$stderr.puts ""
|
1315
|
+
$stderr.puts format % [real, user, sys]
|
1316
|
+
end
|
1317
|
+
|
1318
|
+
|
1319
|
+
end
|
1320
|
+
|
1321
|
+
|
1322
|
+
end
|