In Progress
Unit 1, Lesson 21
In Progress

Kernel Open with Rob Miller

Video transcript & code
Rob Miller

author of

"Text Processing with Ruby"

Kernel#open

File.open

File.open("/usr/share/dict/words", "r") do |file|
    file.gets
end

If you've used Ruby for any length of time, you're probably familiar with the open method of the File class.

Pass it the name of a file and, optionally, the mode in which you want to open the file — it defaults to read-only — and it will return for you a File object that you can use to read from and write to that particular file.

Pass it a block, and it will yield that file object to the block, cleaning up after you once the block is finished.

Here, we're opening our system's dictionary and reading the first word from it; in this case, the slightly uninteresting "A".

open as a shortcut

open("/usr/share/dict/words/", "r") do |file|
    file.gets
end

File.open works well, and is widely used. But to save our precious fingers the effort of typing, Ruby's Kernel module provides a method, also called open, that achieves the same result. Since the Kernel module is mixed in to all objects, it's available as method anywhere you care to use it.

This concision would probably be reason enough to use open rather than the lengthier File.open. But it turns out that Kernel's open method isn't a mere alias for that of the File class. It has some tricks of its own, too.

open with processes

open("|pbpaste", "r") do |clipboard|
    clipboard.read
end

Here, we use the open method to open not a file, but a process. open knows to do this, rather than attempt to read a file, because the first character of the path that we pass to it is a pipe symbol.

Behind the scenes, open is doing all sorts of plumbing for us; it's spawning a subprocess, and connecting the standard output stream of this subprocess to a pipe. This pipe is then yielded to the block passed into open, allowing us to use it to read from the process's output.

In this case, the process that we're spawning is Mac OS X's pbpaste command, which allows us to read from the clipboard. This block, then, returns the text that's on the system clipboard.

open("| ps ax | grep ruby") do |ps|
    ps.gets.split.first
end

But we could use open in this way to read from any command that produces output. We can even pass in pipeline chains, in which case our pipe will be connected to the standard output stream of the final process in the pipeline. In this example, we're using ps to search for Ruby processes running on our system. We're then taking the first line of output from ps, splitting on whitespace, and outputting the first field; this will print the process ID of the first Ruby process running on our system.

Writing not reading

open("|pbcopy", "w") do |clipboard|
    clipboard.write("hello, world")
end

Of course, just like with files, we're not limited only to reading from the process's standard output stream. We can also write to its standard input stream, too.

In this example we use the pbcopy command, which copies text to the system clipboard. Since we opened the command in write mode, by passing the "w" flag to open, we're able to use the write method to write to the process's standard input stream, and in doing so copy text to the clipboard.

Paging output

One way that this behaviour of open can be put to practical use is to redirect our own standard output stream to another process. In doing so, we form a pipeline, just like we might in our shells on the command line.

If you've ever used the version control software Git, you might have noticed that, if you run a command that produces a lot of output, such as git log, it will helpfully scroll the output for you. This ensures that you stay at the top of the output, but can scroll down further if you choose.

Let's look at how open can help us achieve the same behavior in our scripts.

stdout = STDOUT.clone
less = open("|less", "w")
STDOUT.reopen(less)

Here, we clone the existing standard output stream so that we can restore it later. Then, we use open to spawn the less utility in a subprocess; it's less that will be taking care of the actual scrolling functionality.

500.times do |n|
    puts "#{n}: hello world"
end

Next, we output a lot of text; in this case, 500 fairly repetitive lines.

STDOUT.reopen(stdout)
less.close

Finally, at the end of our script, we revert standard output to its original location, and close the pipeline we connected to less, thus ending the subprocess.

If we run this script, it should behave as we'd hoped. We stay at the top of the output, able to see the first line, but we can scroll down further if we wish. Perfect.

open-uri

Its ability to open both files and processes might have convinced you to use Kernel's open method, but it has a further trick up its sleeve.

require "openuri"

open("https://rubytapas.com/") do |page|
    page.base_uri
    page.content_type
    page.gets
end

By requiring the openuri library, which is part of the Ruby standard library, open gains yet another feature: the ability to open URLs. Just as open will spawn a subprocess if the path you pass to it starts with a pipe symbol, after including the openuri library it will request the path as a URL if it looks like one.

This functionality wraps the native Net::HTTP, Net::HTTPS, and Net::FTP libraries, and in a simple script is certainly the easiest way to fetch such URLs.

Here we fetch the RubyTapas homepage. The returned IO object is similar to the ones we saw when opening files and processes, but has a few methods of its own; here we check the URI that we fetched and the content type of the response. But it's otherwise a regular IO stream, and we can use methods like gets, each_line, and so on to process the response body.

Summing up

At first glance, open might seem like a simple shortcut for opening files. But it's actually a general purpose way to create many different types of IO object. Ruby's abstracted IO class makes reading from and writing to these disparate forms of input and output feel similar, so it seems only right that there should be a simple, overarching interface for creating them. Use it! And, as Avdi would say: happy hacking!

Responses