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