cetusql 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/cetusql.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "cetusql/version"
2
+
3
+ module Cetusql
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env ruby -w
2
+ # ----------------------------------------------------------------------------- #
3
+ # File: cli_sqlite.rb
4
+ # Description: common sqlite routines to be sourced in ruby shell programs
5
+ # Author: j kepler http://github.com/mare-imbrium/canis/
6
+ # Date: 2017-03-18 - 17:53
7
+ # License: MIT
8
+ # Last update: 2017-03-29 14:17
9
+ # ----------------------------------------------------------------------------- #
10
+ # YFF Copyright (C) 2012-2016 j kepler
11
+ # ----------------------------------------------------------------------------- #
12
+ require 'sqlite3'
13
+ require 'shellwords'
14
+ require 'pp'
15
+
16
+ def get_table_names db
17
+ #raise "No database file selected." unless $current_db
18
+
19
+ tables = get_data "select name from sqlite_master where type='table'"
20
+ tables.collect!{|x| x[0] } ## 1.9 hack, but will it run on 1.8 ??
21
+ tables
22
+ end
23
+ def get_column_names tbname
24
+ get_metadata tbname
25
+ end
26
+ # connect to given database, and if no name supplied then allow user to choose
27
+ def connect dbname=nil
28
+ dbname ||= getdbname
29
+ return nil unless dbname
30
+ #$log.debug "XXX: CONNECT got #{dbname} "
31
+ $current_db = dbname
32
+ $db = SQLite3::Database.new(dbname) if dbname
33
+ return $db
34
+ end
35
+ def get_data db, sql
36
+ #$log.debug "SQL: #{sql} "
37
+ $columns, *rows = db.execute2(sql)
38
+ #$log.debug "XXX COLUMNS #{sql}, #{rows.count} "
39
+ content = rows
40
+ return nil if content.nil? or content[0].nil?
41
+ $datatypes = content[0].types #if @datatypes.nil?
42
+ return content
43
+ end
44
+ def get_metadata table
45
+ get_data "select * from #{table} limit 1"
46
+ return $columns
47
+ end
48
+ # TODO option of headers
49
+ # run query and put into a temp table and view it using vim
50
+ # If no outputfile name passed, then use temp table
51
+ # What about STDOUT
52
+ # TODO use temp file, format it there and append to given file only after termtable
53
+ def OLDview_data db, sql, options
54
+ outputfile = options[:output_to]
55
+ formatting = options[:formatting]
56
+ headers = options[:headers]
57
+ #str = db.get_data sql
58
+ rs = db.execute_query sql
59
+ str = rs.content
60
+ columns = rs.columns
61
+ #puts "SQL: #{sql}.\nstr: #{str.size}"
62
+ data = []
63
+ if headers
64
+ data << columns.join("\t")
65
+ end
66
+ str.each {|line| data << line.join("\t"); }
67
+ #puts "Rows: #{data.size}"
68
+ require 'tempfile'
69
+ tmpfile = Tempfile.new('SQL.XXXXXX')
70
+ filename = tmpfile.path
71
+ filename = Shellwords.escape(filename)
72
+ #puts "Writing to #{filename}"
73
+ tmpfile.write(data.join("\n"))
74
+ tmpfile.close # need to flush, otherwise write is buffered
75
+ headerstr=nil
76
+ if formatting
77
+ headerstr = "-H" unless headers
78
+ # sometimes this can be slow, and it can fault on UTF-8 chars
79
+ system("cat #{filename} | term-table.rb #{headerstr} | sponge #{filename}")
80
+ end
81
+ if outputfile
82
+ #puts "comes here"
83
+ system("cp #{filename} #{outputfile}")
84
+ filename = outputfile
85
+ end
86
+ system "wc -l #{filename}" if $opt_debug
87
+
88
+ #system "$EDITOR #{filename}"
89
+ system "vim -c ':set nowrap' #{filename}"
90
+ tmpfile.close
91
+ tmpfile.unlink
92
+ end
93
+ def view_data db, sql, options
94
+ outputfile = options[:output_to]
95
+ formatting = options[:formatting]
96
+ headers = options[:headers]
97
+ #str = db.get_data sql
98
+ rs = db.execute_query sql
99
+ str = rs.content
100
+ columns = rs.columns
101
+ #puts "SQL: #{sql}.\nstr: #{str.size}"
102
+ #data = []
103
+ #if headers
104
+ #data << columns.join("\t")
105
+ #end
106
+ #str.each {|line| data << line.join("\t"); }
107
+ #puts "Rows: #{data.size}"
108
+ headerstr=nil
109
+ tmpfile = nil
110
+ if formatting
111
+ if headers
112
+ str.unshift(columns)
113
+ end
114
+ filename = tabulate2 str, options
115
+ else
116
+ data = []
117
+ if headers
118
+ data << columns.join("\t")
119
+ end
120
+ str.each {|line| data << line.join("\t"); }
121
+ tmpfile = mktemp()
122
+ tmpfile.write(data.join("\n"))
123
+ tmpfile.close # need to flush, otherwise write is buffered
124
+ filename = tmpfile.path
125
+ end
126
+ if outputfile
127
+ #puts "comes here"
128
+ system("cp #{filename} #{outputfile}")
129
+ filename = outputfile
130
+ end
131
+
132
+ #system "$EDITOR #{filename}"
133
+ system "vim -c ':set nowrap' #{filename}"
134
+ if tmpfile
135
+ tmpfile.close
136
+ tmpfile.unlink
137
+ end
138
+ end
139
+ def mktemp
140
+ require 'tempfile'
141
+ tmpfile = Tempfile.new('SQL.XXXXXX')
142
+ filename = tmpfile.path
143
+ filename = Shellwords.escape(filename)
144
+ #puts "Writing to #{filename}"
145
+ #tmpfile.write(data.join("\n"))
146
+ #tmpfile.close # need to flush, otherwise write is buffered
147
+ return tmpfile
148
+ end
149
+ # given content returned by get_data, formats and returns in a file
150
+ def tabulate content, options
151
+ data = []
152
+ content.each {|line| data << line.join("\t"); }
153
+ puts "Rows: #{data.size}" if $opt_verbose
154
+ require 'tempfile'
155
+ tmpfile = Tempfile.new('SQL.XXXXXX')
156
+ filename = tmpfile.path
157
+ #filename = Shellwords.escape(filename)
158
+ #puts "Writing to #{filename}"
159
+ tmpfile.write(data.join("\n"))
160
+ tmpfile.close # need to flush, otherwise write is buffered
161
+ if options[:formatting]
162
+ system("term-table.rb < #{filename} | sponge #{filename}")
163
+ end
164
+ return filename
165
+ end
166
+ # rather than use external program with dependencies,
167
+ # we generate tabular format for an array with headings
168
+ # returns filename
169
+ # TODO check for headings true
170
+ def tabulate2 content, options
171
+ widths = calculate_column_widths(content, 99)
172
+ str = "| "
173
+ sep = "+"
174
+ widths.each do |w|
175
+ str << "%-#{w}s | "
176
+ sep << ("-"*(w+2)) + "+"
177
+ end
178
+ data = []
179
+ data << sep
180
+ content.each_with_index {|line, ix|
181
+ data << str % line
182
+ data << sep if ix == 0
183
+ }
184
+ data << sep
185
+ require 'tempfile'
186
+ tmpfile = Tempfile.new('SQL.XXXXXX')
187
+ filename = tmpfile.path
188
+ #filename = Shellwords.escape(filename)
189
+ #puts "Writing to #{filename}"
190
+ tmpfile.write(data.join("\n"))
191
+ tmpfile.close # need to flush, otherwise write is buffered
192
+ return filename
193
+ end
194
+
195
+ def calculate_column_widths content, maxrows=99
196
+ widths = []
197
+ content.first.each_with_index {|r,i| widths << calculate_column_width(content, i, maxrows) }
198
+ return widths
199
+ end
200
+ def calculate_column_width content, col, maxrows=99
201
+ ret = 1
202
+ ctr = 0
203
+ content.each_with_index { |r, i|
204
+ break if ctr > maxrows
205
+ ctr += 1
206
+ c = r[col]
207
+ x = c.to_s.length
208
+ ret = x if x > ret
209
+ }
210
+ ret
211
+ end
212
+ class Database
213
+
214
+ ResultSet = Struct.new(:content, :columns, :datatypes)
215
+
216
+ def initialize(name)
217
+ raise ArgumentError, "Database name cannot be nil" unless name
218
+ @name = name
219
+ connect name
220
+ end
221
+ def connect name
222
+ raise ArgumentError, "Database name cannot be nil" unless name
223
+ @tables = nil
224
+ @db = SQLite3::Database.new(name)
225
+ end
226
+ def tables
227
+ return @tables if @tables
228
+ tables = sql "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
229
+ tables.collect!{|x| x[0] } ## 1.9 hack, but will it run on 1.8 ??
230
+ @tables = tables
231
+ end
232
+ def columns table
233
+ raise ArgumentError, "#{$0}: table name cannot be nil" unless table
234
+ columns, ignore = self.get_metadata table
235
+ return columns
236
+ end
237
+ def indexed_columns table
238
+ indexes = sql %Q{select sql from sqlite_master where type='index' and tbl_name = "#{table}" }
239
+ return nil if indexes == []
240
+ #pp indexes
241
+ arr = []
242
+ indexes.each do |ind|
243
+ if ind.first.nil?
244
+ arr << "auto?"
245
+ next
246
+ end
247
+ m = ind.first.match /\((.*)\)/
248
+ arr << m[1] if m
249
+ end
250
+ return arr.join(",")
251
+ end
252
+ def get_metadata table
253
+ raise ArgumentError, "#{$0}: table name cannot be nil" unless table
254
+ sql = "select * from #{table} limit 1"
255
+ columns, *rows = @db.execute2(sql)
256
+ content = rows
257
+ return nil if content.nil? or content[0].nil?
258
+ datatypes = content[0].types
259
+ return columns, datatypes
260
+ end
261
+ # runs sql query and returns an array of arrays
262
+ def get_data sql
263
+ #$log.debug "SQL: #{sql} "
264
+ columns, *rows = @db.execute2(sql)
265
+ #$log.debug "XXX COLUMNS #{sql}, #{rows.count} "
266
+ content = rows
267
+ return content
268
+ end
269
+ alias :sql :get_data
270
+ def execute_query sql
271
+ columns, *rows = @db.execute2(sql)
272
+ content = rows
273
+ return nil if content.nil? or content[0].nil?
274
+ datatypes = content[0].types
275
+ rs = ResultSet.new(content, columns, datatypes)
276
+ return rs
277
+ end
278
+ def sql_recent_rows tablename
279
+ sql = "SELECT * from #{tablename} ORDER by rowid DESC LIMIT 100"
280
+ end
281
+ def close
282
+ @db = nil
283
+ @tables = nil
284
+ end
285
+ end
@@ -0,0 +1,412 @@
1
+ #!/usr/bin/env ruby
2
+ # ----------------------------------------------------------------------------- #
3
+ # File: cli_utils.rb
4
+ # Description: common stuff for command line utils such as menu, getting a single char
5
+ # inline variable editing
6
+ # Author: j kepler http://github.com/mare-imbrium/canis/
7
+ # Date: 2017-03-18 - 14:33
8
+ # License: MIT
9
+ # Last update: 2017-04-02 19:47
10
+ # ----------------------------------------------------------------------------- #
11
+ # YFF Copyright (C) 2012-2016 j kepler
12
+
13
+
14
+ # edit a variable inline like zsh's vared
15
+ def vared var, prompt=">"
16
+ Readline.pre_input_hook = -> do
17
+ Readline.insert_text var
18
+ Readline.redisplay
19
+ # Remove the hook right away.
20
+ Readline.pre_input_hook = nil
21
+ end
22
+ begin
23
+ input = Readline.readline(prompt, false)
24
+ rescue Exception => e
25
+ return nil
26
+ end
27
+ input
28
+ end
29
+
30
+
31
+ # What if we only want to allow the given keys and ignore others.
32
+ # In menu maybe ENTER and other such keys should be ignored, or atleast
33
+ # option should be there, so i don't accidentally hit enter.
34
+ # print in columns, but take into account size so we don't exceed COLS
35
+ # (Some entries like paths can be long)
36
+ def menu title, h
37
+ return unless h
38
+
39
+ pbold "#{title}"
40
+ ctr = 0
41
+ #h.each_pair { |k, v| puts " #{k}: #{v}" }
42
+ h.each_pair { |k, v|
43
+ print " #{k}: %-20s" % [v]
44
+ if ctr > 1
45
+ print "\n"
46
+ ctr = 0
47
+ else
48
+ ctr += 1
49
+ end
50
+ }
51
+ print "\n >"
52
+ #print "\r >"
53
+ ch = get_char
54
+ puts ch
55
+ #system("clear") # ???
56
+ binding = h[ch]
57
+ binding = h[ch.to_sym] unless binding
58
+ if binding
59
+ if respond_to?(binding, true)
60
+ # 2017-03-19 - we can't send return values from a method ??
61
+ send(binding)
62
+ end
63
+ end
64
+ return ch, binding
65
+ end
66
+ ## get a character from user and return as a string
67
+ # Adapted from:
68
+ #http://stackoverflow.com/questions/174933/how-to-get-a-single-character-without-pressing-enter/8274275#8274275
69
+ # Need to take complex keys and matc against a hash.
70
+ def get_char
71
+ begin
72
+ system("stty raw -echo 2>/dev/null") # turn raw input on
73
+ c = nil
74
+ #if $stdin.ready?
75
+ c = $stdin.getc
76
+ cn=c.ord
77
+ return "ENTER" if cn == 10 || cn == 13
78
+ return "BACKSPACE" if cn == 127
79
+ return "C-SPACE" if cn == 0
80
+ return "SPACE" if cn == 32
81
+ # next does not seem to work, you need to bind C-i
82
+ return "TAB" if cn == 8
83
+ if cn >= 0 && cn < 27
84
+ x= cn + 96
85
+ return "C-#{x.chr}"
86
+ end
87
+ if c == ''
88
+ buff=c.chr
89
+ while true
90
+ k = nil
91
+ if $stdin.ready?
92
+ k = $stdin.getc
93
+ #puts "got #{k}"
94
+ buff += k.chr
95
+ else
96
+ x=$kh[buff]
97
+ return x if x
98
+ #puts "returning with #{buff}"
99
+ if buff.size == 2
100
+ ## possibly a meta/alt char
101
+ k = buff[-1]
102
+ return "M-#{k.chr}"
103
+ end
104
+ return buff
105
+ end
106
+ end
107
+ end
108
+ #end
109
+ return c.chr if c
110
+ ensure
111
+ #system "stty -raw echo" # turn raw input off
112
+ system("stty -raw echo 2>/dev/null") # turn raw input on
113
+ end
114
+ end
115
+ ## clean this up a bit, copied from shell program and macro'd
116
+ $kh=Hash.new
117
+ $kh["OP"]="F1"
118
+ $kh[""]="UP"
119
+ $kh["[5~"]="PGUP"
120
+ $kh['']="ESCAPE"
121
+ KEY_PGDN="[6~"
122
+ KEY_PGUP="[5~"
123
+ ## I needed to replace the O with a [ for this to work
124
+ # in Vim Home comes as ^[OH whereas on the command line it is correct as ^[[H
125
+ KEY_HOME=''
126
+ KEY_END=""
127
+ KEY_F1="OP"
128
+ KEY_UP=""
129
+ KEY_DOWN=""
130
+
131
+ $kh[KEY_PGDN]="PgDn"
132
+ $kh[KEY_PGUP]="PgUp"
133
+ $kh[KEY_HOME]="Home"
134
+ $kh[KEY_END]="End"
135
+ $kh[KEY_F1]="F1"
136
+ $kh[KEY_UP]="UP"
137
+ $kh[KEY_DOWN]="DOWN"
138
+ KEY_LEFT=''
139
+ KEY_RIGHT=''
140
+ $kh["OQ"]="F2"
141
+ $kh["OR"]="F3"
142
+ $kh["OS"]="F4"
143
+ $kh[KEY_LEFT] = "LEFT"
144
+ $kh[KEY_RIGHT]= "RIGHT"
145
+ KEY_F5='[15~'
146
+ KEY_F6='[17~'
147
+ KEY_F7='[18~'
148
+ KEY_F8='[19~'
149
+ KEY_F9='[20~'
150
+ KEY_F10='[21~'
151
+ KEY_S_F1=''
152
+ $kh[KEY_F5]="F5"
153
+ $kh[KEY_F6]="F6"
154
+ $kh[KEY_F7]="F7"
155
+ $kh[KEY_F8]="F8"
156
+ $kh[KEY_F9]="F9"
157
+ $kh[KEY_F10]="F10"
158
+ # testing out shift+Function. these are the codes my kb generates
159
+ $kh[KEY_S_F1]="S-F1"
160
+ $kh['']="S-F2"
161
+
162
+ def pbold text
163
+ puts "#{BOLD}#{text}#{BOLD_OFF}"
164
+ end
165
+ def pgreen text
166
+ puts "#{GREEN}#{text}#{CLEAR}"
167
+ end
168
+ def perror text
169
+ pred text
170
+ get_char
171
+ end
172
+ def pred text
173
+ puts "#{RED}#{text}#{CLEAR}"
174
+ end
175
+ def pause text=" Press a key ..."
176
+ print text
177
+ get_char
178
+ end
179
+ # alternative of menu that takes an array and uses numbers as indices.
180
+ # Hey wait, if there aer more than 10 then we are screwed since we take one character
181
+ # I have handled this somewhere, should check, maybe we should use characters
182
+ # returns text, can be nil if selection not one of choices
183
+ # How do we communicate to caller, that user pressed C-c
184
+ def select_from title, array
185
+ h = {}
186
+ array.each_with_index {|e,ix| ix += 1; h[ix.to_s] = e }
187
+ ch, text = menu title, h
188
+ unless text
189
+ if ch == "ENTER"
190
+ return array.first
191
+ end
192
+ end
193
+ return text
194
+ end
195
+ # multiselect from an array using fzf
196
+ def multi_select title, array
197
+ arr = %x[ echo "#{array.join("\n")}" | fzf --multi --reverse --prompt="#{title} >"]
198
+ return arr.split("\n")
199
+ end
200
+ # single select from an array using fzf
201
+ # CAUTION: this messes with single and double quotes, so don't pass a query in
202
+ def single_select title, array
203
+ str = %x[ echo "#{array.join("\n")}" | fzf --reverse --prompt="#{title} >" -1 -0 ]
204
+ return str
205
+ end
206
+ def editline array
207
+ Readline::HISTORY.push(*array)
208
+ begin
209
+ command = Readline::readline('>', true)
210
+ rescue Exception => e
211
+ return nil
212
+ end
213
+ return command
214
+ end
215
+
216
+ # allows user to select from list, returning string if user pressed ENTER
217
+ # Aborts if user presses Q or C-c or ESCAPE
218
+ def ctrlp arr
219
+ patt = nil
220
+ curr = 0
221
+ while true
222
+ # clear required otherwise will keep drawing itself
223
+ system("clear")
224
+ if patt and patt != ""
225
+ # need fuzzy match here
226
+ view = arr.grep(/^#{patt}/)
227
+ view = view | arr.grep(/#{patt}/)
228
+ fuzzypatt = patt.split("").join(".*")
229
+ view = view | arr.grep(/#{fuzzypatt}/)
230
+ else
231
+ view = arr
232
+ end
233
+ curr = [view.size-1, curr].min
234
+ # if empty then curr becomes -1
235
+ curr = 0 if curr < 0
236
+ view.each_with_index do |a, i|
237
+ mark = " "
238
+ mark = ">" if curr == i
239
+ print "#{mark} #{a} \n"
240
+ end
241
+ #puts " "
242
+ print "\r#{patt} >"
243
+ ch = get_char
244
+ if ch =~ /^[a-z]$/
245
+ patt ||= ""
246
+ patt << ch
247
+ elsif ch == "BACKSPACE"
248
+ if patt && patt.size > 0
249
+ patt = patt[0..-2]
250
+ end
251
+ elsif ch == "Q" or ch == "C-c" or ch == "ESCAPE"
252
+ break
253
+ elsif ch == "UP"
254
+ curr -= 1
255
+ curr = 0 if curr < 0
256
+ elsif ch == "DOWN"
257
+ curr += 1
258
+ curr = [view.size-1, curr].min
259
+ # if empty then curr becomes -1
260
+ curr = 0 if curr < 0
261
+ elsif ch == "ENTER"
262
+ return view[curr]
263
+ else
264
+ # do right and left arrow
265
+
266
+ # get arrow keys here
267
+ end
268
+
269
+ end
270
+ end
271
+ ## CONSTANTS
272
+ GMARK = '*'
273
+ CURMARK = '>'
274
+ SPACE = " "
275
+ CLEAR = "\e[0m"
276
+ BOLD = "\e[1m"
277
+ BOLD_OFF = "\e[22m"
278
+ RED = "\e[31m"
279
+ ON_RED = "\e[41m"
280
+ GREEN = "\e[32m"
281
+ YELLOW = "\e[33m"
282
+ BLUE = "\e[1;34m"
283
+
284
+ ON_BLUE = "\e[44m"
285
+ REVERSE = "\e[7m"
286
+ UNDERLINE = "\e[4m"
287
+ #CURSOR_COLOR = ON_BLUE
288
+ CURSOR_COLOR = CLEAR
289
+
290
+ # --- end constants
291
+ ## check screen size and accordingly adjust some variables
292
+ #
293
+ def screen_settings
294
+ $glines=%x(tput lines).to_i
295
+ $gcols=%x(tput cols).to_i
296
+ $grows = $glines - 3
297
+ $pagesize = 60
298
+ #$gviscols = 3
299
+ $pagesize = $grows * $gviscols
300
+ end
301
+
302
+ # readline version of gets
303
+ def input(prompt="", newline=false)
304
+ prompt += "\n" if newline
305
+ ret = nil
306
+ begin
307
+ ret = Readline.readline(prompt, true).squeeze(" ").strip
308
+ rescue Exception => e
309
+ return nil
310
+ end
311
+ return ret
312
+ end
313
+ def agree(prompt="")
314
+ x = input(prompt)
315
+ return true if x.upcase == "Y"
316
+ false
317
+ end
318
+ alias :confirm :agree
319
+ ##
320
+ #
321
+ # print in columns
322
+ # ary - array of data
323
+ # sz - lines in one column
324
+ #
325
+ def columnate ary, sz
326
+ buff=Array.new
327
+ return buff if ary.nil? || ary.size == 0
328
+
329
+ # determine width based on number of files to show
330
+ # if less than sz then 1 col and full width
331
+ #
332
+ wid = 30
333
+ ars = ary.size
334
+ ars = [$pagesize, ary.size].min
335
+ d = 0
336
+ if ars <= sz
337
+ wid = $gcols - d
338
+ else
339
+ tmp = (ars * 1.000/ sz).ceil
340
+ wid = $gcols / tmp - d
341
+ end
342
+ #elsif ars < sz * 2
343
+ #wid = $gcols/2 - d
344
+ #elsif ars < sz * 3
345
+ #wid = $gcols/3 - d
346
+ #else
347
+ #wid = $gcols/$gviscols - d
348
+ #end
349
+
350
+ # ix refers to the index in the complete file list, wherease we only show 60 at a time
351
+ ix=0
352
+ while true
353
+ ## ctr refers to the index in the column
354
+ ctr=0
355
+ while ctr < sz
356
+
357
+ f = ary[ix]
358
+ fsz = f.size
359
+ if fsz > wid
360
+ f = f[0, wid-2]+"$ "
361
+ ## we do the coloring after trunc so ANSI escpe seq does not get get
362
+ if ix + $sta == $cursor
363
+ f = "#{CURSOR_COLOR}#{f}#{CLEAR}"
364
+ end
365
+ else
366
+ ## we do the coloring before padding so the entire line does not get padded, only file name
367
+ if ix + $sta == $cursor
368
+ f = "#{CURSOR_COLOR}#{f}#{CLEAR}"
369
+ end
370
+ #f = f.ljust(wid)
371
+ f << " " * (wid-fsz)
372
+ end
373
+
374
+ if buff[ctr]
375
+ buff[ctr] += f
376
+ else
377
+ buff[ctr] = f
378
+ end
379
+
380
+ ctr+=1
381
+ ix+=1
382
+ break if ix >= ary.size
383
+ end
384
+ break if ix >= ary.size
385
+ end
386
+ return buff
387
+ end
388
+ # print an array in columns
389
+ # default number of columns is 3 and can be supplied. I actually much prefer that this be derived, so we can get more
390
+ # columns if possible, and te width should be caclulated too
391
+ def print_in_cols a, noc=nil
392
+ unless noc
393
+ noc = 3
394
+ if a.size < 7
395
+ noc = 1
396
+ elsif a.size < 15
397
+ noc = 2
398
+ end
399
+ end
400
+
401
+ x = noc - 1
402
+ cols = a.each_slice((a.size+x)/noc).to_a
403
+ # todo width should be determined based on COLS of screen, and width of data
404
+ cols.first.zip( *cols[1..-1] ).each{|row| puts row.map{|e| e ? '%-30s' % e : ' '}.join(" ") }
405
+ end
406
+ ## expects array of arrays
407
+ #class Array
408
+ #def to_table l = []
409
+ #self.each{|r|r.each_with_index{|f,i|l[i] = [l[i]||0, f.length].max}}
410
+ #self.each{|r|r.each_with_index{|f,i|print "#{f.ljust l[i]}|"};puts ""}
411
+ #end
412
+ #end