cassava 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +42 -0
- data/Rakefile +16 -0
- data/bin/cassava +13 -0
- data/cassava.gemspec +24 -0
- data/lib/cassava.rb +6 -0
- data/lib/cassava/document.rb +388 -0
- data/lib/cassava/main.rb +199 -0
- data/lib/cassava/version.rb +3 -0
- data/spec/cast_strings_spec.rb +103 -0
- data/spec/spec_helper.rb +1 -0
- metadata +116 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color -f d
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Kurt Stephens
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Cassava
|
2
|
+
|
3
|
+
A command-line CSV tool.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'cassava'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install cassava
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
# Concat mulitple CSVs, while merging columns:
|
22
|
+
$ cassava cat first.csv second.csv
|
23
|
+
|
24
|
+
# Cut columns out of result; '-' is used to pipe results to next command:
|
25
|
+
$ cassava cat first.csv second.csv - cut -c first_name,last_name
|
26
|
+
|
27
|
+
# Format as ASCII table.
|
28
|
+
$ cassava format first.csv
|
29
|
+
|
30
|
+
# Select where.
|
31
|
+
$ cassava cat *.csv - where last_name=Smith - format
|
32
|
+
|
33
|
+
# Sort by a column:
|
34
|
+
$ cassava cat *.csv - sort -by last_name,first_name - format
|
35
|
+
|
36
|
+
## Contributing
|
37
|
+
|
38
|
+
1. Fork it
|
39
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
40
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
41
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
42
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
gem 'rspec'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
desc "Default => :test"
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc "Run all tests"
|
9
|
+
task :test => [ :spec ]
|
10
|
+
|
11
|
+
desc "Run specs"
|
12
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
13
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
14
|
+
# Put spec opts in a file named .rspec in root
|
15
|
+
end
|
16
|
+
|
data/bin/cassava
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# -*- ruby -*-
|
3
|
+
$: << File.expand_path("../../lib", __FILE__)
|
4
|
+
if ARGV[0] == '-D'
|
5
|
+
ARGV.shift
|
6
|
+
$DEBUG = true
|
7
|
+
require 'rubygems'
|
8
|
+
gem 'ruby-debug'
|
9
|
+
require 'ruby-debug'
|
10
|
+
end
|
11
|
+
require 'cassava/main'
|
12
|
+
exit(Cassava::Main.new(ARGV).run!.exit_code)
|
13
|
+
|
data/cassava.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'cassava/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "cassava"
|
8
|
+
gem.version = Cassava::VERSION
|
9
|
+
gem.authors = ["Kurt Stephens"]
|
10
|
+
gem.email = ["ks.github@kurtstephens.com"]
|
11
|
+
gem.description = %q{A command-line CSV tool.}
|
12
|
+
gem.summary = %q{A command-line CSV tool.}
|
13
|
+
gem.homepage = "https://github.com/kstephens/cassava"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency 'terminal-table', '~> 1.4.5'
|
21
|
+
|
22
|
+
gem.add_development_dependency "rake", "~> 10.0.2"
|
23
|
+
gem.add_development_dependency "rspec", "~> 2.12.0"
|
24
|
+
end
|
data/lib/cassava.rb
ADDED
@@ -0,0 +1,388 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module Cassava
|
4
|
+
class Document
|
5
|
+
attr_accessor :name, :columns, :column_offset, :offset_column
|
6
|
+
attr_accessor :rows
|
7
|
+
attr_accessor :debug
|
8
|
+
|
9
|
+
def initialize opts = nil
|
10
|
+
@opts = opts
|
11
|
+
@name = opts[:name] or raise ArgumentError, "name not specified"
|
12
|
+
@rows = [ ]
|
13
|
+
@index = { }
|
14
|
+
self.columns = opts[:columns] || [ ]
|
15
|
+
if x = opts[:rows]
|
16
|
+
append_row! x
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def columns= x
|
21
|
+
@columns = x
|
22
|
+
update_columns!
|
23
|
+
end
|
24
|
+
|
25
|
+
def update_columns!
|
26
|
+
@ncols = @columns.size
|
27
|
+
@column_offset = { }
|
28
|
+
@offset_column = [ ]
|
29
|
+
i = -1
|
30
|
+
@columns.map! do | c |
|
31
|
+
i += 1
|
32
|
+
c = c.to_s
|
33
|
+
next if c.empty?
|
34
|
+
c = c.to_sym
|
35
|
+
@column_offset[c] = i
|
36
|
+
@offset_column[i] = c
|
37
|
+
c
|
38
|
+
end
|
39
|
+
@column_types = nil
|
40
|
+
@index.keep_if { | c, h | @column_offset[c] }
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_column! c
|
45
|
+
c = c.to_sym
|
46
|
+
unless i = @column_offset[c]
|
47
|
+
i = @columns.size
|
48
|
+
@columns << c
|
49
|
+
update_columns!
|
50
|
+
end
|
51
|
+
i
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_column_names! a
|
55
|
+
a.map! do | x |
|
56
|
+
case x
|
57
|
+
when Integer
|
58
|
+
c = @columns[x]
|
59
|
+
when String
|
60
|
+
if x == (i = x.to_i).to_s
|
61
|
+
c = @columns[i]
|
62
|
+
else
|
63
|
+
c = x.to_sym
|
64
|
+
end
|
65
|
+
when Symbol
|
66
|
+
c = x
|
67
|
+
else
|
68
|
+
raise TypeError, "#{x.inspect}"
|
69
|
+
end
|
70
|
+
raise TypeError, "#{x.inspect} => #{c.inspect}" unless c
|
71
|
+
c
|
72
|
+
end
|
73
|
+
a
|
74
|
+
end
|
75
|
+
|
76
|
+
def nrows
|
77
|
+
@rows.size
|
78
|
+
end
|
79
|
+
|
80
|
+
def empty_rows!
|
81
|
+
@rows = [ ]
|
82
|
+
@index.clear
|
83
|
+
@column_types = nil
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
def append_rows! rows
|
88
|
+
return self unless rows
|
89
|
+
if Document === rows
|
90
|
+
rows.columns.each { | c | add_column!(c) }
|
91
|
+
rows = rows.rows
|
92
|
+
end
|
93
|
+
rows.map!{ | r | array_to_row(r) }
|
94
|
+
row_i = @rows.size
|
95
|
+
@rows.concat(rows)
|
96
|
+
rows.each do | r |
|
97
|
+
@ncols = r.size if @ncols < r.size
|
98
|
+
r[:_row_i] = row_i
|
99
|
+
row_i += 1
|
100
|
+
end
|
101
|
+
@index.clear
|
102
|
+
@column_types = nil
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
def parse!
|
107
|
+
# debugger if $DEBUG
|
108
|
+
csv = nil
|
109
|
+
if RUBY_VERSION =~ /^1\.8/
|
110
|
+
@rows = [ ]
|
111
|
+
csv = CSV.open(name, "rb", @opts[:col_sep]) do | r |
|
112
|
+
@rows << r
|
113
|
+
end
|
114
|
+
else
|
115
|
+
csv_opts = { }
|
116
|
+
csv_opts[:col_sep] = @opts[:col_sep] if @opts[:col_sep]
|
117
|
+
csv = CSV.open(name, "rb", csv_opts)
|
118
|
+
@rows = csv.read
|
119
|
+
end
|
120
|
+
|
121
|
+
@columns = @rows.shift if @columns.empty?
|
122
|
+
update_columns!
|
123
|
+
|
124
|
+
row_i = 0
|
125
|
+
@rows.map! do | r |
|
126
|
+
@ncols = r.size if @ncols < r.size
|
127
|
+
h = { :_row_i => (row_i += 1) }
|
128
|
+
@column_offset.each do | c, i |
|
129
|
+
h[c] = r[i] if c && i
|
130
|
+
end
|
131
|
+
h
|
132
|
+
end
|
133
|
+
# debugger
|
134
|
+
self
|
135
|
+
ensure
|
136
|
+
csv.close if csv
|
137
|
+
end
|
138
|
+
|
139
|
+
def index! c
|
140
|
+
c = @column[c] if Integer === c
|
141
|
+
unless ind = @index[c]
|
142
|
+
ind = { }
|
143
|
+
@rows.each do | r |
|
144
|
+
(ind[r[c]] ||= [ ]) << r
|
145
|
+
end
|
146
|
+
@index[c] = ind
|
147
|
+
end
|
148
|
+
ind
|
149
|
+
end
|
150
|
+
|
151
|
+
def get c, v
|
152
|
+
index!(c)[v]
|
153
|
+
end
|
154
|
+
|
155
|
+
def emit! file
|
156
|
+
out = nil
|
157
|
+
case file
|
158
|
+
when '/dev/stdout', '/dev/fd/0'
|
159
|
+
tmp_file = true
|
160
|
+
out = $stdout
|
161
|
+
when %r{^/dev/}
|
162
|
+
tmp_file = true
|
163
|
+
end
|
164
|
+
if tmp_file
|
165
|
+
tmp_file = "/tmp/cassava-#{$$}.csv"
|
166
|
+
# $stderr.puts " #{file} => #{tmp_file}"
|
167
|
+
file = tmp_file
|
168
|
+
end
|
169
|
+
|
170
|
+
_emit! file
|
171
|
+
|
172
|
+
if tmp_file
|
173
|
+
out ||= File.open(tmp, "w")
|
174
|
+
# $stderr.puts " #{tmp_file} => #{out}"
|
175
|
+
out.write(File.read(tmp_file))
|
176
|
+
end
|
177
|
+
ensure
|
178
|
+
File.unlink(tmp_file) if tmp_file
|
179
|
+
end
|
180
|
+
|
181
|
+
def _emit! file
|
182
|
+
CSV.open(file, "wb") do | out |
|
183
|
+
a = @offset_column.map do | c |
|
184
|
+
c && c.to_s
|
185
|
+
end
|
186
|
+
out << a
|
187
|
+
@rows.each do | r |
|
188
|
+
a = @offset_column.map do | i |
|
189
|
+
i && r[i]
|
190
|
+
end
|
191
|
+
out << a
|
192
|
+
end
|
193
|
+
end
|
194
|
+
self
|
195
|
+
end
|
196
|
+
alias :emit! :_emit!
|
197
|
+
|
198
|
+
def array_to_row a, columns = nil
|
199
|
+
if Array === a
|
200
|
+
columns ||= self.columns
|
201
|
+
h = { }
|
202
|
+
columns.each_with_index do | c, i |
|
203
|
+
h[c] = a[i]
|
204
|
+
end
|
205
|
+
a = h
|
206
|
+
end
|
207
|
+
a
|
208
|
+
end
|
209
|
+
|
210
|
+
def row_to_array r
|
211
|
+
unless Array === r
|
212
|
+
r = @offset_column.map do | c |
|
213
|
+
c && r[c]
|
214
|
+
end
|
215
|
+
end
|
216
|
+
r
|
217
|
+
end
|
218
|
+
|
219
|
+
def coerce_to_strings!
|
220
|
+
rows.each do | r |
|
221
|
+
r.each do | k, v |
|
222
|
+
r[k] = v.to_s unless String === v
|
223
|
+
end
|
224
|
+
end
|
225
|
+
self
|
226
|
+
end
|
227
|
+
|
228
|
+
def cast_strings rows
|
229
|
+
rows.each do | r |
|
230
|
+
r.each do | k, v |
|
231
|
+
next if v.nil?
|
232
|
+
old_v = v
|
233
|
+
v = v.to_s
|
234
|
+
if String === v
|
235
|
+
case v
|
236
|
+
when /\A[-+]?\d+\Z/
|
237
|
+
v = v.to_i
|
238
|
+
when /\A([-+]?([0-9]+\.[0-9]+|\.[0-9]+|[0-9]+\.)(e[-+]?\d+)?|[-+]?\d+e[-+]?\d+)\Z/i
|
239
|
+
v = v.to_f
|
240
|
+
end
|
241
|
+
# puts "old_v = #{old_v.inspect} => #{v.inspect}"
|
242
|
+
end
|
243
|
+
r[k] = v
|
244
|
+
end
|
245
|
+
end
|
246
|
+
rows
|
247
|
+
end
|
248
|
+
|
249
|
+
def cast_strings!
|
250
|
+
cast_strings @rows
|
251
|
+
@column_types = nil
|
252
|
+
self
|
253
|
+
end
|
254
|
+
|
255
|
+
def sort! by = nil
|
256
|
+
by ||= @columns
|
257
|
+
cast_strings!
|
258
|
+
# by = by.map { | x | column_offset[x] }
|
259
|
+
ct = { }
|
260
|
+
columns.each_with_index do | c, i |
|
261
|
+
ct[c] = column_types[i]
|
262
|
+
end
|
263
|
+
@rows.sort! do | a, b |
|
264
|
+
r = 0
|
265
|
+
by.each do | c |
|
266
|
+
av = a[c]
|
267
|
+
bv = b[c]
|
268
|
+
case
|
269
|
+
when av.nil? && bv.nil?
|
270
|
+
r = 0
|
271
|
+
when av.nil?
|
272
|
+
r = -1
|
273
|
+
when bv.nil?
|
274
|
+
r = 1
|
275
|
+
else
|
276
|
+
r = (av <=> bv rescue nil) || 0
|
277
|
+
end
|
278
|
+
break if r != 0
|
279
|
+
end
|
280
|
+
r
|
281
|
+
end
|
282
|
+
self
|
283
|
+
end
|
284
|
+
|
285
|
+
require 'pp'
|
286
|
+
def infer_column_types rows = self.rows
|
287
|
+
column_types = [ nil ] * @columns.size
|
288
|
+
ancestors_cache = { }
|
289
|
+
common_ancestor_cache = { }
|
290
|
+
rows.each do | r |
|
291
|
+
raise unless Hash === r
|
292
|
+
@columns.each_with_index do | k, i |
|
293
|
+
v = r[k]
|
294
|
+
next if v.nil?
|
295
|
+
ct = column_types[i]
|
296
|
+
vt = v.class
|
297
|
+
if ct.nil?
|
298
|
+
column_types[i] = vt
|
299
|
+
next
|
300
|
+
end
|
301
|
+
common_ancestor =
|
302
|
+
common_ancestor_cache[[ct, vt]] ||=
|
303
|
+
begin
|
304
|
+
ca =
|
305
|
+
ancestors_cache[ct] ||=
|
306
|
+
ct.ancestors.delete_if{|x| x.class == Module}
|
307
|
+
va =
|
308
|
+
ancestors_cache[vt] ||=
|
309
|
+
vt.ancestors.delete_if{|x| x.class == Module}
|
310
|
+
(ca & va).first || Object
|
311
|
+
end
|
312
|
+
if @debug && k == :float
|
313
|
+
pp [ :k, k, :v, v, :ct, ct, :vt, vt, :ca, ca, :va, va, :common_ancestor, common_ancestor ]
|
314
|
+
end
|
315
|
+
# if Value's class is not a specialization of column class.
|
316
|
+
ct = common_ancestor
|
317
|
+
column_types[i] = ct
|
318
|
+
end
|
319
|
+
end
|
320
|
+
# pp columns.zip(column_types)
|
321
|
+
column_types
|
322
|
+
end
|
323
|
+
|
324
|
+
def column_types
|
325
|
+
unless @column_types
|
326
|
+
@column_types ||= infer_column_types
|
327
|
+
end
|
328
|
+
@column_types
|
329
|
+
end
|
330
|
+
|
331
|
+
def clone_rows rows = self.rows
|
332
|
+
rows.map { | r | r.dup }
|
333
|
+
end
|
334
|
+
|
335
|
+
# Format as ASCII table.
|
336
|
+
def to_text opts = { }
|
337
|
+
gem 'terminal-table'
|
338
|
+
require 'terminal-table'
|
339
|
+
|
340
|
+
table = Terminal::Table.new() do | table |
|
341
|
+
# t.title = self.name
|
342
|
+
s = table.style
|
343
|
+
s.border_x = s.border_y = s.border_i = ''
|
344
|
+
# s.border_i = '|'
|
345
|
+
s.padding_left = 0
|
346
|
+
s.padding_right = 1
|
347
|
+
|
348
|
+
table << self.columns.map{|c| { :value => c.to_s, :alignment => :center }}
|
349
|
+
|
350
|
+
# Convert rows to Arrays and handle nil, etc.
|
351
|
+
self.rows.each do | r |
|
352
|
+
r = self.row_to_array(r)
|
353
|
+
r.map! do | c |
|
354
|
+
c = case c
|
355
|
+
when nil
|
356
|
+
''
|
357
|
+
when Integer
|
358
|
+
thousands(c)
|
359
|
+
else
|
360
|
+
c
|
361
|
+
end
|
362
|
+
# c = "#{c} |"
|
363
|
+
end
|
364
|
+
table << r
|
365
|
+
end
|
366
|
+
|
367
|
+
# Align numeric columns to the left.
|
368
|
+
column_types = infer_column_types(cast_strings(clone_rows))
|
369
|
+
column_types.each_with_index do | type, ci |
|
370
|
+
if type && type.ancestors.include?(Numeric)
|
371
|
+
# puts " column #{ci} #{columns[ci]} #{t}"
|
372
|
+
table.align_column(ci, :right)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
|
378
|
+
# Return formatted table.
|
379
|
+
table.to_s
|
380
|
+
end
|
381
|
+
|
382
|
+
def thousands x, sep = '_'
|
383
|
+
x && x.to_s.reverse!.gsub(/(\d{3})/, "\\1#{sep}").reverse!.sub(/^(\D|\A)#{sep}/, '')
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
|
data/lib/cassava/main.rb
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'cassava'
|
2
|
+
require 'cassava/document'
|
3
|
+
|
4
|
+
module Cassava
|
5
|
+
class Main
|
6
|
+
attr_accessor :progname
|
7
|
+
attr_accessor :args, :cmd, :opts, :exit_code
|
8
|
+
attr_accessor :result, :output
|
9
|
+
attr_accessor :by, :columns
|
10
|
+
|
11
|
+
def initialize args = nil
|
12
|
+
@progname = File.basename($0)
|
13
|
+
@args = (args || ARGV).dup
|
14
|
+
@opts = { }
|
15
|
+
@exit_code = 0
|
16
|
+
@output = "/dev/stdout"
|
17
|
+
@debug = true
|
18
|
+
@select_where = { }
|
19
|
+
end
|
20
|
+
|
21
|
+
def run!
|
22
|
+
cmds = [ ]
|
23
|
+
last_arg = nil
|
24
|
+
|
25
|
+
cmd = [ ]
|
26
|
+
args = @args.dup
|
27
|
+
while arg = args.shift
|
28
|
+
if arg == '-'
|
29
|
+
if last_arg
|
30
|
+
cmd << last_arg
|
31
|
+
last_arg = nil
|
32
|
+
end
|
33
|
+
last_arg = '--result'
|
34
|
+
cmds << cmd
|
35
|
+
cmd = [ ]
|
36
|
+
else
|
37
|
+
cmd << arg
|
38
|
+
end
|
39
|
+
end
|
40
|
+
if last_arg
|
41
|
+
cmd << last_arg
|
42
|
+
last_arg = nil
|
43
|
+
end
|
44
|
+
cmds << cmd
|
45
|
+
# pp cmds
|
46
|
+
|
47
|
+
# Run each command:
|
48
|
+
cmds.each do | cmd |
|
49
|
+
@args = cmd.dup
|
50
|
+
next_cmd!
|
51
|
+
end
|
52
|
+
|
53
|
+
result.emit!(@output) if result
|
54
|
+
self
|
55
|
+
rescue ::Exception => exc
|
56
|
+
$stderr.puts "#{progname}: ERROR: #{exc.inspect}"
|
57
|
+
$stderr.puts " #{exc.backtrace * "\n "}" if @debug || $DEBUG
|
58
|
+
@exit_code = 1
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
def next_cmd!
|
63
|
+
@cmd = @args.shift
|
64
|
+
sel = :"_#{cmd}!"
|
65
|
+
raise ArgumentError, "Invalid command: #{cmd.inspect}" unless respond_to?(sel)
|
66
|
+
send(sel)
|
67
|
+
end
|
68
|
+
|
69
|
+
def next_document!
|
70
|
+
doc = nil
|
71
|
+
opts = { }
|
72
|
+
until args.empty?
|
73
|
+
case arg = args.shift
|
74
|
+
when '-cv'
|
75
|
+
k = args.shift.to_sym
|
76
|
+
v = args.shift
|
77
|
+
@select_where[k] = v
|
78
|
+
when /\A([^=]+)=(.*)\Z/
|
79
|
+
k = $1.to_sym
|
80
|
+
v = $2
|
81
|
+
@select_where[k] = v
|
82
|
+
when '--result'
|
83
|
+
doc = @result
|
84
|
+
break
|
85
|
+
when '-c'
|
86
|
+
self.columns = args.shift.split(/\s*,\s*|\s+/)
|
87
|
+
when '-by'
|
88
|
+
self.by = args.shift.split(/\s*,\s*|\s+/)
|
89
|
+
when '-o'
|
90
|
+
self.output = args.shift
|
91
|
+
when '-FS'
|
92
|
+
opts[:col_sep] = args.shift
|
93
|
+
when '-IGNORE'
|
94
|
+
opts[:ignore] = args.shift
|
95
|
+
when '-C'
|
96
|
+
opts[:columns] = args.shift.split(/\s*,\s*|\s+/)
|
97
|
+
else
|
98
|
+
opts[:name] = arg
|
99
|
+
doc = Document.new(opts)
|
100
|
+
doc.parse!
|
101
|
+
break
|
102
|
+
end
|
103
|
+
end
|
104
|
+
doc
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.define_command name, doc, &blk
|
108
|
+
meth = :"_#{name}!"
|
109
|
+
define_method meth, &blk
|
110
|
+
@@commands << { :name => name, :doc => doc }
|
111
|
+
end
|
112
|
+
|
113
|
+
def _cat!
|
114
|
+
self.result = next_document!
|
115
|
+
until args.empty?
|
116
|
+
other = next_document!
|
117
|
+
result.append_rows!(other)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def _where!
|
122
|
+
@select_where = { }
|
123
|
+
self.result = next_document!
|
124
|
+
result.coerce_to_strings!
|
125
|
+
@select_where.each do | col, val |
|
126
|
+
new_result = result.dup
|
127
|
+
new_result.empty_rows!
|
128
|
+
found_rows = result.get(col, val)
|
129
|
+
# puts " #{col}=#{val} => #{found_rows.size} rows"
|
130
|
+
new_result.append_rows!(found_rows)
|
131
|
+
self.result = new_result
|
132
|
+
end
|
133
|
+
end
|
134
|
+
alias :_select! :_where!
|
135
|
+
|
136
|
+
def _cut!
|
137
|
+
self.columns = nil
|
138
|
+
self.result = next_document!
|
139
|
+
self.columns ||= [ result.columns[0] ]
|
140
|
+
# pp result.columns
|
141
|
+
result.to_column_names! columns
|
142
|
+
new_result = result.dup.empty_rows!
|
143
|
+
new_result.columns = columns & result.columns
|
144
|
+
# pp new_result.columns
|
145
|
+
# pp new_result.column_offset
|
146
|
+
rows = self.result.rows.map do | r |
|
147
|
+
r = r.dup
|
148
|
+
r.keep_if { | c | new_result.column_offset[c] }
|
149
|
+
# pp r
|
150
|
+
r
|
151
|
+
end
|
152
|
+
new_result.append_rows! rows
|
153
|
+
self.result = new_result
|
154
|
+
end
|
155
|
+
|
156
|
+
def _join!
|
157
|
+
self.result = next_document!
|
158
|
+
until args.empty?
|
159
|
+
left_key = args.shift.to_sym
|
160
|
+
right_key = args.shift.to_sym
|
161
|
+
right = next_document!
|
162
|
+
new_result = Document.new
|
163
|
+
result.columns.each do | c |
|
164
|
+
new_result.add_column!(c)
|
165
|
+
end
|
166
|
+
right.columns.each do | c |
|
167
|
+
new_result.add_column!(c)
|
168
|
+
end
|
169
|
+
result.rows.each do | lr |
|
170
|
+
rrows = right.get(right_key, lr[left_key])
|
171
|
+
unless rrows.empty?
|
172
|
+
r = lr.dup
|
173
|
+
rrows.each do | rr |
|
174
|
+
r.merge(rr)
|
175
|
+
end
|
176
|
+
new_result.rows << r
|
177
|
+
end
|
178
|
+
end
|
179
|
+
self.result = new_result
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def _sort!
|
184
|
+
self.by = nil
|
185
|
+
self.result = next_document!
|
186
|
+
by = self.by || args
|
187
|
+
by.map! { | c | c.to_sym }
|
188
|
+
result.sort!(by)
|
189
|
+
end
|
190
|
+
|
191
|
+
def _format!
|
192
|
+
rows = result.to_text
|
193
|
+
# $stderr.puts rows
|
194
|
+
rows = rows.split("\n").map { | r | { :_ => r } }
|
195
|
+
# require 'pp'; pp rows
|
196
|
+
self.result = Cassava::Document.new(:name => "#{@result.name}->format", :columns => [ :_ ], :rows => rows)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Exception
|
4
|
+
alias :_message :message
|
5
|
+
def message
|
6
|
+
@message ||= _message
|
7
|
+
end
|
8
|
+
def message= msg
|
9
|
+
@message = msg
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "Cassava::Document" do
|
14
|
+
attr_accessor :d
|
15
|
+
before do
|
16
|
+
self.d = Cassava::Document.new(:name => 'test', :columns => [ :nil, :string, :fixnum, :float, :integer, :numeric ])
|
17
|
+
rows = <<"END".split("\n").map{|r| r.strip.split(/,\s+/).map{|x| x == 'nil' ? nil : x }}
|
18
|
+
nil, nil, nil, nil, nil, nil
|
19
|
+
nil, string, nil, nil, nil, nil
|
20
|
+
nil, +, nil, nil, nil, nil
|
21
|
+
nil, -, nil, nil, nil, nil
|
22
|
+
nil, 234+, nil, nil, nil, nil
|
23
|
+
nil, 1234ws, nil, nil, nil, nil
|
24
|
+
nil, nil, 1234, nil, nil, nil
|
25
|
+
nil, nil, +1234, nil, nil, nil
|
26
|
+
nil, nil, -1234, nil, nil, nil
|
27
|
+
nil, nil, nil, .2, nil, nil
|
28
|
+
nil, nil, nil, -1.2, nil, nil
|
29
|
+
nil, nil, nil, -2., nil, nil
|
30
|
+
nil, nil, nil, 2e10, nil, nil
|
31
|
+
nil, nil, nil, 1e1, nil, nil
|
32
|
+
nil, nil, nil, 2E-2, nil, nil
|
33
|
+
nil, nil, nil, 3E+3, nil, nil
|
34
|
+
nil, nil, nil, nil, 1234, nil
|
35
|
+
nil, nil, nil, nil, 12341231234523452345345234, nil
|
36
|
+
nil, nil, nil, nil, nil, 1234
|
37
|
+
nil, nil, nil, nil, nil, 123412348102394812934
|
38
|
+
nil, nil, nil, nil, nil, 1234.2342
|
39
|
+
END
|
40
|
+
d.append_rows! rows
|
41
|
+
d.cast_strings!
|
42
|
+
end
|
43
|
+
|
44
|
+
def each_row
|
45
|
+
i = 0
|
46
|
+
d.rows.each do | r |
|
47
|
+
begin
|
48
|
+
yield r
|
49
|
+
rescue ::Exception => exc
|
50
|
+
exc.message = "in row #{i} : #{r.inspect} : #{exc.message}"
|
51
|
+
raise exc
|
52
|
+
end
|
53
|
+
i += 1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context "#cast_strings!" do
|
58
|
+
it "should not touch nil columns" do
|
59
|
+
each_row do | r |
|
60
|
+
r[0].should == nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
it "should not touch string only columns" do
|
64
|
+
each_row do | r |
|
65
|
+
next if r[1].nil?
|
66
|
+
r[1].class.should == String
|
67
|
+
end
|
68
|
+
end
|
69
|
+
it "should coerce integer columns" do
|
70
|
+
each_row do | r |
|
71
|
+
next if r[2].nil?
|
72
|
+
r[2].is_a?(Integer).should == true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
it "should coerce float columns" do
|
76
|
+
each_row do | r |
|
77
|
+
next if r[3].nil?
|
78
|
+
r[3].is_a?(Float).should == true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "#infer_types!" do
|
84
|
+
it "should infer nil for all nils" do
|
85
|
+
d.column_types[0].should == nil
|
86
|
+
end
|
87
|
+
it "should infer String for String" do
|
88
|
+
d.column_types[1].should == String
|
89
|
+
end
|
90
|
+
it "should infer Fixum for Fixnum" do
|
91
|
+
d.column_types[2].should == Fixnum
|
92
|
+
end
|
93
|
+
it "should infer Float for all Float" do
|
94
|
+
d.column_types[3].should == Float
|
95
|
+
end
|
96
|
+
it "should infer Integer for Fixnum and Bignum" do
|
97
|
+
d.column_types[4].should == Integer
|
98
|
+
end
|
99
|
+
it "should infer Numeric for Fixnum, Bignum, Float" do
|
100
|
+
d.column_types[5].should == Numeric
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'cassava/main'
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cassava
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kurt Stephens
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-15 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: terminal-table
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.4.5
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.4.5
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 10.0.2
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 10.0.2
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.12.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.12.0
|
62
|
+
description: A command-line CSV tool.
|
63
|
+
email:
|
64
|
+
- ks.github@kurtstephens.com
|
65
|
+
executables:
|
66
|
+
- cassava
|
67
|
+
extensions: []
|
68
|
+
extra_rdoc_files: []
|
69
|
+
files:
|
70
|
+
- .gitignore
|
71
|
+
- .rspec
|
72
|
+
- Gemfile
|
73
|
+
- LICENSE.txt
|
74
|
+
- README.md
|
75
|
+
- Rakefile
|
76
|
+
- bin/cassava
|
77
|
+
- cassava.gemspec
|
78
|
+
- lib/cassava.rb
|
79
|
+
- lib/cassava/document.rb
|
80
|
+
- lib/cassava/main.rb
|
81
|
+
- lib/cassava/version.rb
|
82
|
+
- spec/cast_strings_spec.rb
|
83
|
+
- spec/spec_helper.rb
|
84
|
+
homepage: https://github.com/kstephens/cassava
|
85
|
+
licenses: []
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
segments:
|
97
|
+
- 0
|
98
|
+
hash: -1985077025104642675
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ! '>='
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
segments:
|
106
|
+
- 0
|
107
|
+
hash: -1985077025104642675
|
108
|
+
requirements: []
|
109
|
+
rubyforge_project:
|
110
|
+
rubygems_version: 1.8.23
|
111
|
+
signing_key:
|
112
|
+
specification_version: 3
|
113
|
+
summary: A command-line CSV tool.
|
114
|
+
test_files:
|
115
|
+
- spec/cast_strings_spec.rb
|
116
|
+
- spec/spec_helper.rb
|