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