butler 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +4 -0
- data/GPL.txt +340 -0
- data/LICENSE.txt +52 -0
- data/README +37 -0
- data/Rakefile +334 -0
- data/bin/botcontrol +230 -0
- data/data/butler/config_template.yaml +4 -0
- data/data/butler/dialogs/backup.rb +19 -0
- data/data/butler/dialogs/botcontrol.rb +4 -0
- data/data/butler/dialogs/config.rb +1 -0
- data/data/butler/dialogs/create.rb +53 -0
- data/data/butler/dialogs/delete.rb +3 -0
- data/data/butler/dialogs/en/backup.yaml +6 -0
- data/data/butler/dialogs/en/botcontrol.yaml +5 -0
- data/data/butler/dialogs/en/create.yaml +11 -0
- data/data/butler/dialogs/en/delete.yaml +2 -0
- data/data/butler/dialogs/en/help.yaml +17 -0
- data/data/butler/dialogs/en/info.yaml +13 -0
- data/data/butler/dialogs/en/list.yaml +4 -0
- data/data/butler/dialogs/en/notyetimplemented.yaml +2 -0
- data/data/butler/dialogs/en/rename.yaml +3 -0
- data/data/butler/dialogs/en/start.yaml +3 -0
- data/data/butler/dialogs/en/sync_plugins.yaml +3 -0
- data/data/butler/dialogs/en/uninstall.yaml +5 -0
- data/data/butler/dialogs/en/unknown_command.yaml +2 -0
- data/data/butler/dialogs/help.rb +11 -0
- data/data/butler/dialogs/info.rb +27 -0
- data/data/butler/dialogs/interactive.rb +1 -0
- data/data/butler/dialogs/list.rb +10 -0
- data/data/butler/dialogs/notyetimplemented.rb +1 -0
- data/data/butler/dialogs/rename.rb +4 -0
- data/data/butler/dialogs/selectbot.rb +2 -0
- data/data/butler/dialogs/start.rb +5 -0
- data/data/butler/dialogs/sync_plugins.rb +30 -0
- data/data/butler/dialogs/uninstall.rb +17 -0
- data/data/butler/dialogs/unknown_command.rb +1 -0
- data/data/butler/plugins/core/logout.rb +41 -0
- data/data/butler/plugins/core/plugins.rb +134 -0
- data/data/butler/plugins/core/privilege.rb +103 -0
- data/data/butler/plugins/core/user.rb +166 -0
- data/data/butler/plugins/dev/eval.rb +64 -0
- data/data/butler/plugins/dev/nometa.rb +14 -0
- data/data/butler/plugins/dev/onhandlers.rb +93 -0
- data/data/butler/plugins/dev/raw.rb +36 -0
- data/data/butler/plugins/dev/rawlog.rb +77 -0
- data/data/butler/plugins/games/eightball.rb +54 -0
- data/data/butler/plugins/games/mastermind.rb +174 -0
- data/data/butler/plugins/irc/action.rb +36 -0
- data/data/butler/plugins/irc/join.rb +38 -0
- data/data/butler/plugins/irc/notice.rb +36 -0
- data/data/butler/plugins/irc/part.rb +38 -0
- data/data/butler/plugins/irc/privmsg.rb +36 -0
- data/data/butler/plugins/irc/quit.rb +36 -0
- data/data/butler/plugins/operator/deop.rb +41 -0
- data/data/butler/plugins/operator/devoice.rb +41 -0
- data/data/butler/plugins/operator/limit.rb +47 -0
- data/data/butler/plugins/operator/op.rb +41 -0
- data/data/butler/plugins/operator/voice.rb +41 -0
- data/data/butler/plugins/public/help.rb +69 -0
- data/data/butler/plugins/public/login.rb +72 -0
- data/data/butler/plugins/public/usage.rb +49 -0
- data/data/butler/plugins/service/clones.rb +56 -0
- data/data/butler/plugins/service/define.rb +47 -0
- data/data/butler/plugins/service/log.rb +183 -0
- data/data/butler/plugins/service/svn.rb +91 -0
- data/data/butler/plugins/util/cycle.rb +98 -0
- data/data/butler/plugins/util/load.rb +41 -0
- data/data/butler/plugins/util/pong.rb +29 -0
- data/data/butler/strings/random/acknowledge.en.yaml +5 -0
- data/data/butler/strings/random/gratitude.en.yaml +3 -0
- data/data/butler/strings/random/hello.en.yaml +4 -0
- data/data/butler/strings/random/ignorance.en.yaml +7 -0
- data/data/butler/strings/random/ignorance_about.en.yaml +3 -0
- data/data/butler/strings/random/insult.en.yaml +3 -0
- data/data/butler/strings/random/rejection.en.yaml +12 -0
- data/data/man/botcontrol.1 +17 -0
- data/lib/access.rb +187 -0
- data/lib/access/admin.rb +16 -0
- data/lib/access/privilege.rb +122 -0
- data/lib/access/role.rb +102 -0
- data/lib/access/savable.rb +18 -0
- data/lib/access/user.rb +180 -0
- data/lib/access/yamlbase.rb +126 -0
- data/lib/butler.rb +188 -0
- data/lib/butler/bot.rb +247 -0
- data/lib/butler/control.rb +93 -0
- data/lib/butler/dialog.rb +64 -0
- data/lib/butler/initialvalues.rb +40 -0
- data/lib/butler/irc/channel.rb +135 -0
- data/lib/butler/irc/channels.rb +96 -0
- data/lib/butler/irc/client.rb +351 -0
- data/lib/butler/irc/hostmask.rb +53 -0
- data/lib/butler/irc/message.rb +184 -0
- data/lib/butler/irc/parser.rb +125 -0
- data/lib/butler/irc/parser/commands.rb +83 -0
- data/lib/butler/irc/parser/generic.rb +343 -0
- data/lib/butler/irc/socket.rb +378 -0
- data/lib/butler/irc/string.rb +186 -0
- data/lib/butler/irc/topic.rb +15 -0
- data/lib/butler/irc/user.rb +265 -0
- data/lib/butler/irc/users.rb +112 -0
- data/lib/butler/plugin.rb +249 -0
- data/lib/butler/plugin/configproxy.rb +35 -0
- data/lib/butler/plugin/mapper.rb +85 -0
- data/lib/butler/plugin/matcher.rb +55 -0
- data/lib/butler/plugin/onhandlers.rb +70 -0
- data/lib/butler/plugin/trigger.rb +58 -0
- data/lib/butler/plugins.rb +147 -0
- data/lib/butler/version.rb +17 -0
- data/lib/cloptions.rb +217 -0
- data/lib/cloptions/adapters.rb +24 -0
- data/lib/cloptions/switch.rb +132 -0
- data/lib/configuration.rb +223 -0
- data/lib/dialogline.rb +296 -0
- data/lib/dialogline/localizations.rb +24 -0
- data/lib/durations.rb +57 -0
- data/lib/event.rb +295 -0
- data/lib/event/at.rb +64 -0
- data/lib/event/every.rb +56 -0
- data/lib/event/timed.rb +112 -0
- data/lib/installer.rb +75 -0
- data/lib/iterator.rb +34 -0
- data/lib/log.rb +68 -0
- data/lib/log/comfort.rb +85 -0
- data/lib/log/converter.rb +23 -0
- data/lib/log/entry.rb +152 -0
- data/lib/log/fakeio.rb +55 -0
- data/lib/log/file.rb +54 -0
- data/lib/log/filereader.rb +81 -0
- data/lib/log/forward.rb +49 -0
- data/lib/log/methods.rb +39 -0
- data/lib/log/nolog.rb +18 -0
- data/lib/log/splitter.rb +26 -0
- data/lib/ostructfixed.rb +26 -0
- data/lib/ruby/array/columnize.rb +38 -0
- data/lib/ruby/dir/mktree.rb +28 -0
- data/lib/ruby/enumerable/join.rb +13 -0
- data/lib/ruby/exception/detailed.rb +24 -0
- data/lib/ruby/file/append.rb +11 -0
- data/lib/ruby/file/write.rb +11 -0
- data/lib/ruby/hash/zip.rb +15 -0
- data/lib/ruby/kernel/bench.rb +15 -0
- data/lib/ruby/kernel/daemonize.rb +42 -0
- data/lib/ruby/kernel/non_verbose.rb +17 -0
- data/lib/ruby/kernel/safe_fork.rb +18 -0
- data/lib/ruby/range/stepped.rb +11 -0
- data/lib/ruby/string/arguments.rb +72 -0
- data/lib/ruby/string/chunks.rb +15 -0
- data/lib/ruby/string/post_arguments.rb +44 -0
- data/lib/ruby/string/unescaped.rb +17 -0
- data/lib/scheduler.rb +164 -0
- data/lib/scriptfile.rb +101 -0
- data/lib/templater.rb +86 -0
- data/test/cloptions.rb +134 -0
- data/test/cv.rb +28 -0
- data/test/irc/client.rb +85 -0
- data/test/irc/client_login.txt +53 -0
- data/test/irc/client_subscribe.txt +8 -0
- data/test/irc/message.rb +30 -0
- data/test/irc/messages.txt +64 -0
- data/test/irc/parser.rb +13 -0
- data/test/irc/profile_parser.rb +12 -0
- data/test/irc/users.rb +28 -0
- 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
|
data/lib/cloptions.rb
ADDED
@@ -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
|