[SUMMARY] 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 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:

  # ...
  
  # 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 Bamford problems, and trust me, they
are good stuff...

I suppose Sean Carley'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&#39;\),
                            {: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&quot;\. 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&quot;\. 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

I suppose Sean Carley'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&#39;\),
> {: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.

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, Dave Burt <dave@burt.id.au> wrote:

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

···

On 5/5/06, Ed Howland <ed.howland@gmail.com> wrote:

On 5/5/06, Dave Burt <dave@burt.id.au> wrote:
> I suppose Sean Carley'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&#39;\),
> > {: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.

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