benry-unixcommand 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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