Net::Netrc request for comments

Hi,

I'm a Perl-er learning ruby. I'm creating a ruby port of Perl's Net::Netrc
module for accessing ftp(1)'s .netrc file. I couldn't find an existing ruby
version anywhere.

My current code is below. I've essentially ported the Perl module's usage
semantics, although my internal implementation is quite different. I'd sure
appreciate any feedback, especially related to doing things the "ruby way",
with a view toward submitting this code to the appropriate archive site(s)
at some point.

Example usage:

  require 'net/netrc'

  n = Net::Netrc.locate('example.com')
  if (n.nil?)
    puts "No entry found"
  else
    puts "login = #{n.login}, password = #{n.password}"
  end

Questions:

1. Should I use locate() instead of new()? locate() is how the Perl
module works.

2. Should I return nil if no entry is found, or should I return an
object with the accessors all nil?

3. Should I be returning a Net::Netrc object at all, or just a simple
Hash? Or should Net::Netrc sublcass Hash?

4. Is rasing a SecurityError appropriate? Should I create a specific
exception class instead?

------- BEGIN CODE --------

module Net

  class Netrc

    attr_accessor :machine, :login, :password, :account

    # returns name of .netrc file
    def Netrc.rcname
      # TODO: cross platform? getpwuid() for home dir?
      home = ENV['HOME']
      home ||= ENV['HOMEDRIVE'] + (ENV['HOMEPATH'] || '') if
ENV['HOMEDRIVE']
      File.join(home, '.netrc')
    end

    # opens .netrc file, returning File object if successful.
    # returns nil if .netrc not found.
    # raises SecurityError if .netrc is not owned by the current.
    # user or if it is readable or writable by other than the
    # current user.
    def Netrc.open
      name = rcname
      return nil unless File.exist?(name)
      # TODO: this stat code not applicable to Win32 (and others?)
      s = File.stat(name)
      raise SecurityError, "Not owner: #{name}" unless s.owned?
      raise SecurityError, "Bad permissions: #{name}" if s.mode & 077 != 0
      File.open(name, 'r')
    end

    # given a machine name, returns a Net::Netrc object containing
    # the matching entry for that name, or the default entry. If
    # no match is found an no default entry exists, nil is returned.
    def Netrc.locate(mach)
      f = open or return nil
      entry = nil
      key = nil
      inmacdef = false
      while line = f.gets
        if inmacdef
          inmacdef = false if line.strip.empty?
          next
        end
        toks = line.scan(/"((?:\\.|[^"])*)"|((?:\\.|\S)+)/).flatten.compact
        toks.each { |t| t.gsub!(/\\(.)/, '\1') }
        while toks.length > 0
          tok = toks.shift
          if key
            entry = new if key == 'machine' && tok == mach
            entry.send "#{key}=", tok if entry
            key = nil
          end
          case tok
          when 'default'
            return entry if entry
            entry = new
          when 'machine'
            return entry if entry
            key = 'machine'
          when 'login', 'password', 'account'
            key = tok
          end
        end
      end
      entry
    end

  end

end

------- END CODE --------

TIA for any feedback,

Bob

Hi,

At Fri, 23 Sep 2005 00:54:51 +0900,
Bob Showalter wrote in [ruby-talk:157128]:

I'm a Perl-er learning ruby. I'm creating a ruby port of Perl's Net::Netrc
module for accessing ftp(1)'s .netrc file. I couldn't find an existing ruby
version anywhere.

I'm using <http://www.rubyist.net/~nobu/ruby/netrc.rb&gt;\.

···

--
Nobu Nakada

Hi,

I'm a Perl-er learning ruby. I'm creating a ruby port of Perl's
Net::Netrc module for accessing ftp(1)'s .netrc file. I couldn't find
an existing ruby version anywhere.

My current code is below. I've essentially ported the Perl module's
usage semantics, although my internal implementation is quite
different. I'd sure appreciate any feedback, especially related to
doing things the "ruby way", with a view toward submitting this code
to the appropriate archive site(s) at some point.

Example usage:

require 'net/netrc'

n = Net::Netrc.locate('example.com')
if (n.nil?)
   puts "No entry found"
else
   puts "login = #{n.login}, password = #{n.password}"
end

Questions:

1. Should I use locate() instead of new()? locate() is how the Perl
module works.

I'd use locate only and probably make new private. Reason is that locate will not always return a valid object.

2. Should I return nil if no entry is found, or should I return an
object with the accessors all nil?

Return nil

3. Should I be returning a Net::Netrc object at all, or just a simple
Hash? Or should Net::Netrc sublcass Hash?

Definitely not the latter. I'd probably use an instance of Net::Netrc and add some methods (who says the Ruby version must not be better than the Perl version)? For example, I'd add a method that opens a connection with the info you have:

class Netrc
  def open(do_init = true)
    conn = Ftp.open machine
    begin
      conn.login login, password
      # add code that executes all default actions if defined, maybe argument flag controlled
    rescue Net::FTPPermError
       conn.close
       raise
    end

    if block_given?
      begin
        yield conn
      ensure
        conn.close
      end

      nil
    else
      conn
    end
  end
end

Then you can do

n = Net::Netrc.locate('example.com')

if n
  n.open do |ftp|
    files = ftp.chdir('pub/lang/ruby/contrib')
    files = ftp.list('n*')
    ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
  end
else
  puts "not found"
end

You could even make this more convenient by adding a class method open that does this

class Netrc
  def self.open(machine, do_init = true, &b)
    n = locate machine
    raise "error" unless n
    n.open(do_init, &b)
  end
end

Then you can do

Net::Netrc.open('ftp.foo.bar') do |ftp|
  ...
end

Uh, just detected a name clash. I'd rename your open as this seems to be a rather internal method.

4. Is rasing a SecurityError appropriate? Should I create a specific
exception class instead?

I think it's appropriate. However, it seems to me that if you want to mimic FTP's behavior then you should raise the exception only if you find a password:

------- BEGIN CODE --------

module Net

class Netrc

   attr_accessor :machine, :login, :password, :account

   # returns name of .netrc file
   def Netrc.rcname
     # TODO: cross platform? getpwuid() for home dir?
     home = ENV['HOME']
     home ||= ENV['HOMEDRIVE'] + (ENV['HOMEPATH'] || '') if
ENV['HOMEDRIVE']
     File.join(home, '.netrc')
   end

   # opens .netrc file, returning File object if successful.
   # returns nil if .netrc not found.
   # raises SecurityError if .netrc is not owned by the current.
   # user or if it is readable or writable by other than the
   # current user.
   def Netrc.open
     name = rcname
     return nil unless File.exist?(name)
     # TODO: this stat code not applicable to Win32 (and others?)
     s = File.stat(name)
     raise SecurityError, "Not owner: #{name}" unless s.owned?
     raise SecurityError, "Bad permissions: #{name}" if s.mode & 077
     != 0 File.open(name, 'r')
   end

   # given a machine name, returns a Net::Netrc object containing
   # the matching entry for that name, or the default entry. If
   # no match is found an no default entry exists, nil is returned.
   def Netrc.locate(mach)
     f = open or return nil
     entry = nil
     key = nil
     inmacdef = false
     while line = f.gets
       if inmacdef
         inmacdef = false if line.strip.empty?
         next
       end
       toks =
       line.scan(/"((?:\\.|[^"])*)"|((?:\\.|\S)+)/).flatten.compact
       toks.each { |t| t.gsub!(/\\(.)/, '\1') } while toks.length > 0

# what do you need that "while toks.length > 0" for? Seems completely superfluous to me. Or is this an indentation problem and the "while" should have been on the next line? Hmm, probably...

I'd probably do the parsing a bit different: I would have to think about this a bit more but I'd keep a set of settings for default and a set for the machine and directly return if I found the machine. Just a rough idea...

         tok = toks.shift
         if key
           entry = new if key == 'machine' && tok == mach
           entry.send "#{key}=", tok if entry
           key = nil
         end
         case tok
         when 'default'
           return entry if entry
           entry = new
         when 'machine'
           return entry if entry
           key = 'machine'
         when 'login', 'password', 'account'
           key = tok
         end
       end
     end
     entry
   end

end

end

------- END CODE --------

TIA for any feedback,

Bob

You're welcome!

Kind regards

    robert

···

Bob Showalter <Bob_Showalter@taylorwhite.com> wrote:

"Internal Server Error" - Hm...

    robert

···

nobu.nokada@softhome.net wrote:

Hi,

At Fri, 23 Sep 2005 00:54:51 +0900,
Bob Showalter wrote in [ruby-talk:157128]:

I'm a Perl-er learning ruby. I'm creating a ruby port of Perl's
Net::Netrc module for accessing ftp(1)'s .netrc file. I couldn't
find an existing ruby version anywhere.

I'm using <http://www.rubyist.net/~nobu/ruby/netrc.rb&gt;\.

Hi,

At Fri, 23 Sep 2005 01:46:40 +0900,
Robert Klemme wrote in [ruby-talk:157135]:

> I'm using <http://www.rubyist.net/~nobu/ruby/netrc.rb&gt;\.

"Internal Server Error" - Hm...

Hmmm, I can't stop it from seeing .rb as CGI. Try
<http://www.rubyist.net/~nobu/ruby/netrc_rb.txt&gt;

···

--
Nobu Nakada

Thanks, I can see it now. Looks like you subclassed Hash. I will study your code more. Your parsing logic seems much more complex than mine; perhaps I'm missing something.

I note that your technique is similar to the Perl module in that you load the entire file into a hash. But this conflicts with the documentation in that once a matching "machine" entry is found or the "default" entry is found, parsing stops when the next "machine" or "default" entry (or eof) is found.

So if I have:

   machine foo ...
   default ...
   machine bar ...

ftp(1) will not see the "bar" entry (but instead would return the default login), while your code (and the Perl code) will return the "bar" entry.

···

nobu.nokada@softhome.net wrote:

Hi,

At Fri, 23 Sep 2005 01:46:40 +0900,
Robert Klemme wrote in [ruby-talk:157135]:

I'm using <http://www.rubyist.net/~nobu/ruby/netrc.rb&gt;\.

"Internal Server Error" - Hm...

Hmmm, I can't stop it from seeing .rb as CGI. Try
http://www.rubyist.net/~nobu/ruby/netrc_rb.txt

Hi,

At Fri, 23 Sep 2005 09:52:06 +0900,
Bob Showalter wrote in [ruby-talk:157201]:

I note that your technique is similar to the Perl module in that you load
the entire file into a hash. But this conflicts with the documentation in
that once a matching "machine" entry is found or the "default" entry is
found, parsing stops when the next "machine" or "default" entry (or eof) is
found.

Thank you, I haven't noticed it. But is that behaviors better?

···

--
Nobu Nakada