benry-unixcmd 0.9.0

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