linen 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +2 -0
- data/README +17 -0
- data/Rakefile +155 -0
- data/lib/indifferent_hash.rb +22 -0
- data/lib/linen.rb +44 -0
- data/lib/linen/cli.rb +240 -0
- data/lib/linen/exceptions.rb +39 -0
- data/lib/linen/plugin.rb +189 -0
- data/lib/linen/plugin_registry.rb +65 -0
- data/lib/linen/workspace.rb +20 -0
- data/lib/string_extensions.rb +15 -0
- data/test/test_cli.rb +111 -0
- data/test/test_indifferent_hash.rb +51 -0
- data/test/test_plugins.rb +65 -0
- metadata +62 -0
data/LICENSE
ADDED
data/README
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
= Linen - Easy command line interfaces in Ruby
|
2
|
+
|
3
|
+
Linen is a framework for creating applications that use a command line interface. It features a simple plugin system, readline integration for history and completion, and other features.
|
4
|
+
|
5
|
+
== Usage
|
6
|
+
|
7
|
+
See USAGE[link:files/docs/USAGE.html].
|
8
|
+
|
9
|
+
== Authors
|
10
|
+
|
11
|
+
* Ben Bleything - <mailto:bbleything@laika.com>
|
12
|
+
|
13
|
+
== License and Copyright
|
14
|
+
|
15
|
+
Linen is copyright (c) 2007, LAIKA Inc.
|
16
|
+
|
17
|
+
Portions of the code (notably the Rakefile) contain snippets from other projects. These instances are documented in the header of the files involved.
|
data/Rakefile
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
##############################################################
|
2
|
+
# Copyright 2007, LAIKA, Inc. #
|
3
|
+
# #
|
4
|
+
# Based heavily on Ben Bleything's Rakefile for plist, which #
|
5
|
+
# is in turn based on Geoffrey Grosenbach's Rakefile for #
|
6
|
+
# gruff. #
|
7
|
+
# #
|
8
|
+
# Includes whitespace-fixing task based on code from Typo. #
|
9
|
+
# #
|
10
|
+
# Authors: #
|
11
|
+
# * Ben Bleything <bbleything@laika.com> #
|
12
|
+
##############################################################
|
13
|
+
|
14
|
+
require 'fileutils'
|
15
|
+
require 'rubygems'
|
16
|
+
require 'rake'
|
17
|
+
require 'rake/testtask'
|
18
|
+
require 'rake/rdoctask'
|
19
|
+
require 'rake/packagetask'
|
20
|
+
require 'rake/gempackagetask'
|
21
|
+
require 'rake/contrib/rubyforgepublisher'
|
22
|
+
|
23
|
+
$:.unshift(File.dirname(__FILE__) + "/lib")
|
24
|
+
require 'linen'
|
25
|
+
|
26
|
+
PKG_NAME = 'linen'
|
27
|
+
PKG_VERSION = Linen::VERSION
|
28
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
29
|
+
|
30
|
+
RELEASE_NAME = "REL #{PKG_VERSION}"
|
31
|
+
|
32
|
+
RUBYFORGE_PROJECT = "linen"
|
33
|
+
RUBYFORGE_USER = ENV['RUBYFORGE_USER']
|
34
|
+
|
35
|
+
TEXT_FILES = %w( Rakefile README LICENSE )
|
36
|
+
TEST_FILES = Dir.glob('test/test_*').delete_if { |item| item.include?( "\.svn" ) }
|
37
|
+
LIB_FILES = Dir.glob('lib/**/*').delete_if { |item| item.include?( "\.svn" ) }
|
38
|
+
RELEASE_FILES = TEXT_FILES + LIB_FILES + TEST_FILES
|
39
|
+
|
40
|
+
task :default => [ :test ]
|
41
|
+
|
42
|
+
### Run the unit tests
|
43
|
+
Rake::TestTask.new { |t|
|
44
|
+
t.libs << "test"
|
45
|
+
t.pattern = 'test/test_*.rb'
|
46
|
+
t.verbose = true
|
47
|
+
}
|
48
|
+
|
49
|
+
|
50
|
+
desc "Clean pkg, coverage, and rdoc; remove .bak files"
|
51
|
+
task :clean => [ :clobber_rdoc, :clobber_package, :clobber_coverage ] do
|
52
|
+
puts cmd = "find . -type f -name *.bak -delete"
|
53
|
+
`#{cmd}`
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
task :clobber_coverage do
|
58
|
+
puts cmd = "rm -rf coverage"
|
59
|
+
`#{cmd}`
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
desc "Generate coverage analysis with rcov (requires rcov to be installed)"
|
64
|
+
task :rcov => [ :clobber_coverage ] do
|
65
|
+
puts cmd = "rcov -Ilib --xrefs -T test/*.rb"
|
66
|
+
puts `#{cmd}`
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
desc "Strip trailing whitespace and fix newlines for all release files"
|
71
|
+
task :fix_whitespace => [ :clean ] do
|
72
|
+
RELEASE_FILES.each do |filename|
|
73
|
+
next if File.directory? filename
|
74
|
+
|
75
|
+
File.open(filename) do |file|
|
76
|
+
newfile = ''
|
77
|
+
needs_love = false
|
78
|
+
|
79
|
+
file.readlines.each_with_index do |line, lineno|
|
80
|
+
if line =~ /[ \t]+$/
|
81
|
+
needs_love = true
|
82
|
+
puts "#{filename}: trailing whitespace on line #{lineno}"
|
83
|
+
line.gsub!(/[ \t]*$/, '')
|
84
|
+
end
|
85
|
+
|
86
|
+
if line.chomp == line
|
87
|
+
needs_love = true
|
88
|
+
puts "#{filename}: no newline on line #{lineno}"
|
89
|
+
line << "\n"
|
90
|
+
end
|
91
|
+
|
92
|
+
newfile << line
|
93
|
+
end
|
94
|
+
|
95
|
+
if needs_love
|
96
|
+
tempname = "#{filename}.new"
|
97
|
+
|
98
|
+
File.open(tempname, 'w').write(newfile)
|
99
|
+
File.chmod(File.stat(filename).mode, tempname)
|
100
|
+
|
101
|
+
FileUtils.ln filename, "#{filename}.bak"
|
102
|
+
FileUtils.ln tempname, filename, :force => true
|
103
|
+
File.unlink(tempname)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
desc "Copy documentation to rubyforge"
|
111
|
+
task :update_rdoc => [ :rdoc ] do
|
112
|
+
Rake::SshDirPublisher.new("#{RUBYFORGE_USER}@rubyforge.org", "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}", "rdoc").upload
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
### Genereate the RDoc documentation
|
117
|
+
Rake::RDocTask.new { |rdoc|
|
118
|
+
rdoc.rdoc_dir = 'rdoc'
|
119
|
+
rdoc.title = "Linen - A pluggable command-line interface library"
|
120
|
+
rdoc.options << '-SNmREADME'
|
121
|
+
|
122
|
+
rdoc.rdoc_files.include TEXT_FILES
|
123
|
+
rdoc.rdoc_files.include LIB_FILES
|
124
|
+
rdoc.rdoc_files.include Dir.glob('docs/**').delete_if {|f| f.include? 'jamis' }
|
125
|
+
}
|
126
|
+
|
127
|
+
|
128
|
+
### Create compressed packages
|
129
|
+
spec = Gem::Specification.new do |s|
|
130
|
+
s.name = PKG_NAME
|
131
|
+
s.version = PKG_VERSION
|
132
|
+
|
133
|
+
s.summary = "Linen - A pluggable command-line interface library"
|
134
|
+
s.description = <<-EOD
|
135
|
+
Linen is a library which can be used to build a command-line interface for any purpose. It features a plugin architecture to specify new tasks, Readline support, history, and more.
|
136
|
+
EOD
|
137
|
+
|
138
|
+
s.authors = "LAIKA, Inc."
|
139
|
+
s.homepage = "http://opensource.laika.com"
|
140
|
+
|
141
|
+
s.rubyforge_project = RUBYFORGE_PROJECT
|
142
|
+
|
143
|
+
s.has_rdoc = true
|
144
|
+
|
145
|
+
s.files = RELEASE_FILES
|
146
|
+
s.test_files = TEST_FILES
|
147
|
+
|
148
|
+
s.autorequire = 'linen'
|
149
|
+
end
|
150
|
+
|
151
|
+
Rake::GemPackageTask.new(spec) do |p|
|
152
|
+
p.gem_spec = spec
|
153
|
+
p.need_tar = true
|
154
|
+
p.need_zip = true
|
155
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
##############################################################
|
4
|
+
# Copyright 2007, LAIKA, Inc. #
|
5
|
+
# #
|
6
|
+
# Authors: #
|
7
|
+
# * Ben Bleything <bbleything@laika.com> #
|
8
|
+
##############################################################
|
9
|
+
|
10
|
+
class IndifferentHash < Hash
|
11
|
+
def []( key )
|
12
|
+
candidate = self.fetch( key ) rescue false
|
13
|
+
return candidate if candidate
|
14
|
+
|
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
|
+
|
20
|
+
return nil
|
21
|
+
end
|
22
|
+
end
|
data/lib/linen.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
##############################################################
|
4
|
+
# Copyright 2007, LAIKA, Inc. #
|
5
|
+
# #
|
6
|
+
# Authors: #
|
7
|
+
# * Ben Bleything <bbleything@laika.com> #
|
8
|
+
##############################################################
|
9
|
+
|
10
|
+
### External libraries
|
11
|
+
require 'abbrev'
|
12
|
+
require 'readline'
|
13
|
+
|
14
|
+
|
15
|
+
### internal but non-linen libraries
|
16
|
+
require 'indifferent_hash'
|
17
|
+
require 'string_extensions'
|
18
|
+
|
19
|
+
|
20
|
+
module Linen
|
21
|
+
VERSION = "0.2.0"
|
22
|
+
SVNRev = %q$Rev: 56 $
|
23
|
+
|
24
|
+
|
25
|
+
def self::plugins
|
26
|
+
return Linen::PluginRegistry.instance
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def self::start
|
31
|
+
Linen::CLI.start_loop
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
### Plugin Infrastructure
|
37
|
+
require 'linen/plugin_registry'
|
38
|
+
require 'linen/plugin'
|
39
|
+
|
40
|
+
|
41
|
+
### Other Infrastructure
|
42
|
+
require 'linen/cli'
|
43
|
+
require 'linen/exceptions'
|
44
|
+
require 'linen/workspace'
|
data/lib/linen/cli.rb
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
##############################################################
|
4
|
+
# Copyright 2007, LAIKA, Inc. #
|
5
|
+
# #
|
6
|
+
# Authors: #
|
7
|
+
# * Ben Bleything <bbleything@laika.com> #
|
8
|
+
##############################################################
|
9
|
+
|
10
|
+
class Linen::CLI
|
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
|
+
|
109
|
+
workspace = Linen::Workspace.new
|
110
|
+
|
111
|
+
command.arguments.each do |arg_name|
|
112
|
+
argument = plugin.arguments[ arg_name ]
|
113
|
+
|
114
|
+
arg_value = args.shift
|
115
|
+
|
116
|
+
begin
|
117
|
+
if arg_value.nil?
|
118
|
+
old_completion_proc = Readline.completion_proc
|
119
|
+
Readline.completion_proc = Proc.new {}
|
120
|
+
|
121
|
+
arg_value = Readline.readline( argument.prompt )
|
122
|
+
|
123
|
+
Readline.completion_proc = old_completion_proc
|
124
|
+
end
|
125
|
+
|
126
|
+
argument.validate arg_value
|
127
|
+
|
128
|
+
rescue Linen::Plugin::ArgumentError => e
|
129
|
+
print e
|
130
|
+
|
131
|
+
# reset arg_value to nil so we get prompted on retry
|
132
|
+
arg_value = nil
|
133
|
+
|
134
|
+
retry
|
135
|
+
else
|
136
|
+
arg_value = argument.convert( arg_value )
|
137
|
+
workspace.set_value arg_name, arg_value
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
puts command.execute( workspace )
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
def self::help
|
146
|
+
puts <<-END
|
147
|
+
Usage: <plugin> <command> [<argument>, <argument>...]
|
148
|
+
|
149
|
+
You may shorten the plugin and commands as long as the abbreviation
|
150
|
+
is non-ambiguous. For example, given two plugins 'addition' and
|
151
|
+
'administration', you would need to supply at least three characters.
|
152
|
+
|
153
|
+
Available plugins and commands:
|
154
|
+
|
155
|
+
END
|
156
|
+
|
157
|
+
Linen.plugins.each do |plugin|
|
158
|
+
puts "- #{plugin.short_name}"
|
159
|
+
|
160
|
+
plugin.commands.each do |name, command|
|
161
|
+
puts " - #{name}"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
puts <<-END
|
166
|
+
|
167
|
+
To get help with a plugin, enter "help <plugin>". You may also enter
|
168
|
+
"help <plugin> <command>" for help on a specific command.
|
169
|
+
END
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
### First, try to complete the plugin name. If we can't,
|
174
|
+
### raise an exception saying so.
|
175
|
+
###
|
176
|
+
### Second, try to complete the command name. If we can't,
|
177
|
+
### raise an exception saying so.
|
178
|
+
###
|
179
|
+
### The caller is now responsible for flow control.
|
180
|
+
def self::expand_command( str )
|
181
|
+
plugin_candidates = Linen.plugins.collect {|p| p.short_name}.sort
|
182
|
+
|
183
|
+
### empty string means we're trying to complete the plugin with nothing to go on
|
184
|
+
raise Linen::CLI::AmbiguousPluginError.new( plugin_candidates ) if str.empty?
|
185
|
+
|
186
|
+
plugin, command, *arguments = str.split
|
187
|
+
|
188
|
+
### attempt to complete the plugin, raising an exception if it failes
|
189
|
+
completed_plugin = plugin_candidates.abbrev[ plugin ]
|
190
|
+
|
191
|
+
unless completed_plugin
|
192
|
+
refined_candidates = plugin_candidates.select {|p| p =~ /^#{plugin}/}
|
193
|
+
|
194
|
+
raise Linen::CLI::PluginNotFoundError, "Plugin '#{plugin}' not found." if refined_candidates.empty?
|
195
|
+
raise Linen::CLI::AmbiguousPluginError.new( refined_candidates, plugin )
|
196
|
+
end
|
197
|
+
|
198
|
+
### if there's no command entered and no space after the plugin,
|
199
|
+
### just return the plugin
|
200
|
+
return completed_plugin if command.nil? and str !~ /\s$/
|
201
|
+
|
202
|
+
### If we've gotten here, we've now got the plugin in completed_plugin,
|
203
|
+
### so attempt to complete the command
|
204
|
+
command_candidates = Linen.plugins[ completed_plugin ].commands.keys.map {|k| k.to_s}.sort
|
205
|
+
|
206
|
+
completed_command = command_candidates.abbrev[ command ]
|
207
|
+
|
208
|
+
unless completed_command
|
209
|
+
refined_candidates = command_candidates.select {|c| c =~ /^#{command}/}
|
210
|
+
|
211
|
+
raise Linen::CLI::CommandNotFoundError, "Command '#{command}' not found." if refined_candidates.empty?
|
212
|
+
raise Linen::CLI::AmbiguousCommandError.new( refined_candidates, command ), "The command you entered ('#{command}') is ambiguous; please select from the following:"
|
213
|
+
end
|
214
|
+
|
215
|
+
### if we've gotten here, we're golden. Everything is completed. Rejoice!
|
216
|
+
output = completed_plugin
|
217
|
+
output << " " + completed_command if completed_command
|
218
|
+
output << " " + arguments.join(' ') unless arguments.empty?
|
219
|
+
|
220
|
+
return output
|
221
|
+
end
|
222
|
+
|
223
|
+
Readline.basic_word_break_characters = ""
|
224
|
+
|
225
|
+
Readline.completion_proc = proc do |str|
|
226
|
+
begin
|
227
|
+
output = expand_command( str )
|
228
|
+
rescue Linen::CLI::PluginNotFoundError, Linen::CLI::CommandNotFoundError => e
|
229
|
+
output = ''
|
230
|
+
rescue Linen::CLI::AmbiguousPluginError => e
|
231
|
+
output = e.candidates
|
232
|
+
rescue Linen::CLI::AmbiguousCommandError => e
|
233
|
+
output = e.candidates.map {|c| "#{str.split.first} #{c}"}
|
234
|
+
ensure
|
235
|
+
return output
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
Signal.trap( 'INT' ) { raise Interrupt }
|
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
##############################################################
|
4
|
+
# Copyright 2007, LAIKA, Inc. #
|
5
|
+
# #
|
6
|
+
# Authors: #
|
7
|
+
# * Ben Bleything <bbleything@laika.com> #
|
8
|
+
##############################################################
|
9
|
+
|
10
|
+
class Linen::Plugin::ArgumentError < ArgumentError ; end
|
11
|
+
class Linen::Plugin::PluginError < TypeError ; end
|
12
|
+
|
13
|
+
class Linen::CLI::PluginNotFoundError < NameError ; end
|
14
|
+
class Linen::CLI::CommandNotFoundError < NameError ; end
|
15
|
+
|
16
|
+
class Linen::CLI::AbstractAmbiguityError < NameError
|
17
|
+
attr_accessor :candidates, :input
|
18
|
+
|
19
|
+
def initialize( candidates = [], input = '' )
|
20
|
+
@candidates = candidates
|
21
|
+
@input = input
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
type = self.class.to_s.match( /Ambiguous(.*?)Error/ )[1].downcase
|
26
|
+
|
27
|
+
return "The #{type} you entered ('#{@input}') was ambiguous; please select from the following: #{@candidates.join ', '}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Linen::CLI::AmbiguousPluginError < Linen::CLI::AbstractAmbiguityError ; end
|
32
|
+
|
33
|
+
class Linen::CLI::AmbiguousCommandError < Linen::CLI::AbstractAmbiguityError
|
34
|
+
def to_s
|
35
|
+
@candidates.map! {|c| [plugin, c].join ' '}
|
36
|
+
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
data/lib/linen/plugin.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
##############################################################
|
4
|
+
# Copyright 2007, LAIKA, Inc. #
|
5
|
+
# #
|
6
|
+
# Authors: #
|
7
|
+
# * Ben Bleything <bbleything@laika.com> #
|
8
|
+
##############################################################
|
9
|
+
|
10
|
+
class Linen::Plugin
|
11
|
+
class << self
|
12
|
+
attr_reader :commands
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def self::inherited( plugin )
|
17
|
+
Linen::PluginRegistry.instance.register plugin
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def self::short_name
|
22
|
+
self.to_s.downcase.gsub /plugin$/, ''
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def self::help
|
27
|
+
output = []
|
28
|
+
|
29
|
+
desc = description || "No help for #{short_name}"
|
30
|
+
|
31
|
+
output << desc
|
32
|
+
output << nil #blank line
|
33
|
+
output << "Available commands:"
|
34
|
+
output << nil #blank line
|
35
|
+
|
36
|
+
commands.each do |name, command|
|
37
|
+
output << "- #{name}"
|
38
|
+
end
|
39
|
+
|
40
|
+
output << nil #blank line
|
41
|
+
output << "For detailed help on a command, enter \"help #{short_name} <command>\"".wrap
|
42
|
+
|
43
|
+
return output.join( "\n" )
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
def self::argument( name, opts = {} )
|
48
|
+
@defined_arguments ||= IndifferentHash.new
|
49
|
+
|
50
|
+
if opts[:error_message].nil? and opts['error_message'].nil?
|
51
|
+
opts[ :error_message ] = "The value for #{name} is invalid."
|
52
|
+
end
|
53
|
+
|
54
|
+
@defined_arguments[ name ] = Argument.new( self, name, opts )
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
### If args is empty, assume it was called like Plugin.arguments,
|
59
|
+
### so return the hash.
|
60
|
+
def self::arguments( *args )
|
61
|
+
@defined_arguments ||= IndifferentHash.new
|
62
|
+
|
63
|
+
return @defined_arguments if args.empty?
|
64
|
+
|
65
|
+
args.each do |arg|
|
66
|
+
argument arg
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
def self::command( name, &block )
|
72
|
+
@commands ||= IndifferentHash.new
|
73
|
+
|
74
|
+
@commands[ name ] = Command.new( self, name, &block )
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
### multi-purpose method!
|
79
|
+
###
|
80
|
+
### if passed a block, set this plugin's cleanup proc to the block.
|
81
|
+
### if called without a block, execute the proc.
|
82
|
+
###
|
83
|
+
### this is meant to allow plugins to clean up after themselves.
|
84
|
+
def self::cleanup( &block )
|
85
|
+
if block_given?
|
86
|
+
@cleanup_proc = block
|
87
|
+
else
|
88
|
+
# if we didn't define a proc, don't try to call it
|
89
|
+
@cleanup_proc.call if @cleanup_proc
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
### define the plugin's description, or fetch it if nothing passed
|
95
|
+
def self::description( input = nil )
|
96
|
+
return @description unless input
|
97
|
+
|
98
|
+
@description = input
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class Linen::Plugin::Argument
|
103
|
+
attr_reader :name, :prompt
|
104
|
+
|
105
|
+
def initialize( plugin, name, opts )
|
106
|
+
@plugin = plugin
|
107
|
+
@name = name
|
108
|
+
@prompt = opts[:prompt] || "Please enter the value for #{@name}"
|
109
|
+
@validation = opts[:validation] || /^\w+$/
|
110
|
+
@conversion = opts[:conversion] || nil
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
def convert( value )
|
115
|
+
return value unless @conversion
|
116
|
+
return @conversion.call( value )
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
def validate( value )
|
121
|
+
if @validation.is_a? Proc
|
122
|
+
result = @validation.call( value )
|
123
|
+
else
|
124
|
+
result = ( value =~ @validation )
|
125
|
+
end
|
126
|
+
|
127
|
+
raise Linen::Plugin::ArgumentError, "Value '#{value}' is invalid for #{self.name}. " unless result
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class Linen::Plugin::Command
|
132
|
+
attr_reader :name, :arguments
|
133
|
+
|
134
|
+
def initialize( plugin, name, &block )
|
135
|
+
|
136
|
+
@plugin = plugin
|
137
|
+
@name = name
|
138
|
+
@arguments = []
|
139
|
+
@help_text = "No help for #{plugin.short_name} #{name}"
|
140
|
+
|
141
|
+
self.instance_eval &block
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
def execute( workspace = Linen::Workspace.new )
|
146
|
+
return workspace.instance_eval( &@action_proc )
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
def help
|
151
|
+
output = []
|
152
|
+
|
153
|
+
output << @help_text.wrap
|
154
|
+
output << nil # blank line
|
155
|
+
|
156
|
+
# this map turns our list of args into a list like this:
|
157
|
+
# <arg1> <arg2> <arg3> <arg4>...
|
158
|
+
arg_list = arguments.map {|a| "<#{a.to_s}>"}.join( ' ' )
|
159
|
+
|
160
|
+
output << "Usage: #{@plugin.short_name} #{name} #{arg_list}"
|
161
|
+
|
162
|
+
return output.join( "\n" )
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
#######
|
167
|
+
private
|
168
|
+
#######
|
169
|
+
|
170
|
+
def required_arguments( *args )
|
171
|
+
args.each do |arg|
|
172
|
+
raise Linen::Plugin::ArgumentError,
|
173
|
+
"Argument '#{arg}' has not been defined" unless @plugin.arguments.include? arg
|
174
|
+
|
175
|
+
@arguments << arg
|
176
|
+
end
|
177
|
+
end
|
178
|
+
alias required_argument required_arguments
|
179
|
+
|
180
|
+
|
181
|
+
def help_message( message )
|
182
|
+
@help_text = message
|
183
|
+
end
|
184
|
+
|
185
|
+
|
186
|
+
def action( &block )
|
187
|
+
@action_proc = block
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
##############################################################
|
4
|
+
# Copyright 2007, LAIKA, Inc. #
|
5
|
+
# #
|
6
|
+
# Authors: #
|
7
|
+
# * Ben Bleything <bbleything@laika.com> #
|
8
|
+
##############################################################
|
9
|
+
|
10
|
+
require 'singleton'
|
11
|
+
|
12
|
+
class Linen::PluginRegistry
|
13
|
+
include Singleton
|
14
|
+
|
15
|
+
include Enumerable
|
16
|
+
def each
|
17
|
+
### yield the actual plugin object, which knows its own name
|
18
|
+
@plugins.each { |name, plugin| yield plugin }
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def register_plugin( plugin )
|
23
|
+
raise Linen::Plugin::ArgumentError, "Attempted to register something that is not a Linen::Plugin" unless
|
24
|
+
plugin < Linen::Plugin
|
25
|
+
|
26
|
+
@plugins ||= {}
|
27
|
+
@plugins[ plugin.short_name ] = plugin
|
28
|
+
end
|
29
|
+
alias register register_plugin
|
30
|
+
alias << register_plugin
|
31
|
+
|
32
|
+
|
33
|
+
def size
|
34
|
+
return @plugins.size
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def []( name )
|
39
|
+
return @plugins[ name ]
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def commands
|
44
|
+
if @commands.nil?
|
45
|
+
### create @commands. Each new key will get an empty array as its default value.
|
46
|
+
@commands = IndifferentHash.new
|
47
|
+
|
48
|
+
### populates @commands as a hash of arrays. The hash key is the command name (.to_s) and the value
|
49
|
+
### is an array containing each plugin class in which that command is defined.
|
50
|
+
@plugins.each do |name, plugin|
|
51
|
+
plugin.commands.each do |name, cmd|
|
52
|
+
@commands[ name ] ||= []
|
53
|
+
@commands[ name ] << plugin
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
return @commands
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def find_command( name )
|
63
|
+
return @commands[ name ]
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
##############################################################
|
4
|
+
# Copyright 2007, LAIKA, Inc. #
|
5
|
+
# #
|
6
|
+
# Authors: #
|
7
|
+
# * Ben Bleything <bbleything@laika.com> #
|
8
|
+
##############################################################
|
9
|
+
|
10
|
+
class Linen::Workspace
|
11
|
+
def set_value( name, value )
|
12
|
+
# add getter
|
13
|
+
(class << self ; self ; end).instance_eval {
|
14
|
+
attr_reader name.to_s.intern
|
15
|
+
}
|
16
|
+
|
17
|
+
# add ivar
|
18
|
+
instance_variable_set "@#{name}", value
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
##############################################################
|
4
|
+
# Copyright 2007, LAIKA, Inc. #
|
5
|
+
# #
|
6
|
+
# Authors: #
|
7
|
+
# * Ben Bleything <bbleything@laika.com> #
|
8
|
+
##############################################################
|
9
|
+
|
10
|
+
class String
|
11
|
+
def wrap(line_width = 72)
|
12
|
+
self.gsub(/\n/, "\n\n").gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
data/test/test_cli.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
##############################################################
|
4
|
+
# Copyright 2007, LAIKA, Inc. #
|
5
|
+
# #
|
6
|
+
# Authors: #
|
7
|
+
# * Ben Bleything <bbleything@laika.com> #
|
8
|
+
##############################################################
|
9
|
+
|
10
|
+
require 'test/unit'
|
11
|
+
require 'linen'
|
12
|
+
|
13
|
+
class TestCLICommandCompletion < Test::Unit::TestCase
|
14
|
+
|
15
|
+
################
|
16
|
+
# test plugins #
|
17
|
+
################
|
18
|
+
|
19
|
+
class ::TestPlugin < Linen::Plugin
|
20
|
+
command :add do;end
|
21
|
+
command :administer do;end
|
22
|
+
command :somethingelse do;end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
class ::TeaPlugin < Linen::Plugin
|
27
|
+
command :foo do;end
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
class ::OtherPlugin < Linen::Plugin
|
32
|
+
command :food do;end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
#########
|
37
|
+
# tests #
|
38
|
+
#########
|
39
|
+
|
40
|
+
def test_plugin_completion
|
41
|
+
all_plugins = Linen.plugins.map {|p| p.short_name}.sort
|
42
|
+
|
43
|
+
assert_raises_plugin_ambiguity_error( all_plugins ) do
|
44
|
+
complete ''
|
45
|
+
end
|
46
|
+
|
47
|
+
assert_raises_plugin_ambiguity_error( all_plugins.select {|p| p =~ /^te/} ) do
|
48
|
+
complete 'te'
|
49
|
+
end
|
50
|
+
|
51
|
+
assert_nothing_raised do
|
52
|
+
### strip'ing because completion returns it with a trailing space
|
53
|
+
assert_equal "test", complete( 'tes' ).strip
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def test_command_completion
|
59
|
+
test_commands = Linen.plugins[ 'test' ].commands.keys.map {|k| k.to_s}.sort
|
60
|
+
|
61
|
+
assert_raises_command_ambiguity_error( test_commands ) do
|
62
|
+
complete 'tes '
|
63
|
+
end
|
64
|
+
|
65
|
+
assert_raises_command_ambiguity_error( test_commands ) do
|
66
|
+
complete 'test '
|
67
|
+
end
|
68
|
+
|
69
|
+
starts_with_ad = test_commands.select {|c| c =~ /^ad/}
|
70
|
+
|
71
|
+
assert_raises_command_ambiguity_error( starts_with_ad ) do
|
72
|
+
assert_equal "test ad", complete( 'test ad' )
|
73
|
+
end
|
74
|
+
|
75
|
+
assert_raises_command_ambiguity_error( starts_with_ad ) do
|
76
|
+
assert_equal "test ad", complete( 'tes ad' )
|
77
|
+
end
|
78
|
+
|
79
|
+
assert_nothing_raised do
|
80
|
+
### strip'ing because completion returns it with a trailing space
|
81
|
+
assert_equal "test administer", complete( 'test adm' ).strip
|
82
|
+
assert_equal "test administer", complete( 'tes adm' ).strip
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
#######
|
88
|
+
private
|
89
|
+
#######
|
90
|
+
def complete( str )
|
91
|
+
return Linen::CLI.expand_command( str )
|
92
|
+
end
|
93
|
+
|
94
|
+
def assert_raises_ambiguity_error( exception, candidates, &block )
|
95
|
+
begin
|
96
|
+
block.call
|
97
|
+
rescue exception => e
|
98
|
+
assert_equal candidates, e.candidates
|
99
|
+
else
|
100
|
+
flunk 'no exception raised!'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def assert_raises_plugin_ambiguity_error( candidates, &block )
|
105
|
+
assert_raises_ambiguity_error Linen::CLI::AmbiguousPluginError, candidates, &block
|
106
|
+
end
|
107
|
+
|
108
|
+
def assert_raises_command_ambiguity_error( candidates, &block )
|
109
|
+
assert_raises_ambiguity_error Linen::CLI::AmbiguousCommandError, candidates, &block
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
##############################################################
|
4
|
+
# Copyright 2007, LAIKA, Inc. #
|
5
|
+
# #
|
6
|
+
# Authors: #
|
7
|
+
# * Ben Bleything <bbleything@laika.com> #
|
8
|
+
##############################################################
|
9
|
+
|
10
|
+
require 'test/unit'
|
11
|
+
require 'indifferent_hash'
|
12
|
+
|
13
|
+
class TestIndifferentHash < Test::Unit::TestCase
|
14
|
+
def setup
|
15
|
+
@rhash = {
|
16
|
+
:symbol => 'regular symbol',
|
17
|
+
'string' => 'regular string'
|
18
|
+
}
|
19
|
+
|
20
|
+
@ihash = IndifferentHash.new
|
21
|
+
@ihash[ :symbol ] = 'indifferent symbol'
|
22
|
+
@ihash[ 'string' ] = 'indifferent string'
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def test_string_to_symbol_lookups
|
27
|
+
key = 'symbol'
|
28
|
+
|
29
|
+
assert_nil @rhash[ key ]
|
30
|
+
assert_equal 'indifferent symbol', @ihash[ key ]
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def test_symbol_to_string_lookups
|
35
|
+
key = :string
|
36
|
+
|
37
|
+
assert_nil @rhash[ key ]
|
38
|
+
assert_equal 'indifferent string', @ihash[ key ]
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def test_literal_key_priority
|
43
|
+
ihash = IndifferentHash.new
|
44
|
+
|
45
|
+
ihash[ :key ] = "symbol"
|
46
|
+
ihash[ 'key' ] = "string"
|
47
|
+
|
48
|
+
assert_equal "symbol", ihash[ :key ]
|
49
|
+
assert_equal "string", ihash[ 'key' ]
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
##############################################################
|
4
|
+
# Copyright 2007, LAIKA, Inc. #
|
5
|
+
# #
|
6
|
+
# Authors: #
|
7
|
+
# * Ben Bleything <bbleything@laika.com> #
|
8
|
+
##############################################################
|
9
|
+
|
10
|
+
require 'test/unit'
|
11
|
+
require 'linen'
|
12
|
+
|
13
|
+
### a little helper for later
|
14
|
+
class String
|
15
|
+
def to_regex
|
16
|
+
return /#{self}/
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
class TestPlugins < Test::Unit::TestCase
|
22
|
+
|
23
|
+
########################
|
24
|
+
# "constant" variables #
|
25
|
+
########################
|
26
|
+
|
27
|
+
@description = "This is some descriptions"
|
28
|
+
@help_msg = "This is some help messagesess"
|
29
|
+
class << self
|
30
|
+
attr_reader :description, :help_msg
|
31
|
+
end
|
32
|
+
|
33
|
+
################
|
34
|
+
# test plugins #
|
35
|
+
################
|
36
|
+
|
37
|
+
class ::HelpfulPlugin < Linen::Plugin
|
38
|
+
puts TestPlugins.description
|
39
|
+
description TestPlugins.description
|
40
|
+
|
41
|
+
command :test do
|
42
|
+
help_message TestPlugins.help_msg
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
class ::UnhelpfulPlugin < Linen::Plugin
|
48
|
+
command :test do;end
|
49
|
+
end
|
50
|
+
|
51
|
+
#########
|
52
|
+
# tests #
|
53
|
+
#########
|
54
|
+
|
55
|
+
def test_plugin_help
|
56
|
+
assert HelpfulPlugin.help =~ TestPlugins.description.to_regex
|
57
|
+
assert UnhelpfulPlugin.help =~ /No help for unhelpful/
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def test_command_help
|
62
|
+
assert HelpfulPlugin.commands[ :test ].help =~ TestPlugins.help_msg.to_regex
|
63
|
+
assert UnhelpfulPlugin.commands[ :test ].help =~ /No help for unhelpful test/
|
64
|
+
end
|
65
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.4
|
3
|
+
specification_version: 1
|
4
|
+
name: linen
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.2.0
|
7
|
+
date: 2007-09-10 00:00:00 -07:00
|
8
|
+
summary: Linen - A pluggable command-line interface library
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email:
|
12
|
+
homepage: http://opensource.laika.com
|
13
|
+
rubyforge_project: linen
|
14
|
+
description: Linen is a library which can be used to build a command-line interface for any purpose. It features a plugin architecture to specify new tasks, Readline support, history, and more.
|
15
|
+
autorequire: linen
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- LAIKA, Inc.
|
31
|
+
files:
|
32
|
+
- Rakefile
|
33
|
+
- README
|
34
|
+
- LICENSE
|
35
|
+
- lib/indifferent_hash.rb
|
36
|
+
- lib/linen
|
37
|
+
- lib/linen/cli.rb
|
38
|
+
- lib/linen/exceptions.rb
|
39
|
+
- lib/linen/plugin.rb
|
40
|
+
- lib/linen/plugin_registry.rb
|
41
|
+
- lib/linen/workspace.rb
|
42
|
+
- lib/linen.rb
|
43
|
+
- lib/string_extensions.rb
|
44
|
+
- test/test_cli.rb
|
45
|
+
- test/test_indifferent_hash.rb
|
46
|
+
- test/test_plugins.rb
|
47
|
+
test_files:
|
48
|
+
- test/test_cli.rb
|
49
|
+
- test/test_indifferent_hash.rb
|
50
|
+
- test/test_plugins.rb
|
51
|
+
rdoc_options: []
|
52
|
+
|
53
|
+
extra_rdoc_files: []
|
54
|
+
|
55
|
+
executables: []
|
56
|
+
|
57
|
+
extensions: []
|
58
|
+
|
59
|
+
requirements: []
|
60
|
+
|
61
|
+
dependencies: []
|
62
|
+
|