butler 1.8.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 (164) hide show
  1. data/CHANGELOG +4 -0
  2. data/GPL.txt +340 -0
  3. data/LICENSE.txt +52 -0
  4. data/README +37 -0
  5. data/Rakefile +334 -0
  6. data/bin/botcontrol +230 -0
  7. data/data/butler/config_template.yaml +4 -0
  8. data/data/butler/dialogs/backup.rb +19 -0
  9. data/data/butler/dialogs/botcontrol.rb +4 -0
  10. data/data/butler/dialogs/config.rb +1 -0
  11. data/data/butler/dialogs/create.rb +53 -0
  12. data/data/butler/dialogs/delete.rb +3 -0
  13. data/data/butler/dialogs/en/backup.yaml +6 -0
  14. data/data/butler/dialogs/en/botcontrol.yaml +5 -0
  15. data/data/butler/dialogs/en/create.yaml +11 -0
  16. data/data/butler/dialogs/en/delete.yaml +2 -0
  17. data/data/butler/dialogs/en/help.yaml +17 -0
  18. data/data/butler/dialogs/en/info.yaml +13 -0
  19. data/data/butler/dialogs/en/list.yaml +4 -0
  20. data/data/butler/dialogs/en/notyetimplemented.yaml +2 -0
  21. data/data/butler/dialogs/en/rename.yaml +3 -0
  22. data/data/butler/dialogs/en/start.yaml +3 -0
  23. data/data/butler/dialogs/en/sync_plugins.yaml +3 -0
  24. data/data/butler/dialogs/en/uninstall.yaml +5 -0
  25. data/data/butler/dialogs/en/unknown_command.yaml +2 -0
  26. data/data/butler/dialogs/help.rb +11 -0
  27. data/data/butler/dialogs/info.rb +27 -0
  28. data/data/butler/dialogs/interactive.rb +1 -0
  29. data/data/butler/dialogs/list.rb +10 -0
  30. data/data/butler/dialogs/notyetimplemented.rb +1 -0
  31. data/data/butler/dialogs/rename.rb +4 -0
  32. data/data/butler/dialogs/selectbot.rb +2 -0
  33. data/data/butler/dialogs/start.rb +5 -0
  34. data/data/butler/dialogs/sync_plugins.rb +30 -0
  35. data/data/butler/dialogs/uninstall.rb +17 -0
  36. data/data/butler/dialogs/unknown_command.rb +1 -0
  37. data/data/butler/plugins/core/logout.rb +41 -0
  38. data/data/butler/plugins/core/plugins.rb +134 -0
  39. data/data/butler/plugins/core/privilege.rb +103 -0
  40. data/data/butler/plugins/core/user.rb +166 -0
  41. data/data/butler/plugins/dev/eval.rb +64 -0
  42. data/data/butler/plugins/dev/nometa.rb +14 -0
  43. data/data/butler/plugins/dev/onhandlers.rb +93 -0
  44. data/data/butler/plugins/dev/raw.rb +36 -0
  45. data/data/butler/plugins/dev/rawlog.rb +77 -0
  46. data/data/butler/plugins/games/eightball.rb +54 -0
  47. data/data/butler/plugins/games/mastermind.rb +174 -0
  48. data/data/butler/plugins/irc/action.rb +36 -0
  49. data/data/butler/plugins/irc/join.rb +38 -0
  50. data/data/butler/plugins/irc/notice.rb +36 -0
  51. data/data/butler/plugins/irc/part.rb +38 -0
  52. data/data/butler/plugins/irc/privmsg.rb +36 -0
  53. data/data/butler/plugins/irc/quit.rb +36 -0
  54. data/data/butler/plugins/operator/deop.rb +41 -0
  55. data/data/butler/plugins/operator/devoice.rb +41 -0
  56. data/data/butler/plugins/operator/limit.rb +47 -0
  57. data/data/butler/plugins/operator/op.rb +41 -0
  58. data/data/butler/plugins/operator/voice.rb +41 -0
  59. data/data/butler/plugins/public/help.rb +69 -0
  60. data/data/butler/plugins/public/login.rb +72 -0
  61. data/data/butler/plugins/public/usage.rb +49 -0
  62. data/data/butler/plugins/service/clones.rb +56 -0
  63. data/data/butler/plugins/service/define.rb +47 -0
  64. data/data/butler/plugins/service/log.rb +183 -0
  65. data/data/butler/plugins/service/svn.rb +91 -0
  66. data/data/butler/plugins/util/cycle.rb +98 -0
  67. data/data/butler/plugins/util/load.rb +41 -0
  68. data/data/butler/plugins/util/pong.rb +29 -0
  69. data/data/butler/strings/random/acknowledge.en.yaml +5 -0
  70. data/data/butler/strings/random/gratitude.en.yaml +3 -0
  71. data/data/butler/strings/random/hello.en.yaml +4 -0
  72. data/data/butler/strings/random/ignorance.en.yaml +7 -0
  73. data/data/butler/strings/random/ignorance_about.en.yaml +3 -0
  74. data/data/butler/strings/random/insult.en.yaml +3 -0
  75. data/data/butler/strings/random/rejection.en.yaml +12 -0
  76. data/data/man/botcontrol.1 +17 -0
  77. data/lib/access.rb +187 -0
  78. data/lib/access/admin.rb +16 -0
  79. data/lib/access/privilege.rb +122 -0
  80. data/lib/access/role.rb +102 -0
  81. data/lib/access/savable.rb +18 -0
  82. data/lib/access/user.rb +180 -0
  83. data/lib/access/yamlbase.rb +126 -0
  84. data/lib/butler.rb +188 -0
  85. data/lib/butler/bot.rb +247 -0
  86. data/lib/butler/control.rb +93 -0
  87. data/lib/butler/dialog.rb +64 -0
  88. data/lib/butler/initialvalues.rb +40 -0
  89. data/lib/butler/irc/channel.rb +135 -0
  90. data/lib/butler/irc/channels.rb +96 -0
  91. data/lib/butler/irc/client.rb +351 -0
  92. data/lib/butler/irc/hostmask.rb +53 -0
  93. data/lib/butler/irc/message.rb +184 -0
  94. data/lib/butler/irc/parser.rb +125 -0
  95. data/lib/butler/irc/parser/commands.rb +83 -0
  96. data/lib/butler/irc/parser/generic.rb +343 -0
  97. data/lib/butler/irc/socket.rb +378 -0
  98. data/lib/butler/irc/string.rb +186 -0
  99. data/lib/butler/irc/topic.rb +15 -0
  100. data/lib/butler/irc/user.rb +265 -0
  101. data/lib/butler/irc/users.rb +112 -0
  102. data/lib/butler/plugin.rb +249 -0
  103. data/lib/butler/plugin/configproxy.rb +35 -0
  104. data/lib/butler/plugin/mapper.rb +85 -0
  105. data/lib/butler/plugin/matcher.rb +55 -0
  106. data/lib/butler/plugin/onhandlers.rb +70 -0
  107. data/lib/butler/plugin/trigger.rb +58 -0
  108. data/lib/butler/plugins.rb +147 -0
  109. data/lib/butler/version.rb +17 -0
  110. data/lib/cloptions.rb +217 -0
  111. data/lib/cloptions/adapters.rb +24 -0
  112. data/lib/cloptions/switch.rb +132 -0
  113. data/lib/configuration.rb +223 -0
  114. data/lib/dialogline.rb +296 -0
  115. data/lib/dialogline/localizations.rb +24 -0
  116. data/lib/durations.rb +57 -0
  117. data/lib/event.rb +295 -0
  118. data/lib/event/at.rb +64 -0
  119. data/lib/event/every.rb +56 -0
  120. data/lib/event/timed.rb +112 -0
  121. data/lib/installer.rb +75 -0
  122. data/lib/iterator.rb +34 -0
  123. data/lib/log.rb +68 -0
  124. data/lib/log/comfort.rb +85 -0
  125. data/lib/log/converter.rb +23 -0
  126. data/lib/log/entry.rb +152 -0
  127. data/lib/log/fakeio.rb +55 -0
  128. data/lib/log/file.rb +54 -0
  129. data/lib/log/filereader.rb +81 -0
  130. data/lib/log/forward.rb +49 -0
  131. data/lib/log/methods.rb +39 -0
  132. data/lib/log/nolog.rb +18 -0
  133. data/lib/log/splitter.rb +26 -0
  134. data/lib/ostructfixed.rb +26 -0
  135. data/lib/ruby/array/columnize.rb +38 -0
  136. data/lib/ruby/dir/mktree.rb +28 -0
  137. data/lib/ruby/enumerable/join.rb +13 -0
  138. data/lib/ruby/exception/detailed.rb +24 -0
  139. data/lib/ruby/file/append.rb +11 -0
  140. data/lib/ruby/file/write.rb +11 -0
  141. data/lib/ruby/hash/zip.rb +15 -0
  142. data/lib/ruby/kernel/bench.rb +15 -0
  143. data/lib/ruby/kernel/daemonize.rb +42 -0
  144. data/lib/ruby/kernel/non_verbose.rb +17 -0
  145. data/lib/ruby/kernel/safe_fork.rb +18 -0
  146. data/lib/ruby/range/stepped.rb +11 -0
  147. data/lib/ruby/string/arguments.rb +72 -0
  148. data/lib/ruby/string/chunks.rb +15 -0
  149. data/lib/ruby/string/post_arguments.rb +44 -0
  150. data/lib/ruby/string/unescaped.rb +17 -0
  151. data/lib/scheduler.rb +164 -0
  152. data/lib/scriptfile.rb +101 -0
  153. data/lib/templater.rb +86 -0
  154. data/test/cloptions.rb +134 -0
  155. data/test/cv.rb +28 -0
  156. data/test/irc/client.rb +85 -0
  157. data/test/irc/client_login.txt +53 -0
  158. data/test/irc/client_subscribe.txt +8 -0
  159. data/test/irc/message.rb +30 -0
  160. data/test/irc/messages.txt +64 -0
  161. data/test/irc/parser.rb +13 -0
  162. data/test/irc/profile_parser.rb +12 -0
  163. data/test/irc/users.rb +28 -0
  164. metadata +256 -0
@@ -0,0 +1,17 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ class Butler #:nodoc:
10
+ module VERSION #:nodoc:
11
+ MAJOR = 1
12
+ MINOR = 8
13
+ TINY = 0
14
+
15
+ STRING = [MAJOR, MINOR, TINY].join('.')
16
+ end
17
+ end
@@ -0,0 +1,217 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ require 'cloptions/adapters'
10
+ require 'cloptions/switch'
11
+
12
+
13
+
14
+ # a class for simple command line option parsing
15
+ class CLOptions
16
+ EmptyARGV = Struct.new('EmptyARGV')
17
+
18
+ class Result
19
+ attr_reader :argv
20
+
21
+ def initialize(options, argv, flags={}, hash={})
22
+ @options = options
23
+ @argv = argv
24
+ @flags = flags
25
+ @hash = hash
26
+ end
27
+
28
+ def to_hash
29
+ @hash.dup
30
+ end
31
+
32
+ def [](key)
33
+ @flags[key] && @flags[key].parameters
34
+ end
35
+
36
+ def flag(*key)
37
+ return @flags if key.empty?
38
+ @flags[key.first]
39
+ end
40
+
41
+ # Run the block if given flag is set
42
+ # :yields: flag
43
+ def on(flag)
44
+ yield @flag[key] if flag
45
+ end
46
+
47
+ def process(argv)
48
+ @argv_processing = argv
49
+ while arg = argv.shift
50
+ case arg
51
+ when Switch::Short: add_flag($~, @options.short)
52
+ when Switch::Long: add_flag($~, @options.long)
53
+ else
54
+ argv.unshift(arg)
55
+ break
56
+ end
57
+ end
58
+
59
+ min = (@options.arity[0] < 0 ? -1-@options.arity[0] : @options.arity[0])+(@options.arity[2] < 0 ? -1-@options.arity[2] : @options.arity[2])
60
+ max = (@options.arity[0] < 0 || @options.arity[1] < 0) ? nil : min+@options.arity[1]
61
+ argv_index = -1
62
+ 3.times { |i|
63
+ process_args(i, @argv_processing).each { |arg|
64
+ @argv[argv_index+=1] = arg
65
+ }
66
+ }
67
+ self
68
+ end
69
+
70
+ private
71
+ def add_flag(match, from)
72
+ name, data = match.captures
73
+ raise "Unknown switch -#{name}" unless switch = from[name]
74
+ flag = switch.process(data, @argv_processing)
75
+ switch.mappings.each { |mapping|
76
+ @flags[mapping] = flag
77
+ }
78
+ @hash[switch.hash_key] = flag.parameters if switch.hash_key
79
+ end
80
+
81
+ def process_args(n, params)
82
+ if @options.arity[n] > 0 then
83
+ params.slice!(0,@options.arity[n]) || []
84
+ elsif @options.arity[n] < 0 then
85
+ (params.slice!(0,-1-@options.arity[n]) || [])+[params]
86
+ else
87
+ []
88
+ end
89
+ end
90
+ private :process_args
91
+ end
92
+
93
+ def self.parse(*args, &block)
94
+ options = new(*args, &block)
95
+ options.parse(ARGV)
96
+ end
97
+
98
+ attr_reader :arity
99
+ attr_reader :switch
100
+ attr_reader :short
101
+ attr_reader :long
102
+ attr_reader :argstruct
103
+ def initialize(arguments, opts={}, &description)
104
+ @switch = []
105
+ @short = {}
106
+ @long = {}
107
+ @arguments, @arity = *analyze_args(arguments)
108
+ @str_args = arguments
109
+ @argstruct = (@arguments.empty? ? EmptyARGV : ::Struct.new(*@arguments.map { |a| a.to_sym }))
110
+ @appname = $0
111
+ instance_eval(&description) if description
112
+ end
113
+
114
+ def parse(argv=ARGV)
115
+ result = Result.new(self, @argstruct.new)
116
+ result.process(argv)
117
+ result
118
+ end
119
+
120
+ # figure from argument string the arity and what args
121
+ def analyze_args(argstr)
122
+ current = 0
123
+ arity = [0, 0, 0]
124
+ args = []
125
+ return [args, arity] if argstr.nil?
126
+
127
+ argstr.split(/\s+/).each { |param|
128
+ raise "Invalid argument format '#{argstr}'" if current == 2
129
+ if param == "..." || param == "…" then
130
+ arity[current] = -(arity[current]+1)
131
+ current = 2
132
+ elsif param[0] == ?[ && param[-1] == ?] then
133
+ raise "Invalid parameter format '#{argstr}'" if current == 2
134
+ current = 1
135
+ arity[2] += 1
136
+ args << param[1..-2].freeze
137
+ else
138
+ current = 2 if current == 1
139
+ arity[current] += 1
140
+ args << param.dup.freeze
141
+ end
142
+ }
143
+ [args, arity]
144
+ end
145
+ private :analyze_args
146
+
147
+
148
+ # arguments are positional, starting from 2 arguments, the last argument is considered
149
+ # to be the description/help:
150
+ # 0: short switch
151
+ # 1: long switch (optional)
152
+ # 2: arguments (optional)
153
+ # 3: hash-mapping (optional)
154
+ # -1: description (-1 == the last)
155
+ # option('-s', '--long', :mapping, "Description of flag") { |value| adapt }
156
+ # option('-p', '--port', true, "Description of flag", &CLOptions::Integer)
157
+ def option(short, *rest, &adapter)
158
+ switch = Switch.new(short, *rest, &adapter)
159
+ @short[switch.short] = switch if switch.short
160
+ @long[switch.long] = switch if switch.long
161
+ if switch.short || switch.long then
162
+ @switch << switch
163
+ else
164
+ raise "Neither short nor long in option declaration."
165
+ end
166
+ end
167
+ alias o option
168
+
169
+ def application_name(name)
170
+ @appname = name
171
+ end
172
+
173
+ # an automatically generated help string
174
+ # see #application_name
175
+ def help
176
+ out = "#{@appname}#{' '+@str_args if @str_args}\n\n"
177
+ @switch.inject(out) { |out, switch| out << "#{switch}\n\t#{switch.help}\n" }
178
+ end
179
+
180
+ # an automatically generated usage string
181
+ def usage(include_long_opts=false)
182
+ out = "Usage:\n\t#@appname"
183
+ short = ""
184
+ long = ""
185
+ collected = []
186
+ if include_long_opts then
187
+ @switch.each { |s|
188
+ if s.short then
189
+ if s.params.empty? then
190
+ collected << s.short[1,1]
191
+ else
192
+ short << " #{s.short}#{' '+s.str_params if s.str_params}"
193
+ end
194
+ end
195
+ long << " #{s.long}#{' '+s.str_params if s.str_params}" if s.long
196
+ }
197
+ else
198
+ @switch.each { |s|
199
+ if s.short.nil? then
200
+ long << " #{s.long}#{' '+s.str_params if s.str_params}"
201
+ elsif s.params.empty? then
202
+ collected << s.short[1,1]
203
+ else
204
+ short << " #{s.short}#{' '+s.str_params if s.str_params}"
205
+ end
206
+ }
207
+ end
208
+ out << (collected.empty? ? "" : " -#{collected.join('')}") << short << long << (@str_args ? " #@str_args\n" : "\n")
209
+ out
210
+ end
211
+ end
212
+
213
+ # Developer notes:
214
+ # arity is an array, negative numbers mean infinite, where -1 means 0..Inf, -2 means 1..Inf etc
215
+ # positive numbers mean exactly that many
216
+ # the first number is mandatory head, the last number is mandatory tail, the middle number
217
+ # is optional args
@@ -0,0 +1,24 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ class CLOptions
10
+ module Adapters
11
+ Integer = Kernel.method(:Integer)
12
+ Octal = proc { |value|
13
+ raise ArgumentError, "Not Octal" unless value =~ /\A(-?)([0-7]+)\z/
14
+ Integer("#{$1}0#{$2}")
15
+ }
16
+ Hex = proc { |value|
17
+ raise ArgumentError, "Not Hex" unless value =~ /\A(-?)(?:0[Xx])?([\dA-Fa-f]+)\z/
18
+ Integer("#{$1}0x#{$2}")
19
+ }
20
+ Float = Kernel.method(:Float)
21
+ String = proc { |value| value }
22
+ end
23
+ include Adapters
24
+ end
@@ -0,0 +1,132 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ class CLOptions
10
+ Flag = Struct.new(:switch, :parameters, :errors)
11
+ class Flag
12
+ def inspect
13
+ "#<%s:0x%x switch=#<CLOptions::Switch %s> parameters=%s errors=%s>" % [
14
+ self.class,
15
+ object_id << 1,
16
+ switch.to_s,
17
+ parameters.inspect,
18
+ errors.inspect
19
+ ]
20
+ end
21
+ end
22
+
23
+ # VALID:
24
+ # --foo => [ 0, 0, 0]
25
+ # --foo [PARAM] => [ 0, 1, 0]
26
+ # --foo PARAM => [ 1, 0, 0]
27
+ # --foo PARAM, ... => [-2, 0, 0]
28
+ # --foo [PARAM], ... => [ 0, -1, 0]
29
+ # --foo P1, P2, P3 => [ 3, 0, 0]
30
+ # --foo P1, P2, [P3] => [ 2, 1, 0]
31
+ # --foo P1, P2, ... => [-3, 0, 0]
32
+ #
33
+ # POSSIBLE:
34
+ # --foo P1, [P2], P3 => [ 1, 1, 1]
35
+ # --foo P1, ..., P3 => [-2, 0, 1]
36
+ # --foo P1, [P2], ..., P3 => [ 1, -1, 1]
37
+ #
38
+ # IMPOSSIBLE:
39
+ # --foo P1, [P2], P3, [P4] => [ 0, 0, 0]
40
+ class Switch
41
+ Match = /\A(?:-\w|--[\w-]+)(?:=.*)?\z/
42
+ Short = /\A(-\w)(?:=([^,]+(?:(?:,[^,]+)*)|,))?\z/
43
+ Long = /\A(--[\w-]+)(?:=([^,]+(?:(?:,[^,]+)*)|,))?\z/
44
+
45
+ attr_reader :short
46
+ attr_reader :long
47
+ attr_reader :hash_key
48
+ attr_reader :mappings
49
+ attr_reader :help
50
+ attr_reader :str_params
51
+ attr_reader :to_s
52
+ attr_reader :params
53
+ attr_reader :arity
54
+
55
+ def initialize(short, *rest, &adapter)
56
+ @help = (::String === rest.last ? rest.pop : "").freeze
57
+ @short = short
58
+ @long = rest.shift
59
+ @str_params = rest.shift
60
+ @params, @arity = *analyze_param(@str_params)
61
+ @hash_key = rest.shift
62
+ @hash_key = @long ? @long[2..-1].to_sym : @short[1,1].to_sym if @hash_key == true
63
+ @mappings = [@short, @long, @hash_key].compact
64
+ @adapter = adapter
65
+ @to_s = ("#{short}#{', ' if short and long}#{long}#{' '+@str_params if @str_params}")
66
+ end
67
+
68
+ def analyze_param(param)
69
+ current = 0
70
+ arity = [0, 0, 0]
71
+ params = []
72
+ return [params, arity] if param.nil?
73
+
74
+ param.split(/,\s*/).each { |param|
75
+ raise "Invalid parameter format '#{param}'" if current == 2
76
+ if param == "..." || param == "…" then
77
+ arity[current] = -(arity[current]+1)
78
+ current = 2
79
+ elsif param[0] == ?[ && param[-1] == ?] then
80
+ raise "Invalid parameter format '#{param}'" if current == 2
81
+ current = 1
82
+ arity[2] += 1
83
+ params << param[1..-2].freeze
84
+ else
85
+ current = 2 if current == 1
86
+ arity[current] += 1
87
+ params << param.dup.freeze
88
+ end
89
+ }
90
+
91
+ [params, arity]
92
+ end
93
+ private :analyze_param
94
+
95
+ def process(data, args)
96
+ flag = Flag.new(self, true, nil)
97
+ min = (@arity[0] < 0 ? -1-@arity[0] : @arity[0])+(@arity[2] < 0 ? -1-@arity[2] : @arity[2])
98
+ max = (@arity[0] < 0 || @arity[1] < 0) ? nil : min+@arity[1]
99
+ return flag if max == 0
100
+
101
+ flag[1] = []
102
+ params = []
103
+ args.unshift(*data.split(/,/)) if data
104
+ while args.first !~ Match
105
+ params << (@adapter ? @adapter.call(args.shift) : args.shift)
106
+ break unless args.first and args.first[-1] == ?,
107
+ end
108
+ raise "Invalid parameter count for flag #{self}" if ((max && !params.length.between?(min, max)) || params.length < min)
109
+ if max == 0 then
110
+ flag[1] = nil
111
+ elsif min == 1 && max == 1 then
112
+ flag[1] = params.first
113
+ else
114
+ 3.times { |i| process_params(flag, i, params) }
115
+ end
116
+ flag
117
+ end
118
+
119
+ def process_params(flag, n, params)
120
+ #p [:process_params, flag, n, params]
121
+ #pp self
122
+ return if @arity[n] == 0
123
+ if @arity[n] > 0 then
124
+ flag[1].concat(params.slice!(0,@arity[n]))
125
+ else
126
+ flag[1].concat(params.slice!(0,-1-@arity[n]))
127
+ #flag[1].push(params) -- FIXME why is that in again? write tests, for lobsters sake :-S
128
+ end
129
+ end
130
+ private :process_params
131
+ end
132
+ end
@@ -0,0 +1,223 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ require 'fileutils'
10
+ require 'yaml'
11
+
12
+
13
+
14
+ class String
15
+ def config_key # FIXME %2e instead of . is just ugly...
16
+ gsub(/\./, '%2e')
17
+ end
18
+ end
19
+
20
+ class Configuration
21
+ module VERSION
22
+ MAJOR = 0
23
+ MINOR = 0
24
+ TINY = 1
25
+ STRING = "#{MAJOR}.#{MINOR}.#{TINY}"
26
+ end
27
+ class InvalidKey < StandardError
28
+ attr_reader :key
29
+ def initialize(key)
30
+ @key = key
31
+ super("Keys must not be empty nor contain '.'.")
32
+ end
33
+ end
34
+
35
+ ConfFile = Struct.new(:data, :path, :mtime)
36
+
37
+ attr_reader :base
38
+
39
+ def initialize(base, dir_tree=[])
40
+ # make it resistant against Dir.chdir
41
+ @base = File.expand_path(base).freeze
42
+ @files = {}
43
+
44
+ unless File.directory?(@base) && File.exist?("#@base/.configdir.yaml") then
45
+ if File.file?(@base) then
46
+ raise "Can't create directory #@base because of existing file #@base"
47
+ end
48
+ if File.exist?(@base) then
49
+ raise "Directory #@base already exists and is not a configuration directory"
50
+ end
51
+ end
52
+ unless File.exist?(@base) then
53
+ FileUtils.mkdir_p(@base)
54
+ File.open("#@base/.configdir.yaml", "w") { |fh|
55
+ fh.write({
56
+ :created => Time.now,
57
+ :modified => Time.now,
58
+ :revision => 1,
59
+ }.to_yaml)
60
+ }
61
+ end
62
+ setup(dir_tree)
63
+ end
64
+
65
+ def [](key)
66
+ file, key = split(key)
67
+ return nil unless key_exist?(file, key)
68
+ key ? @files[file][key] : @files[file]
69
+ end
70
+
71
+ def []=(key, value)
72
+ file, key = split(key)
73
+ load(file)
74
+ if key then
75
+ if Hash === @files[file] then
76
+ @files[file][key] = value
77
+ else
78
+ @files[file] = {key => value}
79
+ end
80
+ else
81
+ @files[file] = value
82
+ end
83
+ store(file)
84
+ value
85
+ end
86
+
87
+ def exist?(key)
88
+ file, key = split(key)
89
+ key_exist?(file, key)
90
+ end
91
+ alias has_key? exist?
92
+
93
+ def delete(key)
94
+ file, key = split(key)
95
+ return nil unless key_exist?(file, key) and File.exist?(file)
96
+ if key.empty? then
97
+ File.delete(file)
98
+ else
99
+ load(file)
100
+ @files[file].delete(key)
101
+ store(file)
102
+ end
103
+ end
104
+
105
+ def setup(dirs)
106
+ FileUtils.mkdir_p(dirs.map { |dir| "#@base/#{dir}" })
107
+ end
108
+
109
+ private
110
+ def key_exist?(file, key)
111
+ File.exist?(file) && (!key || load(file).has_key?(key))
112
+ end
113
+
114
+ def split(key)
115
+ keys = Array === key ? key.map { |k| String(k) } : key.split(/\./)
116
+ file = @base.dup
117
+ raise InvalidKey.new(key) if (keys.empty? || keys.join.include?("."))
118
+ file << "/#{encode_filename(keys.shift)}" while (File.directory?(file) && keys.first)
119
+ file << ".yaml"
120
+ return [file, keys.empty? ? nil : keys.join(".")]
121
+ end
122
+
123
+ def encode_filename(name)
124
+ name.gsub(/[\x00-\x1f%\x7f]/) { |m| "%%%02x"%m[0] } # {}()\[\]~
125
+ end
126
+
127
+ def decode_filename(name)
128
+ name.gsub(/%[a-f\d]{2}/) { |m| m[1,2].to_i(16).chr }
129
+ end
130
+
131
+ def load(file, force=false)
132
+ if force || !@files.has_key?(file) then
133
+ @files[file] = nil
134
+ @files[file] = YAML.load_file(file) if File.exist?(file)
135
+ else
136
+ @files[file]
137
+ end
138
+ end
139
+
140
+ def store(file)
141
+ bak = nil
142
+ if File.exist?(file) then
143
+ bak = "#{file}.bak"
144
+ FileUtils.cp(file, bak)
145
+ end
146
+ begin
147
+ File.open(file, "w") { |fh| fh.write(@files[file].to_yaml) }
148
+ rescue
149
+ if bak then
150
+ FileUtils.rm(file)
151
+ FileUtils.mv(bak, file)
152
+ FileUtils.rm(bak)
153
+ end
154
+ raise
155
+ else
156
+ FileUtils.rm(bak) if bak # not in the ensure because the rescue might fail too
157
+ end
158
+ end
159
+ end
160
+
161
+ if $0 == __FILE__ then
162
+ require 'test/unit'
163
+ class TestConfiguration < Test::Unit::TestCase
164
+ def setup
165
+ @conf = Configuration.new("./test")
166
+ end
167
+
168
+ def test_setup
169
+ assert(@conf)
170
+ assert(@conf.base)
171
+ assert(File.directory?(@conf.base))
172
+ end
173
+
174
+ def test_accessor
175
+ assert(@conf["file1"] = "bar")
176
+ assert_equal(@conf[[:file1]], "bar")
177
+ assert_equal(@conf[["file1"]], "bar")
178
+ assert_equal(@conf["file1"], "bar")
179
+ assert(File.exist?("#{@conf.base}/file1.yaml"))
180
+
181
+ assert(@conf["file2"] = "foo")
182
+ assert_equal(@conf[:file2], "foo")
183
+ assert_equal(@conf["file2"], "foo")
184
+ assert(File.exist?("#{@conf.base}/file2.yaml"))
185
+
186
+ assert(@conf[[:file3, :sub]] = "baz")
187
+ assert_equal(@conf[[:file3, :sub]], "baz")
188
+ assert_equal(@conf["file3", "sub"], "baz")
189
+ assert(File.exist?("#{@conf.base}/file3.yaml"))
190
+ assert(!File.exist?("#{@conf.base}/file3"))
191
+
192
+ assert(@conf.exist?(:file3))
193
+ assert(!@conf.exist?(:file))
194
+ #assert_raise { @conf
195
+ end
196
+
197
+ def test_nesting
198
+ end
199
+
200
+ def xtest_common_usecase
201
+ @conf.setup(%w(
202
+ main
203
+ plugins
204
+ plugins/demo
205
+ ))
206
+ @conf.merge(
207
+ "main.language" => "de",
208
+ "main.channels" => %w(foo bar baz),
209
+ "plugins.demo.value" => 42
210
+ )
211
+ @conf["plugins.demo.other"] = 24
212
+ @conf["main.language"].replace("en")
213
+ @conf.update
214
+ file, key = @conf.send(:split, "main.language")
215
+ File.open(file, "w") { |fh| fh.write({"language" => "en"}.to_yaml) }
216
+ @conf.rehash
217
+ end
218
+
219
+ def teardown
220
+ FileUtils.rm_r(@conf.base)
221
+ end
222
+ end
223
+ end