I am working in Chef, trying to create/populate a ruby hash with networking device information, as populated by nmcli. I think the code is correct, as VS Code isn't complaining, and it seems to run just fine in chef-shell -z but I'm not able to query the Ruby Hash as I would expect, and I'm really starting to lose my mind.
Fresh eyes and any expert help here are appreciated, thank you!
interfaces = Hash.new
#DEVICE,TYPE
dev = Mixlib::ShellOut.new("nmcli -terse -field device,type device").run_command.stdout.split(/\n/)
dev.each do |output|
if "#{output.split(":")[1]}" == 'ethernet'
interfaces["ethernet" => "#{output.split(":")[0]}"]
elsif "#{output.split(":")[1]}" == 'wifi'
interfaces["wifi" => "#{output.split(":")[0]}"]
else
Chef::Log.debug("Interface #{output.split(":")} is not supported")
end
end
chef (17.6.18)>
=> ["wlp61s0:wifi", "enp0s31f6:ethernet", "lo:loopback"]
node[interfaces] #nil
node[:interfaces] #nil
node['interfaces'] #nil
node["interfaces"] #nil
When I attempt to edit the code, as suggested by BroiSatse
This line
interfaces["wifi" => "#{output.split(":")[0]}"]
returns the value stored in the hash under the key {"wifi" => "#>{output.split(":")[0]}"}. It does not perform any assignment and most > likely returns nil.
What you need is:
interfaces["wifi"] ="#{output.split(":")[0]}"
So I tried that, but I still get a nil response from the Hash. Here is the Chef output/error:
chef (17.6.18)> interfaces = Hash.new
chef > #DEVICE,TYPE
chef (17.6.18)> dev = Mixlib::ShellOut.new("nmcli -terse -field device,type device").run_command.stdout.split(/\n/)
=> ["wlp61s0:wifi", "enp0s31f6:ethernet", "lo:loopback"]
chef > dev.each do |output|
chef > if "#{output.split(":")[1]}" == 'ethernet'
chef > interfaces["ethernet"] = "#{output.split(":")[0]}"
chef > elsif "#{output.split(":")[1]}" == 'wifi'
chef > interfaces["wifi"] = "#{output.split(":")[0]}"
chef > else
chef > Chef::Log.debug("Interface #{output.split(":")} is not supported")
chef > end
chef (17.6.18)> end
=> ["wlp61s0:wifi", "enp0s31f6:ethernet", "lo:loopback"]
chef (17.6.18)> node[interfaces] #nil
=> nil
chef (17.6.18)> node[:interfaces][:ethernet] #nil
(irb):95:in `<main>': undefined method `[]' for nil:NilClass (NoMethodError)
from /opt/chef/embedded/lib/ruby/gems/3.0.0/gems/chef-17.6.18/lib/chef/shell.rb:93:in `block in start'
from /opt/chef/embedded/lib/ruby/gems/3.0.0/gems/chef-17.6.18/lib/chef/shell.rb:92:in `catch'
from /opt/chef/embedded/lib/ruby/gems/3.0.0/gems/chef-17.6.18/lib/chef/shell.rb:92:in `start'
from /opt/chef/embedded/lib/ruby/gems/3.0.0/gems/chef-bin-17.6.18/bin/chef-shell:31:in `<top (required)>'
from /usr/bin/chef-shell:158:in `load'
from /usr/bin/chef-shell:158:in `<main>'
chef (17.6.18)> node['interfaces'] #nil
chef (17.6.18)> node["interfaces"] #nil
=> nil
chef (17.6.18)>
UPDATE: Monday May 2, 2022 12:08 PST
When I do this command I can see that there is data in the Hash ... but all attempts to actually query the data fail ... I don't know what I'm doing wrong:
chef (17.6.18)> puts "#{interfaces}"
{"wifi"=>"wlp61s0", "ethernet"=>"enp0s31f6"}
=> nil
chef (17.6.18)>
Just call
interfaces["ethernet"]
or
interfaces["wifi"]
interfaces = {}
dev =
Mixlib::ShellOut.new("nmcli -terse -field device,type device").
run_command.
stdout.
lines(chomp: true)
dev.each do |output|
device, type_device = output.split(":")
case type_device
when "ethernet", "wifi"
interfaces[type_device] = device
else
Chef::Log.debug("Interface #{output} is not supported")
end
end
Note: there will be just one key wifi and ethernet. So if you have more devices, just last value will be used
Since you are running this in Chef, you can use automatic attributes collected by Ohai, instead of running command to get its stdout.
Ohai is a tool for collecting system configuration data, which it then provides to Chef Infra Client to use in cookbooks.
Comprehensive details about the machine's network configuration are stored under node['network']['interfaces'] hash.
Try this in your recipe to see what details are captured:
pp node['network']['interfaces']
We can use these automatic attributes in combination with the select filter to get the network device names without looping.
interfaces = Hash.new
# exclude any devices that don't have a "type", such as loopback
ifaces = node['network']['interfaces'].select { |k, v| ! v['type'].nil? }
# select device names for devices of type "en" (ethernet)
interfaces['ethernet'] = ifaces.select { |k, v| v['type'].match(/^en/) }.keys.first
# select device names for devices of type "wl" (wireless/wifi)
interfaces['wifi'] = ifaces.select { |k, v| v['type'].match(/^wl/) }.keys.first
The above code will get the "first" device that matches en or wl respectively. If there are multiple devices of a type, then .first can be removed and the entire list of devices will be available.
The following line of code returns the value stored in the hsh under the key {"wifi" => "#{output.split(":")[0]}"}. It does not perform any assignment and most likely returns nil.
interfaces["wifi" => "#{output.split(":")[0]}"]
What you need is:
interfaces["wifi"] = "#{output.split(":")[0]}"
Related
I have the following hash:
hash = {'name' => { 'Mike' => { 'age' => 10, 'gender' => 'm' } } }
I can access the age by:
hash['name']['Mike']['age']
What if I used Hash#fetch method? How can I retrieve a key from a nested hash?
As Sergio mentioned, the way to do it (without creating something for myself) would be by a chain of fetch methods:
hash.fetch('name').fetch('Mike').fetch('age')
From Ruby 2.3.0 onward, you can use Hash#dig:
hash.dig('name', 'Mike', 'age')
It also comes with the added bonus that if some of the values along the way turned up to be nil, you will get nil instead of exception.
You can use the ruby_dig gem until you migrate.
EDIT: there is a built-in way now, see this answer.
There is no built-in method that I know of. I have this in my current project
class Hash
def fetch_path(*parts)
parts.reduce(self) do |memo, key|
memo[key.to_s] if memo
end
end
end
# usage
hash.fetch_path('name', 'Mike', 'age')
You can easily modify it to use #fetch instead of #[] (if you so wish).
As of Ruby 2.3.0:
You can also use &. called the "safe navigation operator" as: hash&.[]('name')&.[]('Mike')&.[]('age'). This one is perfectly safe.
Using dig is not safe as hash.dig(:name, :Mike, :age) will fail if hash is nil.
However you may combine the two as: hash&.dig(:name, :Mike, :age).
So either of the following is safe to use:
hash&.[]('name')&.[]('Mike')&.[]('age')
hash&.dig(:name, :Mike, :age)
If your goal is to raise a KeyError when any of the intermediate keys are missing, then you need to write your own method. If instead you're using fetch to provide default values for missing keys, then you can circumvent the use of fetch by constructing the Hashes with a default values.
hash = Hash.new { |h1, k1| h1[k1] = Hash.new { |h2, k2| h2[k2] = Hash.new { |h3, k3| } } }
hash['name']['Mike']
# {}
hash['name']['Steve']['age'] = 20
hash
# {"name"=>{"Mike"=>{}, "Steve"=>{"age"=>20}}}
This won't work for arbitrarily nested Hashes, you need to choose the maximum depth when you construct them.
A version that uses a method instead of adding to the Hash class for others using Ruby 2.2 or lower.
def dig(dict, *args)
key = args.shift
if args.empty?
return dict[key]
else
dig(dict[key], *args)
end
end
And so you can do:
data = { a: 1, b: {c: 2}}
dig(data, :a) == 1
dig(data, :b, :c) == 2
If you don't want to monkey patch the standard Ruby class Hash use .fetch(x, {}) variant. So for the example above will look like that:
hash.fetch('name', {}).fetch('Mike', {}).fetch('age')
The point of fetch is that an explicit error is raised at the point of contract violation instead of having to track down a silent nil running amok in the code that can lead to unpredictable state.
Although dig is elegant and useful when you expect nil to be a default, it doesn't offer the same error reporting guarantees of fetch. OP seems to want the explicit errors of fetch but without the ugly verbosity and chaining.
An example use case is receiving a plain nested hash from YAML.load_file() and requiring explicit errors for missing keys.
One option is to alias [] to fetch as shown here, but this isn't a deep operation on a nested structure.
I ultimately used a recursive function and hash.instance_eval {alias [] fetch} to apply the alias to such a plain hash deeply. A class would work just as well, with the benefit of a distinct subclass separate from Hash.
irb(main):001:1* def deeply_alias_fetch!(x)
irb(main):002:2* if x.instance_of? Hash
irb(main):003:2* x.instance_eval {alias [] fetch}
irb(main):004:2* x.each_value {|v| deeply_alias_fetch!(v)}
irb(main):005:2* elsif x.instance_of? Array
irb(main):006:2* x.each {|e| deeply_alias_fetch!(e)}
irb(main):007:1* end
irb(main):008:0> end
=> :deeply_alias_fetch!
irb(main):009:0> h = {:a => {:b => 42}, :c => [{:d => 1, :e => 2}, {}]}
irb(main):010:0> deeply_alias_fetch!(h)
=> {:a=>{:b=>42}, :c=>[{:d=>1, :e=>2}, {}]}
irb(main):011:0> h[:a][:bb]
Traceback (most recent call last):
5: from /usr/bin/irb:23:in `<main>'
4: from /usr/bin/irb:23:in `load'
3: from /usr/lib/ruby/gems/2.7.0/gems/irb-1.2.1/exe/irb:11:in `<top (required)>'
2: from (irb):11
1: from (irb):11:in `fetch'
KeyError (key not found: :bb)
Did you mean? :b
irb(main):012:0> h[:c][0][:e]
=> 2
irb(main):013:0> h[:c][0][:f]
Traceback (most recent call last):
5: from /usr/bin/irb:23:in `<main>'
4: from /usr/bin/irb:23:in `load'
3: from /usr/lib/ruby/gems/2.7.0/gems/irb-1.2.1/exe/irb:11:in `<top (required)>'
2: from (irb):14
1: from (irb):14:in `fetch'
KeyError (key not found: :f)
if you can
use:
hash[["ayy","bee"]]
instead of:
hash["ayy"]["bee"]
it'll save a lot of annoyances
I am trying to geht this script to run: http://dysinger.net/2008/10/13/using-amazon-ec2-metadata-as-a-simple-dns but dosnt work because it is using an old amazon sdk version, i rewrote it to use the new one:
#!/usr/bin/env ruby
require "rubygems"
require "aws-sdk"
%w(optparse rubygems aws-sdk resolv pp).each {|l| require l}
options = {}
parser = OptionParser.new do |p|
p.banner = "Usage: hosts [options]"
p.on("-a", "--access-key USER", "The user's AWS access key ID.") do |aki|
options[:access_key_id] = aki
end
p.on("-s",
"--secret-key PASSWORD",
"The user's AWS secret access key.") do |sak|
options[:secret_access_key] = sak
end
p.on_tail("-h", "--help", "Show this message") {
puts(p)
exit
}
p.parse!(ARGV) rescue puts(p)
end
if options.key?(:access_key_id) and options.key?(:secret_access_key)
puts "127.0.0.1 localhost"
AWS.config(options)
AWS::EC2.new(options)
answer = AWS::EC2::Client.new.describe_instances
answer.reservationSet.item.each do |r|
r.instancesSet.item.each do |i|
if i.instanceState.name =~ /running/
puts(Resolv::DNS.new.getaddress(i.privateDnsName).to_s +
" #{i.keyName}.ec2 #{i.keyName}")
end
end
end
else
puts(parser)
exit(1)
end
What this should do is outputing a new /etc/hosts file with my ec2 instances in it.
And i get a response =D, but answer is a hash and therefore i get the
error undefined method `reservationSet' for #<Hash:0x7f7573b27880>.
And this is my problem, since i dont know Ruby at all ( All I was doing was reading Amazon Documentation and playing around so i get an answer ). Somehow in the original example this seemed to work. I suppose that back then, the API did not return a hash, anyway...how can i iterate through a hash like above, to get this to work?
This code may help you:
answer = AWS::EC2::Client.new.describe_instances
reservations = answer[:reservation_set]
reservations.each do |reservation|
instances = reservation[:instances_set]
instances.each do |instance|
if instance[:instance_state][:name] == "running"
private_dns_name = instance[:private_dns_name]
key_name = instance[:key_name]
address = Resolv::DNS.new.getaddress(private_dns_name)
puts "{address} #{key_name}.ec2 #{key_name}"
end
end
end
Generally change your code from using methods with names e.g. item.fooBarBaz to using a hash e.g. item[:foo_bar_baz]
When you're learning Ruby the "pp" command is very useful for pretty-printing variables as you go, such as:
pp reservations
pp instances
pp private_dns_name
I'm generating a config for my service in chef attributes. However, at some point, I need to turn the attribute mash into a simple ruby hash. This used to work fine in Chef 10:
node.myapp.config.to_hash
However, starting with Chef 11, this does not work. Only the top-level of the attribute is converted to a hash, with then nested values remaining immutable mash objects. Modifying them leads to errors like this:
Chef::Exceptions::ImmutableAttributeModification
------------------------------------------------ Node attributes are read-only when you do not specify which precedence level to set. To
set an attribute use code like `node.default["key"] = "value"'
I've tried a bunch of ways to get around this issue which do not work:
node.myapp.config.dup.to_hash
JSON.parse(node.myapp.config.to_json)
The json parsing hack, which seems like it should work great, results in:
JSON::ParserError
unexpected token at '"#<Chef::Node::Attribute:0x000000020eee88>"'
Is there any actual reliable way, short of including a nested parsing function in each cookbook, to convert attributes to a simple, ordinary, good old ruby hash?
after a resounding lack of answers both here and on the opscode chef mailing list, i ended up using the following hack:
class Chef
class Node
class ImmutableMash
def to_hash
h = {}
self.each do |k,v|
if v.respond_to?('to_hash')
h[k] = v.to_hash
else
h[k] = v
end
end
return h
end
end
end
end
i put this into the libraries dir in my cookbook; now i can use attribute.to_hash in both chef 10 (which already worked properly and which is unaffected by this monkey-patch) and chef 11. i've also reported this as a bug to opscode:
if you don't want to have to monkey-patch your chef, speak up on this issue:
http://tickets.opscode.com/browse/CHEF-3857
Update: monkey-patch ticket was marked closed by these PRs
I hope I am not too late to the party but merging the node object with an empty hash did it for me:
chef (12.6.0)> {}.merge(node).class
=> Hash
I had the same problem and after much hacking around came up with this:
json_string = node[:attr_tree].inspect.gsub(/\=\>/,':')
my_hash = JSON.parse(json_string, {:symbolize_names => true})
inspect does the deep parsing that is missing from the other methods proposed and I end up with a hash that I can modify and pass around as needed.
This has been fixed for a long time now:
[1] pry(main)> require 'chef/node'
=> true
[2] pry(main)> node = Chef::Node.new
[....]
[3] pry(main)> node.default["fizz"]["buzz"] = { "foo" => [ { "bar" => "baz" } ] }
=> {"foo"=>[{"bar"=>"baz"}]}
[4] pry(main)> buzz = node["fizz"]["buzz"].to_hash
=> {"foo"=>[{"bar"=>"baz"}]}
[5] pry(main)> buzz.class
=> Hash
[6] pry(main)> buzz["foo"].class
=> Array
[7] pry(main)> buzz["foo"][0].class
=> Hash
[8] pry(main)>
Probably fixed sometime in or around Chef 12.x or Chef 13.x, it is certainly no longer an issue in Chef 15.x/16.x/17.x
The above answer is a little unnecessary. You can just do this:
json = node[:whatever][:whatever].to_hash.to_json
JSON.parse(json)
I am creating a script to insert the files from folders to the Excel columns, but it seems I am doing wrong. Can any one help me for the same?
updated Ruby Code:
require 'fileutils'
require 'win32ole'
#Excel Application will be started from here.
#--------------------------------------------
excel = WIN32OLE.new('Excel.Application')
excel.visible = true
wb=excel.workbooks.open("E:\\WIPData\\Ruby\\Scripts\\Copy of GSL_File_DownLoad1.xlsx")
wbs= wb.Worksheets(1)
rows=2
column=2
until wbs.cells(rows,1).value == nil do
Dir.entries("E:\\WIPData\\Ruby").each do |f|
if f == wbs.cells(rows,1).value then
files_dir = File.expand_path("..", Dir.pwd)
column=2
Dir.foreach(files_dir.concat("/" + f)) do |x|
full_path=files_dir.concat("/" + x)
wbs.cells(rows,column).Select
wbs.oleobjects.add({
'Filename' => full_path,
'Link' => true,
'DisplayAsIcon' => false,
})
column = column + 1
end
break
end
end
end
wb.Save
wb.Close(0)
excel.Quit()
#Excel Application will be finished here.
#------------
Error:
E:/WIPData/Ruby/Scripts/test.rb:27:in `method_missing': (in OLE method `add': )
(WIN32OLERuntimeError)
OLE error code:800A03EC in Microsoft Excel
Cannot insert object.
HRESULT error code:0x80020009
Exception occurred.
from E:/WIPData/Ruby/Scripts/test.rb:27:in `block (2 levels) in <main>'
from E:/WIPData/Ruby/Scripts/test.rb:23:in `foreach'
from E:/WIPData/Ruby/Scripts/test.rb:23:in `block in <main>'
from E:/WIPData/Ruby/Scripts/test.rb:17:in `each'
from E:/WIPData/Ruby/Scripts/test.rb:17:in `<main>'
Your problem is on line 25 in your code. It is the method wbs.OLEObjects.Add(,full_path,False,True,,,f) that is causing the problem.
In VBA, it is perfectly fine to leave parameters to a method blank if they are not required. However, this is not available in Ruby.
In your original Macro, you passed keyword arguments to the method. One way of doing this in Ruby is with a Hash. An article on the Ruby on Windows blog suggests doing it like so:
wbs.oleobjects.add({
'Filename' => full_path,
'Link' => false,
'DisplayAsIcon' => true,
'IconIndex' => 0,
'IconLabel' => f,
'IconFileName' => icon_path
})
I did not see you provide an icon path in your Ruby code so I'm making an assumption on that final variable.
Also note that true and false are lowercase. Uppercase versions would be read by Ruby as either a constant or a class.
If you are doing work in Microsoft Office with Ruby, I would highly recommend frequenting Ruby on Windows. The author doesn't appear to post anymore but it is still a relevant source.
EDIT:
Your new error is probably due to Dir.entries. This method will grab . and .. when it pulls entries. I'd imagine Excel is tripping up on trying to add those two to the Worksheet.
There are two ways to remove this.
1) Skip them in your each block.
Dir.entries("E:\\WIPData\\Ruby").each do |f|
next if ['.', '..'].include? f
# The rest of your block code
end
2) Use Dir#glob which will not return . and ..
Dir.chdir("E:\\WIPData\\Ruby")
Dir.glob('*').each do |f|
# Your block code
end
EDIT:
For documentation's sake, this topic is also discussed on Ruby Forums.
I've been trying to use autovivification in ruby to do simple record consolidation on this:
2009-08-21|09:30:01|A1|EGLE|Eagle Bulk Shpg|BUY|6000|5.03
2009-08-21|09:30:35|A2|JOYG|Joy Global Inc|BUY|4000|39.76
2009-08-21|09:30:35|A2|LEAP|Leap Wireless|BUY|2100|16.36
2009-08-21|09:30:36|A1|AINV|Apollo Inv Cp|BUY|2300|9.15
2009-08-21|09:30:36|A1|CTAS|Cintas Corp|SELL|9800|27.83
2009-08-21|09:30:38|A1|KRE|SPDR KBW Regional Banking ETF|BUY|9200|21.70
2009-08-21|09:30:39|A1|APA|APACHE CORPORATION|BUY|5700|87.18
2009-08-21|09:30:40|A1|FITB|Fifth Third Bancorp|BUY|9900|10.86
2009-08-21|09:30:40|A1|ICO|INTERNATIONAL COAL GROUP, INC.|SELL|7100|3.45
2009-08-21|09:30:41|A1|NLY|ANNALY CAPITAL MANAGEMENT. INC.|BUY|3000|17.31
2009-08-21|09:30:42|A2|GAZ|iPath Dow Jones - AIG Natural Gas Total Return Sub-Index ETN|SELL|6600|14.09
2009-08-21|09:30:44|A2|CVBF|Cvb Finl|BUY|1100|7.64
2009-08-21|09:30:44|A2|JCP|PENNEY COMPANY, INC.|BUY|300|31.05
2009-08-21|09:30:36|A1|AINV|Apollo Inv Cp|BUY|4500|9.15
so for example I want the record for A1 AINV BUY 9.15 to have a total of 6800. This is a perfect problem to use autovivification on. So heres my code:
#!/usr/bin/ruby
require 'facets'
h = Hash.autonew
File.open('trades_long.dat','r').each do |line|
#date,#time,#account,#ticker,#desc,#type,amount,#price = line.chomp.split('|')
if #account != "account"
puts "#{amount}"
h[#account][#ticker][#type][#price] += amount
end
#puts sum.to_s
end
The problem is no matter how I try to sum up the value in h[#account][#ticker][#type][#price] it gives me this error:
6000
/usr/local/lib/ruby/gems/1.9.1/gems/facets-2.7.0/lib/core/facets/hash/op_add.rb:8:in `merge': can't convert String into Hash (TypeError)
from /usr/local/lib/ruby/gems/1.9.1/gems/facets-2.7.0/lib/core/facets/hash/op_add.rb:8:in `+'
from ./trades_consolidaton.rb:13
from ./trades_consolidaton.rb:8:in `each'
from ./trades_consolidaton.rb:8
I've tried using different "autovivification" methods with no result. This wouldn't happen in perl! The autofvivification would know what you are trying to do. ruby doesn't seem to have this feature.
So my question really is, how do I perform simply "consolidation" of records in ruby. Specifically, how do I get the total for something like:
h[#account][#ticker][#type][#price]
Many thanks for your help!!
Just to clarify on glenn's solution. That would be perfect except it gives (with a few modifications to use the standard CSV library in ruby 1.9:
CSV.foreach("trades_long.dat", :col_sep => "|") do |row|
date,time,account,ticker,desc,type,amount,price = *row
records[[account,ticker,type,price]] += amount
end
gives the following error:
TypeError: String can't be coerced into Fixnum
from (irb):64:in `+'
from (irb):64:in `block in irb_binding'
from /usr/local/lib/ruby/1.9.1/csv.rb:1761:in `each'
from /usr/local/lib/ruby/1.9.1/csv.rb:1197:in `block in foreach'
from /usr/local/lib/ruby/1.9.1/csv.rb:1335:in `open'
from /usr/local/lib/ruby/1.9.1/csv.rb:1196:in `foreach'
from (irb):62
from /usr/local/bin/irb:12:in `<main>'
I agree with Jonas that you (and Sam) are making this more complicated than it needs to be, but I think even his version is too complicated. I'd just do this:
require 'fastercsv'
records = Hash.new(0)
FasterCSV.foreach("trades_long.dat", :col_sep => "|") do |row|
date,time,account,ticker,desc,type,amount,price = row.fields
records[[account,ticker,type,price]] += amount.to_f
end
Now you have a hash with total amounts for each unique combination of account, ticker, type and price.
If you want a hash builder that works that way, you are going to have to redefine the + semantics.
For example, this works fine:
class HashBuilder
def initialize
#hash = {}
end
def []=(k,v)
#hash[k] = v
end
def [](k)
#hash[k] ||= HashBuilder.new
end
def +(val)
val
end
end
h = HashBuilder.new
h[1][2][3] += 1
h[1][2][3] += 3
p h[1][2][3]
# prints 4
Essentially you are trying to apply the + operator to a Hash.
>> {} + {}
NoMethodError: undefined method `+' for {}:Hash
from (irb):1
However in facets{
>> require 'facets'
>> {1 => 10} + {2 => 20}
=> {1 => 10, 2 => 20}
>> {} + 100
TypeError: can't convert Fixnum into Hash
from /usr/lib/ruby/gems/1.8/gems/facets-2.7.0/lib/core/facets/hash/op_add.rb:8:in `merge'
from /usr/lib/ruby/gems/1.8/gems/facets-2.7.0/lib/core/facets/hash/op_add.rb:8:in `+'
from (irb):6
>> {} += {1 => 2}
=> {1=>2}
>>
If you want to redefine the + semantics for your hash in this occasion you can do:
class Hash; def +(v); v; end; end
Place this snippet before your original sample and all should be well. Keep in mind that you are changing the defined behavior for + (note + is not defined on Hash its pulled in with facets)
It looks like you are making it more complicated than it has to be. I would use the FasterCSV gem and Enumerable#inject something like this:
require 'fastercsv'
records=FasterCSV.read("trades_long.dat", :col_sep => "|")
records.sort_by {|r| r[3]}.inject(nil) {|before, curr|
if !before.nil? && curr[3]==before[3]
curr[6]=(curr[6].to_i+before[6].to_i).to_s
records.delete(before)
end
before=curr
}
For others that find their way here, there is now also another option:
require 'xkeys' # on rubygems.org
h = {}.extend XKeys::Hash
...
# Start with 0.0 (instead of nil) and add the amount
h[#account, #ticker, #type, #price, :else => 0.0] += amount.to_f
This will generate a navigable structure. (Traditional keying with arrays of [#account, #ticker, #type, #price] as suggested earlier may be better this particular application). XKeys auto-vivifies on write rather than read, so querying the structure about elements that don't exist won't change the structure.