In Progress
Unit 1, Lesson 1
In Progress

Checking for a Terminal

Video transcript & code

Let's say we've got a command-line program that produces a lot of output. Like, say, the program I wrote in episode 298 to get stats on RubyTapas videos.

require 'pathname'

Pathname("~/Dropbox/rubytapas").expand_path.find do |path|
  Find.prune if path.directory? && path.basename.to_s =~ /^xxx/
  if path.file? && (
      path.basename.to_s =~ /^RubyTapas(\d{3})\b.*\.mp4$/ ||
      path.basename.to_s =~ /^(\d{3})\b.*\.mp4$/)
    number   = $1
    next if path.basename.to_s =~ /sample/
    stats    = `avprobe #{path} 2>&1`
    duration = stats[/Duration: (\d{2}:\d{2}:\d{2})/, 1]
    puts "#{number} #{duration} #{path.basename}"
  end
end
# >> 001 00:01:47 RubyTapas001.mp4
# >> 002 00:00:45 RubyTapas002.mp4
# >> 003 00:01:11 RubyTapas003.mp4
# >> 005 00:03:36 RubyTapas005.mp4
# >> 006 00:04:56 RubyTapas006-Forwardable.mp4

If you've ever used Git, you know it has a handy feature when dealing with lengthy output. By default, when run in a terminal it will pipe long output into the default pager, so that we can page through the results at our leisure. But when it is run as part of a pipeline, it writes the output straight to stdout like any other well-behaved UNIX program.

We'd like to add similar behavior to our program. We can start by figuring out which pager program to use when paging output. There is a UNIX convention that the user's favorite pager can be specified in the PAGER environment variable. So we check this variable for a value, and if none is set, we fall back to the ubiquitous more(1) command.

Now, how to determine if the program is being used directly at the console, or in a pipeline? Fortunately, Ruby makes this very easy. We can use the #tty? predicate method on $stdout to ask if the current standard output is a terminal. If so, we re-execute the current program in a pipeline with the pager program in order to provide nice paged output. Otherwise, we let the program proceed forward to the main body.

require 'pathname'

pager = ENV.fetch('PAGER') { 'more' }

if $stdout.tty?
  exec "ruby #{$0} | #{pager}"
end

Pathname("~/Dropbox/rubytapas").expand_path.find do |path|
  Find.prune if path.directory? && path.basename.to_s =~ /^xxx/
  if path.file? && (
      path.basename.to_s =~ /^RubyTapas(\d{3})\b.*\.mp4$/ ||
      path.basename.to_s =~ /^(\d{3})\b.*\.mp4$/)
    number   = $1
    next if path.basename.to_s =~ /sample/
    stats    = `avprobe #{path} 2>&1`
    duration = stats[/Duration: (\d{2}:\d{2}:\d{2})/, 1]
    puts "#{number} #{duration} #{path.basename}"
  end
end

When we try the program out at the terminal, we can see that the output is presented in a pager. But as when we execute it in a pipeline, it produces raw output.

And that's it for now. Happy hacking!

Responses