文件管理
开篇故事
文件管理是文件系统操作的集合。你经常需要:列出某个目录下的所有文件、查找特定类型的文件、检查文件是否存在、获取文件大小、创建临时目录。File 方法、Dir 的目录操作、Pathname 对象的文件路径操作、FileTest 方法的文件属性检查、临时目录创建。
本章适合谁
如果你需要写脚本、做批量文件处理、或管理文件系统,本章覆盖了 Ruby 标准库中所有文件管理的工具。
你会学到什么
- File 路径操作 — join、basename、dirname、extname
- Dir — pwd、entries、each_child、mkpath、rmdir
- FileTest — exist?、file?、directory?、size、identical?
- Pathname — basename、dirname、extname、ascend、glob
- 临时目录 — tmpdir、mktmpdir
前置要求
第一个例子
# 运行: hello basic file-management
# Pathname — 面向对象的文件路径
pn = Pathname.new("/usr/local/bin/ruby")
puts "basename: #{pn.basename}" # ruby
puts "dir: #{pn.dirname}" # /usr/local/bin
为什么这比 File.basename 好:Pathname 提供路径的"面向对象"接口。/ 操作符拼接路径,比 File.join 更优雅。
File 路径操作
# 拼接路径
joined = File.join("usr", "local", "bin", "ruby")
puts "File.join: #{joined}"
# 提取文件名
basename = File.basename("/usr/local/bin/ruby")
puts "File.basename: #{basename}"
# 提取文件名(去掉扩展名)
basename_no_ext = File.basename("/usr/local/bin/ruby", ".rb")
puts "File.basename (去扩展名): #{basename_no_ext}"
# 提取目录名
dirname = File.dirname("/usr/local/bin/ruby")
puts "File.dirname: #{dirname}"
# 提取扩展名
extname = File.extname("/usr/local/bin/script.rb")
puts "File.extname: #{extname}"
# 判断是否为绝对路径
puts "绝对路径? #{File.absolute_path?("/usr/local")}"
# 展开路径(~ 缩写)
expanded = File.expand_path("~")
puts "扩展 ~/ #{expanded}"
Dir 目录操作
# 当前工作目录
puts "Dir.pwd: #{Dir.pwd}"
# 获取当前目录下的 .rb 文件
puts "当前目录 .rb 文件:"
Dir.glob("*.rb").each { |f| puts " #{f}" }
# 递归查找所有 .rb 文件
rb_count = Dir.glob("**/*.rb").length
puts "项目中 .rb 文件总数: #{rb_count}"
# 匹配多种扩展名(花括号扩展)
all_docs = Dir.glob("**/*.{rb,md}")
puts "Ruby + Markdown 文件总数: #{all_docs.length}"
# Dir.entries — 列出目录所有内容(含 . 和 ..)
puts "Dir.entries('lib/hello/basic'):"
Dir.entries("lib/hello/basic").each { |entry| puts " #{entry}" }
# Dir.each_child — 迭代子目录/文件(不含 . 和 ..)
puts "Dir.each_child('lib/hello/basic'):"
Dir.each_child("lib/hello/basic") { |child| puts " #{child}" }
FileTest — 文件属性检查
gemfile_path = "Gemfile"
# 检查文件是否存在
puts "FileTest.exist?(#{gemfile_path}): #{FileTest.exist?(gemfile_path)}"
# 检查是否为普通文件
puts "FileTest.file?(#{gemfile_path}): #{FileTest.file?(gemfile_path)}"
# 检查是否为目录
puts "FileTest.directory?('lib'): #{FileTest.directory?("lib")}"
# 检查可读/可写权限
puts "FileTest.readable?(#{gemfile_path}): #{FileTest.readable?(gemfile_path)}"
# 获取文件大小(字节)
size = FileTest.size(gemfile_path)
puts "FileTest.size(#{gemfile_path}): #{size} bytes"
# 检查两个路径是否指向同一文件
readme_a = "README.md"
readme_real = File.realpath(readme_a)
puts "FileTest.identical?(#{readme_a}, #{readme_real}): #{FileTest.identical?(readme_a, readme_real)}"
Pathname — 面向对象路径操作
# 创建 Pathname 对象
pn = Pathname.new("/usr/local/bin/ruby")
puts "Pathname('/usr/local/bin/ruby'):"
puts " .basename: #{pn.basename}"
puts " .dirname: #{pn.dirname}"
puts " .extname: #{pn.extname}"
puts " .ascend: #{pn.ascend.to_a}"
# / 运算符拼接路径(比 File.join 更优雅)
project_root = Pathname.new(".")
lib_path = project_root / "lib" / "hello.rb"
puts "\nPathname / 运算符:"
puts " lib/hello.rb 路径: #{lib_path}"
puts " 存在?#{lib_path.exist?}"
# 读取文件内容
readme_path = Pathname.new("README.md")
if readme_path.exist?
first_three = readme_path.readlines.take(3)
puts "\nREADME.md 前 3行:"
first_three.each { |line| puts " ${line.chomp}" }
end
# Pathname.glob — 匹配文件模式
md_count = Pathname.glob("docs/src/**/*.md").length
puts "\nPathname.glob('docs/src/**/*.md'): #{md_count} 个文件"
# relative_path_from — 计算相对路径
absolute = Pathname.new("/usr/local/bin/ruby")
base = Pathname.new("/usr/local")
relative = absolute.relative_path_from(base)
puts "\nPathname#relative_path_from:"
puts " #{absolute} 相对 /usr/local 是: #{relative}"
临时目录 — Dir.mktmpdir
Dir.mktmpdir("hello_") do |tmp_dir|
tmp_path = Pathname.new(tmp_dir)
puts "创建临时目录: #{tmp_dir}"
# 在临时目录中创建文件
demo_file = tmp_path / "demo.txt"
demo_file.write("Hello from 临时文件!")
puts " 写入 #{demo_file}: #{demo_file.read}"
# 创建子目录
subdir = tmp_path / "subdir"
subdir.mkpath
subdir_file = subdir / "nested.txt"
subdir_file.write("嵌套文件内容")
# 列出临时目录的所有内容
puts " 临时目录内容:"
tmp_path.glob("**/*").each do |item|
type = item.directory? ? "[目录]" : "[文件]"
puts " #{type} #{item.relative_path_from(tmp_path)}"
end
puts " 临时目录大小: #{tmp_path.size} (目录元数据大小)"
puts " 临时目录可读:#{tmp_path.readable?}"
end
# 退出块后,临时目录已自动清理
puts "临时目录已自动清理完毕"
mktmpdir 的优势:自动清理临时文件,不需要手动 rm -rf。
常见错误
错误 1:用字符串拼接处理路径
# ❌ 容易出错,不同 OS 的斜杠不同
path = "/tmp/" + filename # 可能 "//tmp//file.txt"
path = filename + "/" # 可能 "/tmp//file.txt"
# ✅ 用 File.join 或 Pathname
path = File.join("/tmp", filename)
path = Pathname.new("/tmp") / filename
错误 2:检查文件存在后操作
# ❌ TOCTOU 问题:检查和操作之间文件可能被删除
if File.exist?("data.txt")
content = File.read("data.txt") # 可能已经不存在了
end
# ✅ 用 rescue 处理
begin
content = File.read("data.txt")
rescue Errno::ENOENT
puts "文件不存在"
end
错误 3:忘记临时目录的清理
# ❌ 手动创建的临时目录不会自动清理
tmp = Dir.mktmpdir
# ... 使用后忘记清理
# ✅ 用 Dir.mktmpdir 块形式
Dir.mktmpdir("hello_") do |tmp_dir|
# ... 自动清理
end
动手练习
练习 1:递归查找大文件
# 查找当前目录下所有超过 1MB 的文件,按大小排序
参考答案
Dir.glob("**/*").map do |f|
next unless File.file?(f)
size = File.size(f)
[f, size] if size > 1_000_000
end.compact.sort_by(&:last)
练习 2:批量重命名
# 将当前目录所有 .txt 文件改为 .md
参考答案
Dir.glob("*.txt").each do |f|
new_name = f.sub(/\.txt$/, ".md")
FileUtils.mv(f, new_name)
end
故障排查 (FAQ)
Q: File.join 和 Pathname / 运算哪个更好?
A: 两者功能相同。Pathname 更面向对象,支持方法链;File.join 更直接。项目一致即可。
Q: 怎么判断路径是文件、目录、或符号链接?
A: 用 FileTest 模块:
FileTest.file?("path") # 是普通文件
FileTest.directory?("path") # 是目录
FileTest.symlink?("path") # 是符号链接
Q: Dir.glob 和 Pathname.glob 的区别?
A: Dir.glob 返回字符串数组;Pathname.glob 返回 Pathname 对象数组,支持后续方法。Pathname.glob 更面向对象。
小结
核心要点:
- File.join 拼接路径:跨平台安全
- Pathname 面向对象路径:支持
/运算符 - Dir.glob 查找文件:
**/*.rb递归 - FileTest 检查属性:exist?、file?、directory?、size
- Dir.mktmpdir 自动清理:块形式自动清理
- FileTest vs File 模块:
FileTest.exist?和File.exist?功能相同
继续学习
运行 hello basic file-management 查看完整示例代码.