Files and Directories

As programming languages increase in power, we programmers get further and further from the details of the underlying machine language. When it comes to the operating system, though, even the most modern programming languages live on a level of abstraction that looks a lot like the C and Unix libraries that have been around for decades.

We covered this kind of situation in Chapter 3 with Ruby's Time objects, but the issue really shows up when you start to work with files. Ruby provides an elegant object-oriented interface that lets you do basic file access, but the more advanced file libraries tend to look like the C libraries they're based on. To lock a file, change its Unix permissions, or read its metadata, you'll need to remember method names like mtime, and the meaning of obscure constants like File::LOCK_EX and 0644. This chapter will show you how to use the simple interfaces, and how to make the more obscure interfaces easier to use.

Looking at Ruby's support for file and directory operations, you'll see four distinct tiers of support. The most common operations tend to show up on the lowernumbered tiers:

  1. File objects to read and write the contents of files, and Dir objects to list the contents of directories. For examples, see Recipes 6.5, 6.7, and 6.17. Also see Recipe 6.13 for a Ruby-idiomatic approach.
  2. Class methods of File to manipulate files without opening them. For instance, to delete a file, examine its metadata, or change its permissions. For examples, see Recipes 6.1, 6.3, and 6.4.
  3. Standard libraries, such as find to walk directory trees, and fileutils to perform common filesystem operations like copying files and creating directories. For examples, see Recipes 6.8, 6.12, and 6.20.
  4. Gems like file-tail, lockfile, and rubyzip, which fill in the gaps left by the standard library. Most of the file-related gems covered in this book deal with specific file formats, and are covered in Chapter 12.

Kernel#open is the simplest way to open a file. It returns a Filel object that you can read from or write to, depending on the "mode" constant you pass in. I'll introduce read mode and write mode here; there are several others, but I'll talk about most of those as they come up in recipes.

To write data to a file, pass a mode of 'w' to open. You can then write lines to the file with File#puts, just like printing to standard output with Kernel#puts. For more possibilities, see Recipe 6.7.

open('beans.txt', "w") do |file| file.puts('lima beans') file.puts('pinto beans') file.puts('human beans') end

To read data from a file, open it for read access by specifying a mode of 'r', or just omitting the mode. You can slurp the entire contents into a string with File#read, or process the file line-by-line with File#each. For more details, see Recipe 6.6.

open('beans.txt') do |file| file.each { |l| puts "A line from the file: #{l}" } end # A line from the file: lima beans # A line from the file: pinto beans # A line from the file: human beans

As seen in the examples above, the best way to use the open method is with a code block. The open method creates a new File object, passes it to your code block, and closes the file automatically after your code block runseven if your code throws an exception. This saves you from having to remember to close the file after you're done with it. You could rely on the Ruby interpreter's garbage collection to close the file once it's no longer being used, but Ruby makes it easy to do things the right way.

To find a file in the first place, you need to specify its disk path. You may specify an absolute path, or one relative to the current directory of your Ruby process (see Recipe 6.21). Relative paths are usually better, because they're more portable across platforms. Relative paths like "beans.txt" or "subdir/beans.txt" will work on any platform, but absolute Unix paths look different from absolute Windows paths:

# A stereotypical Unix path. open('/etc/passwd') # A stereotypical Windows path; note the drive letter. open('c:/windows/Documents and Settings/User1/My Documents/ruby.doc')

Windows paths in Ruby use forward slashes to separate the parts of a path, even though Windows itself uses backslashes. Ruby will also accept backslashes in a Windows path, so long as you escape them:

open('c:\windows\Documents and Settings\User1\My Documents\ruby.doc')

Although this chapter focuses mainly on disk files, most of the methods of File are actually methods of its superclass, IO. You'll encounter many other classes that are also subclasses of IO, or just respond to the same methods. This means that most of the tricks described in this chapter are applicable to classes like the Socket class for Internet sockets and the infinitely useful StringIO (see Recipe 6.15).

Your Ruby program's standard input, output, and error ($stdin, $stdout, and $stderr) are also IO objects, which means you can treat them like files. This one-line program echoes its input to its output:

$stdin.each { |l| puts l }

The Kernel#puts command just calls $stdout.puts, so that one-liner is equivalent to this one:

$stdin.each { |l| $stdout.puts l }

Not all file-like objects support all the methods of IO. See Recipe 6.11 for ways to get around the most common problem with unsupported methods. Also see Recipe 6.16 for more on the default IO objects.

Several of the recipes in this chapter (such as Recipes 6.12 and 6.20) create specific directory structures to demonstrate different concepts. Rather than bore you by filling up recipes with the Ruby code to create a certain directory structure, I've written a method that takes a short description of a directory structure, and creates the appropriate files and subdirectories:

# create_tree.rb def create_tree(directories, parent=".") directories.each_pair do |dir, files| path = File.join(parent, dir) Dir.mkdir path unless File.exists? path files.each do |filename, contents| if filename.respond_to? :each_pair # It's a subdirectory create_tree filename, path else # It's a file open(File.join(path, filename), 'w') { |f| f << contents || "" } end end end end

Now I can present th directory structure as a data structure and you can create it with a single method call:

require 'create_tree' create_tree 'test' => [ 'An empty file', ['A file with contents', 'Contents of file'], { 'Subdirectory' => ['Empty file in subdirectory', ['File in subdirectory', 'Contents of file'] ] }, { 'Empty subdirectory' => [] } ] require 'find' Find.find('test') { |f| puts f } # test # test/Empty subdirectory # test/Subdirectory # test/Subdirectory/File in subdirectory # test/Subdirectory/Empty file in subdirectory # test/A file with contents # test/An empty file File.read('test/Subdirectory/File in subdirectory') # => "Contents of file"

Категории