butler 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
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