Cat2rafb (#77)

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…

I suppose Sean C.'s solution came in too late for the summary, but I
think it’s definitely worth looking at for its terseness. Here’s a bonus
summary.

#!/usr/bin/ruby
require ‘uri’
require ‘net/http’

So Sean’s using a couple of libraries like the other solutions.
Actually, just one, really: net/http. You don’t need to explicitly
require ‘uri’; net/http requires it. No command-line switches here.

Watch as a complete solution unfolds in just three lines. You can
probably read it just as well without my commentary.

result = Net::HTTP.post_form(URI.parse(‘http://rafb.net/paste/paste.php’),
{:text => ARGF.readlines,
:nick => ‘paste user’,
:lang => ‘Ruby’})

Line one uses Net::HTTP.post_form to send a form-style POST request and
get back the result. post_form takes two parameters, the target URI, and
a hash of form data.

The value of the form variable “text” comes from ARGF (like Stefano
Taschini’s solution and others) which will get input from filenames
given as command-line arguments or from standard input.

After one line, we have the paste URL that the quiz wants in
result[‘location’], but Sean’s going for bonus points!

result = Net::HTTP.get_response ‘rubyurl.com’,
‘/rubyurl/remote?website_url=http://rafb.net’ + result[‘location’]

RubyURL provides a URL specifically to be used in other ways than
through its form: “http://rubyurl.com/rubyurl/remote”. Line two uses
Net::HTTP.get_response to send a GET request to this location, passing
as the website_url the URL from line one’s result.

puts result[‘location’].sub(‘rubyurl/show/’, ‘’)

The result from line two is an HTTP redirect with a Location like
http://rubyurl/rubyurl/show/xyz”. This page displays the new RubyURL
that’s been made, but we don’t want that, we want the RubyURL itself.
That can be obtained just by removing the middle bit of the URL.

And we’re done. Too easy!

Cheers,
Dave

On 5/5/06, Dave B. [email protected] wrote:

require ‘uri’; net/http requires it. No command-line switches here.
get back the result. post_form takes two parameters, the target URI, and
a hash of form data.

The value of the form variable “text” comes from ARGF (like Stefano
Taschini’s solution and others) which will get input from filenames
given as command-line arguments or from standard input.

Yes, Sean’s RubyGolf implementation is very good, and plus. it showed
the essence of problem space in about 1 line of code. A lot of the
other solutions (even mine!) went to some expense setting up variable
options, error handing, well factored code, etc. But the goal of the
quiz was to paste the input to rafb.net and get the URL back. Which
his does elegantly.

Not that the other solutions were not good. They showed how to write
idiomatic ruby to solve this general class of problems, and I learned
a lot from reading them, which is the goal of the quiz in general.

Sean freely admits his approach was “slash and burn farming,” and it
wouldn’t work if you wanted to paste “Plain Text” or some other
language. I say, however, that the nopaste website and using it with
IRC indicates a simple solution. If you were pairing or collaborating
over the net and wanted something like this and you didn’t have it,
you could code it in Ruby in a very few minutes.

BTW, we (Sean, Craig Bucheck and I,) worked on this as this week’s
StL.rb (St. Louis Ruby group) weekly hacking night project. Ruby group
members across the land are meeting on the same weeknight as the
regular meeting to participate in Ruby code pair-programming or
working on common problems for general edification. The RubyQuiz which
comes out about once a week makes for a good topic. Try it out in your
own HackNight.

Cheers,
Dave

Till later,
Ed

On 5/5/06, Ed Howland [email protected] wrote:

Actually, just one, really: net/http. You don’t need to explicitly
Line one uses Net::HTTP.post_form to send a form-style POST request and
options, error handing, well factored code, etc. But the goal of the
IRC indicates a simple solution. If you were pairing or collaborating

Cheers,
Dave

Till later,
Ed

I forgot to link to the code for my solution, actually. Doh!
Running site: http://rpaste.com/
Code: http://svn.supremetyrant.com/paste/
Code LOC: 66 Test LOC: 75 Code to Test Ratio: 1:1.1