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.
- 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
|