linen 0.3.1 → 0.3.2
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/lib/indifferent_hash.rb +9 -9
- data/lib/linen.rb +8 -8
- data/lib/linen/argument.rb +21 -21
- data/lib/linen/cli.rb +214 -214
- data/lib/linen/command.rb +118 -118
- data/lib/linen/exceptions.rb +13 -13
- data/lib/linen/plugin.rb +63 -63
- data/lib/linen/plugin_registry.rb +38 -38
- data/lib/linen/workspace.rb +15 -15
- data/lib/string_extensions.rb +3 -3
- data/test/test_cli.rb +72 -72
- data/test/test_indifferent_hash.rb +26 -26
- data/test/test_plugins.rb +35 -35
- metadata +1 -1
data/lib/indifferent_hash.rb
CHANGED
@@ -8,15 +8,15 @@
|
|
8
8
|
##############################################################
|
9
9
|
|
10
10
|
class IndifferentHash < Hash
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
def []( key )
|
12
|
+
candidate = self.fetch( key ) rescue false
|
13
|
+
return candidate if candidate
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
[ :to_s, :intern ].each do |modifier|
|
16
|
+
candidate = self.fetch( key.send(modifier) ) if key.respond_to? modifier rescue false
|
17
|
+
return candidate if candidate
|
18
|
+
end
|
19
19
|
|
20
|
-
|
21
|
-
|
20
|
+
return nil
|
21
|
+
end
|
22
22
|
end
|
data/lib/linen.rb
CHANGED
@@ -18,18 +18,18 @@ require 'string_extensions'
|
|
18
18
|
|
19
19
|
|
20
20
|
module Linen
|
21
|
-
|
22
|
-
|
21
|
+
VERSION = "0.3.2"
|
22
|
+
SVNRev = %q$Rev: 86 $
|
23
23
|
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
def self::plugins
|
26
|
+
return Linen::PluginRegistry.instance
|
27
|
+
end
|
28
28
|
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
def self::start
|
31
|
+
Linen::CLI.start_loop
|
32
|
+
end
|
33
33
|
end
|
34
34
|
|
35
35
|
|
data/lib/linen/argument.rb
CHANGED
@@ -8,31 +8,31 @@
|
|
8
8
|
##############################################################
|
9
9
|
|
10
10
|
class Linen::Plugin::Argument
|
11
|
-
|
11
|
+
attr_reader :prompt
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
13
|
+
def initialize( plugin, name, opts )
|
14
|
+
@plugin = plugin
|
15
|
+
@name = name
|
16
|
+
@prompt = opts[:prompt] || "Please enter the value for #{@name}"
|
17
|
+
@validation = opts[:validation] || /^\w+$/
|
18
|
+
@conversion = opts[:conversion] || nil
|
19
|
+
end
|
20
20
|
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
22
|
+
def convert( value )
|
23
|
+
return value unless @conversion
|
24
|
+
return @conversion.call( value )
|
25
|
+
end
|
26
26
|
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
28
|
+
def validate( value )
|
29
|
+
if @validation.is_a? Proc
|
30
|
+
result = @validation.call( value )
|
31
|
+
else
|
32
|
+
result = ( value =~ @validation )
|
33
|
+
end
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
raise Linen::Plugin::ArgumentError, "Value '#{value}' is invalid for #{@name}." unless result
|
36
|
+
return value
|
37
|
+
end
|
38
38
|
end
|
data/lib/linen/cli.rb
CHANGED
@@ -8,144 +8,144 @@
|
|
8
8
|
##############################################################
|
9
9
|
|
10
10
|
class Linen::CLI
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
workspace.add_values( results )
|
130
|
-
command.execute( workspace )
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
|
135
|
-
def self::reprompt( prompt = "Re-enter: " )
|
136
|
-
old_completion_proc = Readline.completion_proc
|
137
|
-
Readline.completion_proc = proc {}
|
11
|
+
class << self
|
12
|
+
attr_accessor :prompt
|
13
|
+
end
|
14
|
+
|
15
|
+
@prompt = "linen > "
|
16
|
+
|
17
|
+
def self::parse_command( input )
|
18
|
+
### * nil means ctrl-d, so exit.
|
19
|
+
### * if they said "quit" or "exit", do so
|
20
|
+
### * Size == 0 means empty command, so just return.
|
21
|
+
###
|
22
|
+
### otherwise, add to history.
|
23
|
+
if input.nil?
|
24
|
+
### blank line to make ctrl-d not make the error live on the existing line
|
25
|
+
puts ; cleanup and exit
|
26
|
+
elsif input.chomp.size == 0 # empty string
|
27
|
+
return
|
28
|
+
else
|
29
|
+
Readline::HISTORY.push( input )
|
30
|
+
end
|
31
|
+
|
32
|
+
plugin, command, *arguments = input.split
|
33
|
+
|
34
|
+
if ['quit', 'exit'].abbrev.include? plugin
|
35
|
+
cleanup and exit
|
36
|
+
elsif ['help', '?'].abbrev.include? plugin
|
37
|
+
# they entered "help <plugin> <command> or some subset there of, which means
|
38
|
+
# that we have the plugin in command and the command in the first element of args.
|
39
|
+
plugin = command.dup rescue nil
|
40
|
+
command = arguments.shift rescue nil
|
41
|
+
|
42
|
+
if plugin and command
|
43
|
+
plugin, command = canonicalize( "#{plugin} #{command}" ).split rescue nil
|
44
|
+
|
45
|
+
# if either plugin or command is nil, lookup will fail; bail
|
46
|
+
return unless plugin = Linen.plugins[ plugin ]
|
47
|
+
return unless command = plugin.commands[ command ]
|
48
|
+
|
49
|
+
puts command.help
|
50
|
+
elsif plugin
|
51
|
+
return unless plugin = Linen.plugins[ canonicalize( plugin ) ]
|
52
|
+
puts plugin.help
|
53
|
+
else
|
54
|
+
help
|
55
|
+
end
|
56
|
+
elsif plugin.nil? or command.nil?
|
57
|
+
puts "You must enter both a plugin name and a command."
|
58
|
+
else
|
59
|
+
plugin, command, *args = canonicalize( input ).split
|
60
|
+
|
61
|
+
execute_command plugin, command, args
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def self::start_loop
|
67
|
+
loop do
|
68
|
+
begin
|
69
|
+
input = Readline.readline( @prompt )
|
70
|
+
rescue Interrupt
|
71
|
+
puts "\nPlease type 'quit' or 'exit' to quit."
|
72
|
+
else
|
73
|
+
parse_command input
|
74
|
+
end
|
75
|
+
|
76
|
+
puts # blank line to clean things up
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
#######
|
82
|
+
private
|
83
|
+
#######
|
84
|
+
|
85
|
+
def self::canonicalize( input )
|
86
|
+
begin
|
87
|
+
expansion = expand_command( input )
|
88
|
+
rescue Linen::CLI::PluginNotFoundError, Linen::CLI::CommandNotFoundError, Linen::CLI::AmbiguousPluginError, Linen::CLI::AmbiguousCommandError => e
|
89
|
+
puts e
|
90
|
+
end
|
91
|
+
|
92
|
+
return expansion
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def self::cleanup
|
97
|
+
puts "Exiting..."
|
98
|
+
|
99
|
+
Linen.plugins.each do |p|
|
100
|
+
p.cleanup
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def self::execute_command( plugin, command, args )
|
106
|
+
plugin = Linen.plugins[ plugin ]
|
107
|
+
command = plugin.commands[ command ]
|
108
|
+
workspace = Linen::Workspace.new
|
109
|
+
results = command.validate_arguments( args )
|
110
|
+
|
111
|
+
input = ''
|
112
|
+
if command.requires_confirmation?
|
113
|
+
puts "\nRunning '#{plugin.short_name} #{command.name}' with arguments:"
|
114
|
+
|
115
|
+
puts results.map { |arg, value|
|
116
|
+
next unless value
|
117
|
+
"#{arg}: #{value}"
|
118
|
+
}.join( "\n" )
|
119
|
+
|
120
|
+
while input !~ /^(y|n)/i
|
121
|
+
input = Readline.readline( "\nContinue [y/N]? ")
|
122
|
+
input = "n" if input == ''
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
if input =~ /^y/i
|
127
|
+
puts # blank line
|
138
128
|
|
139
|
-
|
129
|
+
workspace.add_values( results )
|
130
|
+
command.execute( workspace )
|
131
|
+
end
|
132
|
+
end
|
140
133
|
|
141
|
-
Readline.completion_proc = old_completion_proc
|
142
|
-
|
143
|
-
return input
|
144
|
-
end
|
145
134
|
|
135
|
+
def self::reprompt( prompt = "Re-enter: " )
|
136
|
+
old_completion_proc = Readline.completion_proc
|
137
|
+
Readline.completion_proc = proc {}
|
146
138
|
|
147
|
-
|
148
|
-
|
139
|
+
input = Readline.readline( prompt )
|
140
|
+
|
141
|
+
Readline.completion_proc = old_completion_proc
|
142
|
+
|
143
|
+
return input
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
def self::help
|
148
|
+
puts <<-END
|
149
149
|
Usage: <plugin> <command> [<argument>, <argument>...]
|
150
150
|
|
151
151
|
You may shorten the plugin and commands as long as the abbreviation
|
@@ -154,92 +154,92 @@ is non-ambiguous. For example, given two plugins 'addition' and
|
|
154
154
|
|
155
155
|
Available plugins and commands:
|
156
156
|
|
157
|
-
|
157
|
+
END
|
158
158
|
|
159
|
-
|
160
|
-
|
159
|
+
Linen.plugins.each do |plugin|
|
160
|
+
puts "- #{plugin.short_name}"
|
161
161
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
162
|
+
plugin.commands.each do |name, command|
|
163
|
+
puts " - #{name}"
|
164
|
+
end
|
165
|
+
end
|
166
166
|
|
167
|
-
|
167
|
+
puts <<-END
|
168
168
|
|
169
169
|
To get help with a plugin, enter "help <plugin>". You may also enter
|
170
170
|
"help <plugin> <command>" for help on a specific command.
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
171
|
+
END
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
### First, try to complete the plugin name. If we can't,
|
176
|
+
### raise an exception saying so.
|
177
|
+
###
|
178
|
+
### Second, try to complete the command name. If we can't,
|
179
|
+
### raise an exception saying so.
|
180
|
+
###
|
181
|
+
### The caller is now responsible for flow control.
|
182
|
+
def self::expand_command( str )
|
183
|
+
plugin_candidates = Linen.plugins.collect {|p| p.short_name}.sort
|
184
|
+
|
185
|
+
### empty string means we're trying to complete the plugin with nothing to go on
|
186
|
+
raise Linen::CLI::AmbiguousPluginError.new( plugin_candidates ) if str.empty?
|
187
|
+
|
188
|
+
plugin, command, *arguments = str.split
|
189
|
+
|
190
|
+
### attempt to complete the plugin, raising an exception if it failes
|
191
|
+
completed_plugin = plugin_candidates.abbrev[ plugin ]
|
192
|
+
|
193
|
+
unless completed_plugin
|
194
|
+
refined_candidates = plugin_candidates.select {|p| p =~ /^#{plugin}/}
|
195
|
+
|
196
|
+
raise Linen::CLI::PluginNotFoundError, "Plugin '#{plugin}' not found." if refined_candidates.empty?
|
197
|
+
raise Linen::CLI::AmbiguousPluginError.new( refined_candidates, plugin )
|
198
|
+
end
|
199
|
+
|
200
|
+
### if there's no command entered and no space after the plugin,
|
201
|
+
### just return the plugin
|
202
|
+
return completed_plugin if command.nil? and str !~ /\s$/
|
203
|
+
|
204
|
+
### If we've gotten here, we've now got the plugin in completed_plugin,
|
205
|
+
### so attempt to complete the command
|
206
|
+
command_candidates = Linen.plugins[ completed_plugin ].commands.keys.map {|k| k.to_s}.sort
|
207
|
+
|
208
|
+
completed_command = command_candidates.abbrev[ command ]
|
209
|
+
|
210
|
+
unless completed_command
|
211
|
+
refined_candidates = command_candidates.select {|c| c =~ /^#{command}/}
|
212
|
+
|
213
|
+
raise Linen::CLI::CommandNotFoundError,
|
214
|
+
"Command '#{command}' not found." if refined_candidates.empty?
|
215
|
+
|
216
|
+
raise Linen::CLI::AmbiguousCommandError.new( refined_candidates, command ),
|
217
|
+
"The command you entered ('#{command}') is ambiguous; please select from the following:"
|
218
|
+
end
|
219
|
+
|
220
|
+
### if we've gotten here, we're golden. Everything is completed. Rejoice!
|
221
|
+
output = completed_plugin
|
222
|
+
output << " " + completed_command if completed_command
|
223
|
+
output << " " + arguments.join(' ') unless arguments.empty?
|
224
|
+
|
225
|
+
return output
|
226
|
+
end
|
227
|
+
|
228
|
+
Readline.basic_word_break_characters = ""
|
229
|
+
|
230
|
+
Readline.completion_proc = proc do |str|
|
231
|
+
begin
|
232
|
+
output = expand_command( str )
|
233
|
+
rescue Linen::CLI::PluginNotFoundError, Linen::CLI::CommandNotFoundError => e
|
234
|
+
output = ''
|
235
|
+
rescue Linen::CLI::AmbiguousPluginError => e
|
236
|
+
output = e.candidates
|
237
|
+
rescue Linen::CLI::AmbiguousCommandError => e
|
238
|
+
output = e.candidates.map {|c| "#{str.split.first} #{c}"}
|
239
|
+
ensure
|
240
|
+
return output
|
241
|
+
end
|
242
|
+
end
|
243
243
|
end
|
244
244
|
|
245
245
|
Signal.trap( 'INT' ) { raise Interrupt }
|