cat2rafb (#77)

by Sean Carley

The goal of cat2rafb is to implement a command line utility that will allow you to submit arbitrary text to http://rafb.net/paste/ and get the URL for the text back. Bonus points for pushing the URL through http://rubyurl.com/ to make it more manageable.

cat2rafb could work like cat, taking files as input or text until ^D after entering the command.


Quiz Summary

When I first saw this quiz I thought, "I'm going to add this as a command for my text editor!" It was probably a day or two later when I learned that it was already in there and had been for some time now. I guess other people thought it was a good idea too.

Let's dive right into a solution. Here's the start of some code by Stefano Taschini:

ruby
#!/usr/bin/env ruby

require 'optparse'
require 'net/http'

# Command-Line Interface.
class Cli

Languages = %w{ C89 C C++ C# Java Pascal Perl PHP PL/I Python Ruby
SQL VB Plain\ Text }

Aliases = {"c99" => "C", "visual basic" => "VB", "text" => "Plain Text"}
PasteUrl = "http://rafb.net/paste/paste.php"

attr :parser
attr :opt

# ...

Obviously this is just some basic setup. You can see that Stefano plans to use OptionParser for the interface and Net::HTTP for the networking. You can also see the list of supported languages here, complete with aliases, which I thought was a nice touch.

Here's the interface code:

ruby
# ...

# Initialize the command-line parser and set default values for the
# options.
def initialize
@opt = {
:lang => "Plain Text",
:nick => "",
:desc => "",
:tabs => "No",
:help => false}
@parser = OptionParser.new do |cli|
cli.banner += " [file ...]"
cli.on('-l','--lang=L', 'select language') { |s|
l = s.downcase
opt[:lang] =
if Aliases.include?(l) then
Aliases[l]
else
Languages.find(proc{ raise OptionParser::InvalidArgument,l }) { |x|
x.downcase == l
}
end
}
cli.on('-n', '--nick=NAME', 'use NAME as nickname') { |s| opt[:nick] = s}
cli.on('-d', '--desc=TEXT', 'use TEXT as description') { |s|
opt[:desc] << s
}
cli.on('--tabs=N', Integer, 'expand tabs to N blanks (N >= 0)') { |n|
raise OptionParser::InvalidArgument, n unless n>=0
opt[:tabs] = n
}
cli.on('-h', '--help', 'show this information and quit') {
opt[:help] = true
}
cli.separator ""
cli.separator "Languages (case insensitive):"
cli.separator " " +
(Languages+Aliases.keys).map{|x|x.downcase}.sort.join(",")
end
end

# ...

I know that looks like a lot of code, but it's all just trivial declarations. This program supports all of NoPaste's form elements through setting command-line options.

You can see that the only option handler worth mentioning is the language handler. All that happens in there is to make sure a valid language is selected. This section of the code uses the default parameter to find() which I don't often come across. When passes a Proc object, find() will call it when a matching object cannot be found. Generally the result of that call is returned, but in this case an Exception is raised before that can happen.

Ready for the huge section of networking code?

ruby
# ...

# Post the given text with the current options to the given uri and
# return the uri for the posted text.
def paste(uri, text)
response = Net::HTTP.post_form(
uri,
{ "lang" => opt[:lang],
"nick" => opt[:nick],
"desc" => opt[:desc],
"cvt_tabs" => opt[:tabs],
"text" => text,
"submit" => "Paste" })
uri.merge response['location'] || raise("No URL returned by server.")
end

# ...

There's not a lot of magic here, is there? One call to post_form() hands the data to the server. After that, the answer is pulled from a header of the response (the url of the post). It doesn't get much easier than that.

Here's the last little bit of code that turns all of this into an application:

ruby
# ...

# Parse the command-line and post the content of the input files to
# PasteUrl. Standard input is used if no input files are specified
# or whenever a single dash is specified as input file.
def run
parser.parse!(ARGV)
if opt[:help]
puts parser.help
else
puts paste(URI.parse(PasteUrl), ARGF.read)
end
rescue OptionParser::ParseError => error
puts error
puts parser.help()
end

end

if __FILE__ == $0
Cli.new.run
end

That's as simple as it looks folks. Parse the arguments, then show usage if requested or paste the code. Any argument errors also trigger a usage statement, after the error is shown.

I thought that was a nice example of a feature rich, yet still simple solution.

There are other ways to handle the networking though and I want to look at another solution with a different approach. Here's the start of Aaron Patterson's code:

ruby
# Solution to [QUIZ] cat2rafb (#77)
# By Aaron Patterson
require 'rubygems'
require 'mechanize'
require 'getoptlong'

PASTE_URL = 'http://rafb.net/paste/'
RUBY_URL = 'http://rubyurl.com/'

# Get options
parser = GetoptLong.new
parser.set_options( ['--lang', '-l', GetoptLong::OPTIONAL_ARGUMENT],
['--nick', '-n', GetoptLong::OPTIONAL_ARGUMENT],
['--desc', '-d', GetoptLong::OPTIONAL_ARGUMENT],
['--cvt_tabs', '-t', GetoptLong::OPTIONAL_ARGUMENT]
)
opt_hash = {}
parser.each_option { |name, arg| opt_hash[name.sub(/^--/, '')] = arg }

# ...

Here GetoptLong is used for the interface and WWW::Mechanize is loaded for the networking. We will get to the networking in a bit, but above we have the option code. Basically GetoptLong is told of the options, and then they can be iterated over and collected into a Hash. This version does not validate the choices though.

Next we need the text to paste:

ruby
# ...

# Get the text to be uploaded
buffer = String.new
if ARGV.length > 0
ARGV.each { |f| File.open(f, "r") { |file| buffer << file.read } }
else
buffer = $stdin.read
end

# ...

What does this do? Treat all arguments as files and slurp their contents into a buffer, or read from $stdin if no files were given. Anyone for a round of golf? Ruby has a special input object for this exact purpose and with it you can collapse the above to a simple one line assignment. Try to come up with the answer, then check your solution by glancing back at how Stefano read the input.

Finally, we are ready for some WWW::Mechanize code:

ruby
# ...

agent = WWW::Mechanize.new

# Get the Paste() page
page = agent.get(PASTE_URL)
form = page.forms.first
form.fields.name('text').first.value = buffer

# Set all the options
opt_hash.each { |k,v| form.fields.name(k).first.value = v }

# Submit the form
page = agent.submit(form)
text_url = page.uri.to_s

# Submit the link to RUBY URL
page = agent.get(RUBY_URL)
form = page.forms.first
form.fields.name('rubyurl[website_url]').first.value = text_url
page = agent.submit(form)
puts page.links.find { |l| l.text == l.href }.href

If you haven't seen WWW::Mechanize in action before, I hope you are suitably impressed by this. The library is basically a code based browser. You load pages, fill out forms, and submit your answers just as you would with your browser.

You can see that this code also filters the results through RubyURL. With WWW::Mechanize you even have access to great iterators for the tags as we see here. Check out that final find() of the link, for example.

If you need to walk some web pages, WWW::Mechanize is definitely worth a look.

My thanks to the quiz creator for a wonderful automation problem and to all the solvers for their great examples of how simple something like this can be.

Starting tomorrow we have two weeks of Ross Bamford problems, and trust me, they are good stuff...