linen 0.2.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/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
|
+
|