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:
#!/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:
# ...
# 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?
# ...
# 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:
# ...
# 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:
# Solution to [QUIZ] cat2rafb (#77)
# By Aaron P.
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:
# ...
# 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:
# ...
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 B. problems, and trust
me, they
are good stuff…