linen 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,2 @@
1
+ Linen is copyright (c) 2007, LAIKA Inc. Linen is proprietary software
2
+ and is not available for use outside LAIKA.
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
@@ -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
+