libisi 0.3.0

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.
Files changed (91) hide show
  1. data/LICENSE +677 -0
  2. data/Manifest +89 -0
  3. data/Rakefile +34 -0
  4. data/lib/inifile.rb +119 -0
  5. data/lib/libisi.rb +948 -0
  6. data/lib/libisi/attribute.rb +32 -0
  7. data/lib/libisi/attribute/activerecord.rb +34 -0
  8. data/lib/libisi/attribute/base.rb +33 -0
  9. data/lib/libisi/base.rb +109 -0
  10. data/lib/libisi/bridge.rb +21 -0
  11. data/lib/libisi/bridge/base.rb +23 -0
  12. data/lib/libisi/bridge/java.rb +71 -0
  13. data/lib/libisi/bridge/python.rb +37 -0
  14. data/lib/libisi/cache.rb +21 -0
  15. data/lib/libisi/cache/base.rb +67 -0
  16. data/lib/libisi/cache/file_cache.rb +24 -0
  17. data/lib/libisi/chart.rb +21 -0
  18. data/lib/libisi/chart/base.rb +320 -0
  19. data/lib/libisi/chart/jfreechart.rb +682 -0
  20. data/lib/libisi/chart/jfreechart_generator.rb +206 -0
  21. data/lib/libisi/color.rb +21 -0
  22. data/lib/libisi/color/base.rb +66 -0
  23. data/lib/libisi/color/colortools.rb +92 -0
  24. data/lib/libisi/color/java.rb +44 -0
  25. data/lib/libisi/concept.rb +33 -0
  26. data/lib/libisi/concept/activerecord.rb +39 -0
  27. data/lib/libisi/concept/base.rb +58 -0
  28. data/lib/libisi/doc.rb +35 -0
  29. data/lib/libisi/doc/base.rb +414 -0
  30. data/lib/libisi/doc/html.rb +85 -0
  31. data/lib/libisi/doc/text.rb +98 -0
  32. data/lib/libisi/doc/wiki.rb +55 -0
  33. data/lib/libisi/environment.rb +21 -0
  34. data/lib/libisi/environment/base.rb +36 -0
  35. data/lib/libisi/environment/http.rb +105 -0
  36. data/lib/libisi/environment/rails.rb +27 -0
  37. data/lib/libisi/environment/root.rb +23 -0
  38. data/lib/libisi/fake_logger/logger.rb +61 -0
  39. data/lib/libisi/function/base.rb +30 -0
  40. data/lib/libisi/hal.rb +558 -0
  41. data/lib/libisi/instance.rb +27 -0
  42. data/lib/libisi/instance/activerecord.rb +21 -0
  43. data/lib/libisi/instance/base.rb +42 -0
  44. data/lib/libisi/log.rb +237 -0
  45. data/lib/libisi/mail/base.rb +32 -0
  46. data/lib/libisi/mail/tmail.rb +120 -0
  47. data/lib/libisi/parameter/base.rb +41 -0
  48. data/lib/libisi/property.rb +27 -0
  49. data/lib/libisi/property/base.rb +28 -0
  50. data/lib/libisi/reciever/base.rb +31 -0
  51. data/lib/libisi/reciever/socket.rb +31 -0
  52. data/lib/libisi/relation.rb +23 -0
  53. data/lib/libisi/request.rb +22 -0
  54. data/lib/libisi/request/base.rb +29 -0
  55. data/lib/libisi/request/http.rb +129 -0
  56. data/lib/libisi/response/base.rb +27 -0
  57. data/lib/libisi/task/base.rb +27 -0
  58. data/lib/libisi/task/http.rb +90 -0
  59. data/lib/libisi/tee.rb +296 -0
  60. data/lib/libisi/ui/base.rb +116 -0
  61. data/lib/libisi/ui/console.rb +238 -0
  62. data/lib/libisi/ui/kde.rb +94 -0
  63. data/lib/libisi/ui/nobody.rb +29 -0
  64. data/lib/libisi/ui/rails.rb +150 -0
  65. data/lib/libisi/ui/x11.rb +55 -0
  66. data/lib/libisi/uri.rb +42 -0
  67. data/lib/libisi/uri/activerecord.rb +152 -0
  68. data/lib/libisi/uri/base.rb +115 -0
  69. data/lib/libisi/uri/file.rb +43 -0
  70. data/lib/libisi/uri/ldap.rb +72 -0
  71. data/lib/libisi/uri/mysql.rb +98 -0
  72. data/lib/libisi/value.rb +31 -0
  73. data/lib/libisi/value/attribute_value.rb +19 -0
  74. data/lib/libisi/value/base.rb +55 -0
  75. data/lib/libisi/value/property_value.rb +19 -0
  76. data/lib/libisi/value/relation_value.rb +19 -0
  77. data/lib/ordered_hash.rb +228 -0
  78. data/libisi.gemspec +31 -0
  79. data/test/bridge_test.rb +77 -0
  80. data/test/cache_test.rb +65 -0
  81. data/test/chart_test.rb +179 -0
  82. data/test/color_test.rb +64 -0
  83. data/test/concept_test.rb +56 -0
  84. data/test/doc_test.rb +172 -0
  85. data/test/fixtures/test.db +0 -0
  86. data/test/ordered_hash_test.rb +39 -0
  87. data/test/profile_test.rb +36 -0
  88. data/test/request_test.rb +121 -0
  89. data/test/test +0 -0
  90. data/test/ui_test.rb +62 -0
  91. metadata +244 -0
data/Manifest ADDED
@@ -0,0 +1,89 @@
1
+ LICENSE
2
+ Rakefile
3
+ lib/inifile.rb
4
+ lib/libisi.rb
5
+ lib/libisi/attribute.rb
6
+ lib/libisi/attribute/activerecord.rb
7
+ lib/libisi/attribute/base.rb
8
+ lib/libisi/base.rb
9
+ lib/libisi/bridge.rb
10
+ lib/libisi/bridge/base.rb
11
+ lib/libisi/bridge/java.rb
12
+ lib/libisi/bridge/python.rb
13
+ lib/libisi/cache.rb
14
+ lib/libisi/cache/base.rb
15
+ lib/libisi/cache/file_cache.rb
16
+ lib/libisi/chart.rb
17
+ lib/libisi/chart/base.rb
18
+ lib/libisi/chart/jfreechart.rb
19
+ lib/libisi/chart/jfreechart_generator.rb
20
+ lib/libisi/color.rb
21
+ lib/libisi/color/base.rb
22
+ lib/libisi/color/colortools.rb
23
+ lib/libisi/color/java.rb
24
+ lib/libisi/concept.rb
25
+ lib/libisi/concept/activerecord.rb
26
+ lib/libisi/concept/base.rb
27
+ lib/libisi/doc.rb
28
+ lib/libisi/doc/base.rb
29
+ lib/libisi/doc/html.rb
30
+ lib/libisi/doc/text.rb
31
+ lib/libisi/doc/wiki.rb
32
+ lib/libisi/environment.rb
33
+ lib/libisi/environment/base.rb
34
+ lib/libisi/environment/http.rb
35
+ lib/libisi/environment/rails.rb
36
+ lib/libisi/environment/root.rb
37
+ lib/libisi/fake_logger/logger.rb
38
+ lib/libisi/function/base.rb
39
+ lib/libisi/hal.rb
40
+ lib/libisi/instance.rb
41
+ lib/libisi/instance/activerecord.rb
42
+ lib/libisi/instance/base.rb
43
+ lib/libisi/log.rb
44
+ lib/libisi/mail/base.rb
45
+ lib/libisi/mail/tmail.rb
46
+ lib/libisi/parameter/base.rb
47
+ lib/libisi/property.rb
48
+ lib/libisi/property/base.rb
49
+ lib/libisi/reciever/base.rb
50
+ lib/libisi/reciever/socket.rb
51
+ lib/libisi/relation.rb
52
+ lib/libisi/request.rb
53
+ lib/libisi/request/base.rb
54
+ lib/libisi/request/http.rb
55
+ lib/libisi/response/base.rb
56
+ lib/libisi/task/base.rb
57
+ lib/libisi/task/http.rb
58
+ lib/libisi/tee.rb
59
+ lib/libisi/ui/base.rb
60
+ lib/libisi/ui/console.rb
61
+ lib/libisi/ui/kde.rb
62
+ lib/libisi/ui/nobody.rb
63
+ lib/libisi/ui/rails.rb
64
+ lib/libisi/ui/x11.rb
65
+ lib/libisi/uri.rb
66
+ lib/libisi/uri/activerecord.rb
67
+ lib/libisi/uri/base.rb
68
+ lib/libisi/uri/file.rb
69
+ lib/libisi/uri/ldap.rb
70
+ lib/libisi/uri/mysql.rb
71
+ lib/libisi/value.rb
72
+ lib/libisi/value/attribute_value.rb
73
+ lib/libisi/value/base.rb
74
+ lib/libisi/value/property_value.rb
75
+ lib/libisi/value/relation_value.rb
76
+ lib/ordered_hash.rb
77
+ test/bridge_test.rb
78
+ test/cache_test.rb
79
+ test/chart_test.rb
80
+ test/color_test.rb
81
+ test/concept_test.rb
82
+ test/doc_test.rb
83
+ test/fixtures/test.db
84
+ test/ordered_hash_test.rb
85
+ test/profile_test.rb
86
+ test/request_test.rb
87
+ test/test
88
+ test/ui_test.rb
89
+ Manifest
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ # Copyright (C) 2007-2010 Logintas AG Switzerland
2
+ #
3
+ # This file is part of Libisi.
4
+ #
5
+ # Libisi is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Libisi is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Libisi. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require 'rubygems'
19
+ require 'rake'
20
+ require 'echoe'
21
+
22
+ # Echoe
23
+ # See http://blog.evanweaver.com/files/doc/fauna/echoe/files/README.html
24
+
25
+ Echoe.new('libisi', '0.3.0') do |p|
26
+ p.description = "Library for easy and fast shell script developing"
27
+ p.url = "http://rubyforge.org/projects/libisi/"
28
+ p.author = "Pellanda Flavio, Copyright Logintas AG"
29
+ p.email = "flavio.pellanda@logintas.ch"
30
+ # p.ignore_pattern = ["svn_user.yml", "svn_project.rake"]
31
+ # p.project = "ucbrb"
32
+ end
33
+
34
+ Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
data/lib/inifile.rb ADDED
@@ -0,0 +1,119 @@
1
+ # Copyright (C) 2004 Gregoire Lejeune <gregoire.lejeune@free.fr>
2
+ #
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation; either version 2 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program; if not, write to the Free Software
15
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16
+
17
+ class IniFile
18
+
19
+ public
20
+
21
+ def initialize( file = nil )
22
+ clear( )
23
+ if file.nil? == false and file.empty? == false
24
+ load( file )
25
+ end
26
+ end
27
+
28
+ def load( file )
29
+ clear( )
30
+ @hhxData = Hash::new( )
31
+ @sectionList = []
32
+ @xIniFile = file
33
+ parseFile( )
34
+ end
35
+
36
+ def write( file = nil )
37
+ xWriteFile = @xIniFile
38
+ if file.nil? == false
39
+ xWriteFile = file
40
+ end
41
+
42
+ fIni = open( xWriteFile, "w" )
43
+ @sectionList.each {|xSection|
44
+ hxPairs = @hhData[xSection]
45
+ fIni.print "[", xSection, "]\n"
46
+ hxPairs.each{ |xKey, xValue|
47
+ fIni.print xKey, " = ", xValue, "\n"
48
+ }
49
+ fIni.puts "\n"
50
+ }
51
+ fIni.close( )
52
+ end
53
+
54
+ def sections
55
+ if @hhxData.nil? == false
56
+ @sectionList.each {|k|
57
+ yield( k )
58
+ }
59
+ end
60
+ end
61
+
62
+ def clear
63
+ @hhxData = nil
64
+ @sectionList = nil
65
+ @xIniFile = nil
66
+ end
67
+
68
+ def [](section)
69
+ if @hhxData.nil? == false
70
+ return @hhxData[section]
71
+ end
72
+ end
73
+
74
+ def []=(section, hash)
75
+ if @hhxData.nil? == true
76
+ @hhxData = Hash::new( )
77
+ @sectionList = []
78
+ end
79
+
80
+ @hhxData[section] = hash
81
+ end
82
+
83
+ private
84
+
85
+ @xIniFile
86
+ @hhxData
87
+ @sectionList
88
+
89
+ def parseFile
90
+ xCurrentSection = nil
91
+
92
+ open( @xIniFile, 'r' ) do |f|
93
+ xLine = ''
94
+ until f.eof?
95
+ xLine = f.gets.chomp.gsub(/;.*/, '').strip;
96
+
97
+ if xLine.empty? == false
98
+ case xLine
99
+ when /^\[(.*)\]$/
100
+ xCurrentSection = $1
101
+ if @hhxData.has_key?( xCurrentSection ) == false
102
+ @hhxData[xCurrentSection] = Hash::new( )
103
+ @sectionList << xCurrentSection
104
+ end
105
+
106
+ when /^([^=]+?)=/
107
+ xKey = $1.strip
108
+ xValue = $'.strip
109
+ @hhxData[xCurrentSection][xKey] = xValue
110
+
111
+ else
112
+ print "ERROR !!!\n"
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ end
data/lib/libisi.rb ADDED
@@ -0,0 +1,948 @@
1
+ # Copyright (C) 2007-2010 Logintas AG Switzerland
2
+ #
3
+ # This file is part of Libisi.
4
+ #
5
+ # Libisi is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Libisi is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Libisi. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require "open3"
19
+ require "ordered_hash"
20
+
21
+ module LibIsi
22
+
23
+ LOG_FORMAT = "[%l] %d :: %m"
24
+ LIBISI = true
25
+ def init_libisi(options = {})
26
+ require "date"
27
+ require "time"
28
+ require "fileutils"
29
+ require "pathname"
30
+ require "libisi/doc"
31
+ require "libisi/log"
32
+ require "libisi/uri"
33
+
34
+ Log.init(options) unless options[:no_logging]
35
+ initialize_ui(options) unless options[:no_ui]
36
+ initialize_environment(options) unless options[:no_environment]
37
+ initialize_mail(options) unless options[:no_mail]
38
+ Doc.init(options) unless options[:no_doc]
39
+ Uri.init(options) unless options[:no_uri]
40
+ end
41
+
42
+ # Mail
43
+ def initialize_mail(options)
44
+ # there is no ohter implementation yet
45
+ raise "Mail already initialized" if $mail
46
+ require "libisi/mail/tmail"
47
+ $mail = TMailMail.new
48
+ end
49
+
50
+ # UI
51
+ def initialize_ui(options)
52
+ raise "UI already initialized" if $ui
53
+
54
+ if options[:ui]
55
+ @ui_overwritten = true
56
+ ui = options[:ui]
57
+ else
58
+ ui = ((rails_available? and "rails") or
59
+ (kde_available? and "kde") or
60
+ (x_available? and "x11") or
61
+ (terminal_available? and "console") or
62
+ "nobody")
63
+ end
64
+ change_ui(ui)
65
+ end
66
+ def change_ui(ui)
67
+ ui = ui.to_s
68
+ raise "Hacking attack!!" unless ui.class == String
69
+ raise "Unexpected UI name #{ui}." unless ui =~ /^[a-zA-Z][a-zA-Z0-9]*$/
70
+ require "libisi/ui/#{ui}.rb"
71
+ klass = eval("#{ui.capitalize}UI")
72
+ $ui = klass.new
73
+ end
74
+ def kde_available?
75
+ return false unless ENV["KDE_FULL_SESSION"] == "true"
76
+ unless system("ps aux | grep -v grep | grep kded > /dev/null")
77
+ $log.warn("kded not running but kde seems to be available. Executing kded --new-startup.")
78
+ return false unless system("kded --new-startup")
79
+ end
80
+ true
81
+ end
82
+ def x_available?
83
+ if ENV["DISPLAY"]
84
+ unless Pathname.new("/usr/bin/xvinfo").exist?
85
+ $log.debug("xvinfo not available => return x_available: false")
86
+ return false
87
+ end
88
+ system("xvinfo 2>/dev/null 1> /dev/null")
89
+ return true if $?.exitstatus != 255
90
+ $log.warn("DISPLAY set to #{ENV["DISPLAY"]} but cannot access X.")
91
+ end
92
+ false
93
+ end
94
+ def terminal_available?
95
+ ENV["TERM"]
96
+ end
97
+
98
+ # OUTPUT
99
+ def add_output(file)
100
+ unless Log.output(file) or
101
+ Doc.output(file)
102
+ raise "No outputter found for #{file}"
103
+ end
104
+ end
105
+
106
+
107
+ # LOG
108
+ def new_logger(name, filename, options = {})
109
+ # function now in Log class
110
+ Log.new_logger(name, filename, options)
111
+ end
112
+
113
+ # ENVIRONMENT
114
+ def initialize_environment(options)
115
+ "Could not parse caller main script: #{caller[-1]}" unless caller[-1] =~ /(.*)\:\d+/
116
+ ENV["main_script"] = $1
117
+ raise "Main script '#{ENV["main_script"]}' not found." unless main_script.exist?
118
+
119
+ ENV["PROGRAM_NAME"] = program_name
120
+ ENV["PROGRAM_IDENT"] = program_name
121
+ ENV["TODAY"] = DateTime.now.strftime("%F")
122
+ require 'socket'
123
+ ENV["HOST"] = open("|hostname") {|f| (f.readlines[0] or "").strip}
124
+ ENV["NET"] = open("|hostname -d") { |f| (f.readlines[0] or "").strip }
125
+ ENV["USER"] = open("|whoami") {|f| f.readlines[0].strip}
126
+ ENV["STARTDATETIME"] = DateTime.now.strftime("%F-%T")
127
+ # Set directory for temporary files
128
+ ENV["TMPDIR"] ||= "/var/tmp"
129
+ # output of compress and zip programs
130
+ # must be in english to work correctly
131
+ ENV["LANGUAGE"] = "en_US.UTF-8"
132
+
133
+ # set benchmarking if defined on evironment
134
+ if ENV["BENCHMARK"] or ENV["BENCHMARKINK"]
135
+ self.benchmarking = true
136
+ end
137
+
138
+ #Catch & log unhandled exceptions
139
+ at_exit {
140
+ if self.profiling
141
+ profiling_stop
142
+ end
143
+
144
+ begin
145
+ pid_file.delete if pid_file.exist?
146
+ rescue
147
+ $log.error("Could not remove pid file #{pid_file}: #{$!}") if $log
148
+ end
149
+
150
+ unless $! .class == SystemExit or $!.nil?
151
+ $log.fatal("#{$!.class.name}: #{$!.to_s}")
152
+ $@.each {|l| $log.debug{l} } if $log.debug?
153
+
154
+ # exit immediately
155
+ exit! 99
156
+ end
157
+ }
158
+
159
+ # add lib directory if parent directory of
160
+ # source script has a lib directory
161
+ libdir = Pathname.new(ENV["main_script"]).dirname.parent + "lib"
162
+ $LOAD_PATH.insert(0, libdir.to_s) if libdir.exist?
163
+ end
164
+
165
+ def program_name; main_script.basename.to_s; end
166
+ def program_instance; ENV["PROGRAM_INSTANCE"]; end
167
+ def program_instance=(inst); ENV["PROGRAM_INSTANCE"] = inst; end
168
+ def main_script; Pathname.new(ENV["main_script"]); end
169
+ def user; ENV["USER"]; end
170
+ def full_qualified_domainname; "#{ENV["HOST"]}.#{ENV["NET"]}"; end
171
+ def host_name; "#{ENV["HOST"]}"; end
172
+
173
+ def paths
174
+ "Could not parse caller: #{caller[-1]}" unless caller[-1] =~ /(.*)\:\d+/
175
+ calling_file = Pathname.new($1).cleanpath
176
+ all_paths = {
177
+ :rails => {
178
+ :config => Pathname.new("config"),
179
+ :binary => Pathname.new("script"),
180
+ :lib => Pathname.new("lib")
181
+ },
182
+ :debian => {
183
+ :data => Pathname.new("/usr/share"),
184
+ :config => Pathname.new("/etc"),
185
+ :binary => Pathname.new("/usr/bin"),
186
+ :lib => Pathname.new("/usr/lib/ruby/1.8"),
187
+ },
188
+ :setup => {
189
+ :data => Pathname.new("data"),
190
+ :config => Pathname.new("conf"),
191
+ :binary => Pathname.new("bin"),
192
+ :lib => Pathname.new("lib"),
193
+ :test => Pathname.new("test"),
194
+ }
195
+ }
196
+ $log.debug("Calling file: #{calling_file}")
197
+ file_type = all_paths.map {|env, files|
198
+ files.map {|type, path|
199
+ # eliminate overlappings
200
+ next if env == :setup and calling_file.to_s =~ /^\/usr\/bin/
201
+ next if env == :setup and calling_file.to_s =~ /rails/
202
+
203
+ next type if calling_file.to_s.starts_with?(path.to_s)
204
+ next type if calling_file.dirname.basename.to_s == path.to_s
205
+ }.compact.map {|type| [env,type]}
206
+ }.flatten
207
+ if file_type.length != 2
208
+ type = :rails if defined?(RAILS_ROOT)
209
+ else
210
+ type = file_type[0]
211
+ end
212
+ raise "Could not determine caller type #{file_type.inspect} from #{calling_file}" unless type
213
+
214
+ ret = all_paths[type].dup
215
+ ret.each {|key,val|
216
+ ret[key] = calling_file.dirname.parent + val
217
+ }
218
+ return ret
219
+ end
220
+
221
+ def with_temp_directory(name = nil)
222
+ dir = Pathname.new("/var/tmp/#{program_name}.#{Process.pid}")
223
+ dir.mkdir
224
+ begin
225
+ FileUtils.cd(dir) {
226
+ begin
227
+ yield
228
+ rescue
229
+ if $log.debug? and $ui.respond_to?(:shell)
230
+ $ui.shell if
231
+ $ui.question("Error ocurred in temporary directory #{dir}\nError: #{$!.to_s}\nDo you want a shell, before removing directory?",:default => false)
232
+ end
233
+ raise
234
+ end
235
+ }
236
+ ensure
237
+ dir.rmtree
238
+ end
239
+ end
240
+
241
+ def temp_file(name = nil)
242
+ if name.nil?
243
+ @temp_file_num ||= -1
244
+ @temp_file_num += 1
245
+ name = @temp_file_num += 1
246
+ end
247
+ temp_files([name]) {|tf|
248
+ yield tf[0]
249
+ }
250
+ end
251
+ def temp_files(*names)
252
+ files = names.map {|n| Pathname.new("/var/tmp/#{program_name}.#{Process.pid}-#{n}") }
253
+ begin
254
+ yield files
255
+ ensure
256
+ files.each {|f|
257
+ f.delete if f.exist?
258
+ }
259
+ end
260
+ end
261
+
262
+ # Profile
263
+ def profiling; $profiling; end
264
+ def profiling=(val)
265
+ $profiling = val
266
+ if self.profiling
267
+ $log.info("Turned on Profiling")
268
+ require "ruby-prof"
269
+ $log.debug("Starting ruby prof")
270
+ RubyProf.start
271
+ else
272
+ $log.info("Turned off Profiling")
273
+ end
274
+ end
275
+ def profiling_stop
276
+ $log.debug("Stopping ruby prof")
277
+ result = RubyProf.stop
278
+
279
+ path = Pathname.new(self.profiling)
280
+ raise "Profile path disappeared: #{path}" unless path.exist?
281
+
282
+ {RubyProf::FlatPrinter => ".txt",
283
+ RubyProf::GraphPrinter => ".txt",
284
+ RubyProf::GraphHtmlPrinter => ".html",
285
+ RubyProf::CallTreePrinter => ".txt"}.each {|printer_class, ending|
286
+ printer = printer_class.new(result)
287
+ output_path = path + "RubyProf_#{printer_class.name}#{ending}"
288
+ $log.debug("Writing #{output_path}")
289
+ output_path.open("w") {|profile_out|
290
+ printer.print(profile_out)
291
+ }
292
+ }
293
+ $profiling = false
294
+ end
295
+
296
+ # Benchmark
297
+ def benchmarking; $benchmarking; end
298
+ def benchmarking=(val)
299
+ $benchmarking = val
300
+ $benchmarks = {}
301
+ if self.benchmarking
302
+ Log.log_level = 2 if Log.log_level > 2
303
+ $log.info("Turned on Benchmarking")
304
+ else
305
+ $log.info("Turned off Benchmarking")
306
+ end
307
+ end
308
+
309
+ def benchmark(name = nil, options = {})
310
+ return yield unless benchmarking
311
+ require "benchmark"
312
+ ret = nil
313
+ bench = Benchmark.measure {
314
+ ret = yield
315
+ }
316
+ $log.info("Benchmark #{name}: #{bench.to_s}")
317
+ if $benchmarks[name]
318
+ $benchmarks[name] += bench
319
+ else
320
+ $benchmarks[name] = bench
321
+ end
322
+ ret
323
+ end
324
+
325
+ # BASH
326
+ def bash_eval(expr)
327
+ n_expr = "echo #{expr}".inspect
328
+ t_expr = ""
329
+ in_quotes = false
330
+ n_expr.length.times {|i|
331
+ ch = n_expr[i..i]
332
+ case ch
333
+ when "'"
334
+ in_quotes = !in_quotes
335
+ when "$"
336
+ if in_quotes
337
+ t_expr += "\\"
338
+ end
339
+ end
340
+ t_expr += ch
341
+
342
+ }
343
+ n_expr = t_expr
344
+ cmd = "/bin/bash -c #{n_expr} "
345
+ # new_expr = "echo '#{expr.gsub("'","\\\\\\\\'")}'"
346
+ # print "-----" + expr + "\n"
347
+ # print "-----" + new_expr + "\n"
348
+ # cmd = "/bin/bash -c #{new_expr}"
349
+ # print "-----" + cmd + "\n\n"
350
+ ret = open("| #{cmd}") {|b|
351
+ b.readlines.join.strip
352
+ }
353
+ raise "Error during evalating #{expr.inspect}" unless $?.success?
354
+ $log.debug{"bash_eval: #{expr.inspect} => #{ret.inspect}"}
355
+ ret
356
+ end
357
+
358
+ def escape_bash(command)
359
+ command.gsub("\\","\\\\").gsub("\"","\\\"").gsub("\$","\\\$")
360
+ end
361
+ def execute_on_remote_command(remote, command)
362
+ return command if remote.nil? or remote == "localhost" or remote == "127.0.0.1"
363
+ command = "ssh -T #{remote} \"#{escape_bash(command)}\""
364
+ end
365
+
366
+ def result_of_system(command, error_ok = false)
367
+ $log.debug{"Execute #{command.inspect}"}
368
+ res = open("|#{command}") {|f| f.readlines.join}
369
+ raise "Error executing #{command.inspect}." if !error_ok and !$?.success?
370
+ $log.debug{"Result is #{res.inspect}"}
371
+ res
372
+ end
373
+ def source(filename)
374
+ $log.debug{"Sourcing file '#{filename}'"}
375
+ open(filename) {|f|
376
+ f.each {|line|
377
+ begin
378
+ case line
379
+ when /^\s*\#/, /^$/
380
+ # comment or empty
381
+ when /\s*(\S+)=(.*)/
382
+ ENV[$1] = bash_eval($2)
383
+ else
384
+ raise "Unexpected line #{line.inspect} in source file '#{filename}'."
385
+ end
386
+ rescue
387
+ raise "Could not parse line #{line.inspect}: #{$!}"
388
+ end
389
+ }
390
+ }
391
+ end
392
+ def save_env
393
+ old = {}
394
+ ENV.each {|key,val|
395
+ old[key] = val
396
+ }
397
+ old
398
+ end
399
+ def load_env(new_env = nil)
400
+ raise "Give either a block or a new environment hash, not both." if
401
+ !block_given? and new_env.nil?
402
+
403
+ old_env = nil
404
+ if block_given?
405
+ old_env = save_env
406
+ end
407
+
408
+ if new_env
409
+ new_env.each {|key,val|
410
+ ENV[key] = val
411
+ }
412
+ ENV.each {|key,val|
413
+ ENV.delete(key) unless new_env.key?(key)
414
+ }
415
+ end
416
+
417
+ if block_given?
418
+ result = yield
419
+ end
420
+
421
+ load_env(old_env) if old_env
422
+ result
423
+ end
424
+
425
+ def command_line_parse(str)
426
+ return str if str.class == Array
427
+ quotes = ["\"","\'"]
428
+ regexp = "(" + (quotes.map {|q| Regexp.escape(q) }.map {|q| "\\#{q}\\#{q}|\\#{q}([^#{q}]|\\\\#{q})*[^\\\\]\\#{q}"} + ["\\S+"]).join("|") + ")"
429
+ regexp = Regexp.new(regexp)
430
+ args = str.scan(regexp).map {|arr| arr.compact[0] }
431
+ args.map {|a|
432
+ if quotes.include?(a[0..0])
433
+ a = a.gsub("\\#{a[0..0]}","#{a[0..0]}")
434
+ a[1..-2]
435
+ else
436
+ a
437
+ end
438
+ }
439
+ end
440
+
441
+ # PROGRESS BAR
442
+ # functions now in UI
443
+ def enable_progress_bar(val = true)
444
+ $ui.enable_progress_bar(val)
445
+ end
446
+ def progress_bar_enabled?
447
+ $ui.progress_bar_enabled?
448
+ end
449
+ def progress_bar(title, total, &block)
450
+ $ui.progress_bar(title, total, &block)
451
+ end
452
+ def progress(count)
453
+ $ui.progress(count)
454
+ end
455
+ def pmsg(action = nil,object = nil)
456
+ $ui.pmsg(action, object)
457
+ end
458
+ def pinc(action = nil, object = nil)
459
+ $ui.pinc(action, object)
460
+ end
461
+
462
+ # SYSTEM CALLS
463
+ def execute_command_popen3(command, input = nil, working_dir = nil, output_file = nil, error_regexps = {})
464
+ raise "Will not execute command, output_file already exist: #{output_file}" if output_file and Pathname.new(output_file).exist?
465
+ $log.debug{"Executing command with popen3: #{command.inspect}"}
466
+ $log.debug{"Changing to directory '#{working_dir}'"}
467
+ my_logs = []
468
+ error_ocurred = false
469
+ FileUtils.cd((working_dir or ".")) {
470
+ begin
471
+ popen3_process = Open3.popen3(*command) { |stdin, stdout, stderr|
472
+ stderrf = Thread.fork {
473
+ $log.debug{"Forked stderr redirect."}
474
+ begin
475
+ while (line = stderr.readline)
476
+ logged = error_regexps.each {|action, regexps|
477
+ if regexps.each {|r| break if line =~ r }.nil?
478
+ my_logs.push [action, regexps, line]
479
+ if action == :print
480
+ print line
481
+ else
482
+ $log.info("#{action}: #{line}")
483
+ end
484
+ break
485
+ end
486
+ }.nil?
487
+ unless logged
488
+ $log.error("Popen3 output error: #{line.strip}")
489
+ error_ocurred = true
490
+ end
491
+ end
492
+ rescue EOFError, IOError
493
+ # OK, this happens ;-)
494
+ rescue
495
+ $log.error{"Error in forked stderr '#{$!.class}': #{$!}"}
496
+ end
497
+ $log.debug{"End of forked stderr redirect."}
498
+ }
499
+ stdoutf = Thread.fork {
500
+ $log.debug{"Forked stdout redirect."}
501
+ if output_file
502
+ bsiz = 65536
503
+ open(output_file, "w") do |o_file|
504
+ begin
505
+ while (r = stdout.read(bsiz))
506
+ o_file.write(r)
507
+ end
508
+ rescue EOFError;
509
+ # OK, this happens ;-)
510
+ end
511
+ end
512
+ else
513
+ begin
514
+ while (line = stdout.readline)
515
+ $log.info(line.strip)
516
+ end
517
+ rescue EOFError, IOError
518
+ # OK, this happens ;-)
519
+ rescue
520
+ $log.error{"Error in forked stdout '#{$!.class}': #{$!}"}
521
+ end
522
+ end
523
+ $log.debug{"End of forked stdout redirect."}
524
+ }
525
+
526
+ begin
527
+ if input
528
+ input.each {|f|
529
+ # DEPRECATED next if f == get_config("DIRLIST_ENTRY")
530
+ $log.debug{"Writing #{f.to_s} to stdin"}
531
+ stdin.write("#{f.to_s}\n")
532
+ }
533
+ end
534
+ stdin.flush
535
+ stdin.close
536
+ rescue
537
+ raise "Error writing to stdin: #{$!}"
538
+ end
539
+ $log.debug{Process.pid}
540
+ $log.debug{"Joining stderr fork."}
541
+ stderrf.join
542
+ $log.debug{"Joining stdout fork."}
543
+ stdoutf.join
544
+ $log.debug{"All foks exited."}
545
+ }
546
+
547
+ raise "Popen3 command execution error." if error_ocurred
548
+ # These checks fail, probably we are too fast.
549
+ # lOutputFile = Pathname.new(lOutputFile.to_s)
550
+ # raise "command successful but target file does not exist!" if !lOutputFile.exist?
551
+ # raise "command successful but target file has size 0!" if lOutputFile.size == 0
552
+ rescue
553
+ $log.error{"Error executing #{command.inspect} in #{Dir.pwd}"}
554
+ raise "Error executing popen3 command: #{$!}"
555
+ end
556
+ }
557
+ my_logs
558
+ end
559
+
560
+ # OPTPARSE
561
+ def parse_arguments(description, arguments)
562
+ argument_names = description.split(" ")
563
+ params = {}
564
+ argument_names.each_with_index {|an,i|
565
+ optional = false
566
+ if an =~ /\[(.*)\]/
567
+ an = $1
568
+ optional = true
569
+ end
570
+ if an =~ /\{(.*)\}/
571
+ an = $1
572
+ optional = true
573
+ params[an.downcase.to_sym] = arguments[i..-1]
574
+ raise "After {..} argument no more arguments allowed!" if argument_names.length > (i+1)
575
+ break
576
+ end
577
+ raise "Argument #{an} (##{i}) not provided and argument is not optional" unless
578
+ optional or arguments[i]
579
+ params[an.downcase.to_sym] = arguments[i]
580
+ }
581
+ params
582
+ end
583
+
584
+ # usage:
585
+ # args = optparse(:arguments => [["ARG1", "Description of arg1"],["ARG2","Desc arg2"]])
586
+ # args: ["bla","bla"...]
587
+ # or
588
+ # action, args = optparse(:actions => {"action1 ARG1 ARG2 {ARG3}"
589
+ # action: "action1"
590
+ # args: {:arg1 => "bl", :arg2 => "bla"}
591
+ def optparse(options = {})
592
+ require 'optparse'
593
+ pbar = false
594
+
595
+ raise "Cannot parse commandline for #{arguments} and #{actions}" if options[:arguments] and options[:actions]
596
+ argument_names = []
597
+ argument_help = nil
598
+ if options[:arguments]
599
+ argument_names = options[:arguments].map{|name,text| name}
600
+ argument_help = options[:arguments]
601
+ end
602
+ if options[:actions]
603
+ argument_names = [options[:actions].keys.map {|k| k.split(" ")[0]}.sort.join("|"), "ARGS"]
604
+ argument_help = options[:actions].map {|a,b| [a,b]}
605
+ end
606
+
607
+ opts = OptionParser.new do |o|
608
+ o.banner += "Usage: #{program_name} [options] [--] #{argument_names.join(" ")}\n"
609
+ if argument_help
610
+ o.banner += "\nArguments:\n"
611
+ width = argument_help.map {|arg, text| arg.length}.max
612
+ argument_help = argument_help.sort_by {|a| a[0]}
613
+ argument_help.each {|arg, text|
614
+ o.banner += " #{arg.ljust(width)} : #{text}\n"
615
+ }
616
+ end
617
+
618
+ if block_given?
619
+ yield o
620
+ end
621
+
622
+ o.banner += "\nOptions:\n"
623
+
624
+ o.on("-Lb","--benchmark","Print out benchmark information on info log") do
625
+ benchmarking = true
626
+ end
627
+
628
+ o.on("-Lp","--profile DIR","Write profiling information to this directory") do |dir|
629
+ self.profiling = dir
630
+ end
631
+
632
+ o.on("-q","--quiet","be quiet, print only errors") do
633
+ Log.log_level = Log.log_level + 1
634
+ end
635
+ o.on("-v","--verbose","be verbose") do
636
+ Log.log_level = Log.log_level - 1
637
+ end
638
+ unless @ui_overwritten
639
+ o.on("--ui <kde,console>","Force userinterface") do |ui|
640
+ change_ui(ui)
641
+ end
642
+ end
643
+ o.on("--progress","Show progress information") do
644
+ pbar = true
645
+ end
646
+ o.on("-O","--output FILENAME", "Output to the file. Possible endings (#{Doc.output_endings.inspect})") {|f|
647
+ # -O output.text -O output.txt
648
+ # -O output.html -O output.htm
649
+ # mail html output to fpellanda: -O "output.html>flavio.pellanda@logintas.ch"
650
+ add_output(f)
651
+ }
652
+ o.on("-h", "--help", "This help." ) do
653
+ puts o
654
+ exit
655
+ end
656
+
657
+ end
658
+
659
+ begin
660
+ $log.debug{"Parsing #{ARGV.inspect}"}
661
+ opts.parse!( ARGV )
662
+ rescue => exc
663
+ $log.error("E: #{exc.message}")
664
+ if $log.debug?
665
+ exc.backtrace.each {|l| $log.debug(l)}
666
+ end
667
+ STDERR.puts opts.to_s
668
+ exit 1
669
+ end
670
+ # must be set after change_ui
671
+ $ui.enable_progress_bar if pbar
672
+
673
+ if options[:arguments]
674
+ min_arguments = argument_names.reject{|a| a =~ /^\{|\[/}.length
675
+ raise "Too few arguments provided (#{ARGV.length} for at least #{min_arguments})." if argument_names and ARGV.length < min_arguments
676
+ return ARGV
677
+ end
678
+ if options[:actions]
679
+ if ARGV[0].nil?
680
+ puts opts
681
+ exit
682
+ end
683
+ action, desc, text = options[:actions].each {|a,t|
684
+ sp = a.split(" ")
685
+ break [ARGV[0],sp[1..-1].join(" "),t] if sp[0].split("|").include?(ARGV[0])
686
+ }
687
+ raise "Action '#{ARGV[0]}' not supported." unless desc
688
+ params = parse_arguments(desc, ARGV[1..-1])
689
+ return [action, params]
690
+ end
691
+ ARGV
692
+ end
693
+
694
+ ## instances
695
+ def pid_file
696
+ if program_instance
697
+ Pathname.new("/tmp/#{program_name}-#{program_instance.gsub(/[^a-zA-Z0-9]/,"_")}-#{program_instance.hash.abs}.pid")
698
+ else
699
+ Pathname.new("/tmp/#{program_name.gsub(/[^a-zA-Z0-9]/,"_")}.pid")
700
+ end
701
+ end
702
+ def ensure_script_not_running_already(error_on_concurrent = true)
703
+ if pid_file.exist?
704
+ pid = pid_file.readlines.join.strip.to_i
705
+ $log.debug{"Pid file exist with pid #{pid}."}
706
+ if system("/bin/ps #{pid} > /dev/null")
707
+ name = program_name
708
+ name += " (#{program_instance})" if program_instance
709
+ if error_on_concurrent
710
+ $log.fatal("#{name} already running (pid:#{pid}).")
711
+ exit 1
712
+ else
713
+ $log.info("#{name} already running (pid:#{pid}). Exiting normally.\n")
714
+ exit 0
715
+ end
716
+ else
717
+ # process not running anymore
718
+ # TODO: this should have level warn, but the pid file is normally not remove properly in current version
719
+ $log.info("Removing #{pid_file} process #{pid} not runnning anymore.")
720
+ pid_file.delete
721
+ end
722
+ end
723
+ $log.debug{"Creating pid file for process #{Process.pid}"}
724
+ pid_file.open("w") {|f| f.write(Process.pid.to_s) }
725
+ end
726
+
727
+ ## KONSOLE
728
+ @konsole = nil
729
+ @libisi_konsole_sessions = {}
730
+ def open_konsole_session(name)
731
+ @konsole = open("|dcopstart konsole-script").gets.strip unless @konsole
732
+ session = open("|dcop #{@konsole} konsole newSession").gets.strip
733
+ system("dcop #{@konsole} #{session} renameSession #{name}")
734
+
735
+ @konsole_sessions[name] = session
736
+ end
737
+
738
+ def send_command(name, command)
739
+ session = @konsole_sessions[name]
740
+ system("dcop #{@konsole} #{session} sendSession \"#{command}\"")
741
+ end
742
+
743
+ ## DCOP
744
+ def dcop_media_list
745
+ media = {}
746
+ open("|dcop kded mediamanager fullList") {|f|
747
+ f.readlines.join.split("---\n").map {|e|
748
+ e.split("\n")
749
+ }
750
+ }.each {|entry|
751
+ name = entry[1]
752
+ media[name] = {}
753
+ #/org/freedesktop/Hal/devices/volume_uuid_02a67e94_d030_4211_8b21_ebf0a517aac5
754
+ media[name][:id] = entry[0]
755
+ # sdd1
756
+ media[name][:name] = name
757
+ #221M Removable Media
758
+ media[name][:description] = entry[2]
759
+ #
760
+ #media[name][] = entry[3]
761
+ #true
762
+ #media[name][] = entry[4]
763
+ #/dev/sdd1
764
+ media[name][:device] = entry[5]
765
+ #/media/user-fpellanda
766
+ media[name][:mount_point] = entry[6]
767
+ #ext3
768
+ media[name][:fs_type] = entry[7]
769
+ #true
770
+ #media[name][] = entry[8]
771
+ #
772
+ #media[name][] = entry[9]
773
+ #media/removable_mounted_decrypted
774
+ media[name][:mime_type] = entry[10]
775
+ #
776
+ #media[name][] = entry[11]
777
+ #true
778
+ #media[name][] = entry[12]
779
+ #/org/freedesktop/Hal/devices/volume_uuid_3eebb364_b2c9_4491_a4a2_04b193fc20ac
780
+ #media[name][] = entry[13]
781
+ }
782
+ media
783
+ end
784
+ def normalize_device_name(name)
785
+ name = case name
786
+ when /^media:\/(.*)/
787
+ $1
788
+ when /^\/dev\/(.*)/
789
+ $1
790
+ when /system:\/media\/(.*)/
791
+ $1
792
+ else
793
+ name
794
+ end
795
+ end
796
+ def dcop_find_media(name)
797
+ name = normalize_device_name
798
+ $log.debug{"Looking for media #{name}"}
799
+ ml = media_list
800
+ $log.debug{"Found medias: #{ml.keys.inspect}"}
801
+ media = ml[name]
802
+ raise "Media #{name} not found." unless media
803
+ media
804
+ end
805
+
806
+ # system
807
+ def daemonize(options = {})
808
+ save_pid = (pid_file.exist? and pid_file.readlines.join.strip.to_i == Process.pid)
809
+
810
+ fork and exit
811
+ if options[:pid_file]
812
+ File.open(options[:pid_file],"w") do |f|
813
+ f << Process.pid
814
+ end
815
+ end
816
+
817
+ # child becomes session leader and disassociates controlling tty.
818
+ # namely do Process.setpgrp + \alpha.
819
+ Process.setsid
820
+
821
+ # at here already the child process have become daemon. the rest
822
+ # is just for behaving well.
823
+
824
+ # save new pid to pid_file
825
+ pid_file.open("w") {|f| f.write(Process.pid.to_s) } if save_pid
826
+
827
+ # there is now no console anymore
828
+ if $ui.name == "ConsoleUI"
829
+ change_ui("nobody")
830
+ ENV.delete("TERM")
831
+ end
832
+
833
+ # ensure no extra I/O.
834
+ File.open("/dev/null", "r+") do
835
+ |devnull|
836
+ $stdin.reopen(devnull)
837
+ if options[:log_file]
838
+ $stdout.reopen("#{options[:log_file]}.log")
839
+ $stderr.reopen("#{options[:log_file]}.err")
840
+ else
841
+ $stdout.reopen(devnull)
842
+ $stderr.reopen(devnull)
843
+ end
844
+ end
845
+
846
+ # ensure daemon process not to prevent shutdown process.
847
+ Dir.chdir("/")
848
+ end
849
+
850
+ # RAILS STUFF
851
+ def rails_root
852
+ return Pathname.new(RAILS_ROOT) if defined?(RAILS_ROOT)
853
+ return Pathname.new(ENV["RAILS_ROOT"]) if ENV["RAILS_ROOT"]
854
+ return nil unless ENV["main_script"]
855
+ return Pathname.new(FileUtils.pwd) if main_script.basename.to_s == "rake"
856
+ main_script.realpath.dirname + ".."
857
+ end
858
+ def rails_available?
859
+ return false unless rails_root
860
+ (rails_root + 'config/boot.rb').exist? and
861
+ (rails_root + 'config/environment.rb').exist?
862
+ end
863
+
864
+ # include in boot:
865
+ # unless defined?(LIBISI)
866
+ # require 'libisi'
867
+ # init_libisi
868
+ # require "libisi/color"
869
+ # Doc.change("html", :doc_started => true)
870
+ # end
871
+ # or in script:
872
+ # initialize_rails
873
+ def initialize_rails
874
+ raise "Rails not available." unless rails_available?
875
+ # add this to load path to avoid real logger.rb class
876
+ # to be loaded
877
+ # $LOAD_PATH.insert(0, "/usr/lib/ruby/1.8/libisi/fake_logger/")
878
+
879
+ $log.debug{"Starting rails environment."}
880
+ require rails_root + 'config/boot'
881
+ require rails_root + 'config/environment'
882
+ $log.debug{"Rails environment started."}
883
+ end
884
+
885
+ end
886
+
887
+ # from activesupport-1.3.1/lib/active_support/core_ext/enumerable.rb
888
+ module Enumerable
889
+ def group_by
890
+ inject({}) do |groups, element|
891
+ (groups[yield(element)] ||= []) << element
892
+ groups
893
+ end
894
+ end
895
+
896
+ def group_bys(*args)
897
+ if args[-1].class == Hash
898
+ options = args[-1]
899
+ functions = args[0..-2]
900
+ else
901
+ options = {}
902
+ functions = args
903
+ end
904
+
905
+ of = ((options[:order_functions] and options[:order_functions][0]) or
906
+ lambda {|e|
907
+ if e.respond_to?(:"<=>") then
908
+ e
909
+ else
910
+ e.to_s
911
+ end
912
+ }
913
+ )
914
+
915
+ if (gf = functions[0])
916
+ unordered = self.group_by(&gf)
917
+ # TODO: Would be good but problems with base table
918
+ #res = OrderedHash.new
919
+ res = []
920
+ begin
921
+ ordered_keys = unordered.keys.sort_by{|k| of.call(k)}
922
+ rescue
923
+ $log.warn("Error ocurred sorting keys: #{$!}!")
924
+ ordered_keys = unordered.keys
925
+ end
926
+
927
+ ordered_keys.each {|key|
928
+ # TODO: Would be good but problems with base table
929
+ #res[key] = unordered[key].group_bys(*functions[1..-1])
930
+ res << [key, unordered[key].group_bys(*functions[1..-1])]
931
+ }
932
+
933
+ else
934
+ res = self
935
+ end
936
+
937
+ res
938
+ end
939
+
940
+ end
941
+
942
+ class String
943
+ def starts_with?(other)
944
+ self[0..(other.length-1)] == other
945
+ end
946
+ end
947
+
948
+ include LibIsi