Porting a small Perl script to Ruby

Dear Ruby experts

I have written this [1] small PoC Perl script, which works, so now I
am ready to port it to Ruby.

In Perl I used functions, but that is of course not the Ruby way.

It will be my first Ruby script on my own, so can anyone help me
getting the classes/over all structure correct from the beginning?

[1]: http://pastebin.com/Qs2ReBjG

Hugs,
Jasmine =)

Ruby was influenced by Perl (and Smalltalk, CLU, …) so I'd suggest going for a direct "translation" first. This would give you some exposure to the various individual classes that you'll need for parts of your script.

Here are some hints:

* Nokogiri (鋸) is an HTML, XML, SAX, and Reader parser with XPath and CSS selector support. http://nokogiri.org/
* you'll also learn about gems if you haven't already
* YAML is the same, of course, but this might help: YAML.rb is YAML for Ruby | Cookbook
* yaml is part of the Ruby Standard Library meaning that it is part of ruby, but you have to require 'yaml' to use it. Index of Classes & Methods in yaml: Ruby Standard Library Documentation (Ruby 2.3.1)
* You can dive right in and use Net::HTTP (also from the Standard Library), but you might find it easier to use a gem with a nicer API. The Ruby Toolbox list *many* alternatives: Category: HTTP clients - The Ruby Toolbox
* you could also go with 'open-uri' and Nokogiri directly:
   Parsing an HTML/XML document - Nokogiri

When you get something "which works" in Ruby, let us know how it went and you can get some advice on your Ruby code.

-Rob

···

On 2016-Aug-4, at 10:52 , Jasmine Lognnes <princess.jasmine.lognnes@gmail.com> wrote:

Dear Ruby experts

I have written this [1] small PoC Perl script, which works, so now I
am ready to port it to Ruby.

In Perl I used functions, but that is of course not the Ruby way.

It will be my first Ruby script on my own, so can anyone help me
getting the classes/over all structure correct from the beginning?

[1]: http://pastebin.com/Qs2ReBjG

Hugs,
Jasmine =)

+1 for porting directly to Ruby as is, with functions, at least first.

Ruby does not HAVE to be OOP. Functions are fine ... for small amounts of code, at least.

···

-----Original Message-----
From: ruby-talk [mailto:ruby-talk-bounces@ruby-lang.org] On Behalf Of Jasmine
Lognnes
Sent: 04 August 2016 3:52 pm
To: ruby-talk@ruby-lang.org
Subject: Porting a small Perl script to Ruby

Dear Ruby experts

I have written this [1] small PoC Perl script, which works, so now I
am ready to port it to Ruby.

In Perl I used functions, but that is of course not the Ruby way.

It will be my first Ruby script on my own, so can anyone help me
getting the classes/over all structure correct from the beginning?

[1]: http://pastebin.com/Qs2ReBjG

Hugs,
Jasmine =)

Unsubscribe: <mailto:ruby-talk-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk&gt;

Click here to view Company Information and Confidentiality Notice.<http://www.jameshall.co.uk/index.php/small-print/email-disclaimer&gt;

Hi again =)

And thanks for the great advise and how to get started!

I have now done as you said and ported it to Ruby and polished it to
the point where I can't do a better job on my own.

So can you take a look at the script again and give feedback on what
should be done differently when dealing with Ruby?

http://pastebin.com/xAUUQesA

If this is not a good script to OOP, can it still be done for teaching purposes?

Wanted to use nokogiri but I couldn't figure out the API...

Hugs,
Jasmine =)

···

On 4 August 2016 at 18:10, Andy Jones <Andy.Jones@jameshall.co.uk> wrote:

+1 for porting directly to Ruby as is, with functions, at least first.

Ruby does not HAVE to be OOP. Functions are fine ... for small amounts of code, at least.

-----Original Message-----
From: ruby-talk [mailto:ruby-talk-bounces@ruby-lang.org] On Behalf Of Jasmine
Lognnes
Sent: 04 August 2016 3:52 pm
To: ruby-talk@ruby-lang.org
Subject: Porting a small Perl script to Ruby

Dear Ruby experts

I have written this [1] small PoC Perl script, which works, so now I
am ready to port it to Ruby.

In Perl I used functions, but that is of course not the Ruby way.

It will be my first Ruby script on my own, so can anyone help me
getting the classes/over all structure correct from the beginning?

[1]: http://pastebin.com/Qs2ReBjG

Hugs,
Jasmine =)

Unsubscribe: <mailto:ruby-talk-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk&gt;

Click here to view Company Information and Confidentiality Notice.<http://www.jameshall.co.uk/index.php/small-print/email-disclaimer&gt;

Unsubscribe: <mailto:ruby-talk-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk&gt;

Hi again =)

And thanks for the great advise and how to get started!

I have now done as you said and ported it to Ruby and polished it to
the point where I can't do a better job on my own.

So can you take a look at the script again and give feedback on what
should be done differently when dealing with Ruby?

Not just Ruby, but programming generally.

Some comments in the code would go a long way toward helping someone understand what you're really expecting to happen.

Two specific examples would be the contents that you'd expect to find in config.yaml and the expected structure of the returned XML.

You should also look at open-uri, Net::HTTPS, or an HTTP client to simplify the string and command construction in get() and get_groups().

Stop using single-letter variables: a, b, c, e, h, r, v

Hash#key? is now preferred over Hash#has_key?

There's no reason to make $config be global. Coming from Perl, you may have to let go of the many sigils.

Some Ruby "idioms" for you:

users = Hash.new {|h,k| h[k] = Hash.new &h.default_proc }
-- Has the same effect (I think) that you're getting with hash_tree
-- When arguments have well-known usage AND the use is a single(few) line(s), single letter local variables are OK. (Using |hash, key| is also common.)

"#{prefix}/stuff"
-- the #{} is Ruby's string interpolation where the inner part is just ruby and if it isn't a string (i.e., doesn't result in a string when evaluated), will call to_s on the object to ask it for a string representation
-- "a/#{b}/c" is also more efficient than "a/" + b + "/c"

http://pastebin.com/xAUUQesA

If this is not a good script to OOP, can it still be done for teaching purposes?

Well, it's probably not right for OOP, but if you want to go there, I'd say expose some objects for User and Group and probably put the get and get_groups functions inside an object that exposes a repository-like API. Initialize that with $config[ARGV[0]] and expose a couple methods to iterate over all the groups (probably best to call this `each` and then look up the Enumerable module's documentation). If a Group knows how to iterate over its Users, I think that your overall script will become much simpler.

-Rob

···

On 2016-Aug-8, at 17:52 , Jasmine Lognnes <princess.jasmine.lognnes@gmail.com> wrote:

Wanted to use nokogiri but I couldn't figure out the API...

Hugs,
Jasmine =)

On 4 August 2016 at 18:10, Andy Jones <Andy.Jones@jameshall.co.uk> wrote:

+1 for porting directly to Ruby as is, with functions, at least first.

Ruby does not HAVE to be OOP. Functions are fine ... for small amounts of code, at least.

-----Original Message-----
From: ruby-talk [mailto:ruby-talk-bounces@ruby-lang.org] On Behalf Of Jasmine
Lognnes
Sent: 04 August 2016 3:52 pm
To: ruby-talk@ruby-lang.org
Subject: Porting a small Perl script to Ruby

Dear Ruby experts

I have written this [1] small PoC Perl script, which works, so now I
am ready to port it to Ruby.

In Perl I used functions, but that is of course not the Ruby way.

It will be my first Ruby script on my own, so can anyone help me
getting the classes/over all structure correct from the beginning?

[1]: http://pastebin.com/Qs2ReBjG

Hugs,
Jasmine =)

Unsubscribe: <mailto:ruby-talk-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk&gt;

Click here to view Company Information and Confidentiality Notice.<http://www.jameshall.co.uk/index.php/small-print/email-disclaimer&gt;

Unsubscribe: <mailto:ruby-talk-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk&gt;

Unsubscribe: <mailto:ruby-talk-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk&gt;

So can you take a look at the script again and give feedback on what
should be done differently when dealing with Ruby?

For what it's worth, and in my opinion only:

* Everything should be a function. The code should end with a short block (ideally one statement) that invokes the "main routine" function. This makes the code *much* easier to read. If you can make all the functions have a clear purpose and have few side effects, it will also make testing much easier.

* get_groups() is a bit long. It would be better if you broke it up with smaller functions.

* get_groups() returns a value OR outputs text. It would be better if it did only the former. Put all the user interaction in one place; it will make the rest of the code easier to debug.

* You access ARGV in a number of places. It would be less confusing if you parsed out ARGV in one place, and passed the resulting values around as parameters.

If this is not a good script to OOP, can it still be done for teaching
purposes?

Any problem can be expressed in OOP. For example you might have a class to deal with the HTML parsing, a class for dealing with the config file, and another class or module to control it all. It might be a better program that way; it might not. Only one way to know for sure, really...

Click here to view Company Information and Confidentiality Notice.<http://www.jameshall.co.uk/index.php/small-print/email-disclaimer&gt;

Some comments in the code would go a long way toward helping someone understand what you're really expecting to happen.

Two specific examples would be the contents that you'd expect to find in config.yaml and the expected structure of the returned XML.

That ends up being my biggest issue when I go back to read old
scripts. Would you offer the 10 lines a data structure would take from
ap ? Or compress it in a not directly readable format?

You should also look at open-uri, Net::HTTPS, or an HTTP client to simplify the string and command construction in get() and get_groups().

I can't quite figure out how to give it the header information. If I do

doc = Nokogiri::HTML(open(url, "X-appname" => "xxx", "X-token" => "xxx"))

then I get

/usr/share/ruby/open-uri.rb:261:in `open_http': userinfo not
supported. [RFC3986] (ArgumentError)

What am I doing wrong?

Stop using single-letter variables: a, b, c, e, h, r, v

Done.

Hash#key? is now preferred over Hash#has_key?

Done.

There's no reason to make $config be global. Coming from Perl, you may have to let go of the many sigils.

I couldn't make it work. Here is my test case which fails

c = 1
def func
  puts c
end
func

users = Hash.new {|h,k| h[k] = Hash.new &h.default_proc }
-- Has the same effect (I think) that you're getting with hash_tree

Very nice! =)

Why is it that Ruby doesn't allow creating nested hashes on-the-fly
like Perl does? Performance? Safety?

-- When arguments have well-known usage AND the use is a single(few) line(s), single letter local variables are OK. (Using |hash, key| is also common.)

Ok.

"#{prefix}/stuff"
-- the #{} is Ruby's string interpolation where the inner part is just ruby and if it isn't a string (i.e., doesn't result in a string when evaluated), will call to_s on the object to ask it for a string representation
-- "a/#{b}/c" is also more efficient than "a/" + b + "/c"

That really made the lines a lot more readable!

Well, it's probably not right for OOP, but if you want to go there, I'd say expose some objects for User and Group and probably put the get and get_groups functions inside an object that exposes a repository-like API. Initialize that with $config[ARGV[0]] and expose a couple methods to iterate over all the groups (probably best to call this `each` and then look up the Enumerable module's documentation). If a Group knows how to iterate over its Users, I think that your overall script will become much simpler.

Thanks. That should be interesting!

* Everything should be a function. The code should end with a short block (ideally one statement) that invokes the "main routine" function. This makes the code *much* easier to read. If you can make all the functions have a clear purpose and have few side effects, it will also make testing much easier.

I will try that. I think that is also often done in C.

* get_groups() is a bit long. It would be better if you broke it up with smaller functions.

I'll do that.

* get_groups() returns a value OR outputs text. It would be better if it did only the former. Put all the user interaction in one place; it will make the rest of the code easier to debug.

There were suppose to be an "exit 0" in there, as it is a critical
error, if it goes into the "else" state. Would you still let the
function return the error and not "puts" it?

* You access ARGV in a number of places. It would be less confusing if you parsed out ARGV in one place, and passed the resulting values around as parameters.

Done.

Any problem can be expressed in OOP. For example you might have a class to deal with the HTML parsing, a class for dealing with the config file, and another class or module to control it all. It might be a better program that way; it might not. Only one way to know for sure, really...

I'll post how it turned out =)

There were suppose to be an "exit 0" in there, as it is a critical
error, if it goes into the "else" state. Would you still let the
function return the error and not "puts" it?

Personally, I would definitely let it throw an error. If that feels wrong to you, you can always have it return nil to indicate something went wrong.

I would have a main routine which dealt with all the inputs and outputs, with a rescue block to trap all the errors. That way you can test all the other functions independently of the program as whole.

Click here to view Company Information and Confidentiality Notice.<http://www.jameshall.co.uk/index.php/small-print/email-disclaimer&gt;

* get_groups() returns a value OR outputs text. It would be better if it did only the former. Put all the user interaction in one place; it will make the rest of the code easier to debug.

There were suppose to be an "exit 0" in there, as it is a critical

Exit 0 signals "everything is OK" so you would not want to do that for
a critical error.

error, if it goes into the "else" state. Would you still let the
function return the error and not "puts" it?

I think the point Andy wants to make is that the function should do
their job and either report status via return value OR throw an
exception. If it is reported via return value, calling code can still
chose to invoke abort("meaningful error message"). But do not do user
facing error handling inside the function because that limits
usability. In this case an exception seems appropriate, next best
would be a nil return.

Kind regards

robert

···

On Tue, Aug 9, 2016 at 11:03 AM, Jasmine Lognnes <princess.jasmine.lognnes@gmail.com> wrote:

--
[guy, jim, charlie].each {|him| remember.him do |as, often| as.you_can
- without end}
http://blog.rubybestpractices.com/

I think the point Andy wants to make is that the function should do
their job and either report status via return value OR throw an
exception. If it is reported via return value, calling code can still
chose to invoke abort("meaningful error message"). But do not do user
facing error handling inside the function because that limits
usability. In this case an exception seems appropriate, next best
would be a nil return.

Good point. I'll change that.

Would you change the functions, so they all return a hash with exit
value and the output?

Personally, I would definitely let it throw an error. If that feels wrong to you, you can always have it return nil to indicate something went wrong.

I would have a main routine which dealt with all the inputs and outputs, with a rescue block to trap all the errors. That way you can test all the other functions independently of the program as whole.

Very nice! I'll cook something up for review =)