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:
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"
Strings
Numbers
Date and Time
Arrays
Hashes
Files and Directories
Code Blocks and Iteration
Objects and Classes8
Modules and Namespaces
Reflection and Metaprogramming
XML and HTML
Graphics and Other File Formats
Databases and Persistence
Internet Services
Web Development Ruby on Rails
Web Services and Distributed Programming
Testing, Debugging, Optimizing, and Documenting
Packaging and Distributing Software
Automating Tasks with Rake
Multitasking and Multithreading
User Interface
Extending Ruby with Other Languages
System Administration