├── test ├── lib │ └── helper.rb └── fileutils │ ├── test_dryrun.rb │ ├── test_nowrite.rb │ ├── test_verbose.rb │ ├── visibility_tests.rb │ ├── clobber.rb │ ├── fileasserts.rb │ └── test_fileutils.rb ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── sync-ruby.yml │ ├── test.yml │ └── push_gem.yml ├── bin ├── setup └── console ├── CONTRIBUTING.md ├── Gemfile ├── Rakefile ├── README.md ├── fileutils.gemspec ├── BSDL ├── COPYING └── lib └── fileutils.rb /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "core_assertions" 3 | 4 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Build and test 2 | 3 | Please follow the ["Making Changes To Standard Libraries"](https://docs.ruby-lang.org/en/master/contributing/making_changes_to_stdlibs_md.html) guideline. 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "rake" 7 | gem "bundler" 8 | gem "test-unit" 9 | gem "test-unit-ruby-core" 10 | end 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test/lib" 6 | t.ruby_opts << "-rhelper" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /test/fileutils/test_dryrun.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # $Id$ 3 | 4 | require 'fileutils' 5 | require 'test/unit' 6 | require_relative 'visibility_tests' 7 | 8 | class TestFileUtilsDryRun < Test::Unit::TestCase 9 | 10 | include FileUtils::DryRun 11 | include TestFileUtilsIncVisibility 12 | 13 | def setup 14 | super 15 | @fu_module = FileUtils::DryRun 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /test/fileutils/test_nowrite.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # $Id$ 3 | 4 | require 'fileutils' 5 | require 'test/unit' 6 | require_relative 'visibility_tests' 7 | 8 | class TestFileUtilsNoWrite < Test::Unit::TestCase 9 | 10 | include FileUtils::NoWrite 11 | include TestFileUtilsIncVisibility 12 | 13 | def setup 14 | super 15 | @fu_module = FileUtils::NoWrite 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /test/fileutils/test_verbose.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # $Id$ 3 | 4 | require 'test/unit' 5 | require 'fileutils' 6 | require_relative 'visibility_tests' 7 | 8 | class TestFileUtilsVerbose < Test::Unit::TestCase 9 | 10 | include FileUtils::Verbose 11 | include TestFileUtilsIncVisibility 12 | 13 | def setup 14 | super 15 | @fu_module = FileUtils::Verbose 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "fileutils" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FileUtils 2 | 3 | Namespace for several file utility methods for copying, moving, removing, etc. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'fileutils' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install fileutils 20 | 21 | ## Usage 22 | 23 | Just call `FileUtils` methods. For example: 24 | 25 | ```ruby 26 | FileUtils.mkdir("somedir") 27 | # => ["somedir"] 28 | 29 | FileUtils.cd("/usr/bin") 30 | FileUtils.pwd 31 | # => "/usr/bin" 32 | ``` 33 | 34 | You can find a full method list in the [documentation](https://docs.ruby-lang.org/en/master/FileUtils.html). 35 | 36 | ## Contributing 37 | 38 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/fileutils. 39 | 40 | ## License 41 | 42 | The gem is available as open source under the terms of the [2-Clause BSD License](https://opensource.org/licenses/BSD-2-Clause). 43 | -------------------------------------------------------------------------------- /fileutils.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source_version = ["", "lib/"].find do |dir| 4 | begin 5 | break File.open(File.join(__dir__, "#{dir}fileutils.rb")) {|f| 6 | f.gets("\n VERSION = ") 7 | f.gets[/\s*"(.+)"/, 1] 8 | } 9 | rescue Errno::ENOENT 10 | end 11 | end 12 | 13 | Gem::Specification.new do |s| 14 | s.name = "fileutils" 15 | s.version = source_version 16 | s.summary = "Several file utility methods for copying, moving, removing, etc." 17 | s.description = "Several file utility methods for copying, moving, removing, etc." 18 | 19 | s.require_path = %w{lib} 20 | s.files = ["COPYING", "BSDL", "README.md", "Rakefile", "fileutils.gemspec", "lib/fileutils.rb"] 21 | s.required_ruby_version = ">= 2.5.0" 22 | 23 | s.authors = ["Minero Aoki"] 24 | s.email = [nil] 25 | s.homepage = "https://github.com/ruby/fileutils" 26 | s.licenses = ["Ruby", "BSD-2-Clause"] 27 | 28 | s.metadata = { 29 | "source_code_uri" => "https://github.com/ruby/fileutils" 30 | } 31 | end 32 | -------------------------------------------------------------------------------- /.github/workflows/sync-ruby.yml: -------------------------------------------------------------------------------- 1 | name: Sync ruby 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | sync: 7 | name: Sync ruby 8 | runs-on: ubuntu-latest 9 | if: ${{ github.repository_owner == 'ruby' }} 10 | steps: 11 | - uses: actions/checkout@v6 12 | 13 | - name: Create GitHub App token 14 | id: app-token 15 | uses: actions/create-github-app-token@v2 16 | with: 17 | app-id: 2060836 18 | private-key: ${{ secrets.RUBY_SYNC_DEFAULT_GEMS_PRIVATE_KEY }} 19 | owner: ruby 20 | repositories: ruby 21 | 22 | - name: Sync to ruby/ruby 23 | uses: convictional/trigger-workflow-and-wait@v1.6.5 24 | with: 25 | owner: ruby 26 | repo: ruby 27 | workflow_file_name: sync_default_gems.yml 28 | github_token: ${{ steps.app-token.outputs.token }} 29 | ref: master 30 | client_payload: | 31 | {"gem":"${{ github.event.repository.name }}","before":"${{ github.event.before }}","after":"${{ github.event.after }}"} 32 | propagate_failure: true 33 | wait_interval: 10 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | min_version: 2.5 10 | 11 | test: 12 | needs: ruby-versions 13 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 14 | strategy: 15 | matrix: 16 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 17 | os: [ 'ubuntu-latest', 'macos-latest', 'windows-latest' ] 18 | exclude: 19 | - { os: macos-latest, ruby: 2.5 } 20 | - { os: windows-latest, ruby: 'truffleruby' } 21 | - { os: windows-latest, ruby: 'truffleruby-head' } 22 | - { os: windows-latest, ruby: 'jruby-head' } 23 | - { os: windows-latest, ruby: 'jruby' } 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - uses: actions/checkout@v6 27 | - name: Set up Ruby ${{ matrix.ruby }} 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | bundler-cache: true # 'bundle install' and cache 32 | - name: Run test 33 | run: bundle exec rake 34 | env: 35 | JRUBY_OPTS: -X+O 36 | -------------------------------------------------------------------------------- /test/fileutils/visibility_tests.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test/unit' 3 | require 'fileutils' 4 | 5 | ## 6 | # These tests are reused in the FileUtils::Verbose, FileUtils::NoWrite and 7 | # FileUtils::DryRun tests 8 | 9 | module TestFileUtilsIncVisibility 10 | 11 | FileUtils::METHODS.each do |m| 12 | define_method "test_singleton_visibility_#{m}" do 13 | assert @fu_module.respond_to?(m, true), 14 | "FileUtils::Verbose.#{m} is not defined" 15 | assert @fu_module.respond_to?(m, false), 16 | "FileUtils::Verbose.#{m} is not public" 17 | end 18 | 19 | define_method "test_visibility_#{m}" do 20 | assert respond_to?(m, true), 21 | "FileUtils::Verbose\##{m} is not defined" 22 | assert @fu_module.private_method_defined?(m), 23 | "FileUtils::Verbose\##{m} is not private" 24 | end 25 | end 26 | 27 | FileUtils::StreamUtils_.private_instance_methods.each do |m| 28 | define_method "test_singleton_visibility_#{m}" do 29 | assert @fu_module.respond_to?(m, true), 30 | "FileUtils::Verbose\##{m} is not defined" 31 | end 32 | 33 | define_method "test_visibility_#{m}" do 34 | assert respond_to?(m, true), 35 | "FileUtils::Verbose\##{m} is not defined" 36 | end 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/fileutils' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/fileutils 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 31 | 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0 34 | with: 35 | bundler-cache: true 36 | ruby-version: ruby 37 | 38 | - name: Publish to RubyGems 39 | uses: rubygems/release-gem@1c162a739e8b4cb21a676e97b087e8268d8fc40b # v1.1.2 40 | 41 | - name: Create GitHub release 42 | run: | 43 | tag_name="$(git describe --tags --abbrev=0)" 44 | gh release create "${tag_name}" --verify-tag --generate-notes 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /test/fileutils/clobber.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'fileutils' 3 | require 'test/unit' 4 | require 'tmpdir' 5 | require_relative 'fileasserts' 6 | 7 | module TestFileUtilsClobber 8 | include Test::Unit::FileAssertions 9 | 10 | def my_rm_rf(path) 11 | if File.exist?('/bin/rm') 12 | system %Q[/bin/rm -rf "#{path}"] 13 | else 14 | FileUtils.rm_rf path 15 | end 16 | end 17 | 18 | SRC = 'data/src' 19 | COPY = 'data/copy' 20 | 21 | def setup 22 | @prevdir = Dir.pwd 23 | class << (@fileutils_output = "") 24 | alias puts << 25 | end 26 | tmproot = "#{Dir.tmpdir}/fileutils.rb.#{$$}" 27 | Dir.mkdir tmproot unless File.directory?(tmproot) 28 | Dir.chdir tmproot 29 | my_rm_rf 'data'; Dir.mkdir 'data' 30 | my_rm_rf 'tmp'; Dir.mkdir 'tmp' 31 | File.open(SRC, 'w') {|f| f.puts 'dummy' } 32 | File.open(COPY, 'w') {|f| f.puts 'dummy' } 33 | end 34 | 35 | def teardown 36 | tmproot = Dir.pwd 37 | Dir.chdir @prevdir 38 | my_rm_rf tmproot 39 | end 40 | 41 | def test_cp 42 | cp SRC, 'tmp/cp' 43 | check 'tmp/cp' 44 | end 45 | 46 | def test_mv 47 | mv SRC, 'tmp/mv' 48 | check 'tmp/mv' 49 | end 50 | 51 | def check(dest, message=nil) 52 | assert_file_not_exist dest, message 53 | assert_file_exist SRC, message 54 | assert_same_file SRC, COPY, message 55 | end 56 | 57 | def test_rm 58 | rm SRC 59 | assert_file_exist SRC 60 | assert_same_file SRC, COPY 61 | end 62 | 63 | def test_rm_f 64 | rm_f SRC 65 | assert_file_exist SRC 66 | assert_same_file SRC, COPY 67 | end 68 | 69 | def test_rm_rf 70 | rm_rf SRC 71 | assert_file_exist SRC 72 | assert_same_file SRC, COPY 73 | end 74 | 75 | def test_mkdir 76 | mkdir 'dir' 77 | assert_file_not_exist 'dir' 78 | end 79 | 80 | def test_mkdir_p 81 | mkdir 'dir/dir/dir' 82 | assert_file_not_exist 'dir' 83 | end 84 | 85 | def test_copy_entry 86 | copy_entry SRC, 'tmp/copy_entry' 87 | check 'tmp/copy_entry', bug4331 = '[ruby-dev:43129]' 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /test/fileutils/fileasserts.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # $Id$ 3 | 4 | module Test 5 | module Unit 6 | module FileAssertions 7 | def assert_same_file(from, to, message=nil) 8 | assert_equal(File.read(from), File.read(to), "file #{from} != #{to}#{message&&': '}#{message||''}") 9 | end 10 | 11 | def assert_same_entry(from, to, message=nil) 12 | a = File.stat(from) 13 | b = File.stat(to) 14 | msg = "#{message&&': '}#{message||''}" 15 | assert_equal a.mode, b.mode, "mode #{a.mode} != #{b.mode}#{msg}" 16 | #assert_equal a.atime, b.atime 17 | assert_equal_timestamp a.mtime, b.mtime, "mtime #{a.mtime} != #{b.mtime}#{msg}" 18 | assert_equal a.uid, b.uid, "uid #{a.uid} != #{b.uid}#{msg}" 19 | assert_equal a.gid, b.gid, "gid #{a.gid} != #{b.gid}#{msg}" 20 | end 21 | 22 | def assert_file_exist(path, message=nil) 23 | assert(File.exist?(path), "file not exist: #{path}#{message&&': '}#{message||''}") 24 | end 25 | 26 | def assert_file_not_exist(path, message=nil) 27 | assert(!File.exist?(path), "file exist: #{path}#{message&&': '}#{message||''}") 28 | end 29 | 30 | def assert_directory(path, message=nil) 31 | assert(File.directory?(path), "is not directory: #{path}#{message&&': '}#{message||''}") 32 | end 33 | 34 | def assert_symlink(path, message=nil) 35 | assert(File.symlink?(path), "is not a symlink: #{path}#{message&&': '}#{message||''}") 36 | end 37 | 38 | def assert_not_symlink(path, message=nil) 39 | assert(!File.symlink?(path), "is a symlink: #{path}#{message&&': '}#{message||''}") 40 | end 41 | 42 | def assert_equal_time(expected, actual, message=nil) 43 | expected_str = expected.to_s 44 | actual_str = actual.to_s 45 | if expected_str == actual_str 46 | expected_str << " (nsec=#{expected.nsec})" 47 | actual_str << " (nsec=#{actual.nsec})" 48 | end 49 | full_message = build_message(message, < expected but was 51 | <#{actual_str}>. 52 | EOT 53 | assert_equal(expected, actual, full_message) 54 | end 55 | 56 | def assert_equal_timestamp(expected, actual, message=nil) 57 | expected_str = expected.to_s 58 | actual_str = actual.to_s 59 | if expected_str == actual_str 60 | expected_str << " (nsec=#{expected.nsec})" 61 | actual_str << " (nsec=#{actual.nsec})" 62 | end 63 | full_message = build_message(message, < expected but was 65 | <#{actual_str}>. 66 | EOT 67 | # subsecond timestamp is not portable. 68 | assert_equal(expected.tv_sec, actual.tv_sec, full_message) 69 | end 70 | 71 | def assert_filemode(expected, file, message=nil, mask: 07777) 72 | width = ('%o' % mask).size 73 | actual = File.stat(file).mode & mask 74 | assert expected == actual, < 77 | Actual: <#{'%0*o' % [width, actual]}> 78 | EOT 79 | end 80 | 81 | def assert_equal_filemode(file1, file2, message=nil, mask: 07777) 82 | mode1, mode2 = [file1, file2].map { |file| 83 | File.stat(file).mode & mask 84 | } 85 | width = ('%o' % mask).size 86 | assert mode1 == mode2, <: "#{file1}" 89 | <#{'%0*o' % [width, mode2]}>: "#{file2}" 90 | EOT 91 | end 92 | 93 | def assert_ownership_group(expected, file) 94 | actual = File.stat(file).gid 95 | assert expected == actual, < 98 | Actual: <#{actual}> 99 | EOT 100 | end 101 | 102 | def assert_ownership_user(expected, file) 103 | actual = File.stat(file).uid 104 | assert expected == actual, < 107 | Actual: <#{actual}> 108 | EOT 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /test/fileutils/test_fileutils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # $Id$ 3 | 4 | require 'fileutils' 5 | require 'etc' 6 | require_relative 'fileasserts' 7 | require 'pathname' 8 | require 'tmpdir' 9 | require 'stringio' 10 | require 'test/unit' 11 | 12 | class TestFileUtils < Test::Unit::TestCase 13 | include Test::Unit::FileAssertions 14 | 15 | def assert_output_lines(expected, fu = self, message=nil) 16 | old = fu.instance_variables.include?(:@fileutils_output) && fu.instance_variable_get(:@fileutils_output) 17 | IO.pipe {|read, write| 18 | fu.instance_variable_set(:@fileutils_output, write) 19 | th = Thread.new { read.read } 20 | th2 = Thread.new { 21 | begin 22 | yield 23 | ensure 24 | write.close 25 | end 26 | } 27 | th_value, _ = assert_join_threads([th, th2]) 28 | lines = th_value.lines.map {|l| l.chomp } 29 | assert_equal(expected, lines) 30 | } 31 | ensure 32 | fu.instance_variable_set(:@fileutils_output, old) if old 33 | end 34 | 35 | m = Module.new do 36 | def have_drive_letter? 37 | /mswin(?!ce)|mingw|bcc|emx/ =~ RUBY_PLATFORM 38 | end 39 | 40 | def have_file_perm? 41 | /mswin|mingw|bcc|emx/ !~ RUBY_PLATFORM 42 | end 43 | 44 | @@have_symlink = nil 45 | 46 | def have_symlink? 47 | if @@have_symlink == nil 48 | @@have_symlink = check_have_symlink? 49 | end 50 | @@have_symlink 51 | end 52 | 53 | def check_have_symlink? 54 | Dir.mktmpdir do |dir| 55 | Dir.chdir(dir) do 56 | File.symlink "symlink", "symlink" 57 | end 58 | end 59 | rescue NotImplementedError, Errno::EACCES 60 | return false 61 | rescue 62 | return true 63 | end 64 | 65 | @@have_hardlink = nil 66 | 67 | def have_hardlink? 68 | if @@have_hardlink == nil 69 | @@have_hardlink = check_have_hardlink? 70 | end 71 | @@have_hardlink 72 | end 73 | 74 | def check_have_hardlink? 75 | Dir.mktmpdir do |dir| 76 | Dir.chdir(dir) do 77 | File.write "dummy", "dummy" 78 | File.link "dummy", "hardlink" 79 | end 80 | end 81 | rescue NotImplementedError, Errno::EACCES 82 | return false 83 | rescue 84 | return true 85 | end 86 | 87 | @@no_broken_symlink = false 88 | if /cygwin/ =~ RUBY_PLATFORM and /\bwinsymlinks:native(?:strict)?\b/ =~ ENV["CYGWIN"] 89 | @@no_broken_symlink = true 90 | end 91 | 92 | def no_broken_symlink? 93 | @@no_broken_symlink 94 | end 95 | 96 | def has_capsh? 97 | !!system('capsh', '--print', out: File::NULL, err: File::NULL) 98 | end 99 | 100 | def has_root_file_capabilities? 101 | !!system( 102 | 'capsh', '--has-p=CAP_DAC_OVERRIDE', '--has-p=CAP_CHOWN', '--has-p=CAP_FOWNER', 103 | out: File::NULL, err: File::NULL 104 | ) 105 | end 106 | 107 | def root_in_posix? 108 | if /cygwin/ =~ RUBY_PLATFORM 109 | # FIXME: privilege if groups include root user? 110 | return Process.groups.include?(0) 111 | elsif has_capsh? 112 | return has_root_file_capabilities? 113 | elsif Process.respond_to?('uid') 114 | return Process.uid == 0 115 | else 116 | return false 117 | end 118 | end 119 | 120 | def distinct_uids(n = 2) 121 | return unless user = Etc.getpwent 122 | uids = [user.uid] 123 | while user = Etc.getpwent 124 | uid = user.uid 125 | unless uids.include?(uid) 126 | uids << uid 127 | break if uids.size >= n 128 | end 129 | end 130 | uids 131 | ensure 132 | Etc.endpwent 133 | end 134 | 135 | begin 136 | tmproot = Dir.mktmpdir "fileutils" 137 | Dir.chdir tmproot do 138 | Dir.mkdir("\n") 139 | Dir.rmdir("\n") 140 | end 141 | def lf_in_path_allowed? 142 | true 143 | end 144 | rescue 145 | def lf_in_path_allowed? 146 | false 147 | end 148 | ensure 149 | begin 150 | Dir.rmdir tmproot 151 | rescue 152 | STDERR.puts $!.inspect 153 | STDERR.puts Dir.entries(tmproot).inspect 154 | end 155 | end 156 | end 157 | include m 158 | extend m 159 | 160 | UID_1, UID_2 = distinct_uids(2) 161 | 162 | include FileUtils 163 | 164 | def check_singleton(name) 165 | assert_respond_to ::FileUtils, name 166 | end 167 | 168 | def my_rm_rf(path) 169 | if File.exist?('/bin/rm') 170 | system "/bin/rm", "-rf", path 171 | elsif /mswin|mingw/ =~ RUBY_PLATFORM 172 | system "rmdir", "/q/s", path.gsub('/', '\\'), err: IO::NULL 173 | else 174 | FileUtils.rm_rf path 175 | end 176 | end 177 | 178 | def mymkdir(path) 179 | Dir.mkdir path 180 | File.chown nil, Process.gid, path if have_file_perm? 181 | end 182 | 183 | def setup 184 | @prevdir = Dir.pwd 185 | @groups = [Process.gid] | Process.groups if have_file_perm? 186 | tmproot = @tmproot = Dir.mktmpdir "fileutils" 187 | Dir.chdir tmproot 188 | my_rm_rf 'data'; mymkdir 'data' 189 | my_rm_rf 'tmp'; mymkdir 'tmp' 190 | prepare_data_file 191 | end 192 | 193 | def teardown 194 | Dir.chdir @prevdir 195 | my_rm_rf @tmproot 196 | end 197 | 198 | 199 | TARGETS = %w( data/a data/all data/random data/zero ) 200 | 201 | def prepare_data_file 202 | File.open('data/a', 'w') {|f| 203 | 32.times do 204 | f.puts 'a' * 50 205 | end 206 | } 207 | 208 | all_chars = (0..255).map {|n| n.chr }.join('') 209 | File.open('data/all', 'w') {|f| 210 | 32.times do 211 | f.puts all_chars 212 | end 213 | } 214 | 215 | random_chars = (0...50).map { rand(256).chr }.join('') 216 | File.open('data/random', 'w') {|f| 217 | 32.times do 218 | f.puts random_chars 219 | end 220 | } 221 | 222 | File.open('data/zero', 'w') {|f| 223 | ; 224 | } 225 | end 226 | 227 | BIGFILE = 'data/big' 228 | 229 | def prepare_big_file 230 | File.open('data/big', 'w') {|f| 231 | (4 * 1024 * 1024 / 256).times do # 4MB 232 | f.print "aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa\n" 233 | end 234 | } 235 | end 236 | 237 | def prepare_time_data 238 | File.open('data/old', 'w') {|f| f.puts 'dummy' } 239 | File.open('data/newer', 'w') {|f| f.puts 'dummy' } 240 | File.open('data/newest', 'w') {|f| f.puts 'dummy' } 241 | t = Time.now 242 | File.utime t-8, t-8, 'data/old' 243 | File.utime t-4, t-4, 'data/newer' 244 | end 245 | 246 | def each_srcdest 247 | TARGETS.each do |path| 248 | yield path, "tmp/#{File.basename(path)}" 249 | end 250 | end 251 | 252 | # 253 | # Test Cases 254 | # 255 | 256 | def test_assert_output_lines 257 | assert_raise(Test::Unit::AssertionFailedError) { 258 | Timeout.timeout(0.5) { 259 | assert_output_lines([]) { 260 | Thread.current.report_on_exception = false 261 | raise "ok" 262 | } 263 | } 264 | } 265 | end 266 | 267 | def test_pwd 268 | check_singleton :pwd 269 | 270 | assert_equal Dir.pwd, pwd() 271 | 272 | cwd = Dir.pwd 273 | root = have_drive_letter? ? 'C:/' : '/' 274 | cd(root) { 275 | assert_equal root, pwd() 276 | } 277 | assert_equal cwd, pwd() 278 | end 279 | 280 | def test_cmp 281 | check_singleton :cmp 282 | 283 | TARGETS.each do |fname| 284 | assert cmp(fname, fname), 'not same?' 285 | end 286 | assert_raise(ArgumentError) { 287 | cmp TARGETS[0], TARGETS[0], :undefinedoption => true 288 | } 289 | 290 | # pathname 291 | touch 'tmp/cmptmp' 292 | assert_nothing_raised { 293 | cmp Pathname.new('tmp/cmptmp'), 'tmp/cmptmp' 294 | cmp 'tmp/cmptmp', Pathname.new('tmp/cmptmp') 295 | cmp Pathname.new('tmp/cmptmp'), Pathname.new('tmp/cmptmp') 296 | } 297 | end 298 | 299 | def test_cp 300 | check_singleton :cp 301 | 302 | each_srcdest do |srcpath, destpath| 303 | cp srcpath, destpath 304 | assert_same_file srcpath, destpath 305 | 306 | cp srcpath, File.dirname(destpath) 307 | assert_same_file srcpath, destpath 308 | 309 | cp srcpath, File.dirname(destpath) + '/' 310 | assert_same_file srcpath, destpath 311 | 312 | cp srcpath, destpath, :preserve => true 313 | assert_same_file srcpath, destpath 314 | assert_same_entry srcpath, destpath 315 | end 316 | 317 | assert_raise(Errno::ENOENT) { 318 | cp 'tmp/cptmp', 'tmp/cptmp_new' 319 | } 320 | assert_file_not_exist('tmp/cptmp_new') 321 | 322 | # src==dest (1) same path 323 | touch 'tmp/cptmp' 324 | assert_raise(ArgumentError) { 325 | cp 'tmp/cptmp', 'tmp/cptmp' 326 | } 327 | end 328 | 329 | def test_cp_preserve_permissions 330 | bug4507 = '[ruby-core:35518]' 331 | touch 'tmp/cptmp' 332 | chmod 0o755, 'tmp/cptmp' 333 | cp 'tmp/cptmp', 'tmp/cptmp2' 334 | 335 | assert_equal_filemode('tmp/cptmp', 'tmp/cptmp2', bug4507, mask: ~File.umask) 336 | end 337 | 338 | def test_cp_preserve_permissions_dir 339 | bug7246 = '[ruby-core:48603]' 340 | mkdir 'tmp/cptmp' 341 | mkdir 'tmp/cptmp/d1' 342 | chmod 0o745, 'tmp/cptmp/d1' 343 | mkdir 'tmp/cptmp/d2' 344 | chmod 0o700, 'tmp/cptmp/d2' 345 | cp_r 'tmp/cptmp', 'tmp/cptmp2', :preserve => true 346 | assert_equal_filemode('tmp/cptmp/d1', 'tmp/cptmp2/d1', bug7246) 347 | assert_equal_filemode('tmp/cptmp/d2', 'tmp/cptmp2/d2', bug7246) 348 | end 349 | 350 | def test_cp_symlink 351 | touch 'tmp/cptmp' 352 | # src==dest (2) symlink and its target 353 | File.symlink 'cptmp', 'tmp/cptmp_symlink' 354 | assert_raise(ArgumentError) { 355 | cp 'tmp/cptmp', 'tmp/cptmp_symlink' 356 | } 357 | assert_raise(ArgumentError) { 358 | cp 'tmp/cptmp_symlink', 'tmp/cptmp' 359 | } 360 | return if no_broken_symlink? 361 | # src==dest (3) looped symlink 362 | File.symlink 'symlink', 'tmp/symlink' 363 | assert_raise(Errno::ELOOP) { 364 | cp 'tmp/symlink', 'tmp/symlink' 365 | } 366 | end if have_symlink? 367 | 368 | def test_cp_pathname 369 | # pathname 370 | touch 'tmp/cptmp' 371 | assert_nothing_raised { 372 | cp 'tmp/cptmp', Pathname.new('tmp/tmpdest') 373 | cp Pathname.new('tmp/cptmp'), 'tmp/tmpdest' 374 | cp Pathname.new('tmp/cptmp'), Pathname.new('tmp/tmpdest') 375 | mkdir 'tmp/tmpdir' 376 | cp ['tmp/cptmp', 'tmp/tmpdest'], Pathname.new('tmp/tmpdir') 377 | } 378 | end 379 | 380 | def test_cp_r 381 | check_singleton :cp_r 382 | 383 | cp_r 'data', 'tmp' 384 | TARGETS.each do |fname| 385 | assert_same_file fname, "tmp/#{fname}" 386 | end 387 | 388 | cp_r 'data', 'tmp2', :preserve => true 389 | TARGETS.each do |fname| 390 | assert_same_entry fname, "tmp2/#{File.basename(fname)}" 391 | assert_same_file fname, "tmp2/#{File.basename(fname)}" 392 | end 393 | 394 | # a/* -> b/* 395 | mkdir 'tmp/cpr_src' 396 | mkdir 'tmp/cpr_dest' 397 | File.open('tmp/cpr_src/a', 'w') {|f| f.puts 'a' } 398 | File.open('tmp/cpr_src/b', 'w') {|f| f.puts 'b' } 399 | File.open('tmp/cpr_src/c', 'w') {|f| f.puts 'c' } 400 | mkdir 'tmp/cpr_src/d' 401 | cp_r 'tmp/cpr_src/.', 'tmp/cpr_dest' 402 | assert_same_file 'tmp/cpr_src/a', 'tmp/cpr_dest/a' 403 | assert_same_file 'tmp/cpr_src/b', 'tmp/cpr_dest/b' 404 | assert_same_file 'tmp/cpr_src/c', 'tmp/cpr_dest/c' 405 | assert_directory 'tmp/cpr_dest/d' 406 | assert_raise(ArgumentError) do 407 | cp_r 'tmp/cpr_src', './tmp/cpr_src' 408 | end 409 | assert_raise(ArgumentError) do 410 | cp_r './tmp/cpr_src', 'tmp/cpr_src' 411 | end 412 | assert_raise(ArgumentError) do 413 | cp_r './tmp/cpr_src', File.expand_path('tmp/cpr_src') 414 | end 415 | 416 | my_rm_rf 'tmp/cpr_src' 417 | my_rm_rf 'tmp/cpr_dest' 418 | 419 | bug3588 = '[ruby-core:31360]' 420 | assert_nothing_raised(ArgumentError, bug3588) do 421 | cp_r 'tmp', 'tmp2' 422 | end 423 | assert_directory 'tmp2/tmp' 424 | assert_raise(ArgumentError, bug3588) do 425 | cp_r 'tmp2', 'tmp2/new_tmp2' 426 | end 427 | 428 | bug12892 = '[ruby-core:77885] [Bug #12892]' 429 | assert_raise(Errno::ENOENT, bug12892) do 430 | cp_r 'non/existent', 'tmp' 431 | end 432 | end 433 | 434 | def test_cp_r_symlink 435 | # symlink in a directory 436 | mkdir 'tmp/cpr_src' 437 | touch 'tmp/cpr_src/SLdest' 438 | ln_s 'SLdest', 'tmp/cpr_src/symlink' 439 | cp_r 'tmp/cpr_src', 'tmp/cpr_dest' 440 | assert_symlink 'tmp/cpr_dest/symlink' 441 | assert_equal 'SLdest', File.readlink('tmp/cpr_dest/symlink') 442 | 443 | # root is a symlink 444 | ln_s 'cpr_src', 'tmp/cpr_src2' 445 | cp_r 'tmp/cpr_src2', 'tmp/cpr_dest2' 446 | assert_directory 'tmp/cpr_dest2' 447 | assert_not_symlink 'tmp/cpr_dest2' 448 | assert_symlink 'tmp/cpr_dest2/symlink' 449 | assert_equal 'SLdest', File.readlink('tmp/cpr_dest2/symlink') 450 | end if have_symlink? 451 | 452 | def test_cp_r_symlink_preserve 453 | mkdir 'tmp/cross' 454 | mkdir 'tmp/cross/a' 455 | mkdir 'tmp/cross/b' 456 | touch 'tmp/cross/a/f' 457 | touch 'tmp/cross/b/f' 458 | ln_s '../a/f', 'tmp/cross/b/l' 459 | ln_s '../b/f', 'tmp/cross/a/l' 460 | assert_nothing_raised { 461 | cp_r 'tmp/cross', 'tmp/cross2', :preserve => true 462 | } 463 | end if have_symlink? and !no_broken_symlink? 464 | 465 | def test_cp_r_fifo 466 | Dir.mkdir('tmp/cpr_src') 467 | File.mkfifo 'tmp/cpr_src/fifo', 0600 468 | cp_r 'tmp/cpr_src', 'tmp/cpr_dest' 469 | assert_equal(true, File.pipe?('tmp/cpr_dest/fifo')) 470 | end if File.respond_to?(:mkfifo) 471 | 472 | def test_cp_r_dev 473 | devs = Dir['/dev/*'] 474 | chardev = devs.find{|f| File.chardev?(f)} 475 | blockdev = devs.find{|f| File.blockdev?(f)} 476 | Dir.mkdir('tmp/cpr_dest') 477 | assert_raise(RuntimeError) { cp_r chardev, 'tmp/cpr_dest/cd' } if chardev 478 | assert_raise(RuntimeError) { cp_r blockdev, 'tmp/cpr_dest/bd' } if blockdev 479 | end 480 | 481 | begin 482 | require 'socket' 483 | rescue LoadError 484 | else 485 | def test_cp_r_socket 486 | pend "Skipping socket test on JRuby" if RUBY_ENGINE == 'jruby' 487 | 488 | Dir.mkdir('tmp/cpr_src') 489 | UNIXServer.new('tmp/cpr_src/socket').close 490 | cp_r 'tmp/cpr_src', 'tmp/cpr_dest' 491 | assert_equal(true, File.socket?('tmp/cpr_dest/socket')) 492 | rescue Errno::EINVAL => error 493 | # On some platforms (windows) sockets cannot be copied by FileUtils. 494 | omit error.message 495 | end if defined?(UNIXServer) 496 | end 497 | 498 | def test_cp_r_pathname 499 | # pathname 500 | touch 'tmp/cprtmp' 501 | assert_nothing_raised { 502 | cp_r Pathname.new('tmp/cprtmp'), 'tmp/tmpdest' 503 | cp_r 'tmp/cprtmp', Pathname.new('tmp/tmpdest') 504 | cp_r Pathname.new('tmp/cprtmp'), Pathname.new('tmp/tmpdest') 505 | } 506 | end 507 | 508 | def test_cp_r_symlink_remove_destination 509 | Dir.mkdir 'tmp/src' 510 | Dir.mkdir 'tmp/dest' 511 | Dir.mkdir 'tmp/src/dir' 512 | File.symlink 'tmp/src/dir', 'tmp/src/a' 513 | cp_r 'tmp/src', 'tmp/dest/', remove_destination: true 514 | cp_r 'tmp/src', 'tmp/dest/', remove_destination: true 515 | end if have_symlink? 516 | 517 | def test_cp_lr 518 | check_singleton :cp_lr 519 | 520 | cp_lr 'data', 'tmp' 521 | TARGETS.each do |fname| 522 | assert_same_file fname, "tmp/#{fname}" 523 | end 524 | 525 | # a/* -> b/* 526 | mkdir 'tmp/cpr_src' 527 | mkdir 'tmp/cpr_dest' 528 | File.open('tmp/cpr_src/a', 'w') {|f| f.puts 'a' } 529 | File.open('tmp/cpr_src/b', 'w') {|f| f.puts 'b' } 530 | File.open('tmp/cpr_src/c', 'w') {|f| f.puts 'c' } 531 | mkdir 'tmp/cpr_src/d' 532 | cp_lr 'tmp/cpr_src/.', 'tmp/cpr_dest' 533 | assert_same_file 'tmp/cpr_src/a', 'tmp/cpr_dest/a' 534 | assert_same_file 'tmp/cpr_src/b', 'tmp/cpr_dest/b' 535 | assert_same_file 'tmp/cpr_src/c', 'tmp/cpr_dest/c' 536 | assert_directory 'tmp/cpr_dest/d' 537 | my_rm_rf 'tmp/cpr_src' 538 | my_rm_rf 'tmp/cpr_dest' 539 | 540 | bug3588 = '[ruby-core:31360]' 541 | mkdir 'tmp2' 542 | assert_nothing_raised(ArgumentError, bug3588) do 543 | cp_lr 'tmp', 'tmp2' 544 | end 545 | assert_directory 'tmp2/tmp' 546 | assert_raise(ArgumentError, bug3588) do 547 | cp_lr 'tmp2', 'tmp2/new_tmp2' 548 | end 549 | 550 | bug12892 = '[ruby-core:77885] [Bug #12892]' 551 | assert_raise(Errno::ENOENT, bug12892) do 552 | cp_lr 'non/existent', 'tmp' 553 | end 554 | end if have_hardlink? 555 | 556 | def test_mv 557 | check_singleton :mv 558 | 559 | mkdir 'tmp/dest' 560 | TARGETS.each do |fname| 561 | cp fname, 'tmp/mvsrc' 562 | mv 'tmp/mvsrc', 'tmp/mvdest' 563 | assert_same_file fname, 'tmp/mvdest' 564 | 565 | mv 'tmp/mvdest', 'tmp/dest/' 566 | assert_same_file fname, 'tmp/dest/mvdest' 567 | 568 | mv 'tmp/dest/mvdest', 'tmp' 569 | assert_same_file fname, 'tmp/mvdest' 570 | end 571 | 572 | mkdir 'tmp/tmpdir' 573 | mkdir_p 'tmp/dest2/tmpdir' 574 | assert_raise_with_message(Errno::EEXIST, %r' - tmp/dest2/tmpdir\z', 575 | '[ruby-core:68706] [Bug #11021]') { 576 | mv 'tmp/tmpdir', 'tmp/dest2' 577 | } 578 | mkdir 'tmp/dest2/tmpdir/junk' 579 | assert_raise(Errno::EEXIST, "[ruby-talk:124368]") { 580 | mv 'tmp/tmpdir', 'tmp/dest2' 581 | } 582 | 583 | # src==dest (1) same path 584 | touch 'tmp/cptmp' 585 | assert_raise(ArgumentError) { 586 | mv 'tmp/cptmp', 'tmp/cptmp' 587 | } 588 | end 589 | 590 | def test_mv_symlink 591 | touch 'tmp/cptmp' 592 | # src==dest (2) symlink and its target 593 | File.symlink 'cptmp', 'tmp/cptmp_symlink' 594 | assert_raise(ArgumentError) { 595 | mv 'tmp/cptmp', 'tmp/cptmp_symlink' 596 | } 597 | assert_raise(ArgumentError) { 598 | mv 'tmp/cptmp_symlink', 'tmp/cptmp' 599 | } 600 | end if have_symlink? 601 | 602 | def test_mv_broken_symlink 603 | # src==dest (3) looped symlink 604 | File.symlink 'symlink', 'tmp/symlink' 605 | assert_raise(Errno::ELOOP) { 606 | mv 'tmp/symlink', 'tmp/symlink' 607 | } 608 | # unexist symlink 609 | File.symlink 'xxx', 'tmp/src' 610 | assert_nothing_raised { 611 | mv 'tmp/src', 'tmp/dest' 612 | } 613 | assert_equal true, File.symlink?('tmp/dest') 614 | end if have_symlink? and !no_broken_symlink? 615 | 616 | def test_mv_pathname 617 | # pathname 618 | assert_nothing_raised { 619 | touch 'tmp/mvtmpsrc' 620 | mv Pathname.new('tmp/mvtmpsrc'), 'tmp/mvtmpdest' 621 | touch 'tmp/mvtmpsrc' 622 | mv 'tmp/mvtmpsrc', Pathname.new('tmp/mvtmpdest') 623 | touch 'tmp/mvtmpsrc' 624 | mv Pathname.new('tmp/mvtmpsrc'), Pathname.new('tmp/mvtmpdest') 625 | } 626 | end 627 | 628 | def test_rm 629 | check_singleton :rm 630 | 631 | TARGETS.each do |fname| 632 | cp fname, 'tmp/rmsrc' 633 | rm 'tmp/rmsrc' 634 | assert_file_not_exist 'tmp/rmsrc' 635 | end 636 | 637 | # pathname 638 | touch 'tmp/rmtmp1' 639 | touch 'tmp/rmtmp2' 640 | touch 'tmp/rmtmp3' 641 | assert_nothing_raised { 642 | rm Pathname.new('tmp/rmtmp1') 643 | rm [Pathname.new('tmp/rmtmp2'), Pathname.new('tmp/rmtmp3')] 644 | } 645 | assert_file_not_exist 'tmp/rmtmp1' 646 | assert_file_not_exist 'tmp/rmtmp2' 647 | assert_file_not_exist 'tmp/rmtmp3' 648 | end 649 | 650 | def test_rm_f 651 | check_singleton :rm_f 652 | 653 | TARGETS.each do |fname| 654 | cp fname, 'tmp/rmsrc' 655 | rm_f 'tmp/rmsrc' 656 | assert_file_not_exist 'tmp/rmsrc' 657 | end 658 | end 659 | 660 | def test_rm_symlink 661 | File.open('tmp/lnf_symlink_src', 'w') {|f| f.puts 'dummy' } 662 | File.symlink 'lnf_symlink_src', 'tmp/lnf_symlink_dest' 663 | rm_f 'tmp/lnf_symlink_dest' 664 | assert_file_not_exist 'tmp/lnf_symlink_dest' 665 | assert_file_exist 'tmp/lnf_symlink_src' 666 | 667 | rm_f 'notexistdatafile' 668 | rm_f 'tmp/notexistdatafile' 669 | my_rm_rf 'tmpdatadir' 670 | Dir.mkdir 'tmpdatadir' 671 | # rm_f 'tmpdatadir' 672 | Dir.rmdir 'tmpdatadir' 673 | end if have_symlink? 674 | 675 | def test_rm_f_2 676 | Dir.mkdir 'tmp/tmpdir' 677 | File.open('tmp/tmpdir/a', 'w') {|f| f.puts 'dummy' } 678 | File.open('tmp/tmpdir/c', 'w') {|f| f.puts 'dummy' } 679 | rm_f ['tmp/tmpdir/a', 'tmp/tmpdir/b', 'tmp/tmpdir/c'] 680 | assert_file_not_exist 'tmp/tmpdir/a' 681 | assert_file_not_exist 'tmp/tmpdir/c' 682 | Dir.rmdir 'tmp/tmpdir' 683 | end 684 | 685 | def test_rm_pathname 686 | # pathname 687 | touch 'tmp/rmtmp1' 688 | touch 'tmp/rmtmp2' 689 | touch 'tmp/rmtmp3' 690 | touch 'tmp/rmtmp4' 691 | assert_nothing_raised { 692 | rm_f Pathname.new('tmp/rmtmp1') 693 | rm_f [Pathname.new('tmp/rmtmp2'), Pathname.new('tmp/rmtmp3')] 694 | } 695 | assert_file_not_exist 'tmp/rmtmp1' 696 | assert_file_not_exist 'tmp/rmtmp2' 697 | assert_file_not_exist 'tmp/rmtmp3' 698 | assert_file_exist 'tmp/rmtmp4' 699 | 700 | # [ruby-dev:39345] 701 | touch 'tmp/[rmtmp]' 702 | FileUtils.rm_f 'tmp/[rmtmp]' 703 | assert_file_not_exist 'tmp/[rmtmp]' 704 | end 705 | 706 | def test_rm_r 707 | check_singleton :rm_r 708 | 709 | my_rm_rf 'tmpdatadir' 710 | 711 | Dir.mkdir 'tmpdatadir' 712 | rm_r 'tmpdatadir' 713 | assert_file_not_exist 'tmpdatadir' 714 | 715 | Dir.mkdir 'tmpdatadir' 716 | rm_r 'tmpdatadir/' 717 | assert_file_not_exist 'tmpdatadir' 718 | 719 | Dir.mkdir 'tmp/tmpdir' 720 | rm_r 'tmp/tmpdir/' 721 | assert_file_not_exist 'tmp/tmpdir' 722 | assert_file_exist 'tmp' 723 | 724 | Dir.mkdir 'tmp/tmpdir' 725 | rm_r 'tmp/tmpdir' 726 | assert_file_not_exist 'tmp/tmpdir' 727 | assert_file_exist 'tmp' 728 | 729 | Dir.mkdir 'tmp/tmpdir' 730 | File.open('tmp/tmpdir/a', 'w') {|f| f.puts 'dummy' } 731 | File.open('tmp/tmpdir/b', 'w') {|f| f.puts 'dummy' } 732 | File.open('tmp/tmpdir/c', 'w') {|f| f.puts 'dummy' } 733 | rm_r 'tmp/tmpdir' 734 | assert_file_not_exist 'tmp/tmpdir' 735 | assert_file_exist 'tmp' 736 | 737 | Dir.mkdir 'tmp/tmpdir' 738 | File.open('tmp/tmpdir/a', 'w') {|f| f.puts 'dummy' } 739 | File.open('tmp/tmpdir/c', 'w') {|f| f.puts 'dummy' } 740 | rm_r ['tmp/tmpdir/a', 'tmp/tmpdir/b', 'tmp/tmpdir/c'], :force => true 741 | assert_file_not_exist 'tmp/tmpdir/a' 742 | assert_file_not_exist 'tmp/tmpdir/c' 743 | Dir.rmdir 'tmp/tmpdir' 744 | end 745 | 746 | def test_rm_r_symlink 747 | # [ruby-talk:94635] a symlink to the directory 748 | Dir.mkdir 'tmp/tmpdir' 749 | File.symlink '..', 'tmp/tmpdir/symlink_to_dir' 750 | rm_r 'tmp/tmpdir' 751 | assert_file_not_exist 'tmp/tmpdir' 752 | assert_file_exist 'tmp' 753 | end if have_symlink? 754 | 755 | def test_rm_r_pathname 756 | # pathname 757 | Dir.mkdir 'tmp/tmpdir1'; touch 'tmp/tmpdir1/tmp' 758 | Dir.mkdir 'tmp/tmpdir2'; touch 'tmp/tmpdir2/tmp' 759 | Dir.mkdir 'tmp/tmpdir3'; touch 'tmp/tmpdir3/tmp' 760 | assert_nothing_raised { 761 | rm_r Pathname.new('tmp/tmpdir1') 762 | rm_r [Pathname.new('tmp/tmpdir2'), Pathname.new('tmp/tmpdir3')] 763 | } 764 | assert_file_not_exist 'tmp/tmpdir1' 765 | assert_file_not_exist 'tmp/tmpdir2' 766 | assert_file_not_exist 'tmp/tmpdir3' 767 | end 768 | 769 | def test_rm_r_no_permissions 770 | check_singleton :rm_rf 771 | 772 | return if /mswin|mingw/ =~ RUBY_PLATFORM 773 | 774 | mkdir 'tmpdatadir' 775 | touch 'tmpdatadir/tmpdata' 776 | chmod "-x", 'tmpdatadir' 777 | 778 | begin 779 | assert_raise Errno::EACCES do 780 | rm_r 'tmpdatadir' 781 | end 782 | ensure 783 | chmod "+x", 'tmpdatadir' 784 | end 785 | end 786 | 787 | def test_remove_entry_cjk_path 788 | dir = "tmpdir\u3042" 789 | my_rm_rf dir 790 | 791 | Dir.mkdir dir 792 | File.write("#{dir}/\u3042.txt", "test_remove_entry_cjk_path") 793 | 794 | remove_entry dir 795 | assert_file_not_exist dir 796 | end 797 | 798 | def test_remove_entry_multibyte_path 799 | c = "\u00a7" 800 | begin 801 | c = c.encode('filesystem') 802 | rescue EncodingError 803 | c = c.b 804 | end 805 | dir = "tmpdir#{c}" 806 | my_rm_rf dir 807 | 808 | Dir.mkdir dir 809 | File.write("#{dir}/#{c}.txt", "test_remove_entry_multibyte_path") 810 | 811 | remove_entry dir 812 | assert_file_not_exist dir 813 | end 814 | 815 | def test_remove_entry_secure 816 | check_singleton :remove_entry_secure 817 | 818 | my_rm_rf 'tmpdatadir' 819 | 820 | Dir.mkdir 'tmpdatadir' 821 | remove_entry_secure 'tmpdatadir' 822 | assert_file_not_exist 'tmpdatadir' 823 | 824 | Dir.mkdir 'tmpdatadir' 825 | remove_entry_secure 'tmpdatadir/' 826 | assert_file_not_exist 'tmpdatadir' 827 | 828 | Dir.mkdir 'tmp/tmpdir' 829 | remove_entry_secure 'tmp/tmpdir/' 830 | assert_file_not_exist 'tmp/tmpdir' 831 | assert_file_exist 'tmp' 832 | 833 | Dir.mkdir 'tmp/tmpdir' 834 | remove_entry_secure 'tmp/tmpdir' 835 | assert_file_not_exist 'tmp/tmpdir' 836 | assert_file_exist 'tmp' 837 | 838 | Dir.mkdir 'tmp/tmpdir' 839 | File.open('tmp/tmpdir/a', 'w') {|f| f.puts 'dummy' } 840 | File.open('tmp/tmpdir/b', 'w') {|f| f.puts 'dummy' } 841 | File.open('tmp/tmpdir/c', 'w') {|f| f.puts 'dummy' } 842 | remove_entry_secure 'tmp/tmpdir' 843 | assert_file_not_exist 'tmp/tmpdir' 844 | assert_file_exist 'tmp' 845 | 846 | Dir.mkdir 'tmp/tmpdir' 847 | File.open('tmp/tmpdir/a', 'w') {|f| f.puts 'dummy' } 848 | File.open('tmp/tmpdir/c', 'w') {|f| f.puts 'dummy' } 849 | remove_entry_secure 'tmp/tmpdir/a', true 850 | remove_entry_secure 'tmp/tmpdir/b', true 851 | remove_entry_secure 'tmp/tmpdir/c', true 852 | assert_file_not_exist 'tmp/tmpdir/a' 853 | assert_file_not_exist 'tmp/tmpdir/c' 854 | 855 | unless root_in_posix? 856 | File.chmod(01777, 'tmp/tmpdir') 857 | if File.sticky?('tmp/tmpdir') 858 | Dir.mkdir 'tmp/tmpdir/d', 0 859 | assert_raise(Errno::EACCES) {remove_entry_secure 'tmp/tmpdir/d'} 860 | File.chmod 0o777, 'tmp/tmpdir/d' 861 | Dir.rmdir 'tmp/tmpdir/d' 862 | end 863 | end 864 | 865 | Dir.rmdir 'tmp/tmpdir' 866 | end 867 | 868 | def test_remove_entry_secure_symlink 869 | # [ruby-talk:94635] a symlink to the directory 870 | Dir.mkdir 'tmp/tmpdir' 871 | File.symlink '..', 'tmp/tmpdir/symlink_to_dir' 872 | remove_entry_secure 'tmp/tmpdir' 873 | assert_file_not_exist 'tmp/tmpdir' 874 | assert_file_exist 'tmp' 875 | end if have_symlink? 876 | 877 | def test_remove_entry_secure_pathname 878 | # pathname 879 | Dir.mkdir 'tmp/tmpdir1'; touch 'tmp/tmpdir1/tmp' 880 | assert_nothing_raised { 881 | remove_entry_secure Pathname.new('tmp/tmpdir1') 882 | } 883 | assert_file_not_exist 'tmp/tmpdir1' 884 | end 885 | 886 | def test_with_big_file 887 | prepare_big_file 888 | 889 | cp BIGFILE, 'tmp/cpdest' 890 | assert_same_file BIGFILE, 'tmp/cpdest' 891 | assert cmp(BIGFILE, 'tmp/cpdest'), 'orig != copied' 892 | 893 | mv 'tmp/cpdest', 'tmp/mvdest' 894 | assert_same_file BIGFILE, 'tmp/mvdest' 895 | assert_file_not_exist 'tmp/cpdest' 896 | 897 | rm 'tmp/mvdest' 898 | assert_file_not_exist 'tmp/mvdest' 899 | end 900 | 901 | def test_ln 902 | TARGETS.each do |fname| 903 | ln fname, 'tmp/lndest' 904 | assert_same_file fname, 'tmp/lndest' 905 | File.unlink 'tmp/lndest' 906 | end 907 | 908 | ln TARGETS, 'tmp' 909 | TARGETS.each do |fname| 910 | assert_same_file fname, 'tmp/' + File.basename(fname) 911 | end 912 | TARGETS.each do |fname| 913 | File.unlink 'tmp/' + File.basename(fname) 914 | end 915 | 916 | # src==dest (1) same path 917 | touch 'tmp/cptmp' 918 | assert_raise(Errno::EEXIST) { 919 | ln 'tmp/cptmp', 'tmp/cptmp' 920 | } 921 | end if have_hardlink? 922 | 923 | def test_ln_symlink 924 | touch 'tmp/cptmp' 925 | # src==dest (2) symlink and its target 926 | File.symlink 'cptmp', 'tmp/symlink' 927 | assert_raise(Errno::EEXIST) { 928 | ln 'tmp/cptmp', 'tmp/symlink' # normal file -> symlink 929 | } 930 | assert_raise(Errno::EEXIST) { 931 | ln 'tmp/symlink', 'tmp/cptmp' # symlink -> normal file 932 | } 933 | end if have_symlink? 934 | 935 | def test_ln_broken_symlink 936 | # src==dest (3) looped symlink 937 | File.symlink 'cptmp_symlink', 'tmp/cptmp_symlink' 938 | begin 939 | ln 'tmp/cptmp_symlink', 'tmp/cptmp_symlink' 940 | rescue => err 941 | assert_kind_of SystemCallError, err 942 | end 943 | end if have_symlink? and !no_broken_symlink? 944 | 945 | def test_ln_pathname 946 | # pathname 947 | touch 'tmp/lntmp' 948 | assert_nothing_raised { 949 | ln Pathname.new('tmp/lntmp'), 'tmp/lndesttmp1' 950 | ln 'tmp/lntmp', Pathname.new('tmp/lndesttmp2') 951 | ln Pathname.new('tmp/lntmp'), Pathname.new('tmp/lndesttmp3') 952 | } 953 | end if have_hardlink? 954 | 955 | def test_ln_s 956 | check_singleton :ln_s 957 | 958 | ln_s TARGETS, 'tmp' 959 | each_srcdest do |fname, lnfname| 960 | assert_equal fname, File.readlink(lnfname) 961 | ensure 962 | rm_f lnfname 963 | end 964 | 965 | lnfname = 'symlink' 966 | assert_raise(Errno::ENOENT, "multiple targets need a destination directory") { 967 | ln_s TARGETS, lnfname 968 | } 969 | assert_file.not_exist?(lnfname) 970 | 971 | TARGETS.each do |fname| 972 | fname = "../#{fname}" 973 | lnfname = 'tmp/lnsdest' 974 | ln_s fname, lnfname 975 | assert_file.symlink?(lnfname) 976 | assert_equal fname, File.readlink(lnfname) 977 | ensure 978 | rm_f lnfname 979 | end 980 | end if have_symlink? and !no_broken_symlink? 981 | 982 | def test_ln_s_broken_symlink 983 | assert_nothing_raised { 984 | ln_s 'symlink', 'tmp/symlink' 985 | } 986 | assert_symlink 'tmp/symlink' 987 | end if have_symlink? and !no_broken_symlink? 988 | 989 | def test_ln_s_pathname 990 | # pathname 991 | touch 'tmp/lnsdest' 992 | assert_nothing_raised { 993 | ln_s Pathname.new('lnsdest'), 'tmp/symlink_tmp1' 994 | ln_s 'lnsdest', Pathname.new('tmp/symlink_tmp2') 995 | ln_s Pathname.new('lnsdest'), Pathname.new('tmp/symlink_tmp3') 996 | } 997 | end if have_symlink? 998 | 999 | def test_ln_sf 1000 | check_singleton :ln_sf 1001 | 1002 | TARGETS.each do |fname| 1003 | fname = "../#{fname}" 1004 | ln_sf fname, 'tmp/lnsdest' 1005 | assert FileTest.symlink?('tmp/lnsdest'), 'not symlink' 1006 | assert_equal fname, File.readlink('tmp/lnsdest') 1007 | ln_sf fname, 'tmp/lnsdest' 1008 | ln_sf fname, 'tmp/lnsdest' 1009 | end 1010 | end if have_symlink? 1011 | 1012 | def test_ln_sf_broken_symlink 1013 | assert_nothing_raised { 1014 | ln_sf 'symlink', 'tmp/symlink' 1015 | } 1016 | end if have_symlink? and !no_broken_symlink? 1017 | 1018 | def test_ln_sf_pathname 1019 | # pathname 1020 | touch 'tmp/lns_dest' 1021 | assert_nothing_raised { 1022 | ln_sf Pathname.new('lns_dest'), 'tmp/symlink_tmp1' 1023 | ln_sf 'lns_dest', Pathname.new('tmp/symlink_tmp2') 1024 | ln_sf Pathname.new('lns_dest'), Pathname.new('tmp/symlink_tmp3') 1025 | } 1026 | end if have_symlink? 1027 | 1028 | def test_ln_sr 1029 | check_singleton :ln_sr 1030 | 1031 | assert_all_assertions_foreach(nil, *TARGETS) do |fname| 1032 | lnfname = 'tmp/lnsdest' 1033 | ln_sr fname, lnfname 1034 | assert_file.symlink?(lnfname) 1035 | assert_file.identical?(lnfname, fname) 1036 | assert_equal "../#{fname}", File.readlink(lnfname) 1037 | ensure 1038 | rm_f lnfname 1039 | end 1040 | 1041 | ln_sr TARGETS, 'tmp' 1042 | assert_all_assertions do |all| 1043 | each_srcdest do |fname, lnfname| 1044 | all.for(fname) do 1045 | assert_equal "../#{fname}", File.readlink(lnfname) 1046 | end 1047 | ensure 1048 | rm_f lnfname 1049 | end 1050 | end 1051 | 1052 | File.symlink 'data', 'link' 1053 | mkdir 'link/d1' 1054 | mkdir 'link/d2' 1055 | ln_sr 'link/d1/z', 'link/d2' 1056 | assert_equal '../d1/z', File.readlink('data/d2/z') 1057 | 1058 | mkdir 'data/src' 1059 | File.write('data/src/xxx', 'ok') 1060 | File.symlink '../data/src', 'tmp/src' 1061 | ln_sr 'tmp/src/xxx', 'data' 1062 | assert_file.symlink?('data/xxx') 1063 | assert_equal 'ok', File.read('data/xxx') 1064 | assert_equal 'src/xxx', File.readlink('data/xxx') 1065 | end 1066 | 1067 | def test_ln_sr_not_target_directory 1068 | assert_raise(ArgumentError) { 1069 | ln_sr TARGETS, 'tmp', target_directory: false 1070 | } 1071 | assert_empty(Dir.children('tmp')) 1072 | 1073 | lnfname = 'symlink' 1074 | assert_raise(ArgumentError) { 1075 | ln_sr TARGETS, lnfname, target_directory: false 1076 | } 1077 | assert_file.not_exist?(lnfname) 1078 | 1079 | assert_all_assertions_foreach(nil, *TARGETS) do |fname| 1080 | assert_raise(Errno::EEXIST, Errno::EACCES) { 1081 | ln_sr fname, 'tmp', target_directory: false 1082 | } 1083 | dest = File.join('tmp/', File.basename(fname)) 1084 | assert_file.not_exist? dest 1085 | ln_sr fname, dest, target_directory: false 1086 | assert_file.symlink?(dest) 1087 | assert_equal("../#{fname}", File.readlink(dest)) 1088 | end 1089 | end if have_symlink? 1090 | 1091 | def test_ln_sr_broken_symlink 1092 | assert_nothing_raised { 1093 | ln_sr 'tmp/symlink', 'tmp/symlink' 1094 | } 1095 | end if have_symlink? and !no_broken_symlink? 1096 | 1097 | def test_ln_sr_pathname 1098 | # pathname 1099 | touch 'tmp/lns_dest' 1100 | assert_nothing_raised { 1101 | ln_sr Pathname.new('tmp/lns_dest'), 'tmp/symlink_tmp1' 1102 | ln_sr 'tmp/lns_dest', Pathname.new('tmp/symlink_tmp2') 1103 | ln_sr Pathname.new('tmp/lns_dest'), Pathname.new('tmp/symlink_tmp3') 1104 | } 1105 | end if have_symlink? 1106 | 1107 | def test_mkdir 1108 | check_singleton :mkdir 1109 | 1110 | my_rm_rf 'tmpdatadir' 1111 | mkdir 'tmpdatadir' 1112 | assert_directory 'tmpdatadir' 1113 | Dir.rmdir 'tmpdatadir' 1114 | 1115 | mkdir 'tmpdatadir/' 1116 | assert_directory 'tmpdatadir' 1117 | Dir.rmdir 'tmpdatadir' 1118 | 1119 | mkdir 'tmp/mkdirdest' 1120 | assert_directory 'tmp/mkdirdest' 1121 | Dir.rmdir 'tmp/mkdirdest' 1122 | 1123 | mkdir 'tmp/tmp', :mode => 0700 1124 | assert_directory 'tmp/tmp' 1125 | assert_filemode 0700, 'tmp/tmp', mask: 0777 if have_file_perm? 1126 | Dir.rmdir 'tmp/tmp' 1127 | 1128 | # EISDIR on OS X, FreeBSD; EEXIST on Linux; Errno::EACCES on Windows 1129 | assert_raise(Errno::EISDIR, Errno::EEXIST, Errno::EACCES) { 1130 | mkdir '/' 1131 | } 1132 | end 1133 | 1134 | def test_mkdir_file_perm 1135 | mkdir 'tmp/tmp', :mode => 07777 1136 | assert_directory 'tmp/tmp' 1137 | assert_filemode 07777, 'tmp/tmp' 1138 | Dir.rmdir 'tmp/tmp' 1139 | end if have_file_perm? 1140 | 1141 | def test_mkdir_lf_in_path 1142 | mkdir "tmp-first-line\ntmp-second-line" 1143 | assert_directory "tmp-first-line\ntmp-second-line" 1144 | Dir.rmdir "tmp-first-line\ntmp-second-line" 1145 | end if lf_in_path_allowed? 1146 | 1147 | def test_mkdir_pathname 1148 | # pathname 1149 | assert_nothing_raised { 1150 | mkdir Pathname.new('tmp/tmpdirtmp') 1151 | mkdir [Pathname.new('tmp/tmpdirtmp2'), Pathname.new('tmp/tmpdirtmp3')] 1152 | } 1153 | end 1154 | 1155 | def test_mkdir_p 1156 | check_singleton :mkdir_p 1157 | 1158 | dirs = %w( 1159 | tmpdir/dir/ 1160 | tmpdir/dir/./ 1161 | tmpdir/dir/./.././dir/ 1162 | tmpdir/a 1163 | tmpdir/a/ 1164 | tmpdir/a/b 1165 | tmpdir/a/b/ 1166 | tmpdir/a/b/c/ 1167 | tmpdir/a/b/c 1168 | tmpdir/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a 1169 | tmpdir/a/a 1170 | ) 1171 | my_rm_rf 'tmpdir' 1172 | dirs.each do |d| 1173 | mkdir_p d 1174 | assert_directory d 1175 | assert_file_not_exist "#{d}/a" 1176 | assert_file_not_exist "#{d}/b" 1177 | assert_file_not_exist "#{d}/c" 1178 | my_rm_rf 'tmpdir' 1179 | end 1180 | dirs.each do |d| 1181 | mkdir_p d 1182 | assert_directory d 1183 | end 1184 | rm_rf 'tmpdir' 1185 | dirs.each do |d| 1186 | mkdir_p "#{Dir.pwd}/#{d}" 1187 | assert_directory d 1188 | end 1189 | rm_rf 'tmpdir' 1190 | 1191 | mkdir_p 'tmp/tmp/tmp', :mode => 0700 1192 | assert_directory 'tmp/tmp' 1193 | assert_directory 'tmp/tmp/tmp' 1194 | assert_filemode 0700, 'tmp/tmp', mask: 0777 if have_file_perm? 1195 | assert_filemode 0700, 'tmp/tmp/tmp', mask: 0777 if have_file_perm? 1196 | rm_rf 'tmp/tmp' 1197 | 1198 | mkdir_p 'tmp/tmp', :mode => 0 1199 | assert_directory 'tmp/tmp' 1200 | assert_filemode 0, 'tmp/tmp', mask: 0777 if have_file_perm? 1201 | # DO NOT USE rm_rf here. 1202 | # (rm(1) try to chdir to parent directory, it fails to remove directory.) 1203 | Dir.rmdir 'tmp/tmp' 1204 | Dir.rmdir 'tmp' 1205 | 1206 | mkdir_p '/' 1207 | end 1208 | 1209 | if /mswin|mingw|cygwin/ =~ RUBY_PLATFORM 1210 | def test_mkdir_p_root 1211 | if /cygwin/ =~ RUBY_PLATFORM 1212 | tmpdir = `cygpath -ma .`.chomp 1213 | else 1214 | tmpdir = Dir.pwd 1215 | end 1216 | pend "No drive letter" unless /\A[a-z]:/i =~ tmpdir 1217 | drive = "./#{$&}" 1218 | assert_file_not_exist drive 1219 | mkdir_p "#{tmpdir}/none/dir" 1220 | assert_directory "none/dir" 1221 | assert_file_not_exist drive 1222 | ensure 1223 | Dir.rmdir(drive) if drive and File.directory?(drive) 1224 | end 1225 | 1226 | def test_mkdir_p_offline_drive 1227 | offline_drive = ("A".."Z").to_a.reverse.find {|d| !File.exist?("#{d}:/") } 1228 | 1229 | assert_raise(Errno::ENOENT) { 1230 | mkdir_p "#{offline_drive}:/new_dir" 1231 | } 1232 | end 1233 | end 1234 | 1235 | def test_mkdir_p_file_perm 1236 | mkdir_p 'tmp/tmp/tmp', :mode => 07777 1237 | assert_directory 'tmp/tmp/tmp' 1238 | assert_filemode 07777, 'tmp/tmp/tmp' 1239 | Dir.rmdir 'tmp/tmp/tmp' 1240 | Dir.rmdir 'tmp/tmp' 1241 | end if have_file_perm? 1242 | 1243 | def test_mkdir_p_pathname 1244 | # pathname 1245 | assert_nothing_raised { 1246 | mkdir_p Pathname.new('tmp/tmp/tmp') 1247 | } 1248 | end 1249 | 1250 | def test_install 1251 | check_singleton :install 1252 | 1253 | File.open('tmp/aaa', 'w') {|f| f.puts 'aaa' } 1254 | File.open('tmp/bbb', 'w') {|f| f.puts 'bbb' } 1255 | install 'tmp/aaa', 'tmp/bbb', :mode => 0600 1256 | assert_equal "aaa\n", File.read('tmp/bbb') 1257 | assert_filemode 0600, 'tmp/bbb', mask: 0777 if have_file_perm? 1258 | 1259 | t = File.mtime('tmp/bbb') 1260 | install 'tmp/aaa', 'tmp/bbb' 1261 | assert_equal "aaa\n", File.read('tmp/bbb') 1262 | assert_filemode 0600, 'tmp/bbb', mask: 0777 if have_file_perm? 1263 | assert_equal_time t, File.mtime('tmp/bbb') 1264 | 1265 | File.unlink 'tmp/aaa' 1266 | File.unlink 'tmp/bbb' 1267 | 1268 | # src==dest (1) same path 1269 | touch 'tmp/cptmp' 1270 | assert_raise(ArgumentError) { 1271 | install 'tmp/cptmp', 'tmp/cptmp' 1272 | } 1273 | end 1274 | 1275 | def test_install_symlink 1276 | touch 'tmp/cptmp' 1277 | # src==dest (2) symlink and its target 1278 | File.symlink 'cptmp', 'tmp/cptmp_symlink' 1279 | assert_raise(ArgumentError) { 1280 | install 'tmp/cptmp', 'tmp/cptmp_symlink' 1281 | } 1282 | assert_raise(ArgumentError) { 1283 | install 'tmp/cptmp_symlink', 'tmp/cptmp' 1284 | } 1285 | end if have_symlink? 1286 | 1287 | def test_install_broken_symlink 1288 | # src==dest (3) looped symlink 1289 | File.symlink 'symlink', 'tmp/symlink' 1290 | assert_raise(Errno::ELOOP) { 1291 | # File#install invokes open(2), always ELOOP must be raised 1292 | install 'tmp/symlink', 'tmp/symlink' 1293 | } 1294 | end if have_symlink? and !no_broken_symlink? 1295 | 1296 | def test_install_pathname 1297 | # pathname 1298 | assert_nothing_raised { 1299 | rm_f 'tmp/a'; touch 'tmp/a' 1300 | install 'tmp/a', Pathname.new('tmp/b') 1301 | rm_f 'tmp/a'; touch 'tmp/a' 1302 | install Pathname.new('tmp/a'), 'tmp/b' 1303 | rm_f 'tmp/a'; touch 'tmp/a' 1304 | install Pathname.new('tmp/a'), Pathname.new('tmp/b') 1305 | my_rm_rf 'tmp/new_dir_end_with_slash' 1306 | install Pathname.new('tmp/a'), 'tmp/new_dir_end_with_slash/' 1307 | my_rm_rf 'tmp/new_dir_end_with_slash' 1308 | my_rm_rf 'tmp/new_dir' 1309 | install Pathname.new('tmp/a'), 'tmp/new_dir/a' 1310 | my_rm_rf 'tmp/new_dir' 1311 | install Pathname.new('tmp/a'), 'tmp/new_dir/new_dir_end_with_slash/' 1312 | my_rm_rf 'tmp/new_dir' 1313 | rm_f 'tmp/a' 1314 | touch 'tmp/a' 1315 | touch 'tmp/b' 1316 | mkdir 'tmp/dest' 1317 | install [Pathname.new('tmp/a'), Pathname.new('tmp/b')], 'tmp/dest' 1318 | my_rm_rf 'tmp/dest' 1319 | mkdir 'tmp/dest' 1320 | install [Pathname.new('tmp/a'), Pathname.new('tmp/b')], Pathname.new('tmp/dest') 1321 | } 1322 | end 1323 | 1324 | def test_install_owner_option 1325 | File.open('tmp/aaa', 'w') {|f| f.puts 'aaa' } 1326 | File.open('tmp/bbb', 'w') {|f| f.puts 'bbb' } 1327 | assert_nothing_raised { 1328 | install 'tmp/aaa', 'tmp/bbb', :owner => "nobody", :noop => true 1329 | } 1330 | end 1331 | 1332 | def test_install_group_option 1333 | File.open('tmp/aaa', 'w') {|f| f.puts 'aaa' } 1334 | File.open('tmp/bbb', 'w') {|f| f.puts 'bbb' } 1335 | assert_nothing_raised { 1336 | install 'tmp/aaa', 'tmp/bbb', :group => "nobody", :noop => true 1337 | } 1338 | end 1339 | 1340 | def test_install_mode_option 1341 | File.open('tmp/a', 'w') {|f| f.puts 'aaa' } 1342 | install 'tmp/a', 'tmp/b', :mode => "u=wrx,g=rx,o=x" 1343 | assert_filemode 0751, 'tmp/b' 1344 | install 'tmp/b', 'tmp/c', :mode => "g+w-x" 1345 | assert_filemode 0761, 'tmp/c' 1346 | install 'tmp/c', 'tmp/d', :mode => "o+r,g=o+w,o-r,u-o" # 761 => 763 => 773 => 771 => 671 1347 | assert_filemode 0671, 'tmp/d' 1348 | install 'tmp/d', 'tmp/e', :mode => "go=u" 1349 | assert_filemode 0666, 'tmp/e' 1350 | install 'tmp/e', 'tmp/f', :mode => "u=wrx,g=,o=" 1351 | assert_filemode 0700, 'tmp/f' 1352 | install 'tmp/f', 'tmp/g', :mode => "u=rx,go=" 1353 | assert_filemode 0500, 'tmp/g' 1354 | install 'tmp/g', 'tmp/h', :mode => "+wrx" 1355 | assert_filemode 0777, 'tmp/h' 1356 | install 'tmp/h', 'tmp/i', :mode => "u+s,o=s" 1357 | assert_filemode 04770, 'tmp/i' 1358 | install 'tmp/i', 'tmp/j', :mode => "u-w,go-wrx" 1359 | assert_filemode 04500, 'tmp/j' 1360 | install 'tmp/j', 'tmp/k', :mode => "+s" 1361 | assert_filemode 06500, 'tmp/k' 1362 | install 'tmp/a', 'tmp/l', :mode => "o+X" 1363 | assert_equal_filemode 'tmp/a', 'tmp/l' 1364 | end if have_file_perm? 1365 | 1366 | def test_chmod 1367 | check_singleton :chmod 1368 | 1369 | touch 'tmp/a' 1370 | chmod 0o700, 'tmp/a' 1371 | assert_filemode 0700, 'tmp/a' 1372 | chmod 0o500, 'tmp/a' 1373 | assert_filemode 0500, 'tmp/a' 1374 | end if have_file_perm? 1375 | 1376 | def test_chmod_symbol_mode 1377 | check_singleton :chmod 1378 | 1379 | touch 'tmp/a' 1380 | chmod "u=wrx,g=rx,o=x", 'tmp/a' 1381 | assert_filemode 0751, 'tmp/a' 1382 | chmod "g+w-x", 'tmp/a' 1383 | assert_filemode 0761, 'tmp/a' 1384 | chmod "o+r,g=o+w,o-r,u-o", 'tmp/a' # 761 => 763 => 773 => 771 => 671 1385 | assert_filemode 0671, 'tmp/a' 1386 | chmod "go=u", 'tmp/a' 1387 | assert_filemode 0666, 'tmp/a' 1388 | chmod "u=wrx,g=,o=", 'tmp/a' 1389 | assert_filemode 0700, 'tmp/a' 1390 | chmod "u=rx,go=", 'tmp/a' 1391 | assert_filemode 0500, 'tmp/a' 1392 | chmod "+wrx", 'tmp/a' 1393 | assert_filemode 0777, 'tmp/a' 1394 | chmod "u+s,o=s", 'tmp/a' 1395 | assert_filemode 04770, 'tmp/a' 1396 | chmod "u-w,go-wrx", 'tmp/a' 1397 | assert_filemode 04500, 'tmp/a' 1398 | chmod "+s", 'tmp/a' 1399 | assert_filemode 06500, 'tmp/a' 1400 | 1401 | # FreeBSD ufs and tmpfs don't allow to change sticky bit against 1402 | # regular file. It's slightly strange. Anyway it's no effect bit. 1403 | # see /usr/src/sys/ufs/ufs/ufs_chmod() 1404 | # NetBSD, OpenBSD, Solaris, and AIX also deny it. 1405 | if /freebsd|netbsd|openbsd|aix/ !~ RUBY_PLATFORM 1406 | chmod "u+t,o+t", 'tmp/a' 1407 | assert_filemode 07500, 'tmp/a' 1408 | chmod "a-t,a-s", 'tmp/a' 1409 | assert_filemode 0500, 'tmp/a' 1410 | end 1411 | 1412 | assert_raise_with_message(ArgumentError, /invalid\b.*\bfile mode/) { 1413 | chmod "a", 'tmp/a' 1414 | } 1415 | 1416 | assert_raise_with_message(ArgumentError, /invalid\b.*\bfile mode/) { 1417 | chmod "x+a", 'tmp/a' 1418 | } 1419 | 1420 | assert_raise_with_message(ArgumentError, /invalid\b.*\bfile mode/) { 1421 | chmod "u+z", 'tmp/a' 1422 | } 1423 | 1424 | assert_raise_with_message(ArgumentError, /invalid\b.*\bfile mode/) { 1425 | chmod ",+x", 'tmp/a' 1426 | } 1427 | 1428 | assert_raise_with_message(ArgumentError, /invalid\b.*\bfile mode/) { 1429 | chmod "755", 'tmp/a' 1430 | } 1431 | 1432 | end if have_file_perm? 1433 | 1434 | 1435 | def test_chmod_R 1436 | check_singleton :chmod_R 1437 | 1438 | mkdir_p 'tmp/dir/dir' 1439 | touch %w( tmp/dir/file tmp/dir/dir/file ) 1440 | chmod_R 0700, 'tmp/dir' 1441 | assert_filemode 0700, 'tmp/dir', mask: 0777 1442 | assert_filemode 0700, 'tmp/dir/file', mask: 0777 1443 | assert_filemode 0700, 'tmp/dir/dir', mask: 0777 1444 | assert_filemode 0700, 'tmp/dir/dir/file', mask: 0777 1445 | chmod_R 0500, 'tmp/dir' 1446 | assert_filemode 0500, 'tmp/dir', mask: 0777 1447 | assert_filemode 0500, 'tmp/dir/file', mask: 0777 1448 | assert_filemode 0500, 'tmp/dir/dir', mask: 0777 1449 | assert_filemode 0500, 'tmp/dir/dir/file', mask: 0777 1450 | chmod_R 0700, 'tmp/dir' # to remove 1451 | end if have_file_perm? 1452 | 1453 | def test_chmod_symbol_mode_R 1454 | check_singleton :chmod_R 1455 | 1456 | mkdir_p 'tmp/dir/dir' 1457 | touch %w( tmp/dir/file tmp/dir/dir/file ) 1458 | chmod_R "u=wrx,g=,o=", 'tmp/dir' 1459 | assert_filemode 0700, 'tmp/dir', mask: 0777 1460 | assert_filemode 0700, 'tmp/dir/file', mask: 0777 1461 | assert_filemode 0700, 'tmp/dir/dir', mask: 0777 1462 | assert_filemode 0700, 'tmp/dir/dir/file', mask: 0777 1463 | chmod_R "u=xr,g+X,o=", 'tmp/dir' 1464 | assert_filemode 0510, 'tmp/dir', mask: 0777 1465 | assert_filemode 0500, 'tmp/dir/file', mask: 0777 1466 | assert_filemode 0510, 'tmp/dir/dir', mask: 0777 1467 | assert_filemode 0500, 'tmp/dir/dir/file', mask: 0777 1468 | chmod_R 0700, 'tmp/dir' # to remove 1469 | end if have_file_perm? 1470 | 1471 | def test_chmod_verbose 1472 | check_singleton :chmod 1473 | 1474 | assert_output_lines(["chmod 700 tmp/a", "chmod 500 tmp/a"]) { 1475 | touch 'tmp/a' 1476 | chmod 0o700, 'tmp/a', verbose: true 1477 | assert_filemode 0700, 'tmp/a', mask: 0777 1478 | chmod 0o500, 'tmp/a', verbose: true 1479 | assert_filemode 0500, 'tmp/a', mask: 0777 1480 | } 1481 | end if have_file_perm? 1482 | 1483 | def test_s_chmod_verbose 1484 | assert_output_lines(["chmod 700 tmp/a"], FileUtils) { 1485 | touch 'tmp/a' 1486 | FileUtils.chmod 0o700, 'tmp/a', verbose: true 1487 | assert_filemode 0700, 'tmp/a', mask: 0777 1488 | } 1489 | end if have_file_perm? 1490 | 1491 | def test_chown 1492 | check_singleton :chown 1493 | 1494 | return unless @groups[1] 1495 | 1496 | input_group_1 = @groups[0] 1497 | assert_output_lines([]) { 1498 | touch 'tmp/a' 1499 | # integer input for group, nil for user 1500 | chown nil, input_group_1, 'tmp/a' 1501 | assert_ownership_group @groups[0], 'tmp/a' 1502 | } 1503 | 1504 | input_group_2 = Etc.getgrgid(@groups[1]).name 1505 | assert_output_lines([]) { 1506 | touch 'tmp/b' 1507 | # string input for group, -1 for user 1508 | chown(-1, input_group_2, 'tmp/b') 1509 | assert_ownership_group @groups[1], 'tmp/b' 1510 | } 1511 | end if have_file_perm? 1512 | 1513 | def test_chown_verbose 1514 | assert_output_lines(["chown :#{@groups[0]} tmp/a1 tmp/a2"]) { 1515 | touch 'tmp/a1' 1516 | touch 'tmp/a2' 1517 | chown nil, @groups[0], ['tmp/a1', 'tmp/a2'], verbose: true 1518 | assert_ownership_group @groups[0], 'tmp/a1' 1519 | assert_ownership_group @groups[0], 'tmp/a2' 1520 | } 1521 | end if have_file_perm? 1522 | 1523 | def test_chown_noop 1524 | return unless @groups[1] 1525 | assert_output_lines([]) { 1526 | touch 'tmp/a' 1527 | chown nil, @groups[0], 'tmp/a', :noop => false 1528 | assert_ownership_group @groups[0], 'tmp/a' 1529 | chown nil, @groups[1], 'tmp/a', :noop => true 1530 | assert_ownership_group @groups[0], 'tmp/a' 1531 | chown nil, @groups[1], 'tmp/a' 1532 | assert_ownership_group @groups[1], 'tmp/a' 1533 | } 1534 | end if have_file_perm? 1535 | 1536 | if have_file_perm? 1537 | def test_chown_error 1538 | uid = UID_1 1539 | return unless uid 1540 | 1541 | touch 'tmp/a' 1542 | 1543 | # getpwnam("") on Mac OS X doesn't err. 1544 | # passwd & group databases format is colon-separated, so user & 1545 | # group name can't contain a colon. 1546 | 1547 | assert_raise_with_message(ArgumentError, "can't find user for :::") { 1548 | chown ":::", @groups[0], 'tmp/a' 1549 | } 1550 | 1551 | assert_raise_with_message(ArgumentError, "can't find group for :::") { 1552 | chown uid, ":::", 'tmp/a' 1553 | } 1554 | 1555 | assert_raise_with_message(Errno::ENOENT, /No such file or directory/) { 1556 | chown nil, @groups[0], '' 1557 | } 1558 | end 1559 | 1560 | def test_chown_dir_group_ownership_not_recursive 1561 | return unless @groups[1] 1562 | 1563 | input_group_1 = @groups[0] 1564 | input_group_2 = @groups[1] 1565 | assert_output_lines([]) { 1566 | mkdir 'tmp/dir' 1567 | touch 'tmp/dir/a' 1568 | chown nil, input_group_1, ['tmp/dir', 'tmp/dir/a'] 1569 | assert_ownership_group @groups[0], 'tmp/dir' 1570 | assert_ownership_group @groups[0], 'tmp/dir/a' 1571 | chown nil, input_group_2, 'tmp/dir' 1572 | assert_ownership_group @groups[1], 'tmp/dir' 1573 | # Make sure FileUtils.chown does not chown recursively 1574 | assert_ownership_group @groups[0], 'tmp/dir/a' 1575 | } 1576 | end 1577 | 1578 | def test_chown_R 1579 | check_singleton :chown_R 1580 | 1581 | return unless @groups[1] 1582 | 1583 | input_group_1 = @groups[0] 1584 | input_group_2 = @groups[1] 1585 | assert_output_lines([]) { 1586 | list = ['tmp/dir', 'tmp/dir/a', 'tmp/dir/a/b', 'tmp/dir/a/b/c'] 1587 | mkdir_p 'tmp/dir/a/b/c' 1588 | touch 'tmp/d' 1589 | # string input 1590 | chown_R nil, input_group_1, 'tmp/dir' 1591 | list.each {|dir| 1592 | assert_ownership_group @groups[0], dir 1593 | } 1594 | chown_R nil, input_group_1, 'tmp/d' 1595 | assert_ownership_group @groups[0], 'tmp/d' 1596 | # list input 1597 | chown_R nil, input_group_2, ['tmp/dir', 'tmp/d'] 1598 | list += ['tmp/d'] 1599 | list.each {|dir| 1600 | assert_ownership_group @groups[1], dir 1601 | } 1602 | } 1603 | end 1604 | 1605 | def test_chown_R_verbose 1606 | assert_output_lines(["chown -R :#{@groups[0]} tmp/dir tmp/d"]) { 1607 | list = ['tmp/dir', 'tmp/dir/a', 'tmp/dir/a/b', 'tmp/dir/a/b/c'] 1608 | mkdir_p 'tmp/dir/a/b/c' 1609 | touch 'tmp/d' 1610 | chown_R nil, @groups[0], ['tmp/dir', 'tmp/d'], :verbose => true 1611 | list.each {|dir| 1612 | assert_ownership_group @groups[0], dir 1613 | } 1614 | } 1615 | end 1616 | 1617 | def test_chown_R_noop 1618 | return unless @groups[1] 1619 | 1620 | assert_output_lines([]) { 1621 | list = ['tmp/dir', 'tmp/dir/a', 'tmp/dir/a/b', 'tmp/dir/a/b/c'] 1622 | mkdir_p 'tmp/dir/a/b/c' 1623 | chown_R nil, @groups[0], 'tmp/dir', :noop => false 1624 | list.each {|dir| 1625 | assert_ownership_group @groups[0], dir 1626 | } 1627 | chown_R nil, @groups[1], 'tmp/dir', :noop => true 1628 | list.each {|dir| 1629 | assert_ownership_group @groups[0], dir 1630 | } 1631 | } 1632 | end 1633 | 1634 | def test_chown_R_force 1635 | assert_output_lines([]) { 1636 | list = ['tmp/dir', 'tmp/dir/a', 'tmp/dir/a/b', 'tmp/dir/a/b/c'] 1637 | mkdir_p 'tmp/dir/a/b/c' 1638 | assert_raise_with_message(Errno::ENOENT, /No such file or directory/) { 1639 | chown_R nil, @groups[0], ['tmp/dir', 'invalid'], :force => false 1640 | } 1641 | chown_R nil, @groups[0], ['tmp/dir', 'invalid'], :force => true 1642 | list.each {|dir| 1643 | assert_ownership_group @groups[0], dir 1644 | } 1645 | } 1646 | end 1647 | 1648 | if root_in_posix? 1649 | def test_chown_with_root 1650 | gid = @groups[0] # Most of the time, root only has one group 1651 | 1652 | files = ['tmp/a1', 'tmp/a2'] 1653 | files.each {|file| touch file} 1654 | [UID_1, UID_2].each {|uid| 1655 | assert_output_lines(["chown #{uid}:#{gid} tmp/a1 tmp/a2"]) { 1656 | chown uid, gid, files, verbose: true 1657 | files.each {|file| 1658 | assert_ownership_group gid, file 1659 | assert_ownership_user uid, file 1660 | } 1661 | } 1662 | } 1663 | end 1664 | 1665 | def test_chown_dir_user_ownership_not_recursive_with_root 1666 | assert_output_lines([]) { 1667 | mkdir 'tmp/dir' 1668 | touch 'tmp/dir/a' 1669 | chown UID_1, nil, ['tmp/dir', 'tmp/dir/a'] 1670 | assert_ownership_user UID_1, 'tmp/dir' 1671 | assert_ownership_user UID_1, 'tmp/dir/a' 1672 | chown UID_2, nil, 'tmp/dir' 1673 | assert_ownership_user UID_2, 'tmp/dir' 1674 | # Make sure FileUtils.chown does not chown recursively 1675 | assert_ownership_user UID_1, 'tmp/dir/a' 1676 | } 1677 | end 1678 | 1679 | def test_chown_R_with_root 1680 | assert_output_lines([]) { 1681 | list = ['tmp/dir', 'tmp/dir/a', 'tmp/dir/a/b', 'tmp/dir/a/b/c'] 1682 | mkdir_p 'tmp/dir/a/b/c' 1683 | touch 'tmp/d' 1684 | # string input 1685 | chown_R UID_1, nil, 'tmp/dir' 1686 | list.each {|dir| 1687 | assert_ownership_user UID_1, dir 1688 | } 1689 | chown_R UID_1, nil, 'tmp/d' 1690 | assert_ownership_user UID_1, 'tmp/d' 1691 | # list input 1692 | chown_R UID_2, nil, ['tmp/dir', 'tmp/d'] 1693 | list += ['tmp/d'] 1694 | list.each {|dir| 1695 | assert_ownership_user UID_2, dir 1696 | } 1697 | } 1698 | end 1699 | else 1700 | def test_chown_without_permission 1701 | touch 'tmp/a' 1702 | assert_raise(Errno::EPERM) { 1703 | chown UID_1, nil, 'tmp/a' 1704 | chown UID_2, nil, 'tmp/a' 1705 | } 1706 | end 1707 | 1708 | def test_chown_R_without_permission 1709 | touch 'tmp/a' 1710 | assert_raise(Errno::EPERM) { 1711 | chown_R UID_1, nil, 'tmp/a' 1712 | chown_R UID_2, nil, 'tmp/a' 1713 | } 1714 | end 1715 | end 1716 | end if UID_1 and UID_2 1717 | 1718 | def test_copy_entry 1719 | check_singleton :copy_entry 1720 | 1721 | each_srcdest do |srcpath, destpath| 1722 | copy_entry srcpath, destpath 1723 | assert_same_file srcpath, destpath 1724 | assert_equal File.stat(srcpath).ftype, File.stat(destpath).ftype 1725 | end 1726 | end 1727 | 1728 | def test_copy_entry_symlink 1729 | # root is a symlink 1730 | touch 'tmp/somewhere' 1731 | File.symlink 'somewhere', 'tmp/symsrc' 1732 | copy_entry 'tmp/symsrc', 'tmp/symdest' 1733 | assert_symlink 'tmp/symdest' 1734 | assert_equal 'somewhere', File.readlink('tmp/symdest') 1735 | 1736 | # content is a symlink 1737 | mkdir 'tmp/dir' 1738 | touch 'tmp/dir/somewhere' 1739 | File.symlink 'somewhere', 'tmp/dir/sym' 1740 | copy_entry 'tmp/dir', 'tmp/dirdest' 1741 | assert_directory 'tmp/dirdest' 1742 | assert_not_symlink 'tmp/dirdest' 1743 | assert_symlink 'tmp/dirdest/sym' 1744 | assert_equal 'somewhere', File.readlink('tmp/dirdest/sym') 1745 | end if have_symlink? 1746 | 1747 | def test_copy_entry_symlink_remove_destination 1748 | Dir.mkdir 'tmp/dir' 1749 | File.symlink 'tmp/dir', 'tmp/dest' 1750 | touch 'tmp/src' 1751 | copy_entry 'tmp/src', 'tmp/dest', false, false, true 1752 | assert_file_exist 'tmp/dest' 1753 | end if have_symlink? 1754 | 1755 | def test_copy_file 1756 | check_singleton :copy_file 1757 | 1758 | each_srcdest do |srcpath, destpath| 1759 | copy_file srcpath, destpath 1760 | assert_same_file srcpath, destpath 1761 | end 1762 | end 1763 | 1764 | def test_copy_stream 1765 | check_singleton :copy_stream 1766 | # IO 1767 | each_srcdest do |srcpath, destpath| 1768 | File.open(srcpath, 'rb') {|src| 1769 | File.open(destpath, 'wb') {|dest| 1770 | copy_stream src, dest 1771 | } 1772 | } 1773 | assert_same_file srcpath, destpath 1774 | end 1775 | end 1776 | 1777 | def test_copy_stream_duck 1778 | check_singleton :copy_stream 1779 | # duck typing test [ruby-dev:25369] 1780 | each_srcdest do |srcpath, destpath| 1781 | File.open(srcpath, 'rb') {|src| 1782 | File.open(destpath, 'wb') {|dest| 1783 | copy_stream Stream.new(src), Stream.new(dest) 1784 | } 1785 | } 1786 | assert_same_file srcpath, destpath 1787 | end 1788 | end 1789 | 1790 | def test_remove_file 1791 | check_singleton :remove_file 1792 | File.open('data/tmp', 'w') {|f| f.puts 'dummy' } 1793 | remove_file 'data/tmp' 1794 | assert_file_not_exist 'data/tmp' 1795 | end 1796 | 1797 | def test_remove_file_file_perm 1798 | File.open('data/tmp', 'w') {|f| f.puts 'dummy' } 1799 | File.chmod 0o000, 'data/tmp' 1800 | remove_file 'data/tmp' 1801 | assert_file_not_exist 'data/tmp' 1802 | end if have_file_perm? 1803 | 1804 | def test_remove_dir 1805 | check_singleton :remove_dir 1806 | Dir.mkdir 'data/tmpdir' 1807 | File.open('data/tmpdir/a', 'w') {|f| f.puts 'dummy' } 1808 | remove_dir 'data/tmpdir' 1809 | assert_file_not_exist 'data/tmpdir' 1810 | end 1811 | 1812 | def test_remove_dir_file_perm 1813 | Dir.mkdir 'data/tmpdir' 1814 | File.chmod 0o555, 'data/tmpdir' 1815 | remove_dir 'data/tmpdir' 1816 | assert_file_not_exist 'data/tmpdir' 1817 | end if have_file_perm? 1818 | 1819 | def test_remove_dir_with_file 1820 | File.write('data/tmpfile', 'dummy') 1821 | assert_raise(Errno::ENOTDIR) { remove_dir 'data/tmpfile' } 1822 | assert_file_exist 'data/tmpfile' 1823 | ensure 1824 | File.unlink('data/tmpfile') if File.exist?('data/tmpfile') 1825 | end 1826 | 1827 | def test_compare_file 1828 | check_singleton :compare_file 1829 | # FIXME 1830 | end 1831 | 1832 | def test_compare_stream 1833 | check_singleton :compare_stream 1834 | # FIXME 1835 | end 1836 | 1837 | class Stream 1838 | def initialize(f) 1839 | @f = f 1840 | end 1841 | 1842 | def read(*args) 1843 | @f.read(*args) 1844 | end 1845 | 1846 | def write(str) 1847 | @f.write str 1848 | end 1849 | end 1850 | 1851 | def test_uptodate? 1852 | check_singleton :uptodate? 1853 | prepare_time_data 1854 | Dir.chdir('data') { 1855 | assert( uptodate?('newest', %w(old newer notexist)) ) 1856 | assert( ! uptodate?('newer', %w(old newest notexist)) ) 1857 | assert( ! uptodate?('notexist', %w(old newest newer)) ) 1858 | } 1859 | 1860 | # pathname 1861 | touch 'tmp/a' 1862 | touch 'tmp/b' 1863 | touch 'tmp/c' 1864 | assert_nothing_raised { 1865 | uptodate? Pathname.new('tmp/a'), ['tmp/b', 'tmp/c'] 1866 | uptodate? 'tmp/a', [Pathname.new('tmp/b'), 'tmp/c'] 1867 | uptodate? 'tmp/a', ['tmp/b', Pathname.new('tmp/c')] 1868 | uptodate? Pathname.new('tmp/a'), [Pathname.new('tmp/b'), Pathname.new('tmp/c')] 1869 | } 1870 | # [Bug #6708] [ruby-core:46256] 1871 | assert_raise_with_message(ArgumentError, /wrong number of arguments \(.*\b3\b.* 2\)/) { 1872 | uptodate?('new',['old', 'oldest'], {}) 1873 | } 1874 | end 1875 | 1876 | def test_cd 1877 | check_singleton :cd 1878 | end 1879 | 1880 | def test_cd_result 1881 | assert_equal 42, cd('.') { 42 } 1882 | end 1883 | 1884 | def test_chdir 1885 | check_singleton :chdir 1886 | end 1887 | 1888 | def test_chdir_verbose 1889 | assert_output_lines(["cd .", "cd -"], FileUtils) do 1890 | FileUtils.chdir('.', verbose: true){} 1891 | end 1892 | end 1893 | 1894 | def test_chdir_verbose_frozen 1895 | o = Object.new 1896 | o.extend(FileUtils) 1897 | o.singleton_class.send(:public, :chdir) 1898 | o.freeze 1899 | orig_stdout = $stdout 1900 | $stdout = StringIO.new 1901 | o.chdir('.', verbose: true){} 1902 | $stdout.rewind 1903 | assert_equal(<<-END, $stdout.read) 1904 | cd . 1905 | cd - 1906 | END 1907 | ensure 1908 | $stdout = orig_stdout if orig_stdout 1909 | end 1910 | 1911 | def test_getwd 1912 | check_singleton :getwd 1913 | end 1914 | 1915 | def test_identical? 1916 | check_singleton :identical? 1917 | end 1918 | 1919 | def test_link 1920 | check_singleton :link 1921 | end 1922 | 1923 | def test_makedirs 1924 | check_singleton :makedirs 1925 | end 1926 | 1927 | def test_mkpath 1928 | check_singleton :mkpath 1929 | end 1930 | 1931 | def test_move 1932 | check_singleton :move 1933 | end 1934 | 1935 | def test_rm_rf 1936 | check_singleton :rm_rf 1937 | 1938 | return if /mswin|mingw/ =~ RUBY_PLATFORM 1939 | 1940 | mkdir 'tmpdatadir' 1941 | chmod 0o000, 'tmpdatadir' 1942 | rm_rf 'tmpdatadir' 1943 | 1944 | assert_file_not_exist 'tmpdatadir' 1945 | end 1946 | 1947 | def test_rmdir 1948 | check_singleton :rmdir 1949 | 1950 | begin 1951 | Dir.rmdir '/' 1952 | rescue Errno::ENOTEMPTY 1953 | rescue => e 1954 | assert_raise(e.class) { 1955 | # Dir.rmdir('') raises Errno::ENOENT. 1956 | # FileUtils#rmdir ignores it. 1957 | # And this test failed as expected. 1958 | rmdir '/' 1959 | } 1960 | end 1961 | 1962 | subdir = 'data/sub/dir' 1963 | mkdir_p(subdir) 1964 | File.write("#{subdir}/file", '') 1965 | msg = "should fail to remove non-empty directory" 1966 | assert_raise(Errno::ENOTEMPTY, Errno::EEXIST, msg) { 1967 | rmdir(subdir) 1968 | } 1969 | assert_raise(Errno::ENOTEMPTY, Errno::EEXIST, msg) { 1970 | rmdir(subdir, parents: true) 1971 | } 1972 | File.unlink("#{subdir}/file") 1973 | assert_raise(Errno::ENOENT) { 1974 | rmdir("#{subdir}/nonexistent") 1975 | } 1976 | assert_raise(Errno::ENOENT) { 1977 | rmdir("#{subdir}/nonexistent", parents: true) 1978 | } 1979 | assert_nothing_raised(Errno::ENOENT) { 1980 | rmdir(subdir, parents: true) 1981 | } 1982 | assert_file_not_exist(subdir) 1983 | assert_file_not_exist('data/sub') 1984 | assert_directory('data') 1985 | end 1986 | 1987 | def test_rmtree 1988 | check_singleton :rmtree 1989 | end 1990 | 1991 | def test_safe_unlink 1992 | check_singleton :safe_unlink 1993 | end 1994 | 1995 | def test_symlink 1996 | check_singleton :symlink 1997 | end 1998 | 1999 | def test_touch 2000 | check_singleton :touch 2001 | end 2002 | 2003 | def test_collect_methods 2004 | end 2005 | 2006 | def test_commands 2007 | end 2008 | 2009 | def test_have_option? 2010 | end 2011 | 2012 | def test_options 2013 | end 2014 | 2015 | def test_options_of 2016 | end 2017 | 2018 | end 2019 | -------------------------------------------------------------------------------- /lib/fileutils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require 'rbconfig' 5 | rescue LoadError 6 | # for make rjit-headers 7 | end 8 | 9 | # Namespace for file utility methods for copying, moving, removing, etc. 10 | # 11 | # == What's Here 12 | # 13 | # First, what’s elsewhere. \Module \FileUtils: 14 | # 15 | # - Inherits from {class Object}[https://docs.ruby-lang.org/en/master/Object.html]. 16 | # - Supplements {class File}[https://docs.ruby-lang.org/en/master/File.html] 17 | # (but is not included or extended there). 18 | # 19 | # Here, module \FileUtils provides methods that are useful for: 20 | # 21 | # - {Creating}[rdoc-ref:FileUtils@Creating]. 22 | # - {Deleting}[rdoc-ref:FileUtils@Deleting]. 23 | # - {Querying}[rdoc-ref:FileUtils@Querying]. 24 | # - {Setting}[rdoc-ref:FileUtils@Setting]. 25 | # - {Comparing}[rdoc-ref:FileUtils@Comparing]. 26 | # - {Copying}[rdoc-ref:FileUtils@Copying]. 27 | # - {Moving}[rdoc-ref:FileUtils@Moving]. 28 | # - {Options}[rdoc-ref:FileUtils@Options]. 29 | # 30 | # === Creating 31 | # 32 | # - ::mkdir: Creates directories. 33 | # - ::mkdir_p, ::makedirs, ::mkpath: Creates directories, 34 | # also creating ancestor directories as needed. 35 | # - ::link_entry: Creates a hard link. 36 | # - ::ln, ::link: Creates hard links. 37 | # - ::ln_s, ::symlink: Creates symbolic links. 38 | # - ::ln_sf: Creates symbolic links, overwriting if necessary. 39 | # - ::ln_sr: Creates symbolic links relative to targets 40 | # 41 | # === Deleting 42 | # 43 | # - ::remove_dir: Removes a directory and its descendants. 44 | # - ::remove_entry: Removes an entry, including its descendants if it is a directory. 45 | # - ::remove_entry_secure: Like ::remove_entry, but removes securely. 46 | # - ::remove_file: Removes a file entry. 47 | # - ::rm, ::remove: Removes entries. 48 | # - ::rm_f, ::safe_unlink: Like ::rm, but removes forcibly. 49 | # - ::rm_r: Removes entries and their descendants. 50 | # - ::rm_rf, ::rmtree: Like ::rm_r, but removes forcibly. 51 | # - ::rmdir: Removes directories. 52 | # 53 | # === Querying 54 | # 55 | # - ::pwd, ::getwd: Returns the path to the working directory. 56 | # - ::uptodate?: Returns whether a given entry is newer than given other entries. 57 | # 58 | # === Setting 59 | # 60 | # - ::cd, ::chdir: Sets the working directory. 61 | # - ::chmod: Sets permissions for an entry. 62 | # - ::chmod_R: Sets permissions for an entry and its descendants. 63 | # - ::chown: Sets the owner and group for entries. 64 | # - ::chown_R: Sets the owner and group for entries and their descendants. 65 | # - ::touch: Sets modification and access times for entries, 66 | # creating if necessary. 67 | # 68 | # === Comparing 69 | # 70 | # - ::compare_file, ::cmp, ::identical?: Returns whether two entries are identical. 71 | # - ::compare_stream: Returns whether two streams are identical. 72 | # 73 | # === Copying 74 | # 75 | # - ::copy_entry: Recursively copies an entry. 76 | # - ::copy_file: Copies an entry. 77 | # - ::copy_stream: Copies a stream. 78 | # - ::cp, ::copy: Copies files. 79 | # - ::cp_lr: Recursively creates hard links. 80 | # - ::cp_r: Recursively copies files, retaining mode, owner, and group. 81 | # - ::install: Recursively copies files, optionally setting mode, 82 | # owner, and group. 83 | # 84 | # === Moving 85 | # 86 | # - ::mv, ::move: Moves entries. 87 | # 88 | # === Options 89 | # 90 | # - ::collect_method: Returns the names of methods that accept a given option. 91 | # - ::commands: Returns the names of methods that accept options. 92 | # - ::have_option?: Returns whether a given method accepts a given option. 93 | # - ::options: Returns all option names. 94 | # - ::options_of: Returns the names of the options for a given method. 95 | # 96 | # == Path Arguments 97 | # 98 | # Some methods in \FileUtils accept _path_ arguments, 99 | # which are interpreted as paths to filesystem entries: 100 | # 101 | # - If the argument is a string, that value is the path. 102 | # - If the argument has method +:to_path+, it is converted via that method. 103 | # - If the argument has method +:to_str+, it is converted via that method. 104 | # 105 | # == About the Examples 106 | # 107 | # Some examples here involve trees of file entries. 108 | # For these, we sometimes display trees using the 109 | # {tree command-line utility}[https://en.wikipedia.org/wiki/Tree_(command)], 110 | # which is a recursive directory-listing utility that produces 111 | # a depth-indented listing of files and directories. 112 | # 113 | # We use a helper method to launch the command and control the format: 114 | # 115 | # def tree(dirpath = '.') 116 | # command = "tree --noreport --charset=ascii #{dirpath}" 117 | # system(command) 118 | # end 119 | # 120 | # To illustrate: 121 | # 122 | # tree('src0') 123 | # # => src0 124 | # # |-- sub0 125 | # # | |-- src0.txt 126 | # # | `-- src1.txt 127 | # # `-- sub1 128 | # # |-- src2.txt 129 | # # `-- src3.txt 130 | # 131 | # == Avoiding the TOCTTOU Vulnerability 132 | # 133 | # For certain methods that recursively remove entries, 134 | # there is a potential vulnerability called the 135 | # {Time-of-check to time-of-use}[https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use], 136 | # or TOCTTOU, vulnerability that can exist when: 137 | # 138 | # - An ancestor directory of the entry at the target path is world writable; 139 | # such directories include /tmp. 140 | # - The directory tree at the target path includes: 141 | # 142 | # - A world-writable descendant directory. 143 | # - A symbolic link. 144 | # 145 | # To avoid that vulnerability, you can use this method to remove entries: 146 | # 147 | # - FileUtils.remove_entry_secure: removes recursively 148 | # if the target path points to a directory. 149 | # 150 | # Also available are these methods, 151 | # each of which calls \FileUtils.remove_entry_secure: 152 | # 153 | # - FileUtils.rm_r with keyword argument secure: true. 154 | # - FileUtils.rm_rf with keyword argument secure: true. 155 | # 156 | # Finally, this method for moving entries calls \FileUtils.remove_entry_secure 157 | # if the source and destination are on different file systems 158 | # (which means that the "move" is really a copy and remove): 159 | # 160 | # - FileUtils.mv with keyword argument secure: true. 161 | # 162 | # \Method \FileUtils.remove_entry_secure removes securely 163 | # by applying a special pre-process: 164 | # 165 | # - If the target path points to a directory, this method uses methods 166 | # {File#chown}[https://docs.ruby-lang.org/en/master/File.html#method-i-chown] 167 | # and {File#chmod}[https://docs.ruby-lang.org/en/master/File.html#method-i-chmod] 168 | # in removing directories. 169 | # - The owner of the target directory should be either the current process 170 | # or the super user (root). 171 | # 172 | # WARNING: You must ensure that *ALL* parent directories cannot be 173 | # moved by other untrusted users. For example, parent directories 174 | # should not be owned by untrusted users, and should not be world 175 | # writable except when the sticky bit is set. 176 | # 177 | # For details of this security vulnerability, see Perl cases: 178 | # 179 | # - {CVE-2005-0448}[https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-0448]. 180 | # - {CVE-2004-0452}[https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2004-0452]. 181 | # 182 | module FileUtils 183 | # The version number. 184 | VERSION = "1.8.0" 185 | 186 | def self.private_module_function(name) #:nodoc: 187 | module_function name 188 | private_class_method name 189 | end 190 | 191 | # 192 | # Returns a string containing the path to the current directory: 193 | # 194 | # FileUtils.pwd # => "/rdoc/fileutils" 195 | # 196 | # Related: FileUtils.cd. 197 | # 198 | def pwd 199 | Dir.pwd 200 | end 201 | module_function :pwd 202 | 203 | alias getwd pwd 204 | module_function :getwd 205 | 206 | # Changes the working directory to the given +dir+, which 207 | # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]: 208 | # 209 | # With no block given, 210 | # changes the current directory to the directory at +dir+; returns zero: 211 | # 212 | # FileUtils.pwd # => "/rdoc/fileutils" 213 | # FileUtils.cd('..') 214 | # FileUtils.pwd # => "/rdoc" 215 | # FileUtils.cd('fileutils') 216 | # 217 | # With a block given, changes the current directory to the directory 218 | # at +dir+, calls the block with argument +dir+, 219 | # and restores the original current directory; returns the block's value: 220 | # 221 | # FileUtils.pwd # => "/rdoc/fileutils" 222 | # FileUtils.cd('..') { |arg| [arg, FileUtils.pwd] } # => ["..", "/rdoc"] 223 | # FileUtils.pwd # => "/rdoc/fileutils" 224 | # 225 | # Keyword arguments: 226 | # 227 | # - verbose: true - prints an equivalent command: 228 | # 229 | # FileUtils.cd('..') 230 | # FileUtils.cd('fileutils') 231 | # 232 | # Output: 233 | # 234 | # cd .. 235 | # cd fileutils 236 | # 237 | # Related: FileUtils.pwd. 238 | # 239 | def cd(dir, verbose: nil, &block) # :yield: dir 240 | fu_output_message "cd #{dir}" if verbose 241 | result = Dir.chdir(dir, &block) 242 | fu_output_message 'cd -' if verbose and block 243 | result 244 | end 245 | module_function :cd 246 | 247 | alias chdir cd 248 | module_function :chdir 249 | 250 | # 251 | # Returns +true+ if the file at path +new+ 252 | # is newer than all the files at paths in array +old_list+; 253 | # +false+ otherwise. 254 | # 255 | # Argument +new+ and the elements of +old_list+ 256 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]: 257 | # 258 | # FileUtils.uptodate?('Rakefile', ['Gemfile', 'README.md']) # => true 259 | # FileUtils.uptodate?('Gemfile', ['Rakefile', 'README.md']) # => false 260 | # 261 | # A non-existent file is considered to be infinitely old. 262 | # 263 | # Related: FileUtils.touch. 264 | # 265 | def uptodate?(new, old_list) 266 | return false unless File.exist?(new) 267 | new_time = File.mtime(new) 268 | old_list.each do |old| 269 | if File.exist?(old) 270 | return false unless new_time > File.mtime(old) 271 | end 272 | end 273 | true 274 | end 275 | module_function :uptodate? 276 | 277 | def remove_trailing_slash(dir) #:nodoc: 278 | dir == '/' ? dir : dir.chomp(?/) 279 | end 280 | private_module_function :remove_trailing_slash 281 | 282 | # 283 | # Creates directories at the paths in the given +list+ 284 | # (a single path or an array of paths); 285 | # returns +list+ if it is an array, [list] otherwise. 286 | # 287 | # Argument +list+ or its elements 288 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 289 | # 290 | # With no keyword arguments, creates a directory at each +path+ in +list+ 291 | # by calling: Dir.mkdir(path, mode); 292 | # see {Dir.mkdir}[https://docs.ruby-lang.org/en/master/Dir.html#method-c-mkdir]: 293 | # 294 | # FileUtils.mkdir(%w[tmp0 tmp1]) # => ["tmp0", "tmp1"] 295 | # FileUtils.mkdir('tmp4') # => ["tmp4"] 296 | # 297 | # Keyword arguments: 298 | # 299 | # - mode: mode - also calls File.chmod(mode, path); 300 | # see {File.chmod}[https://docs.ruby-lang.org/en/master/File.html#method-c-chmod]. 301 | # - noop: true - does not create directories. 302 | # - verbose: true - prints an equivalent command: 303 | # 304 | # FileUtils.mkdir(%w[tmp0 tmp1], verbose: true) 305 | # FileUtils.mkdir(%w[tmp2 tmp3], mode: 0700, verbose: true) 306 | # 307 | # Output: 308 | # 309 | # mkdir tmp0 tmp1 310 | # mkdir -m 700 tmp2 tmp3 311 | # 312 | # Raises an exception if any path points to an existing 313 | # file or directory, or if for any reason a directory cannot be created. 314 | # 315 | # Related: FileUtils.mkdir_p. 316 | # 317 | def mkdir(list, mode: nil, noop: nil, verbose: nil) 318 | list = fu_list(list) 319 | fu_output_message "mkdir #{mode ? ('-m %03o ' % mode) : ''}#{list.join ' '}" if verbose 320 | return if noop 321 | 322 | list.each do |dir| 323 | fu_mkdir dir, mode 324 | end 325 | end 326 | module_function :mkdir 327 | 328 | # 329 | # Creates directories at the paths in the given +list+ 330 | # (a single path or an array of paths), 331 | # also creating ancestor directories as needed; 332 | # returns +list+ if it is an array, [list] otherwise. 333 | # 334 | # Argument +list+ or its elements 335 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 336 | # 337 | # With no keyword arguments, creates a directory at each +path+ in +list+, 338 | # along with any needed ancestor directories, 339 | # by calling: Dir.mkdir(path, mode); 340 | # see {Dir.mkdir}[https://docs.ruby-lang.org/en/master/Dir.html#method-c-mkdir]: 341 | # 342 | # FileUtils.mkdir_p(%w[tmp0/tmp1 tmp2/tmp3]) # => ["tmp0/tmp1", "tmp2/tmp3"] 343 | # FileUtils.mkdir_p('tmp4/tmp5') # => ["tmp4/tmp5"] 344 | # 345 | # Keyword arguments: 346 | # 347 | # - mode: mode - also calls File.chmod(mode, path); 348 | # see {File.chmod}[https://docs.ruby-lang.org/en/master/File.html#method-c-chmod]. 349 | # - noop: true - does not create directories. 350 | # - verbose: true - prints an equivalent command: 351 | # 352 | # FileUtils.mkdir_p(%w[tmp0 tmp1], verbose: true) 353 | # FileUtils.mkdir_p(%w[tmp2 tmp3], mode: 0700, verbose: true) 354 | # 355 | # Output: 356 | # 357 | # mkdir -p tmp0 tmp1 358 | # mkdir -p -m 700 tmp2 tmp3 359 | # 360 | # Raises an exception if for any reason a directory cannot be created. 361 | # 362 | # FileUtils.mkpath and FileUtils.makedirs are aliases for FileUtils.mkdir_p. 363 | # 364 | # Related: FileUtils.mkdir. 365 | # 366 | def mkdir_p(list, mode: nil, noop: nil, verbose: nil) 367 | list = fu_list(list) 368 | fu_output_message "mkdir -p #{mode ? ('-m %03o ' % mode) : ''}#{list.join ' '}" if verbose 369 | return *list if noop 370 | 371 | list.each do |item| 372 | path = remove_trailing_slash(item) 373 | 374 | stack = [] 375 | until File.directory?(path) || File.dirname(path) == path 376 | stack.push path 377 | path = File.dirname(path) 378 | end 379 | stack.reverse_each do |dir| 380 | begin 381 | fu_mkdir dir, mode 382 | rescue SystemCallError 383 | raise unless File.directory?(dir) 384 | end 385 | end 386 | end 387 | 388 | return *list 389 | end 390 | module_function :mkdir_p 391 | 392 | alias mkpath mkdir_p 393 | alias makedirs mkdir_p 394 | module_function :mkpath 395 | module_function :makedirs 396 | 397 | def fu_mkdir(path, mode) #:nodoc: 398 | path = remove_trailing_slash(path) 399 | if mode 400 | Dir.mkdir path, mode 401 | File.chmod mode, path 402 | else 403 | Dir.mkdir path 404 | end 405 | end 406 | private_module_function :fu_mkdir 407 | 408 | # 409 | # Removes directories at the paths in the given +list+ 410 | # (a single path or an array of paths); 411 | # returns +list+, if it is an array, [list] otherwise. 412 | # 413 | # Argument +list+ or its elements 414 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 415 | # 416 | # With no keyword arguments, removes the directory at each +path+ in +list+, 417 | # by calling: Dir.rmdir(path); 418 | # see {Dir.rmdir}[https://docs.ruby-lang.org/en/master/Dir.html#method-c-rmdir]: 419 | # 420 | # FileUtils.rmdir(%w[tmp0/tmp1 tmp2/tmp3]) # => ["tmp0/tmp1", "tmp2/tmp3"] 421 | # FileUtils.rmdir('tmp4/tmp5') # => ["tmp4/tmp5"] 422 | # 423 | # Keyword arguments: 424 | # 425 | # - parents: true - removes successive ancestor directories 426 | # if empty. 427 | # - noop: true - does not remove directories. 428 | # - verbose: true - prints an equivalent command: 429 | # 430 | # FileUtils.rmdir(%w[tmp0/tmp1 tmp2/tmp3], parents: true, verbose: true) 431 | # FileUtils.rmdir('tmp4/tmp5', parents: true, verbose: true) 432 | # 433 | # Output: 434 | # 435 | # rmdir -p tmp0/tmp1 tmp2/tmp3 436 | # rmdir -p tmp4/tmp5 437 | # 438 | # Raises an exception if a directory does not exist 439 | # or if for any reason a directory cannot be removed. 440 | # 441 | # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. 442 | # 443 | def rmdir(list, parents: nil, noop: nil, verbose: nil) 444 | list = fu_list(list) 445 | fu_output_message "rmdir #{parents ? '-p ' : ''}#{list.join ' '}" if verbose 446 | return if noop 447 | list.each do |dir| 448 | Dir.rmdir(dir = remove_trailing_slash(dir)) 449 | if parents 450 | begin 451 | until (parent = File.dirname(dir)) == '.' or parent == dir 452 | dir = parent 453 | Dir.rmdir(dir) 454 | end 455 | rescue Errno::ENOTEMPTY, Errno::EEXIST, Errno::ENOENT 456 | end 457 | end 458 | end 459 | end 460 | module_function :rmdir 461 | 462 | # Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link]. 463 | # 464 | # Arguments +src+ (a single path or an array of paths) 465 | # and +dest+ (a single path) 466 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 467 | # 468 | # When +src+ is the path to an existing file 469 | # and +dest+ is the path to a non-existent file, 470 | # creates a hard link at +dest+ pointing to +src+; returns zero: 471 | # 472 | # Dir.children('tmp0/') # => ["t.txt"] 473 | # Dir.children('tmp1/') # => [] 474 | # FileUtils.ln('tmp0/t.txt', 'tmp1/t.lnk') # => 0 475 | # Dir.children('tmp1/') # => ["t.lnk"] 476 | # 477 | # When +src+ is the path to an existing file 478 | # and +dest+ is the path to an existing directory, 479 | # creates a hard link at dest/src pointing to +src+; returns zero: 480 | # 481 | # Dir.children('tmp2') # => ["t.dat"] 482 | # Dir.children('tmp3') # => [] 483 | # FileUtils.ln('tmp2/t.dat', 'tmp3') # => 0 484 | # Dir.children('tmp3') # => ["t.dat"] 485 | # 486 | # When +src+ is an array of paths to existing files 487 | # and +dest+ is the path to an existing directory, 488 | # then for each path +target+ in +src+, 489 | # creates a hard link at dest/target pointing to +target+; 490 | # returns +src+: 491 | # 492 | # Dir.children('tmp4/') # => [] 493 | # FileUtils.ln(['tmp0/t.txt', 'tmp2/t.dat'], 'tmp4/') # => ["tmp0/t.txt", "tmp2/t.dat"] 494 | # Dir.children('tmp4/') # => ["t.dat", "t.txt"] 495 | # 496 | # Keyword arguments: 497 | # 498 | # - force: true - overwrites +dest+ if it exists. 499 | # - noop: true - does not create links. 500 | # - verbose: true - prints an equivalent command: 501 | # 502 | # FileUtils.ln('tmp0/t.txt', 'tmp1/t.lnk', verbose: true) 503 | # FileUtils.ln('tmp2/t.dat', 'tmp3', verbose: true) 504 | # FileUtils.ln(['tmp0/t.txt', 'tmp2/t.dat'], 'tmp4/', verbose: true) 505 | # 506 | # Output: 507 | # 508 | # ln tmp0/t.txt tmp1/t.lnk 509 | # ln tmp2/t.dat tmp3 510 | # ln tmp0/t.txt tmp2/t.dat tmp4/ 511 | # 512 | # Raises an exception if +dest+ is the path to an existing file 513 | # and keyword argument +force+ is not +true+. 514 | # 515 | # Related: FileUtils.link_entry (has different options). 516 | # 517 | def ln(src, dest, force: nil, noop: nil, verbose: nil) 518 | fu_output_message "ln#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose 519 | return if noop 520 | fu_each_src_dest0(src, dest) do |s,d| 521 | remove_file d, true if force 522 | File.link s, d 523 | end 524 | end 525 | module_function :ln 526 | 527 | alias link ln 528 | module_function :link 529 | 530 | # Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link]. 531 | # 532 | # Arguments +src+ (a single path or an array of paths) 533 | # and +dest+ (a single path) 534 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 535 | # 536 | # If +src+ is the path to a directory and +dest+ does not exist, 537 | # creates links +dest+ and descendents pointing to +src+ and its descendents: 538 | # 539 | # tree('src0') 540 | # # => src0 541 | # # |-- sub0 542 | # # | |-- src0.txt 543 | # # | `-- src1.txt 544 | # # `-- sub1 545 | # # |-- src2.txt 546 | # # `-- src3.txt 547 | # File.exist?('dest0') # => false 548 | # FileUtils.cp_lr('src0', 'dest0') 549 | # tree('dest0') 550 | # # => dest0 551 | # # |-- sub0 552 | # # | |-- src0.txt 553 | # # | `-- src1.txt 554 | # # `-- sub1 555 | # # |-- src2.txt 556 | # # `-- src3.txt 557 | # 558 | # If +src+ and +dest+ are both paths to directories, 559 | # creates links dest/src and descendents 560 | # pointing to +src+ and its descendents: 561 | # 562 | # tree('src1') 563 | # # => src1 564 | # # |-- sub0 565 | # # | |-- src0.txt 566 | # # | `-- src1.txt 567 | # # `-- sub1 568 | # # |-- src2.txt 569 | # # `-- src3.txt 570 | # FileUtils.mkdir('dest1') 571 | # FileUtils.cp_lr('src1', 'dest1') 572 | # tree('dest1') 573 | # # => dest1 574 | # # `-- src1 575 | # # |-- sub0 576 | # # | |-- src0.txt 577 | # # | `-- src1.txt 578 | # # `-- sub1 579 | # # |-- src2.txt 580 | # # `-- src3.txt 581 | # 582 | # If +src+ is an array of paths to entries and +dest+ is the path to a directory, 583 | # for each path +filepath+ in +src+, creates a link at dest/filepath 584 | # pointing to that path: 585 | # 586 | # tree('src2') 587 | # # => src2 588 | # # |-- sub0 589 | # # | |-- src0.txt 590 | # # | `-- src1.txt 591 | # # `-- sub1 592 | # # |-- src2.txt 593 | # # `-- src3.txt 594 | # FileUtils.mkdir('dest2') 595 | # FileUtils.cp_lr(['src2/sub0', 'src2/sub1'], 'dest2') 596 | # tree('dest2') 597 | # # => dest2 598 | # # |-- sub0 599 | # # | |-- src0.txt 600 | # # | `-- src1.txt 601 | # # `-- sub1 602 | # # |-- src2.txt 603 | # # `-- src3.txt 604 | # 605 | # Keyword arguments: 606 | # 607 | # - dereference_root: false - if +src+ is a symbolic link, 608 | # does not dereference it. 609 | # - noop: true - does not create links. 610 | # - remove_destination: true - removes +dest+ before creating links. 611 | # - verbose: true - prints an equivalent command: 612 | # 613 | # FileUtils.cp_lr('src0', 'dest0', noop: true, verbose: true) 614 | # FileUtils.cp_lr('src1', 'dest1', noop: true, verbose: true) 615 | # FileUtils.cp_lr(['src2/sub0', 'src2/sub1'], 'dest2', noop: true, verbose: true) 616 | # 617 | # Output: 618 | # 619 | # cp -lr src0 dest0 620 | # cp -lr src1 dest1 621 | # cp -lr src2/sub0 src2/sub1 dest2 622 | # 623 | # Raises an exception if +dest+ is the path to an existing file or directory 624 | # and keyword argument remove_destination: true is not given. 625 | # 626 | # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. 627 | # 628 | def cp_lr(src, dest, noop: nil, verbose: nil, 629 | dereference_root: true, remove_destination: false) 630 | fu_output_message "cp -lr#{remove_destination ? ' --remove-destination' : ''} #{[src,dest].flatten.join ' '}" if verbose 631 | return if noop 632 | fu_each_src_dest(src, dest) do |s, d| 633 | link_entry s, d, dereference_root, remove_destination 634 | end 635 | end 636 | module_function :cp_lr 637 | 638 | # Creates {symbolic links}[https://en.wikipedia.org/wiki/Symbolic_link]. 639 | # 640 | # Arguments +src+ (a single path or an array of paths) 641 | # and +dest+ (a single path) 642 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 643 | # 644 | # If +src+ is the path to an existing file: 645 | # 646 | # - When +dest+ is the path to a non-existent file, 647 | # creates a symbolic link at +dest+ pointing to +src+: 648 | # 649 | # FileUtils.touch('src0.txt') 650 | # File.exist?('dest0.txt') # => false 651 | # FileUtils.ln_s('src0.txt', 'dest0.txt') 652 | # File.symlink?('dest0.txt') # => true 653 | # 654 | # - When +dest+ is the path to an existing file, 655 | # creates a symbolic link at +dest+ pointing to +src+ 656 | # if and only if keyword argument force: true is given 657 | # (raises an exception otherwise): 658 | # 659 | # FileUtils.touch('src1.txt') 660 | # FileUtils.touch('dest1.txt') 661 | # FileUtils.ln_s('src1.txt', 'dest1.txt', force: true) 662 | # FileTest.symlink?('dest1.txt') # => true 663 | # 664 | # FileUtils.ln_s('src1.txt', 'dest1.txt') # Raises Errno::EEXIST. 665 | # 666 | # If +dest+ is the path to a directory, 667 | # creates a symbolic link at dest/src pointing to +src+: 668 | # 669 | # FileUtils.touch('src2.txt') 670 | # FileUtils.mkdir('destdir2') 671 | # FileUtils.ln_s('src2.txt', 'destdir2') 672 | # File.symlink?('destdir2/src2.txt') # => true 673 | # 674 | # If +src+ is an array of paths to existing files and +dest+ is a directory, 675 | # for each child +child+ in +src+ creates a symbolic link dest/child 676 | # pointing to +child+: 677 | # 678 | # FileUtils.mkdir('srcdir3') 679 | # FileUtils.touch('srcdir3/src0.txt') 680 | # FileUtils.touch('srcdir3/src1.txt') 681 | # FileUtils.mkdir('destdir3') 682 | # FileUtils.ln_s(['srcdir3/src0.txt', 'srcdir3/src1.txt'], 'destdir3') 683 | # File.symlink?('destdir3/src0.txt') # => true 684 | # File.symlink?('destdir3/src1.txt') # => true 685 | # 686 | # Keyword arguments: 687 | # 688 | # - force: true - overwrites +dest+ if it exists. 689 | # - relative: false - create links relative to +dest+. 690 | # - noop: true - does not create links. 691 | # - verbose: true - prints an equivalent command: 692 | # 693 | # FileUtils.ln_s('src0.txt', 'dest0.txt', noop: true, verbose: true) 694 | # FileUtils.ln_s('src1.txt', 'destdir1', noop: true, verbose: true) 695 | # FileUtils.ln_s('src2.txt', 'dest2.txt', force: true, noop: true, verbose: true) 696 | # FileUtils.ln_s(['srcdir3/src0.txt', 'srcdir3/src1.txt'], 'destdir3', noop: true, verbose: true) 697 | # 698 | # Output: 699 | # 700 | # ln -s src0.txt dest0.txt 701 | # ln -s src1.txt destdir1 702 | # ln -sf src2.txt dest2.txt 703 | # ln -s srcdir3/src0.txt srcdir3/src1.txt destdir3 704 | # 705 | # Related: FileUtils.ln_sf. 706 | # 707 | def ln_s(src, dest, force: nil, relative: false, target_directory: true, noop: nil, verbose: nil) 708 | if relative 709 | return ln_sr(src, dest, force: force, target_directory: target_directory, noop: noop, verbose: verbose) 710 | end 711 | fu_output_message "ln -s#{force ? 'f' : ''}#{ 712 | target_directory ? '' : 'T'} #{[src,dest].flatten.join ' '}" if verbose 713 | return if noop 714 | fu_each_src_dest0(src, dest, target_directory) do |s,d| 715 | remove_file d, true if force 716 | File.symlink s, d 717 | end 718 | end 719 | module_function :ln_s 720 | 721 | alias symlink ln_s 722 | module_function :symlink 723 | 724 | # Like FileUtils.ln_s, but always with keyword argument force: true given. 725 | # 726 | def ln_sf(src, dest, noop: nil, verbose: nil) 727 | ln_s src, dest, force: true, noop: noop, verbose: verbose 728 | end 729 | module_function :ln_sf 730 | 731 | # Like FileUtils.ln_s, but create links relative to +dest+. 732 | # 733 | def ln_sr(src, dest, target_directory: true, force: nil, noop: nil, verbose: nil) 734 | cmd = "ln -s#{force ? 'f' : ''}#{target_directory ? '' : 'T'}" if verbose 735 | fu_each_src_dest0(src, dest, target_directory) do |s,d| 736 | if target_directory 737 | parent = File.dirname(d) 738 | destdirs = fu_split_path(parent) 739 | real_ddirs = fu_split_path(File.realpath(parent)) 740 | else 741 | destdirs ||= fu_split_path(dest) 742 | real_ddirs ||= fu_split_path(File.realdirpath(dest)) 743 | end 744 | srcdirs = fu_split_path(s) 745 | i = fu_common_components(srcdirs, destdirs) 746 | n = destdirs.size - i 747 | n -= 1 unless target_directory 748 | link1 = fu_clean_components(*Array.new([n, 0].max, '..'), *srcdirs[i..-1]) 749 | begin 750 | real_sdirs = fu_split_path(File.realdirpath(s)) rescue nil 751 | rescue 752 | else 753 | i = fu_common_components(real_sdirs, real_ddirs) 754 | n = real_ddirs.size - i 755 | n -= 1 unless target_directory 756 | link2 = fu_clean_components(*Array.new([n, 0].max, '..'), *real_sdirs[i..-1]) 757 | link1 = link2 if link1.size > link2.size 758 | end 759 | s = File.join(link1) 760 | fu_output_message [cmd, s, d].flatten.join(' ') if verbose 761 | next if noop 762 | remove_file d, true if force 763 | File.symlink s, d 764 | end 765 | end 766 | module_function :ln_sr 767 | 768 | # Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link]; returns +nil+. 769 | # 770 | # Arguments +src+ and +dest+ 771 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 772 | # 773 | # If +src+ is the path to a file and +dest+ does not exist, 774 | # creates a hard link at +dest+ pointing to +src+: 775 | # 776 | # FileUtils.touch('src0.txt') 777 | # File.exist?('dest0.txt') # => false 778 | # FileUtils.link_entry('src0.txt', 'dest0.txt') 779 | # File.file?('dest0.txt') # => true 780 | # 781 | # If +src+ is the path to a directory and +dest+ does not exist, 782 | # recursively creates hard links at +dest+ pointing to paths in +src+: 783 | # 784 | # FileUtils.mkdir_p(['src1/dir0', 'src1/dir1']) 785 | # src_file_paths = [ 786 | # 'src1/dir0/t0.txt', 787 | # 'src1/dir0/t1.txt', 788 | # 'src1/dir1/t2.txt', 789 | # 'src1/dir1/t3.txt', 790 | # ] 791 | # FileUtils.touch(src_file_paths) 792 | # File.directory?('dest1') # => true 793 | # FileUtils.link_entry('src1', 'dest1') 794 | # File.file?('dest1/dir0/t0.txt') # => true 795 | # File.file?('dest1/dir0/t1.txt') # => true 796 | # File.file?('dest1/dir1/t2.txt') # => true 797 | # File.file?('dest1/dir1/t3.txt') # => true 798 | # 799 | # Optional arguments: 800 | # 801 | # - +dereference_root+ - dereferences +src+ if it is a symbolic link (+false+ by default). 802 | # - +remove_destination+ - removes +dest+ before creating links (+false+ by default). 803 | # 804 | # Raises an exception if +dest+ is the path to an existing file or directory 805 | # and optional argument +remove_destination+ is not given. 806 | # 807 | # Related: FileUtils.ln (has different options). 808 | # 809 | def link_entry(src, dest, dereference_root = false, remove_destination = false) 810 | Entry_.new(src, nil, dereference_root).traverse do |ent| 811 | destent = Entry_.new(dest, ent.rel, false) 812 | File.unlink destent.path if remove_destination && File.file?(destent.path) 813 | ent.link destent.path 814 | end 815 | end 816 | module_function :link_entry 817 | 818 | # Copies files. 819 | # 820 | # Arguments +src+ (a single path or an array of paths) 821 | # and +dest+ (a single path) 822 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 823 | # 824 | # If +src+ is the path to a file and +dest+ is not the path to a directory, 825 | # copies +src+ to +dest+: 826 | # 827 | # FileUtils.touch('src0.txt') 828 | # File.exist?('dest0.txt') # => false 829 | # FileUtils.cp('src0.txt', 'dest0.txt') 830 | # File.file?('dest0.txt') # => true 831 | # 832 | # If +src+ is the path to a file and +dest+ is the path to a directory, 833 | # copies +src+ to dest/src: 834 | # 835 | # FileUtils.touch('src1.txt') 836 | # FileUtils.mkdir('dest1') 837 | # FileUtils.cp('src1.txt', 'dest1') 838 | # File.file?('dest1/src1.txt') # => true 839 | # 840 | # If +src+ is an array of paths to files and +dest+ is the path to a directory, 841 | # copies from each +src+ to +dest+: 842 | # 843 | # src_file_paths = ['src2.txt', 'src2.dat'] 844 | # FileUtils.touch(src_file_paths) 845 | # FileUtils.mkdir('dest2') 846 | # FileUtils.cp(src_file_paths, 'dest2') 847 | # File.file?('dest2/src2.txt') # => true 848 | # File.file?('dest2/src2.dat') # => true 849 | # 850 | # Keyword arguments: 851 | # 852 | # - preserve: true - preserves file times. 853 | # - noop: true - does not copy files. 854 | # - verbose: true - prints an equivalent command: 855 | # 856 | # FileUtils.cp('src0.txt', 'dest0.txt', noop: true, verbose: true) 857 | # FileUtils.cp('src1.txt', 'dest1', noop: true, verbose: true) 858 | # FileUtils.cp(src_file_paths, 'dest2', noop: true, verbose: true) 859 | # 860 | # Output: 861 | # 862 | # cp src0.txt dest0.txt 863 | # cp src1.txt dest1 864 | # cp src2.txt src2.dat dest2 865 | # 866 | # Raises an exception if +src+ is a directory. 867 | # 868 | # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. 869 | # 870 | def cp(src, dest, preserve: nil, noop: nil, verbose: nil) 871 | fu_output_message "cp#{preserve ? ' -p' : ''} #{[src,dest].flatten.join ' '}" if verbose 872 | return if noop 873 | fu_each_src_dest(src, dest) do |s, d| 874 | copy_file s, d, preserve 875 | end 876 | end 877 | module_function :cp 878 | 879 | alias copy cp 880 | module_function :copy 881 | 882 | # Recursively copies files. 883 | # 884 | # Arguments +src+ (a single path or an array of paths) 885 | # and +dest+ (a single path) 886 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 887 | # 888 | # The mode, owner, and group are retained in the copy; 889 | # to change those, use FileUtils.install instead. 890 | # 891 | # If +src+ is the path to a file and +dest+ is not the path to a directory, 892 | # copies +src+ to +dest+: 893 | # 894 | # FileUtils.touch('src0.txt') 895 | # File.exist?('dest0.txt') # => false 896 | # FileUtils.cp_r('src0.txt', 'dest0.txt') 897 | # File.file?('dest0.txt') # => true 898 | # 899 | # If +src+ is the path to a file and +dest+ is the path to a directory, 900 | # copies +src+ to dest/src: 901 | # 902 | # FileUtils.touch('src1.txt') 903 | # FileUtils.mkdir('dest1') 904 | # FileUtils.cp_r('src1.txt', 'dest1') 905 | # File.file?('dest1/src1.txt') # => true 906 | # 907 | # If +src+ is the path to a directory and +dest+ does not exist, 908 | # recursively copies +src+ to +dest+: 909 | # 910 | # tree('src2') 911 | # # => src2 912 | # # |-- dir0 913 | # # | |-- src0.txt 914 | # # | `-- src1.txt 915 | # # `-- dir1 916 | # # |-- src2.txt 917 | # # `-- src3.txt 918 | # FileUtils.exist?('dest2') # => false 919 | # FileUtils.cp_r('src2', 'dest2') 920 | # tree('dest2') 921 | # # => dest2 922 | # # |-- dir0 923 | # # | |-- src0.txt 924 | # # | `-- src1.txt 925 | # # `-- dir1 926 | # # |-- src2.txt 927 | # # `-- src3.txt 928 | # 929 | # If +src+ and +dest+ are paths to directories, 930 | # recursively copies +src+ to dest/src: 931 | # 932 | # tree('src3') 933 | # # => src3 934 | # # |-- dir0 935 | # # | |-- src0.txt 936 | # # | `-- src1.txt 937 | # # `-- dir1 938 | # # |-- src2.txt 939 | # # `-- src3.txt 940 | # FileUtils.mkdir('dest3') 941 | # FileUtils.cp_r('src3', 'dest3') 942 | # tree('dest3') 943 | # # => dest3 944 | # # `-- src3 945 | # # |-- dir0 946 | # # | |-- src0.txt 947 | # # | `-- src1.txt 948 | # # `-- dir1 949 | # # |-- src2.txt 950 | # # `-- src3.txt 951 | # 952 | # If +src+ is an array of paths and +dest+ is a directory, 953 | # recursively copies from each path in +src+ to +dest+; 954 | # the paths in +src+ may point to files and/or directories. 955 | # 956 | # Keyword arguments: 957 | # 958 | # - dereference_root: false - if +src+ is a symbolic link, 959 | # does not dereference it. 960 | # - noop: true - does not copy files. 961 | # - preserve: true - preserves file times. 962 | # - remove_destination: true - removes +dest+ before copying files. 963 | # - verbose: true - prints an equivalent command: 964 | # 965 | # FileUtils.cp_r('src0.txt', 'dest0.txt', noop: true, verbose: true) 966 | # FileUtils.cp_r('src1.txt', 'dest1', noop: true, verbose: true) 967 | # FileUtils.cp_r('src2', 'dest2', noop: true, verbose: true) 968 | # FileUtils.cp_r('src3', 'dest3', noop: true, verbose: true) 969 | # 970 | # Output: 971 | # 972 | # cp -r src0.txt dest0.txt 973 | # cp -r src1.txt dest1 974 | # cp -r src2 dest2 975 | # cp -r src3 dest3 976 | # 977 | # Raises an exception of +src+ is the path to a directory 978 | # and +dest+ is the path to a file. 979 | # 980 | # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. 981 | # 982 | def cp_r(src, dest, preserve: nil, noop: nil, verbose: nil, 983 | dereference_root: true, remove_destination: nil) 984 | fu_output_message "cp -r#{preserve ? 'p' : ''}#{remove_destination ? ' --remove-destination' : ''} #{[src,dest].flatten.join ' '}" if verbose 985 | return if noop 986 | fu_each_src_dest(src, dest) do |s, d| 987 | copy_entry s, d, preserve, dereference_root, remove_destination 988 | end 989 | end 990 | module_function :cp_r 991 | 992 | # Recursively copies files from +src+ to +dest+. 993 | # 994 | # Arguments +src+ and +dest+ 995 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 996 | # 997 | # If +src+ is the path to a file, copies +src+ to +dest+: 998 | # 999 | # FileUtils.touch('src0.txt') 1000 | # File.exist?('dest0.txt') # => false 1001 | # FileUtils.copy_entry('src0.txt', 'dest0.txt') 1002 | # File.file?('dest0.txt') # => true 1003 | # 1004 | # If +src+ is a directory, recursively copies +src+ to +dest+: 1005 | # 1006 | # tree('src1') 1007 | # # => src1 1008 | # # |-- dir0 1009 | # # | |-- src0.txt 1010 | # # | `-- src1.txt 1011 | # # `-- dir1 1012 | # # |-- src2.txt 1013 | # # `-- src3.txt 1014 | # FileUtils.copy_entry('src1', 'dest1') 1015 | # tree('dest1') 1016 | # # => dest1 1017 | # # |-- dir0 1018 | # # | |-- src0.txt 1019 | # # | `-- src1.txt 1020 | # # `-- dir1 1021 | # # |-- src2.txt 1022 | # # `-- src3.txt 1023 | # 1024 | # The recursive copying preserves file types for regular files, 1025 | # directories, and symbolic links; 1026 | # other file types (FIFO streams, device files, etc.) are not supported. 1027 | # 1028 | # Optional arguments: 1029 | # 1030 | # - +dereference_root+ - if +src+ is a symbolic link, 1031 | # follows the link (+false+ by default). 1032 | # - +preserve+ - preserves file times (+false+ by default). 1033 | # - +remove_destination+ - removes +dest+ before copying files (+false+ by default). 1034 | # 1035 | # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. 1036 | # 1037 | def copy_entry(src, dest, preserve = false, dereference_root = false, remove_destination = false) 1038 | if dereference_root 1039 | src = File.realpath(src) 1040 | end 1041 | 1042 | Entry_.new(src, nil, false).wrap_traverse(proc do |ent| 1043 | destent = Entry_.new(dest, ent.rel, false) 1044 | File.unlink destent.path if remove_destination && (File.file?(destent.path) || File.symlink?(destent.path)) 1045 | ent.copy destent.path 1046 | end, proc do |ent| 1047 | destent = Entry_.new(dest, ent.rel, false) 1048 | ent.copy_metadata destent.path if preserve 1049 | end) 1050 | end 1051 | module_function :copy_entry 1052 | 1053 | # Copies file from +src+ to +dest+, which should not be directories. 1054 | # 1055 | # Arguments +src+ and +dest+ 1056 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 1057 | # 1058 | # Examples: 1059 | # 1060 | # FileUtils.touch('src0.txt') 1061 | # FileUtils.copy_file('src0.txt', 'dest0.txt') 1062 | # File.file?('dest0.txt') # => true 1063 | # 1064 | # Optional arguments: 1065 | # 1066 | # - +dereference+ - if +src+ is a symbolic link, 1067 | # follows the link (+true+ by default). 1068 | # - +preserve+ - preserves file times (+false+ by default). 1069 | # - +remove_destination+ - removes +dest+ before copying files (+false+ by default). 1070 | # 1071 | # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. 1072 | # 1073 | def copy_file(src, dest, preserve = false, dereference = true) 1074 | ent = Entry_.new(src, nil, dereference) 1075 | ent.copy_file dest 1076 | ent.copy_metadata dest if preserve 1077 | end 1078 | module_function :copy_file 1079 | 1080 | # Copies \IO stream +src+ to \IO stream +dest+ via 1081 | # {IO.copy_stream}[https://docs.ruby-lang.org/en/master/IO.html#method-c-copy_stream]. 1082 | # 1083 | # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. 1084 | # 1085 | def copy_stream(src, dest) 1086 | IO.copy_stream(src, dest) 1087 | end 1088 | module_function :copy_stream 1089 | 1090 | # Moves entries. 1091 | # 1092 | # Arguments +src+ (a single path or an array of paths) 1093 | # and +dest+ (a single path) 1094 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 1095 | # 1096 | # If +src+ and +dest+ are on different file systems, 1097 | # first copies, then removes +src+. 1098 | # 1099 | # May cause a local vulnerability if not called with keyword argument 1100 | # secure: true; 1101 | # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability]. 1102 | # 1103 | # If +src+ is the path to a single file or directory and +dest+ does not exist, 1104 | # moves +src+ to +dest+: 1105 | # 1106 | # tree('src0') 1107 | # # => src0 1108 | # # |-- src0.txt 1109 | # # `-- src1.txt 1110 | # File.exist?('dest0') # => false 1111 | # FileUtils.mv('src0', 'dest0') 1112 | # File.exist?('src0') # => false 1113 | # tree('dest0') 1114 | # # => dest0 1115 | # # |-- src0.txt 1116 | # # `-- src1.txt 1117 | # 1118 | # If +src+ is an array of paths to files and directories 1119 | # and +dest+ is the path to a directory, 1120 | # copies from each path in the array to +dest+: 1121 | # 1122 | # File.file?('src1.txt') # => true 1123 | # tree('src1') 1124 | # # => src1 1125 | # # |-- src.dat 1126 | # # `-- src.txt 1127 | # Dir.empty?('dest1') # => true 1128 | # FileUtils.mv(['src1.txt', 'src1'], 'dest1') 1129 | # tree('dest1') 1130 | # # => dest1 1131 | # # |-- src1 1132 | # # | |-- src.dat 1133 | # # | `-- src.txt 1134 | # # `-- src1.txt 1135 | # 1136 | # Keyword arguments: 1137 | # 1138 | # - force: true - if the move includes removing +src+ 1139 | # (that is, if +src+ and +dest+ are on different file systems), 1140 | # ignores raised exceptions of StandardError and its descendants. 1141 | # - noop: true - does not move files. 1142 | # - secure: true - removes +src+ securely; 1143 | # see details at FileUtils.remove_entry_secure. 1144 | # - verbose: true - prints an equivalent command: 1145 | # 1146 | # FileUtils.mv('src0', 'dest0', noop: true, verbose: true) 1147 | # FileUtils.mv(['src1.txt', 'src1'], 'dest1', noop: true, verbose: true) 1148 | # 1149 | # Output: 1150 | # 1151 | # mv src0 dest0 1152 | # mv src1.txt src1 dest1 1153 | # 1154 | def mv(src, dest, force: nil, noop: nil, verbose: nil, secure: nil) 1155 | fu_output_message "mv#{force ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if verbose 1156 | return if noop 1157 | fu_each_src_dest(src, dest) do |s, d| 1158 | destent = Entry_.new(d, nil, true) 1159 | begin 1160 | if destent.exist? 1161 | if destent.directory? 1162 | raise Errno::EEXIST, d 1163 | end 1164 | end 1165 | begin 1166 | File.rename s, d 1167 | rescue Errno::EXDEV, 1168 | Errno::EPERM # move from unencrypted to encrypted dir (ext4) 1169 | copy_entry s, d, true 1170 | if secure 1171 | remove_entry_secure s, force 1172 | else 1173 | remove_entry s, force 1174 | end 1175 | end 1176 | rescue SystemCallError 1177 | raise unless force 1178 | end 1179 | end 1180 | end 1181 | module_function :mv 1182 | 1183 | alias move mv 1184 | module_function :move 1185 | 1186 | # Removes entries at the paths in the given +list+ 1187 | # (a single path or an array of paths) 1188 | # returns +list+, if it is an array, [list] otherwise. 1189 | # 1190 | # Argument +list+ or its elements 1191 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 1192 | # 1193 | # With no keyword arguments, removes files at the paths given in +list+: 1194 | # 1195 | # FileUtils.touch(['src0.txt', 'src0.dat']) 1196 | # FileUtils.rm(['src0.dat', 'src0.txt']) # => ["src0.dat", "src0.txt"] 1197 | # 1198 | # Keyword arguments: 1199 | # 1200 | # - force: true - ignores raised exceptions of StandardError 1201 | # and its descendants. 1202 | # - noop: true - does not remove files; returns +nil+. 1203 | # - verbose: true - prints an equivalent command: 1204 | # 1205 | # FileUtils.rm(['src0.dat', 'src0.txt'], noop: true, verbose: true) 1206 | # 1207 | # Output: 1208 | # 1209 | # rm src0.dat src0.txt 1210 | # 1211 | # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. 1212 | # 1213 | def rm(list, force: nil, noop: nil, verbose: nil) 1214 | list = fu_list(list) 1215 | fu_output_message "rm#{force ? ' -f' : ''} #{list.join ' '}" if verbose 1216 | return if noop 1217 | 1218 | list.each do |path| 1219 | remove_file path, force 1220 | end 1221 | end 1222 | module_function :rm 1223 | 1224 | alias remove rm 1225 | module_function :remove 1226 | 1227 | # Equivalent to: 1228 | # 1229 | # FileUtils.rm(list, force: true, **kwargs) 1230 | # 1231 | # Argument +list+ (a single path or an array of paths) 1232 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 1233 | # 1234 | # See FileUtils.rm for keyword arguments. 1235 | # 1236 | # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. 1237 | # 1238 | def rm_f(list, noop: nil, verbose: nil) 1239 | rm list, force: true, noop: noop, verbose: verbose 1240 | end 1241 | module_function :rm_f 1242 | 1243 | alias safe_unlink rm_f 1244 | module_function :safe_unlink 1245 | 1246 | # Removes entries at the paths in the given +list+ 1247 | # (a single path or an array of paths); 1248 | # returns +list+, if it is an array, [list] otherwise. 1249 | # 1250 | # Argument +list+ or its elements 1251 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 1252 | # 1253 | # May cause a local vulnerability if not called with keyword argument 1254 | # secure: true; 1255 | # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability]. 1256 | # 1257 | # For each file path, removes the file at that path: 1258 | # 1259 | # FileUtils.touch(['src0.txt', 'src0.dat']) 1260 | # FileUtils.rm_r(['src0.dat', 'src0.txt']) 1261 | # File.exist?('src0.txt') # => false 1262 | # File.exist?('src0.dat') # => false 1263 | # 1264 | # For each directory path, recursively removes files and directories: 1265 | # 1266 | # tree('src1') 1267 | # # => src1 1268 | # # |-- dir0 1269 | # # | |-- src0.txt 1270 | # # | `-- src1.txt 1271 | # # `-- dir1 1272 | # # |-- src2.txt 1273 | # # `-- src3.txt 1274 | # FileUtils.rm_r('src1') 1275 | # File.exist?('src1') # => false 1276 | # 1277 | # Keyword arguments: 1278 | # 1279 | # - force: true - ignores raised exceptions of StandardError 1280 | # and its descendants. 1281 | # - noop: true - does not remove entries; returns +nil+. 1282 | # - secure: true - removes +src+ securely; 1283 | # see details at FileUtils.remove_entry_secure. 1284 | # - verbose: true - prints an equivalent command: 1285 | # 1286 | # FileUtils.rm_r(['src0.dat', 'src0.txt'], noop: true, verbose: true) 1287 | # FileUtils.rm_r('src1', noop: true, verbose: true) 1288 | # 1289 | # Output: 1290 | # 1291 | # rm -r src0.dat src0.txt 1292 | # rm -r src1 1293 | # 1294 | # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. 1295 | # 1296 | def rm_r(list, force: nil, noop: nil, verbose: nil, secure: nil) 1297 | list = fu_list(list) 1298 | fu_output_message "rm -r#{force ? 'f' : ''} #{list.join ' '}" if verbose 1299 | return if noop 1300 | list.each do |path| 1301 | if secure 1302 | remove_entry_secure path, force 1303 | else 1304 | remove_entry path, force 1305 | end 1306 | end 1307 | end 1308 | module_function :rm_r 1309 | 1310 | # Equivalent to: 1311 | # 1312 | # FileUtils.rm_r(list, force: true, **kwargs) 1313 | # 1314 | # Argument +list+ or its elements 1315 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 1316 | # 1317 | # May cause a local vulnerability if not called with keyword argument 1318 | # secure: true; 1319 | # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability]. 1320 | # 1321 | # See FileUtils.rm_r for keyword arguments. 1322 | # 1323 | # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. 1324 | # 1325 | def rm_rf(list, noop: nil, verbose: nil, secure: nil) 1326 | rm_r list, force: true, noop: noop, verbose: verbose, secure: secure 1327 | end 1328 | module_function :rm_rf 1329 | 1330 | alias rmtree rm_rf 1331 | module_function :rmtree 1332 | 1333 | # Securely removes the entry given by +path+, 1334 | # which should be the entry for a regular file, a symbolic link, 1335 | # or a directory. 1336 | # 1337 | # Argument +path+ 1338 | # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]. 1339 | # 1340 | # Avoids a local vulnerability that can exist in certain circumstances; 1341 | # see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability]. 1342 | # 1343 | # Optional argument +force+ specifies whether to ignore 1344 | # raised exceptions of StandardError and its descendants. 1345 | # 1346 | # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. 1347 | # 1348 | def remove_entry_secure(path, force = false) 1349 | unless fu_have_symlink? 1350 | remove_entry path, force 1351 | return 1352 | end 1353 | fullpath = File.expand_path(path) 1354 | st = File.lstat(fullpath) 1355 | unless st.directory? 1356 | File.unlink fullpath 1357 | return 1358 | end 1359 | # is a directory. 1360 | parent_st = File.stat(File.dirname(fullpath)) 1361 | unless parent_st.world_writable? 1362 | remove_entry path, force 1363 | return 1364 | end 1365 | unless parent_st.sticky? 1366 | raise ArgumentError, "parent directory is world writable, FileUtils#remove_entry_secure does not work; abort: #{path.inspect} (parent directory mode #{'%o' % parent_st.mode})" 1367 | end 1368 | 1369 | # freeze tree root 1370 | euid = Process.euid 1371 | dot_file = fullpath + "/." 1372 | begin 1373 | File.open(dot_file) {|f| 1374 | unless fu_stat_identical_entry?(st, f.stat) 1375 | # symlink (TOC-to-TOU attack?) 1376 | File.unlink fullpath 1377 | return 1378 | end 1379 | f.chown euid, -1 1380 | f.chmod 0700 1381 | } 1382 | rescue Errno::EISDIR # JRuby in non-native mode can't open files as dirs 1383 | File.lstat(dot_file).tap {|fstat| 1384 | unless fu_stat_identical_entry?(st, fstat) 1385 | # symlink (TOC-to-TOU attack?) 1386 | File.unlink fullpath 1387 | return 1388 | end 1389 | File.chown euid, -1, dot_file 1390 | File.chmod 0700, dot_file 1391 | } 1392 | end 1393 | 1394 | unless fu_stat_identical_entry?(st, File.lstat(fullpath)) 1395 | # TOC-to-TOU attack? 1396 | File.unlink fullpath 1397 | return 1398 | end 1399 | 1400 | # ---- tree root is frozen ---- 1401 | root = Entry_.new(path) 1402 | root.preorder_traverse do |ent| 1403 | if ent.directory? 1404 | ent.chown euid, -1 1405 | ent.chmod 0700 1406 | end 1407 | end 1408 | root.postorder_traverse do |ent| 1409 | begin 1410 | ent.remove 1411 | rescue 1412 | raise unless force 1413 | end 1414 | end 1415 | rescue 1416 | raise unless force 1417 | end 1418 | module_function :remove_entry_secure 1419 | 1420 | def fu_have_symlink? #:nodoc: 1421 | File.symlink nil, nil 1422 | rescue NotImplementedError 1423 | return false 1424 | rescue TypeError 1425 | return true 1426 | end 1427 | private_module_function :fu_have_symlink? 1428 | 1429 | def fu_stat_identical_entry?(a, b) #:nodoc: 1430 | a.dev == b.dev and a.ino == b.ino 1431 | end 1432 | private_module_function :fu_stat_identical_entry? 1433 | 1434 | # Removes the entry given by +path+, 1435 | # which should be the entry for a regular file, a symbolic link, 1436 | # or a directory. 1437 | # 1438 | # Argument +path+ 1439 | # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]. 1440 | # 1441 | # Optional argument +force+ specifies whether to ignore 1442 | # raised exceptions of StandardError and its descendants. 1443 | # 1444 | # Related: FileUtils.remove_entry_secure. 1445 | # 1446 | def remove_entry(path, force = false) 1447 | Entry_.new(path).postorder_traverse do |ent| 1448 | begin 1449 | ent.remove 1450 | rescue 1451 | raise unless force 1452 | end 1453 | end 1454 | rescue 1455 | raise unless force 1456 | end 1457 | module_function :remove_entry 1458 | 1459 | # Removes the file entry given by +path+, 1460 | # which should be the entry for a regular file or a symbolic link. 1461 | # 1462 | # Argument +path+ 1463 | # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]. 1464 | # 1465 | # Optional argument +force+ specifies whether to ignore 1466 | # raised exceptions of StandardError and its descendants. 1467 | # 1468 | # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. 1469 | # 1470 | def remove_file(path, force = false) 1471 | Entry_.new(path).remove_file 1472 | rescue 1473 | raise unless force 1474 | end 1475 | module_function :remove_file 1476 | 1477 | # Recursively removes the directory entry given by +path+, 1478 | # which should be the entry for a regular file, a symbolic link, 1479 | # or a directory. 1480 | # 1481 | # Argument +path+ 1482 | # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]. 1483 | # 1484 | # Optional argument +force+ specifies whether to ignore 1485 | # raised exceptions of StandardError and its descendants. 1486 | # 1487 | # Related: {methods for deleting}[rdoc-ref:FileUtils@Deleting]. 1488 | # 1489 | def remove_dir(path, force = false) 1490 | raise Errno::ENOTDIR, path unless force or File.directory?(path) 1491 | remove_entry path, force 1492 | end 1493 | module_function :remove_dir 1494 | 1495 | # Returns +true+ if the contents of files +a+ and +b+ are identical, 1496 | # +false+ otherwise. 1497 | # 1498 | # Arguments +a+ and +b+ 1499 | # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]. 1500 | # 1501 | # FileUtils.identical? and FileUtils.cmp are aliases for FileUtils.compare_file. 1502 | # 1503 | # Related: FileUtils.compare_stream. 1504 | # 1505 | def compare_file(a, b) 1506 | return false unless File.size(a) == File.size(b) 1507 | File.open(a, 'rb') {|fa| 1508 | File.open(b, 'rb') {|fb| 1509 | return compare_stream(fa, fb) 1510 | } 1511 | } 1512 | end 1513 | module_function :compare_file 1514 | 1515 | alias identical? compare_file 1516 | alias cmp compare_file 1517 | module_function :identical? 1518 | module_function :cmp 1519 | 1520 | # Returns +true+ if the contents of streams +a+ and +b+ are identical, 1521 | # +false+ otherwise. 1522 | # 1523 | # Arguments +a+ and +b+ 1524 | # should be {interpretable as a path}[rdoc-ref:FileUtils@Path+Arguments]. 1525 | # 1526 | # Related: FileUtils.compare_file. 1527 | # 1528 | def compare_stream(a, b) 1529 | bsize = fu_stream_blksize(a, b) 1530 | 1531 | sa = String.new(capacity: bsize) 1532 | sb = String.new(capacity: bsize) 1533 | 1534 | begin 1535 | a.read(bsize, sa) 1536 | b.read(bsize, sb) 1537 | return true if sa.empty? && sb.empty? 1538 | end while sa == sb 1539 | false 1540 | end 1541 | module_function :compare_stream 1542 | 1543 | # Copies a file entry. 1544 | # See {install(1)}[https://man7.org/linux/man-pages/man1/install.1.html]. 1545 | # 1546 | # Arguments +src+ (a single path or an array of paths) 1547 | # and +dest+ (a single path) 1548 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]; 1549 | # 1550 | # If the entry at +dest+ does not exist, copies from +src+ to +dest+: 1551 | # 1552 | # File.read('src0.txt') # => "aaa\n" 1553 | # File.exist?('dest0.txt') # => false 1554 | # FileUtils.install('src0.txt', 'dest0.txt') 1555 | # File.read('dest0.txt') # => "aaa\n" 1556 | # 1557 | # If +dest+ is a file entry, copies from +src+ to +dest+, overwriting: 1558 | # 1559 | # File.read('src1.txt') # => "aaa\n" 1560 | # File.read('dest1.txt') # => "bbb\n" 1561 | # FileUtils.install('src1.txt', 'dest1.txt') 1562 | # File.read('dest1.txt') # => "aaa\n" 1563 | # 1564 | # If +dest+ is a directory entry, copies from +src+ to dest/src, 1565 | # overwriting if necessary: 1566 | # 1567 | # File.read('src2.txt') # => "aaa\n" 1568 | # File.read('dest2/src2.txt') # => "bbb\n" 1569 | # FileUtils.install('src2.txt', 'dest2') 1570 | # File.read('dest2/src2.txt') # => "aaa\n" 1571 | # 1572 | # If +src+ is an array of paths and +dest+ points to a directory, 1573 | # copies each path +path+ in +src+ to dest/path: 1574 | # 1575 | # File.file?('src3.txt') # => true 1576 | # File.file?('src3.dat') # => true 1577 | # FileUtils.mkdir('dest3') 1578 | # FileUtils.install(['src3.txt', 'src3.dat'], 'dest3') 1579 | # File.file?('dest3/src3.txt') # => true 1580 | # File.file?('dest3/src3.dat') # => true 1581 | # 1582 | # Keyword arguments: 1583 | # 1584 | # - group: group - changes the group if not +nil+, 1585 | # using {File.chown}[https://docs.ruby-lang.org/en/master/File.html#method-c-chown]. 1586 | # - mode: permissions - changes the permissions. 1587 | # using {File.chmod}[https://docs.ruby-lang.org/en/master/File.html#method-c-chmod]. 1588 | # - noop: true - does not copy entries; returns +nil+. 1589 | # - owner: owner - changes the owner if not +nil+, 1590 | # using {File.chown}[https://docs.ruby-lang.org/en/master/File.html#method-c-chown]. 1591 | # - preserve: true - preserve timestamps 1592 | # using {File.utime}[https://docs.ruby-lang.org/en/master/File.html#method-c-utime]. 1593 | # - verbose: true - prints an equivalent command: 1594 | # 1595 | # FileUtils.install('src0.txt', 'dest0.txt', noop: true, verbose: true) 1596 | # FileUtils.install('src1.txt', 'dest1.txt', noop: true, verbose: true) 1597 | # FileUtils.install('src2.txt', 'dest2', noop: true, verbose: true) 1598 | # 1599 | # Output: 1600 | # 1601 | # install -c src0.txt dest0.txt 1602 | # install -c src1.txt dest1.txt 1603 | # install -c src2.txt dest2 1604 | # 1605 | # Related: {methods for copying}[rdoc-ref:FileUtils@Copying]. 1606 | # 1607 | def install(src, dest, mode: nil, owner: nil, group: nil, preserve: nil, 1608 | noop: nil, verbose: nil) 1609 | if verbose 1610 | msg = +"install -c" 1611 | msg << ' -p' if preserve 1612 | msg << ' -m ' << mode_to_s(mode) if mode 1613 | msg << " -o #{owner}" if owner 1614 | msg << " -g #{group}" if group 1615 | msg << ' ' << [src,dest].flatten.join(' ') 1616 | fu_output_message msg 1617 | end 1618 | return if noop 1619 | uid = fu_get_uid(owner) 1620 | gid = fu_get_gid(group) 1621 | fu_each_src_dest(src, dest) do |s, d| 1622 | st = File.stat(s) 1623 | unless File.exist?(d) and compare_file(s, d) 1624 | remove_file d, true 1625 | if d.end_with?('/') 1626 | mkdir_p d 1627 | copy_file s, d + File.basename(s) 1628 | else 1629 | mkdir_p File.expand_path('..', d) 1630 | copy_file s, d 1631 | end 1632 | File.utime st.atime, st.mtime, d if preserve 1633 | File.chmod fu_mode(mode, st), d if mode 1634 | File.chown uid, gid, d if uid or gid 1635 | end 1636 | end 1637 | end 1638 | module_function :install 1639 | 1640 | def user_mask(target) #:nodoc: 1641 | target.each_char.inject(0) do |mask, chr| 1642 | case chr 1643 | when "u" 1644 | mask | 04700 1645 | when "g" 1646 | mask | 02070 1647 | when "o" 1648 | mask | 01007 1649 | when "a" 1650 | mask | 07777 1651 | else 1652 | raise ArgumentError, "invalid 'who' symbol in file mode: #{chr}" 1653 | end 1654 | end 1655 | end 1656 | private_module_function :user_mask 1657 | 1658 | def apply_mask(mode, user_mask, op, mode_mask) #:nodoc: 1659 | case op 1660 | when '=' 1661 | (mode & ~user_mask) | (user_mask & mode_mask) 1662 | when '+' 1663 | mode | (user_mask & mode_mask) 1664 | when '-' 1665 | mode & ~(user_mask & mode_mask) 1666 | end 1667 | end 1668 | private_module_function :apply_mask 1669 | 1670 | def symbolic_modes_to_i(mode_sym, path) #:nodoc: 1671 | path = File.stat(path) unless File::Stat === path 1672 | mode = path.mode 1673 | mode_sym.split(/,/).inject(mode & 07777) do |current_mode, clause| 1674 | target, *actions = clause.split(/([=+-])/) 1675 | raise ArgumentError, "invalid file mode: #{mode_sym}" if actions.empty? 1676 | target = 'a' if target.empty? 1677 | user_mask = user_mask(target) 1678 | actions.each_slice(2) do |op, perm| 1679 | need_apply = op == '=' 1680 | mode_mask = (perm || '').each_char.inject(0) do |mask, chr| 1681 | case chr 1682 | when "r" 1683 | mask | 0444 1684 | when "w" 1685 | mask | 0222 1686 | when "x" 1687 | mask | 0111 1688 | when "X" 1689 | if path.directory? 1690 | mask | 0111 1691 | else 1692 | mask 1693 | end 1694 | when "s" 1695 | mask | 06000 1696 | when "t" 1697 | mask | 01000 1698 | when "u", "g", "o" 1699 | if mask.nonzero? 1700 | current_mode = apply_mask(current_mode, user_mask, op, mask) 1701 | end 1702 | need_apply = false 1703 | copy_mask = user_mask(chr) 1704 | (current_mode & copy_mask) / (copy_mask & 0111) * (user_mask & 0111) 1705 | else 1706 | raise ArgumentError, "invalid 'perm' symbol in file mode: #{chr}" 1707 | end 1708 | end 1709 | 1710 | if mode_mask.nonzero? || need_apply 1711 | current_mode = apply_mask(current_mode, user_mask, op, mode_mask) 1712 | end 1713 | end 1714 | current_mode 1715 | end 1716 | end 1717 | private_module_function :symbolic_modes_to_i 1718 | 1719 | def fu_mode(mode, path) #:nodoc: 1720 | mode.is_a?(String) ? symbolic_modes_to_i(mode, path) : mode 1721 | end 1722 | private_module_function :fu_mode 1723 | 1724 | def mode_to_s(mode) #:nodoc: 1725 | mode.is_a?(String) ? mode : "%o" % mode 1726 | end 1727 | private_module_function :mode_to_s 1728 | 1729 | # Changes permissions on the entries at the paths given in +list+ 1730 | # (a single path or an array of paths) 1731 | # to the permissions given by +mode+; 1732 | # returns +list+ if it is an array, [list] otherwise: 1733 | # 1734 | # - Modifies each entry that is a regular file using 1735 | # {File.chmod}[https://docs.ruby-lang.org/en/master/File.html#method-c-chmod]. 1736 | # - Modifies each entry that is a symbolic link using 1737 | # {File.lchmod}[https://docs.ruby-lang.org/en/master/File.html#method-c-lchmod]. 1738 | # 1739 | # Argument +list+ or its elements 1740 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 1741 | # 1742 | # Argument +mode+ may be either an integer or a string: 1743 | # 1744 | # - \Integer +mode+: represents the permission bits to be set: 1745 | # 1746 | # FileUtils.chmod(0755, 'src0.txt') 1747 | # FileUtils.chmod(0644, ['src0.txt', 'src0.dat']) 1748 | # 1749 | # - \String +mode+: represents the permissions to be set: 1750 | # 1751 | # The string is of the form [targets][[operator][perms[,perms]], where: 1752 | # 1753 | # - +targets+ may be any combination of these letters: 1754 | # 1755 | # - 'u': permissions apply to the file's owner. 1756 | # - 'g': permissions apply to users in the file's group. 1757 | # - 'o': permissions apply to other users not in the file's group. 1758 | # - 'a' (the default): permissions apply to all users. 1759 | # 1760 | # - +operator+ may be one of these letters: 1761 | # 1762 | # - '+': adds permissions. 1763 | # - '-': removes permissions. 1764 | # - '=': sets (replaces) permissions. 1765 | # 1766 | # - +perms+ (may be repeated, with separating commas) 1767 | # may be any combination of these letters: 1768 | # 1769 | # - 'r': Read. 1770 | # - 'w': Write. 1771 | # - 'x': Execute (search, for a directory). 1772 | # - 'X': Search (for a directories only; 1773 | # must be used with '+') 1774 | # - 's': Uid or gid. 1775 | # - 't': Sticky bit. 1776 | # 1777 | # Examples: 1778 | # 1779 | # FileUtils.chmod('u=wrx,go=rx', 'src1.txt') 1780 | # FileUtils.chmod('u=wrx,go=rx', '/usr/bin/ruby') 1781 | # 1782 | # Keyword arguments: 1783 | # 1784 | # - noop: true - does not change permissions; returns +nil+. 1785 | # - verbose: true - prints an equivalent command: 1786 | # 1787 | # FileUtils.chmod(0755, 'src0.txt', noop: true, verbose: true) 1788 | # FileUtils.chmod(0644, ['src0.txt', 'src0.dat'], noop: true, verbose: true) 1789 | # FileUtils.chmod('u=wrx,go=rx', 'src1.txt', noop: true, verbose: true) 1790 | # FileUtils.chmod('u=wrx,go=rx', '/usr/bin/ruby', noop: true, verbose: true) 1791 | # 1792 | # Output: 1793 | # 1794 | # chmod 755 src0.txt 1795 | # chmod 644 src0.txt src0.dat 1796 | # chmod u=wrx,go=rx src1.txt 1797 | # chmod u=wrx,go=rx /usr/bin/ruby 1798 | # 1799 | # Related: FileUtils.chmod_R. 1800 | # 1801 | def chmod(mode, list, noop: nil, verbose: nil) 1802 | list = fu_list(list) 1803 | fu_output_message sprintf('chmod %s %s', mode_to_s(mode), list.join(' ')) if verbose 1804 | return if noop 1805 | list.each do |path| 1806 | Entry_.new(path).chmod(fu_mode(mode, path)) 1807 | end 1808 | end 1809 | module_function :chmod 1810 | 1811 | # Like FileUtils.chmod, but changes permissions recursively. 1812 | # 1813 | def chmod_R(mode, list, noop: nil, verbose: nil, force: nil) 1814 | list = fu_list(list) 1815 | fu_output_message sprintf('chmod -R%s %s %s', 1816 | (force ? 'f' : ''), 1817 | mode_to_s(mode), list.join(' ')) if verbose 1818 | return if noop 1819 | list.each do |root| 1820 | Entry_.new(root).traverse do |ent| 1821 | begin 1822 | ent.chmod(fu_mode(mode, ent.path)) 1823 | rescue 1824 | raise unless force 1825 | end 1826 | end 1827 | end 1828 | end 1829 | module_function :chmod_R 1830 | 1831 | # Changes the owner and group on the entries at the paths given in +list+ 1832 | # (a single path or an array of paths) 1833 | # to the given +user+ and +group+; 1834 | # returns +list+ if it is an array, [list] otherwise: 1835 | # 1836 | # - Modifies each entry that is a regular file using 1837 | # {File.chown}[https://docs.ruby-lang.org/en/master/File.html#method-c-chown]. 1838 | # - Modifies each entry that is a symbolic link using 1839 | # {File.lchown}[https://docs.ruby-lang.org/en/master/File.html#method-c-lchown]. 1840 | # 1841 | # Argument +list+ or its elements 1842 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 1843 | # 1844 | # User and group: 1845 | # 1846 | # - Argument +user+ may be a user name or a user id; 1847 | # if +nil+ or +-1+, the user is not changed. 1848 | # - Argument +group+ may be a group name or a group id; 1849 | # if +nil+ or +-1+, the group is not changed. 1850 | # - The user must be a member of the group. 1851 | # 1852 | # Examples: 1853 | # 1854 | # # One path. 1855 | # # User and group as string names. 1856 | # File.stat('src0.txt').uid # => 1004 1857 | # File.stat('src0.txt').gid # => 1004 1858 | # FileUtils.chown('user2', 'group1', 'src0.txt') 1859 | # File.stat('src0.txt').uid # => 1006 1860 | # File.stat('src0.txt').gid # => 1005 1861 | # 1862 | # # User and group as uid and gid. 1863 | # FileUtils.chown(1004, 1004, 'src0.txt') 1864 | # File.stat('src0.txt').uid # => 1004 1865 | # File.stat('src0.txt').gid # => 1004 1866 | # 1867 | # # Array of paths. 1868 | # FileUtils.chown(1006, 1005, ['src0.txt', 'src0.dat']) 1869 | # 1870 | # # Directory (not recursive). 1871 | # FileUtils.chown('user2', 'group1', '.') 1872 | # 1873 | # Keyword arguments: 1874 | # 1875 | # - noop: true - does not change permissions; returns +nil+. 1876 | # - verbose: true - prints an equivalent command: 1877 | # 1878 | # FileUtils.chown('user2', 'group1', 'src0.txt', noop: true, verbose: true) 1879 | # FileUtils.chown(1004, 1004, 'src0.txt', noop: true, verbose: true) 1880 | # FileUtils.chown(1006, 1005, ['src0.txt', 'src0.dat'], noop: true, verbose: true) 1881 | # FileUtils.chown('user2', 'group1', path, noop: true, verbose: true) 1882 | # FileUtils.chown('user2', 'group1', '.', noop: true, verbose: true) 1883 | # 1884 | # Output: 1885 | # 1886 | # chown user2:group1 src0.txt 1887 | # chown 1004:1004 src0.txt 1888 | # chown 1006:1005 src0.txt src0.dat 1889 | # chown user2:group1 src0.txt 1890 | # chown user2:group1 . 1891 | # 1892 | # Related: FileUtils.chown_R. 1893 | # 1894 | def chown(user, group, list, noop: nil, verbose: nil) 1895 | list = fu_list(list) 1896 | fu_output_message sprintf('chown %s %s', 1897 | (group ? "#{user}:#{group}" : user || ':'), 1898 | list.join(' ')) if verbose 1899 | return if noop 1900 | uid = fu_get_uid(user) 1901 | gid = fu_get_gid(group) 1902 | list.each do |path| 1903 | Entry_.new(path).chown uid, gid 1904 | end 1905 | end 1906 | module_function :chown 1907 | 1908 | # Like FileUtils.chown, but changes owner and group recursively. 1909 | # 1910 | def chown_R(user, group, list, noop: nil, verbose: nil, force: nil) 1911 | list = fu_list(list) 1912 | fu_output_message sprintf('chown -R%s %s %s', 1913 | (force ? 'f' : ''), 1914 | (group ? "#{user}:#{group}" : user || ':'), 1915 | list.join(' ')) if verbose 1916 | return if noop 1917 | uid = fu_get_uid(user) 1918 | gid = fu_get_gid(group) 1919 | list.each do |root| 1920 | Entry_.new(root).traverse do |ent| 1921 | begin 1922 | ent.chown uid, gid 1923 | rescue 1924 | raise unless force 1925 | end 1926 | end 1927 | end 1928 | end 1929 | module_function :chown_R 1930 | 1931 | def fu_get_uid(user) #:nodoc: 1932 | return nil unless user 1933 | case user 1934 | when Integer 1935 | user 1936 | when /\A\d+\z/ 1937 | user.to_i 1938 | else 1939 | require 'etc' 1940 | Etc.getpwnam(user) ? Etc.getpwnam(user).uid : nil 1941 | end 1942 | end 1943 | private_module_function :fu_get_uid 1944 | 1945 | def fu_get_gid(group) #:nodoc: 1946 | return nil unless group 1947 | case group 1948 | when Integer 1949 | group 1950 | when /\A\d+\z/ 1951 | group.to_i 1952 | else 1953 | require 'etc' 1954 | Etc.getgrnam(group) ? Etc.getgrnam(group).gid : nil 1955 | end 1956 | end 1957 | private_module_function :fu_get_gid 1958 | 1959 | # Updates modification times (mtime) and access times (atime) 1960 | # of the entries given by the paths in +list+ 1961 | # (a single path or an array of paths); 1962 | # returns +list+ if it is an array, [list] otherwise. 1963 | # 1964 | # By default, creates an empty file for any path to a non-existent entry; 1965 | # use keyword argument +nocreate+ to raise an exception instead. 1966 | # 1967 | # Argument +list+ or its elements 1968 | # should be {interpretable as paths}[rdoc-ref:FileUtils@Path+Arguments]. 1969 | # 1970 | # Examples: 1971 | # 1972 | # # Single path. 1973 | # f = File.new('src0.txt') # Existing file. 1974 | # f.atime # => 2022-06-10 11:11:21.200277 -0700 1975 | # f.mtime # => 2022-06-10 11:11:21.200277 -0700 1976 | # FileUtils.touch('src0.txt') 1977 | # f = File.new('src0.txt') 1978 | # f.atime # => 2022-06-11 08:28:09.8185343 -0700 1979 | # f.mtime # => 2022-06-11 08:28:09.8185343 -0700 1980 | # 1981 | # # Array of paths. 1982 | # FileUtils.touch(['src0.txt', 'src0.dat']) 1983 | # 1984 | # Keyword arguments: 1985 | # 1986 | # - mtime: time - sets the entry's mtime to the given time, 1987 | # instead of the current time. 1988 | # - nocreate: true - raises an exception if the entry does not exist. 1989 | # - noop: true - does not touch entries; returns +nil+. 1990 | # - verbose: true - prints an equivalent command: 1991 | # 1992 | # FileUtils.touch('src0.txt', noop: true, verbose: true) 1993 | # FileUtils.touch(['src0.txt', 'src0.dat'], noop: true, verbose: true) 1994 | # FileUtils.touch(path, noop: true, verbose: true) 1995 | # 1996 | # Output: 1997 | # 1998 | # touch src0.txt 1999 | # touch src0.txt src0.dat 2000 | # touch src0.txt 2001 | # 2002 | # Related: FileUtils.uptodate?. 2003 | # 2004 | def touch(list, noop: nil, verbose: nil, mtime: nil, nocreate: nil) 2005 | list = fu_list(list) 2006 | t = mtime 2007 | if verbose 2008 | fu_output_message "touch #{nocreate ? '-c ' : ''}#{t ? t.strftime('-t %Y%m%d%H%M.%S ') : ''}#{list.join ' '}" 2009 | end 2010 | return if noop 2011 | list.each do |path| 2012 | created = nocreate 2013 | begin 2014 | File.utime(t, t, path) 2015 | rescue Errno::ENOENT 2016 | raise if created 2017 | File.open(path, 'a') { 2018 | ; 2019 | } 2020 | created = true 2021 | retry if t 2022 | end 2023 | end 2024 | end 2025 | module_function :touch 2026 | 2027 | private 2028 | 2029 | module StreamUtils_ # :nodoc: 2030 | 2031 | private 2032 | 2033 | case (defined?(::RbConfig) ? ::RbConfig::CONFIG['host_os'] : ::RUBY_PLATFORM) 2034 | when /mswin|mingw/ 2035 | def fu_windows?; true end #:nodoc: 2036 | else 2037 | def fu_windows?; false end #:nodoc: 2038 | end 2039 | 2040 | def fu_copy_stream0(src, dest, blksize = nil) #:nodoc: 2041 | IO.copy_stream(src, dest) 2042 | end 2043 | 2044 | def fu_stream_blksize(*streams) #:nodoc: 2045 | streams.each do |s| 2046 | next unless s.respond_to?(:stat) 2047 | size = fu_blksize(s.stat) 2048 | return size if size 2049 | end 2050 | fu_default_blksize() 2051 | end 2052 | 2053 | def fu_blksize(st) #:nodoc: 2054 | s = st.blksize 2055 | return nil unless s 2056 | return nil if s == 0 2057 | s 2058 | end 2059 | 2060 | def fu_default_blksize #:nodoc: 2061 | 1024 2062 | end 2063 | end 2064 | 2065 | include StreamUtils_ 2066 | extend StreamUtils_ 2067 | 2068 | class Entry_ #:nodoc: internal use only 2069 | include StreamUtils_ 2070 | 2071 | def initialize(a, b = nil, deref = false) 2072 | @prefix = @rel = @path = nil 2073 | if b 2074 | @prefix = a 2075 | @rel = b 2076 | else 2077 | @path = a 2078 | end 2079 | @deref = deref 2080 | @stat = nil 2081 | @lstat = nil 2082 | end 2083 | 2084 | def inspect 2085 | "\#<#{self.class} #{path()}>" 2086 | end 2087 | 2088 | def path 2089 | if @path 2090 | File.path(@path) 2091 | else 2092 | join(@prefix, @rel) 2093 | end 2094 | end 2095 | 2096 | def prefix 2097 | @prefix || @path 2098 | end 2099 | 2100 | def rel 2101 | @rel 2102 | end 2103 | 2104 | def dereference? 2105 | @deref 2106 | end 2107 | 2108 | def exist? 2109 | begin 2110 | lstat 2111 | true 2112 | rescue Errno::ENOENT 2113 | false 2114 | end 2115 | end 2116 | 2117 | def file? 2118 | s = lstat! 2119 | s and s.file? 2120 | end 2121 | 2122 | def directory? 2123 | s = lstat! 2124 | s and s.directory? 2125 | end 2126 | 2127 | def symlink? 2128 | s = lstat! 2129 | s and s.symlink? 2130 | end 2131 | 2132 | def chardev? 2133 | s = lstat! 2134 | s and s.chardev? 2135 | end 2136 | 2137 | def blockdev? 2138 | s = lstat! 2139 | s and s.blockdev? 2140 | end 2141 | 2142 | def socket? 2143 | s = lstat! 2144 | s and s.socket? 2145 | end 2146 | 2147 | def pipe? 2148 | s = lstat! 2149 | s and s.pipe? 2150 | end 2151 | 2152 | S_IF_DOOR = 0xD000 2153 | 2154 | def door? 2155 | s = lstat! 2156 | s and (s.mode & 0xF000 == S_IF_DOOR) 2157 | end 2158 | 2159 | def entries 2160 | opts = {} 2161 | opts[:encoding] = fu_windows? ? ::Encoding::UTF_8 : path.encoding 2162 | 2163 | files = Dir.children(path, **opts) 2164 | 2165 | untaint = RUBY_VERSION < '2.7' 2166 | files.map {|n| Entry_.new(prefix(), join(rel(), untaint ? n.untaint : n)) } 2167 | end 2168 | 2169 | def stat 2170 | return @stat if @stat 2171 | if lstat() and lstat().symlink? 2172 | @stat = File.stat(path()) 2173 | else 2174 | @stat = lstat() 2175 | end 2176 | @stat 2177 | end 2178 | 2179 | def stat! 2180 | return @stat if @stat 2181 | if lstat! and lstat!.symlink? 2182 | @stat = File.stat(path()) 2183 | else 2184 | @stat = lstat! 2185 | end 2186 | @stat 2187 | rescue SystemCallError 2188 | nil 2189 | end 2190 | 2191 | def lstat 2192 | if dereference? 2193 | @lstat ||= File.stat(path()) 2194 | else 2195 | @lstat ||= File.lstat(path()) 2196 | end 2197 | end 2198 | 2199 | def lstat! 2200 | lstat() 2201 | rescue SystemCallError 2202 | nil 2203 | end 2204 | 2205 | def chmod(mode) 2206 | if symlink? 2207 | File.lchmod mode, path() if have_lchmod? 2208 | else 2209 | File.chmod mode, path() 2210 | end 2211 | rescue Errno::EOPNOTSUPP 2212 | end 2213 | 2214 | def chown(uid, gid) 2215 | if symlink? 2216 | File.lchown uid, gid, path() if have_lchown? 2217 | else 2218 | File.chown uid, gid, path() 2219 | end 2220 | end 2221 | 2222 | def link(dest) 2223 | case 2224 | when directory? 2225 | if !File.exist?(dest) and descendant_directory?(dest, path) 2226 | raise ArgumentError, "cannot link directory %s to itself %s" % [path, dest] 2227 | end 2228 | begin 2229 | Dir.mkdir dest 2230 | rescue 2231 | raise unless File.directory?(dest) 2232 | end 2233 | else 2234 | File.link path(), dest 2235 | end 2236 | end 2237 | 2238 | def copy(dest) 2239 | lstat 2240 | case 2241 | when file? 2242 | copy_file dest 2243 | when directory? 2244 | if !File.exist?(dest) and descendant_directory?(dest, path) 2245 | raise ArgumentError, "cannot copy directory %s to itself %s" % [path, dest] 2246 | end 2247 | begin 2248 | Dir.mkdir dest 2249 | rescue 2250 | raise unless File.directory?(dest) 2251 | end 2252 | when symlink? 2253 | File.symlink File.readlink(path()), dest 2254 | when chardev?, blockdev? 2255 | raise "cannot handle device file" 2256 | when socket? 2257 | begin 2258 | require 'socket' 2259 | rescue LoadError 2260 | raise "cannot handle socket" 2261 | else 2262 | raise "cannot handle socket" unless defined?(UNIXServer) 2263 | end 2264 | UNIXServer.new(dest).close 2265 | File.chmod lstat().mode, dest 2266 | when pipe? 2267 | raise "cannot handle FIFO" unless File.respond_to?(:mkfifo) 2268 | File.mkfifo dest, lstat().mode 2269 | when door? 2270 | raise "cannot handle door: #{path()}" 2271 | else 2272 | raise "unknown file type: #{path()}" 2273 | end 2274 | end 2275 | 2276 | def copy_file(dest) 2277 | File.open(path()) do |s| 2278 | File.open(dest, 'wb', s.stat.mode) do |f| 2279 | IO.copy_stream(s, f) 2280 | end 2281 | end 2282 | end 2283 | 2284 | def copy_metadata(path) 2285 | st = lstat() 2286 | if !st.symlink? 2287 | File.utime st.atime, st.mtime, path 2288 | end 2289 | mode = st.mode 2290 | begin 2291 | if st.symlink? 2292 | begin 2293 | File.lchown st.uid, st.gid, path 2294 | rescue NotImplementedError 2295 | end 2296 | else 2297 | File.chown st.uid, st.gid, path 2298 | end 2299 | rescue Errno::EPERM, Errno::EACCES 2300 | # clear setuid/setgid 2301 | mode &= 01777 2302 | end 2303 | if st.symlink? 2304 | begin 2305 | File.lchmod mode, path 2306 | rescue NotImplementedError, Errno::EOPNOTSUPP 2307 | end 2308 | else 2309 | File.chmod mode, path 2310 | end 2311 | end 2312 | 2313 | def remove 2314 | if directory? 2315 | remove_dir1 2316 | else 2317 | remove_file 2318 | end 2319 | end 2320 | 2321 | def remove_dir1 2322 | platform_support { 2323 | Dir.rmdir path().chomp(?/) 2324 | } 2325 | end 2326 | 2327 | def remove_file 2328 | platform_support { 2329 | File.unlink path 2330 | } 2331 | end 2332 | 2333 | def platform_support 2334 | return yield unless fu_windows? 2335 | first_time_p = true 2336 | begin 2337 | yield 2338 | rescue Errno::ENOENT 2339 | raise 2340 | rescue => err 2341 | if first_time_p 2342 | first_time_p = false 2343 | begin 2344 | File.chmod 0700, path() # Windows does not have symlink 2345 | retry 2346 | rescue SystemCallError 2347 | end 2348 | end 2349 | raise err 2350 | end 2351 | end 2352 | 2353 | def preorder_traverse 2354 | stack = [self] 2355 | while ent = stack.pop 2356 | yield ent 2357 | stack.concat ent.entries.reverse if ent.directory? 2358 | end 2359 | end 2360 | 2361 | alias traverse preorder_traverse 2362 | 2363 | def postorder_traverse 2364 | if directory? 2365 | begin 2366 | children = entries() 2367 | rescue Errno::EACCES 2368 | # Failed to get the list of children. 2369 | # Assuming there is no children, try to process the parent directory. 2370 | yield self 2371 | return 2372 | end 2373 | 2374 | children.each do |ent| 2375 | ent.postorder_traverse do |e| 2376 | yield e 2377 | end 2378 | end 2379 | end 2380 | yield self 2381 | end 2382 | 2383 | def wrap_traverse(pre, post) 2384 | pre.call self 2385 | if directory? 2386 | entries.each do |ent| 2387 | ent.wrap_traverse pre, post 2388 | end 2389 | end 2390 | post.call self 2391 | end 2392 | 2393 | private 2394 | 2395 | @@fileutils_rb_have_lchmod = nil 2396 | 2397 | def have_lchmod? 2398 | # This is not MT-safe, but it does not matter. 2399 | if @@fileutils_rb_have_lchmod == nil 2400 | @@fileutils_rb_have_lchmod = check_have_lchmod? 2401 | end 2402 | @@fileutils_rb_have_lchmod 2403 | end 2404 | 2405 | def check_have_lchmod? 2406 | return false unless File.respond_to?(:lchmod) 2407 | File.lchmod 0 2408 | return true 2409 | rescue NotImplementedError 2410 | return false 2411 | end 2412 | 2413 | @@fileutils_rb_have_lchown = nil 2414 | 2415 | def have_lchown? 2416 | # This is not MT-safe, but it does not matter. 2417 | if @@fileutils_rb_have_lchown == nil 2418 | @@fileutils_rb_have_lchown = check_have_lchown? 2419 | end 2420 | @@fileutils_rb_have_lchown 2421 | end 2422 | 2423 | def check_have_lchown? 2424 | return false unless File.respond_to?(:lchown) 2425 | File.lchown nil, nil 2426 | return true 2427 | rescue NotImplementedError 2428 | return false 2429 | end 2430 | 2431 | def join(dir, base) 2432 | return File.path(dir) if not base or base == '.' 2433 | return File.path(base) if not dir or dir == '.' 2434 | begin 2435 | File.join(dir, base) 2436 | rescue EncodingError 2437 | if fu_windows? 2438 | File.join(dir.encode(::Encoding::UTF_8), base.encode(::Encoding::UTF_8)) 2439 | else 2440 | raise 2441 | end 2442 | end 2443 | end 2444 | 2445 | if File::ALT_SEPARATOR 2446 | DIRECTORY_TERM = "(?=[/#{Regexp.quote(File::ALT_SEPARATOR)}]|\\z)" 2447 | else 2448 | DIRECTORY_TERM = "(?=/|\\z)" 2449 | end 2450 | 2451 | def descendant_directory?(descendant, ascendant) 2452 | if File::FNM_SYSCASE.nonzero? 2453 | File.expand_path(File.dirname(descendant)).casecmp(File.expand_path(ascendant)) == 0 2454 | else 2455 | File.expand_path(File.dirname(descendant)) == File.expand_path(ascendant) 2456 | end 2457 | end 2458 | end # class Entry_ 2459 | 2460 | def fu_list(arg) #:nodoc: 2461 | [arg].flatten.map {|path| File.path(path) } 2462 | end 2463 | private_module_function :fu_list 2464 | 2465 | def fu_each_src_dest(src, dest) #:nodoc: 2466 | fu_each_src_dest0(src, dest) do |s, d| 2467 | raise ArgumentError, "same file: #{s} and #{d}" if fu_same?(s, d) 2468 | yield s, d 2469 | end 2470 | end 2471 | private_module_function :fu_each_src_dest 2472 | 2473 | def fu_each_src_dest0(src, dest, target_directory = true) #:nodoc: 2474 | if tmp = Array.try_convert(src) 2475 | unless target_directory or tmp.size <= 1 2476 | tmp = tmp.map {|f| File.path(f)} # A workaround for RBS 2477 | raise ArgumentError, "extra target #{tmp}" 2478 | end 2479 | tmp.each do |s| 2480 | s = File.path(s) 2481 | yield s, (target_directory ? File.join(dest, File.basename(s)) : dest) 2482 | end 2483 | else 2484 | src = File.path(src) 2485 | if target_directory and File.directory?(dest) 2486 | yield src, File.join(dest, File.basename(src)) 2487 | else 2488 | yield src, File.path(dest) 2489 | end 2490 | end 2491 | end 2492 | private_module_function :fu_each_src_dest0 2493 | 2494 | def fu_same?(a, b) #:nodoc: 2495 | File.identical?(a, b) 2496 | end 2497 | private_module_function :fu_same? 2498 | 2499 | def fu_output_message(msg) #:nodoc: 2500 | output = @fileutils_output if defined?(@fileutils_output) 2501 | output ||= $stdout 2502 | if defined?(@fileutils_label) 2503 | msg = @fileutils_label + msg 2504 | end 2505 | output.puts msg 2506 | end 2507 | private_module_function :fu_output_message 2508 | 2509 | def fu_split_path(path) #:nodoc: 2510 | path = File.path(path) 2511 | list = [] 2512 | until (parent, base = File.split(path); parent == path or parent == ".") 2513 | if base != '..' and list.last == '..' and !(fu_have_symlink? && File.symlink?(path)) 2514 | list.pop 2515 | else 2516 | list << base 2517 | end 2518 | path = parent 2519 | end 2520 | list << path 2521 | list.reverse! 2522 | end 2523 | private_module_function :fu_split_path 2524 | 2525 | def fu_common_components(target, base) #:nodoc: 2526 | i = 0 2527 | while target[i]&.== base[i] 2528 | i += 1 2529 | end 2530 | i 2531 | end 2532 | private_module_function :fu_common_components 2533 | 2534 | def fu_clean_components(*comp) #:nodoc: 2535 | comp.shift while comp.first == "." 2536 | return comp if comp.empty? 2537 | clean = [comp.shift] 2538 | path = File.join(*clean, "") # ending with File::SEPARATOR 2539 | while c = comp.shift 2540 | if c == ".." and clean.last != ".." and !(fu_have_symlink? && File.symlink?(path)) 2541 | clean.pop 2542 | path.sub!(%r((?<=\A|/)[^/]+/\z), "") 2543 | else 2544 | clean << c 2545 | path << c << "/" 2546 | end 2547 | end 2548 | clean 2549 | end 2550 | private_module_function :fu_clean_components 2551 | 2552 | if fu_windows? 2553 | def fu_starting_path?(path) #:nodoc: 2554 | path&.start_with?(%r(\w:|/)) 2555 | end 2556 | else 2557 | def fu_starting_path?(path) #:nodoc: 2558 | path&.start_with?("/") 2559 | end 2560 | end 2561 | private_module_function :fu_starting_path? 2562 | 2563 | # This hash table holds command options. 2564 | OPT_TABLE = {} #:nodoc: internal use only 2565 | (private_instance_methods & methods(false)).inject(OPT_TABLE) {|tbl, name| 2566 | (tbl[name.to_s] = instance_method(name).parameters).map! {|t, n| n if t == :key}.compact! 2567 | tbl 2568 | } 2569 | 2570 | public 2571 | 2572 | # Returns an array of the string names of \FileUtils methods 2573 | # that accept one or more keyword arguments: 2574 | # 2575 | # FileUtils.commands.sort.take(3) # => ["cd", "chdir", "chmod"] 2576 | # 2577 | def self.commands 2578 | OPT_TABLE.keys 2579 | end 2580 | 2581 | # Returns an array of the string keyword names: 2582 | # 2583 | # FileUtils.options.take(3) # => ["noop", "verbose", "force"] 2584 | # 2585 | def self.options 2586 | OPT_TABLE.values.flatten.uniq.map {|sym| sym.to_s } 2587 | end 2588 | 2589 | # Returns +true+ if method +mid+ accepts the given option +opt+, +false+ otherwise; 2590 | # the arguments may be strings or symbols: 2591 | # 2592 | # FileUtils.have_option?(:chmod, :noop) # => true 2593 | # FileUtils.have_option?('chmod', 'secure') # => false 2594 | # 2595 | def self.have_option?(mid, opt) 2596 | li = OPT_TABLE[mid.to_s] or raise ArgumentError, "no such method: #{mid}" 2597 | li.include?(opt) 2598 | end 2599 | 2600 | # Returns an array of the string keyword name for method +mid+; 2601 | # the argument may be a string or a symbol: 2602 | # 2603 | # FileUtils.options_of(:rm) # => ["force", "noop", "verbose"] 2604 | # FileUtils.options_of('mv') # => ["force", "noop", "verbose", "secure"] 2605 | # 2606 | def self.options_of(mid) 2607 | OPT_TABLE[mid.to_s].map {|sym| sym.to_s } 2608 | end 2609 | 2610 | # Returns an array of the string method names of the methods 2611 | # that accept the given keyword option +opt+; 2612 | # the argument must be a symbol: 2613 | # 2614 | # FileUtils.collect_method(:preserve) # => ["cp", "copy", "cp_r", "install"] 2615 | # 2616 | def self.collect_method(opt) 2617 | OPT_TABLE.keys.select {|m| OPT_TABLE[m].include?(opt) } 2618 | end 2619 | 2620 | private 2621 | 2622 | LOW_METHODS = singleton_methods(false) - collect_method(:noop).map(&:intern) # :nodoc: 2623 | module LowMethods # :nodoc: internal use only 2624 | private 2625 | def _do_nothing(*)end 2626 | ::FileUtils::LOW_METHODS.map {|name| alias_method name, :_do_nothing} 2627 | end 2628 | 2629 | METHODS = singleton_methods() - [:private_module_function, # :nodoc: 2630 | :commands, :options, :have_option?, :options_of, :collect_method] 2631 | 2632 | # 2633 | # This module has all methods of FileUtils module, but it outputs messages 2634 | # before acting. This equates to passing the :verbose flag to 2635 | # methods in FileUtils. 2636 | # 2637 | module Verbose 2638 | include FileUtils 2639 | names = ::FileUtils.collect_method(:verbose) 2640 | names.each do |name| 2641 | module_eval(<<-EOS, __FILE__, __LINE__ + 1) 2642 | def #{name}(*args, **options) 2643 | super(*args, **options, verbose: true) 2644 | end 2645 | EOS 2646 | end 2647 | private(*names) 2648 | extend self 2649 | class << self 2650 | public(*::FileUtils::METHODS) 2651 | end 2652 | end 2653 | 2654 | # 2655 | # This module has all methods of FileUtils module, but never changes 2656 | # files/directories. This equates to passing the :noop flag 2657 | # to methods in FileUtils. 2658 | # 2659 | module NoWrite 2660 | include FileUtils 2661 | include LowMethods 2662 | names = ::FileUtils.collect_method(:noop) 2663 | names.each do |name| 2664 | module_eval(<<-EOS, __FILE__, __LINE__ + 1) 2665 | def #{name}(*args, **options) 2666 | super(*args, **options, noop: true) 2667 | end 2668 | EOS 2669 | end 2670 | private(*names) 2671 | extend self 2672 | class << self 2673 | public(*::FileUtils::METHODS) 2674 | end 2675 | end 2676 | 2677 | # 2678 | # This module has all methods of FileUtils module, but never changes 2679 | # files/directories, with printing message before acting. 2680 | # This equates to passing the :noop and :verbose flag 2681 | # to methods in FileUtils. 2682 | # 2683 | module DryRun 2684 | include FileUtils 2685 | include LowMethods 2686 | names = ::FileUtils.collect_method(:noop) 2687 | names.each do |name| 2688 | module_eval(<<-EOS, __FILE__, __LINE__ + 1) 2689 | def #{name}(*args, **options) 2690 | super(*args, **options, noop: true, verbose: true) 2691 | end 2692 | EOS 2693 | end 2694 | private(*names) 2695 | extend self 2696 | class << self 2697 | public(*::FileUtils::METHODS) 2698 | end 2699 | end 2700 | 2701 | end 2702 | --------------------------------------------------------------------------------