Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically creating a multi-dimensional hash in Ruby

Tags:

ruby

hash

I'm a PHP developer who's trying to gain some proficiency in Ruby. One of the projects I'm cutting my teeth on now is a source-code auditing tool that scans webapp files for potentially dangerous functions in several web programming languages. When matches are found, the script saves the relevant information in a poi (point-of-interest) class for display later on.

An example instance of that class would look something like this (modeled in YAML):

poi:
    file_type: "php"
    file: "the-scanned-file.php"
    line_number: 100
    match: "eval()"
    snippet: "echo eval()"

On display, I want to organize these points of interest like so:

- file_type
-- file
--- match (the searched payload)

Thus, before presentation, I'm trying to structure a flat array of poi objects into a hash mirroring the structure above. This will allow me to simply iterate over the items in the hash to produce the desired on-screen organization. (Or at least, that's the plan.)

And now, for my question: how do I do that in Ruby?

In PHP, I could do something like this really easily:

<?php

$sorted_pois = array();
foreach($points_of_interest as $point){
    $sorted_pois[$point->file_type][$point->file][$point->match][] = $point;
}

?>

I've tried translating that thought from PHP to Ruby like this, but to no avail:

sorted_pois = {}
@points_of_interest.each_with_index do |point, index|
    sorted_pois[point.file_type.to_sym][point.file.to_sym][point.match.to_sym].push point
end

I've spent a few hours on this, and I'm kind of banging my head against the wall at this point, so presumably I'm way off-base. What's the proper way to handle this in Ruby?

Update:

For reference, this is the precise method I have defined:

# sort the points of interest into a structured hash
def sort
  sorted_pois = {}
  @points_of_interest.each_with_index do |point, index|
    sorted_pois[point.file_type.to_sym][point.file.to_sym][point.match.to_sym].push point
  end
end

This is the error I receive when I run the code:

./lib/models/vulnscanner.rb:63:in `sort': undefined method `[]' for nil:NilClass (NoMethodError)
    from /usr/lib/ruby/1.8/rubygems/custom_require.rb:31:in `each_with_index'
    from ./lib/models/vulnscanner.rb:62:in `each'
    from ./lib/models/vulnscanner.rb:62:in `each_with_index'
    from ./lib/models/vulnscanner.rb:62:in `sort'
    from ./webapp-vulnscan:69

Line 62 (as you can likely infer) is this line in particular:

@points_of_interest.each_with_index do |point, index|

As an additional reference, here's what (a snippet of) @points_of_interest looks like when converted to YAML:

- !ruby/object:PoI 
  file: models/couponkimoffer.php
  file_type: php
  group: :dangerous_functions
  line_number: "472"
  match: `
  snippet: ORDER BY `created_at` DESC
- !ruby/object:PoI 
  file: models/couponkimoffer.php
  file_type: php
  group: :dangerous_functions
  line_number: "818"
  match: `
  snippet: WHERE `company_slug` = '$company_slug'
- !ruby/object:PoI 
  file: models/couponkimoffer.php
  file_type: php
  group: :dangerous_functions
  line_number: "819"
  match: `
  snippet: ORDER BY `created_at` DESC
like image 808
Chris Allen Lane Avatar asked Apr 20 '12 20:04

Chris Allen Lane


People also ask

How do you create multiple hashes in Ruby?

With modern Ruby, you wont even have to use merge unless you need to change the variable in place using the ! variant, you can just double splat (**) your way through. h = { a: 1 } h2 = { b: 2 } h3 = { c: 3 } merged_hash = { **h, **h2, **h3 } => { a: 1, b: 2, c:3 }

How do you create a Hash in Ruby?

In Ruby you can create a Hash by assigning a key to a value with => , separate these key/value pairs with commas, and enclose the whole thing with curly braces.

What is Hash map in Ruby?

A Hash is a collection of key-value pairs like this: "employee" = > "salary". It is similar to an Array, except that indexing is done via arbitrary keys of any object type, not an integer index.

What is Hash function in Ruby?

In Ruby, Hash is a collection of unique keys and their values. Hash is like an Array, except the indexing is done with the help of arbitrary keys of any object type. In Hash, the order of returning keys and their value by various iterators is arbitrary and will generally not be in the insertion order.


2 Answers

@John's Enumerable#group_by suggestion is one good way to solve your needs. Another would be to create an auto-vivifying Hash (like you appear to have in PHP) like so:

hash = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
hash[:a][:b][:c] = 42
p hash
#=> {:a=>{:b=>{:c=>42}}}

Note that this sort of auto-vivification can be 'dangerous' if you access keys that don't exist, as it creates them for you:

p hash["does this exist?"]
#=> {}

p hash
#=> {:a=>{:b=>{:c=>42}}, "does this exist?"=>{}}

You can still use the vivifying default_proc without hitting this danger if you use key? to test for the key first:

val = hash["OH NOES"] if hash.key?("OH NOES")
#=> nil

p hash
#=> {:a=>{:b=>{:c=>42}}, "does this exist?"=>{}}

FWIW, the error you are getting says, "Hey, you put [] after something that evaluated to nil, and nil doesn't have a [] method." Specifically, your code...

sorted_pois[point.file_type.to_sym]

evaluated to nil (because the hash did not yet have a value for this key) and then you attempted to ask for

nil[point.file.to_sym]
like image 80
Phrogz Avatar answered Oct 30 '22 10:10

Phrogz


You might be interested in group_by.

Sample usage:

birds = ["Golden Eagle", "Gyrfalcon", "American Robin",
         "Mountain BlueBird", "Mountain-Hawk Eagle"]
grouped_by_first_letter = birds.group_by { |s| s[0] }

# { "G"=>["Golden Eagle", "Gyrfalcon"], "A"=>["American Robin"],
#   "M"=>["Mountain BlueBird", "Mountain-Hawk Eagle"] }
like image 36
John Ledbetter Avatar answered Oct 30 '22 09:10

John Ledbetter