benry-unixcmd 0.9.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.
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