oyster 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2008-07-09
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,17 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/oyster.rb
6
+ lib/oyster/specification.rb
7
+ lib/oyster/option.rb
8
+ lib/oyster/options/flag.rb
9
+ lib/oyster/options/string.rb
10
+ lib/oyster/options/integer.rb
11
+ lib/oyster/options/float.rb
12
+ lib/oyster/options/file.rb
13
+ lib/oyster/options/array.rb
14
+ lib/oyster/options/glob.rb
15
+ lib/oyster/options/shortcut.rb
16
+ lib/oyster/options/subcommand.rb
17
+ test/test_oyster.rb
data/README.txt ADDED
@@ -0,0 +1,214 @@
1
+ = Oyster
2
+
3
+ http://github.com/jcoglan/oyster
4
+
5
+ === Description
6
+
7
+ Oyster is a command-line input parser that doesn't hate you. It provides a simple
8
+ API that you use to write a spec for the user interface to your program, and it
9
+ handles mapping the input to a hash for you. It supports both long and short option
10
+ names, subcommands, and various types of input data.
11
+
12
+ === Features
13
+
14
+ * Parses command line options into a hash for easy access
15
+ * Supports long (--example) and short (-e) names, including compound (-zxvf) short options
16
+ * Supports subcommand recognition
17
+ * Can parse options as booleans, strings, arrays, files, globs
18
+ * Automatically handles single-letter shortcuts for option names
19
+ * Allows shortcuts to be specified for common groupings of flags
20
+ * Is easily extensible to support custom input types
21
+ * Automatically outputs man-page-style help for your program
22
+
23
+ === Usage
24
+
25
+ You begin your command-line script by writing a spec for its options, layed out
26
+ like a Unix manual page. This spec will be used to parse input and to generate
27
+ help text using the --help flag. This example demonstrates a wide range of the
28
+ spec API. You can use as much or as little of it as you like, none of the fields
29
+ are required.
30
+
31
+ require 'rubygems'
32
+ require 'oyster'
33
+
34
+ spec = Oyster.spec do
35
+ name 'myprog -- something to move files around'
36
+
37
+ synopsis <<-EOS
38
+ myprog [options] --sources SCR --dest DEST
39
+ myprog [options] --sources SRC --exec SCRIPT
40
+ EOS
41
+
42
+ description <<-EOS
43
+ myprog is a command-line utility for moving files around or executing
44
+ scripts against them. It can be invoked from any directory.
45
+ EOS
46
+
47
+ flag :verbose, :default => false,
48
+ :desc => 'Print verbose output'
49
+
50
+ flag :recurse, :default => true,
51
+ :desc => 'Enter directories recursively'
52
+
53
+ string :type, :default => 'f',
54
+ :desc => 'Which type of files to move'
55
+
56
+ integer :status, :default => 200,
57
+ :desc => 'Tell the program the status code to return'
58
+
59
+ float :quality, :default => 0.5,
60
+ :desc => 'Level of compression loss incurred when copying'
61
+
62
+ glob :files, :desc => <<-EOS
63
+ Pattern for selecting which files to move. For example, to select all the
64
+ JavaScript files, you might use:
65
+
66
+ --files ./*.js (this directory)
67
+ --files **/*.js (search recursively)
68
+ EOS
69
+
70
+ array :sources, :desc => 'List of files to move'
71
+
72
+ string :dest, :desc => 'Location of directory to move to'
73
+
74
+ file :exec, :desc => 'File to read script from'
75
+
76
+ notes <<-EOS
77
+ This program may make destructive changes to your files. Make
78
+ sure you have a full backup before running any dangerous scripts.
79
+ EOS
80
+
81
+ author 'James Coglan <jcoglan@nospam.com>'
82
+
83
+ copyright <<-EOS
84
+ (c) 2008 James Coglan. This program is free software, distributed under
85
+ the MIT license. You are free to use it for whatever purpose you see fit.
86
+ EOS
87
+ end
88
+
89
+ Having defined your spec, you can use it to parse user input. Input is specified
90
+ as an array of string tokens, and defaults to ARGV. If the program is invoked using
91
+ --help, Oyster will throw a <tt>Oyster::HelpRendered</tt> exception that you can
92
+ use to halt your program if necessary. An example taking input from the command
93
+ line:
94
+
95
+ begin; opts = spec.parse
96
+ rescue Oyster::HelpRendered; exit
97
+ end
98
+
99
+ <tt>spec.parse</tt> will return a <tt>Hash</tt> containing the values of the options
100
+ as specified by the user. For example:
101
+
102
+ Input: --verbose
103
+ Output: opts[:verbose] == true
104
+
105
+ Input: --no-recurse
106
+ Oupput: opts[:recurse] == false
107
+
108
+ Input: --dest /path/to/mydir
109
+ Output: opts[:dest] == '/path/to/mydir'
110
+
111
+ Input: -q 0.7
112
+ Output: opts[:quality] == 0.7
113
+
114
+ Input: --sources foo bar baz -d somewhere
115
+ Output: opts[:sources] == ['foo', 'bar', 'baz']
116
+ opts[:dest] == 'somewhere'
117
+
118
+ Options specified as +file+ options will take the input and read the contents of
119
+ the specified file. Use this option if you want to take input from files without
120
+ knowing the name of the file itself:
121
+
122
+ Input: --exec myscript.sh
123
+ Output: opts[:exec] == '(contents of myscript.sh)'
124
+
125
+ If you have a +glob+ option, it will expand its input using <tt>Dir.glob</tt>.
126
+ You must quote your input for this to work, otherwise the shell will expand the
127
+ glob before handing it to the Ruby interpreter.
128
+
129
+ Input: -f **/*.rb
130
+ Output: ARGV == ['-f', 'foo.rb', 'bar.rb']
131
+ -- Oyster will call Dir.glob('foo.rb')
132
+ opts[:files] == ['foo.rb']
133
+
134
+ Input: -f '**/*.rb'
135
+ Output: ARGV == ['-f', '**/*.rb']
136
+ -- Oyster will call Dir.glob('**/*.rb')
137
+ opts[:files] == ['foo.rb', 'bar.rb', 'dir/baz.rb', ...]
138
+
139
+ === Unclaimed input
140
+
141
+ Any input tokens not absorbed by one of the option flags will be written to an
142
+ array in <tt>opts[:unclaimed]</tt>:
143
+
144
+ Input: -s foo.rb bar.rb -d /path/to/dir some_arg
145
+ Output: opts[:sources] == ['foo.rb', 'bar.rb']
146
+ opts[:dest] == '/path/to/dir'
147
+ opts[:unclaimed] == ['some_arg']
148
+
149
+ === Subcommands
150
+
151
+ You can easily create subcommands by nesting specs inside the main one:
152
+
153
+ # Main program spec
154
+ spec = Oyster.spec do
155
+ # Front matter
156
+ name 'someprog'
157
+
158
+ # Options
159
+ flag :verbose, :default => true
160
+
161
+ # Subcommand 'add'
162
+ subcommand :add do
163
+ name 'someprog-add'
164
+ flag :force, :default => false
165
+ end
166
+ end
167
+
168
+ Subcommand options are stored as a hash inside the main options hash:
169
+
170
+ Input: --no-verbose
171
+ Output: opts == {:verbose => false}
172
+
173
+ Input: -v add -f
174
+ Output: opts == {:verbose => true, :add => {:force => true}}
175
+
176
+ Input: add --help
177
+ Output: prints help for 'add' command only
178
+
179
+ Beware that you cannot give a subcommand the same name as an option flag,
180
+ otherwise you'll get a name collision in the output.
181
+
182
+ === Requirements
183
+
184
+ * Rubygems
185
+ * Oyster gem
186
+
187
+ === Installation
188
+
189
+ sudo gem install oyster
190
+
191
+ === License
192
+
193
+ (The MIT License)
194
+
195
+ Copyright (c) 2008 James Coglan
196
+
197
+ Permission is hereby granted, free of charge, to any person obtaining
198
+ a copy of this software and associated documentation files (the
199
+ 'Software'), to deal in the Software without restriction, including
200
+ without limitation the rights to use, copy, modify, merge, publish,
201
+ distribute, sublicense, and/or sell copies of the Software, and to
202
+ permit persons to whom the Software is furnished to do so, subject to
203
+ the following conditions:
204
+
205
+ The above copyright notice and this permission notice shall be
206
+ included in all copies or substantial portions of the Software.
207
+
208
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
209
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
210
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
211
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
212
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
213
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
214
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/oyster.rb'
6
+
7
+ Hoe.new('oyster', Oyster::VERSION) do |p|
8
+ p.developer('James Coglan', 'jcoglan@googlemail.com')
9
+ end
10
+
11
+ # vim: syntax=Ruby
data/lib/oyster.rb ADDED
@@ -0,0 +1,40 @@
1
+ module Oyster
2
+ VERSION = '0.9.0'
3
+
4
+ LONG_NAME = /^--([a-z\[][a-z0-9\]\-]+)$/i
5
+ SHORT_NAME = /^-([a-z0-9]+)$/i
6
+
7
+ HELP_INDENT = 7
8
+ HELP_WIDTH = 72
9
+
10
+ WINDOWS = RUBY_PLATFORM.split('-').any? { |part| part =~ /mswin\d*/i }
11
+
12
+ class HelpRendered < StandardError; end
13
+
14
+ def self.spec(*args, &block)
15
+ spec = Specification.new
16
+ spec.instance_eval(&block)
17
+ spec.flag(:help, :default => false, :desc => 'Displays this help message') unless spec.has_option?(:help)
18
+ spec
19
+ end
20
+
21
+ def self.is_name?(string)
22
+ !string.nil? and !!(string =~ LONG_NAME || string =~ SHORT_NAME || string == '--')
23
+ end
24
+ end
25
+
26
+ [ 'specification',
27
+ 'option',
28
+ 'options/flag',
29
+ 'options/string',
30
+ 'options/integer',
31
+ 'options/float',
32
+ 'options/file',
33
+ 'options/array',
34
+ 'options/glob',
35
+ 'options/shortcut',
36
+ 'options/subcommand'
37
+ ].each do |file|
38
+ require File.dirname(__FILE__) + '/oyster/' + file
39
+ end
40
+
@@ -0,0 +1,43 @@
1
+ module Oyster
2
+ class Option
3
+
4
+ def self.create(type, *args)
5
+ name = type.to_s.sub(/^(.)/) { |m| m.upcase } + 'Option'
6
+ klass = Oyster.const_get(name)
7
+ klass.new(*args)
8
+ end
9
+
10
+ attr_reader :description
11
+
12
+ def initialize(name, options = {})
13
+ @names = [name.to_sym]
14
+ @description = options[:desc] || ''
15
+ @settings = options
16
+ end
17
+
18
+ def has_name?(name)
19
+ name && @names.include?(name.to_sym)
20
+ end
21
+
22
+ def name
23
+ @names.first
24
+ end
25
+
26
+ def alternate(name)
27
+ @names << name.to_sym unless has_name?(name) || name.nil?
28
+ end
29
+
30
+ def consume(list); end
31
+
32
+ def default_value(value = nil)
33
+ @settings[:default].nil? ? value : @settings[:default]
34
+ end
35
+
36
+ def help_names
37
+ @names.map { |name| name.to_s }.sort.map {
38
+ |name| (name.size > 1 ? '--' : '-') + name }
39
+ end
40
+
41
+ end
42
+ end
43
+
@@ -0,0 +1,20 @@
1
+ module Oyster
2
+ class ArrayOption < Option
3
+
4
+ def consume(list)
5
+ data = []
6
+ data << list.shift while list.first and !Oyster.is_name?(list.first)
7
+ data
8
+ end
9
+
10
+ def default_value
11
+ super([])
12
+ end
13
+
14
+ def help_names
15
+ super.map { |name| name + ' ARG1 [ARG2 [...]]' }
16
+ end
17
+
18
+ end
19
+ end
20
+
@@ -0,0 +1,18 @@
1
+ module Oyster
2
+ class FileOption < Option
3
+
4
+ def consume(list)
5
+ File.read(list.shift)
6
+ end
7
+
8
+ def default_value
9
+ super(nil)
10
+ end
11
+
12
+ def help_names
13
+ super.map { |name| name + ' ARG' }
14
+ end
15
+
16
+ end
17
+ end
18
+
@@ -0,0 +1,23 @@
1
+ module Oyster
2
+ class FlagOption < Option
3
+
4
+ def consume(list)
5
+ end
6
+
7
+ def default_value
8
+ super(false)
9
+ end
10
+
11
+ def help_names
12
+ default_value ?
13
+ super.map { |name| name.sub(/^--/, '--[no-]') } :
14
+ super
15
+ end
16
+
17
+ def description
18
+ super + (default_value ? ' (This is the default)' : '')
19
+ end
20
+
21
+ end
22
+ end
23
+
@@ -0,0 +1,14 @@
1
+ module Oyster
2
+ class FloatOption < StringOption
3
+
4
+ def consume(list)
5
+ super.to_f
6
+ end
7
+
8
+ def default_value
9
+ super(0)
10
+ end
11
+
12
+ end
13
+ end
14
+
@@ -0,0 +1,18 @@
1
+ module Oyster
2
+ class GlobOption < Option
3
+
4
+ def consume(list)
5
+ Dir.glob(list.shift)
6
+ end
7
+
8
+ def default_value
9
+ super([])
10
+ end
11
+
12
+ def help_names
13
+ super.map { |name| name + ' ARG' }
14
+ end
15
+
16
+ end
17
+ end
18
+
@@ -0,0 +1,14 @@
1
+ module Oyster
2
+ class IntegerOption < StringOption
3
+
4
+ def consume(list)
5
+ super.to_i
6
+ end
7
+
8
+ def default_value
9
+ super(0)
10
+ end
11
+
12
+ end
13
+ end
14
+
@@ -0,0 +1,20 @@
1
+ module Oyster
2
+ class ShortcutOption < Option
3
+
4
+ def initialize(name, expansion, options = {})
5
+ super(name, options)
6
+ @expansion = expansion.split(/\s+/).reverse
7
+ end
8
+
9
+ def consume(list)
10
+ @expansion.each { |e| list.unshift(e) }
11
+ nil
12
+ end
13
+
14
+ def description
15
+ "Same as '#{@expansion.reverse.join(' ')}'"
16
+ end
17
+
18
+ end
19
+ end
20
+
@@ -0,0 +1,18 @@
1
+ module Oyster
2
+ class StringOption < Option
3
+
4
+ def consume(list)
5
+ list.shift
6
+ end
7
+
8
+ def default_value(value = nil)
9
+ super(value || nil)
10
+ end
11
+
12
+ def help_names
13
+ super.map { |name| name + ' ARG' }
14
+ end
15
+
16
+ end
17
+ end
18
+
@@ -0,0 +1,21 @@
1
+ module Oyster
2
+ class SubcommandOption < Option
3
+
4
+ def initialize(name, spec)
5
+ super(name)
6
+ @spec = spec
7
+ end
8
+
9
+ def consume(list)
10
+ output = @spec.parse(list)
11
+ list.clear
12
+ output
13
+ end
14
+
15
+ def parse(*args)
16
+ @spec.parse(*args)
17
+ end
18
+
19
+ end
20
+ end
21
+
@@ -0,0 +1,170 @@
1
+ module Oyster
2
+ class Specification
3
+
4
+ include Enumerable
5
+
6
+ def initialize
7
+ @options = []
8
+ @subcommands = []
9
+ @data = {}
10
+ end
11
+
12
+ def each(&block)
13
+ @options.sort_by { |o| o.name.to_s }.each(&block)
14
+ end
15
+
16
+ def method_missing(*args)
17
+ opt = Option.create(*args)
18
+ raise "Option name '#{opt.name}' is already used" if has_option?(opt.name)
19
+ opt.alternate(shorthand_for(opt.name))
20
+ @options << opt
21
+ rescue
22
+ name, value = args[0..1]
23
+ @data[name.to_sym] = value.to_s
24
+ end
25
+
26
+ def subcommand(name, &block)
27
+ opt = SubcommandOption.new(name, Oyster.spec(&block))
28
+ raise "Subcommand name '#{opt.name}' is already used" if has_command?(name)
29
+ @subcommands << opt
30
+ end
31
+
32
+ def has_option?(name)
33
+ !self[name].nil?
34
+ end
35
+
36
+ def has_command?(name)
37
+ !command(name).nil?
38
+ end
39
+
40
+ def command(name)
41
+ @subcommands.each do |command|
42
+ return command if command.has_name?(name)
43
+ end
44
+ nil
45
+ end
46
+
47
+ def parse(input = ARGV)
48
+ input = input.dup
49
+ output = {:unclaimed => []}
50
+
51
+ while token = input.shift
52
+ if token == '--'
53
+ output[:unclaimed] = output[:unclaimed] + input
54
+ break
55
+ end
56
+
57
+ option = command(token)
58
+
59
+ long, short = token.scan(LONG_NAME), token.scan(SHORT_NAME)
60
+ long, short = [long, short].map { |s| s.flatten.first }
61
+
62
+ input = short.scan(/./).map { |s| "-#{s}" } + input and next if short and short.size > 1
63
+
64
+ negative = !!(long && long =~ /^no-/)
65
+ long.sub!(/^no-/, '') if negative
66
+
67
+ option ||= self[long] || self[short]
68
+ output[:unclaimed] << token and next unless option
69
+
70
+ output[option.name] = option.is_a?(FlagOption) ? !negative : option.consume(input)
71
+ end
72
+
73
+ @options.each do |option|
74
+ next unless output[option.name].nil?
75
+ output[option.name] ||= option.default_value
76
+ end
77
+
78
+ help and raise HelpRendered if output[:help]
79
+ output
80
+ end
81
+
82
+ private
83
+
84
+ def [](name)
85
+ @options.each do |opt|
86
+ return opt if opt.has_name?(name)
87
+ end
88
+ nil
89
+ end
90
+
91
+ def shorthand_for(name)
92
+ initial = name.to_s.scan(/^./).first.downcase
93
+ initial.upcase! if has_option?(initial)
94
+ return nil if has_option?(initial)
95
+ initial
96
+ end
97
+
98
+ def help
99
+ display(@data[:name], 1, 'NAME')
100
+ display(@data[:synopsis], 1, 'SYNOPSIS', false, true)
101
+ display(@data[:description], 1, 'DESCRIPTION')
102
+ puts "\n#{ bold }OPTIONS#{ normal }"
103
+ each do |option|
104
+ display(option.help_names.join(', '), 1, nil, false, true)
105
+ display(option.description, 2)
106
+ puts "\n"
107
+ end
108
+ display(@data[:notes], 1, 'NOTES')
109
+ display(@data[:author], 1, 'AUTHOR')
110
+ display(@data[:copyright], 1, 'COPYRIGHT')
111
+ self
112
+ end
113
+
114
+ def display(text, level = 1, title = nil, join = true, man = false)
115
+ return unless text
116
+ puts "\n" + format("#{ bold }#{ title }#{ normal }", level - 1) if title
117
+ text = man_format(text) if man
118
+ puts format(text, level, join)
119
+ end
120
+
121
+ def format(text, level = 1, join = true)
122
+ lines = text.split(/\n/)
123
+ outdent = lines.inject(1000) { |n,s| [s.scan(/^\s*/).first.size, n].min }
124
+ indent = level * HELP_INDENT
125
+ width = HELP_WIDTH - indent
126
+
127
+ lines.map { |line|
128
+ line.sub(/\s*$/, '').sub(%r{^\s{#{outdent}}}, '')
129
+ }.inject(['']) { |groups, line|
130
+ groups << '' if line.empty? && !groups.last.empty?
131
+ buffer = groups.last
132
+ buffer << (line =~ /^\s+/ || !join ? "\n" : " ") unless buffer.empty?
133
+ buffer << line
134
+ groups
135
+ }.map { |buffer|
136
+ lines = (buffer =~ /\n/) ?
137
+ buffer.split(/\n/) :
138
+ buffer.scan(%r{((?:.(?:\e\[\dm)*){1,#{width}}\S*)\s*}).flatten
139
+ lines.map { |l| (' ' * indent) + l }.join("\n")
140
+ }.join("\n\n")
141
+ end
142
+
143
+ def man_format(text)
144
+ text.split(/\n/).map { |line|
145
+ " #{line}".scan(/(.+?)([a-z0-9\-\_]*)/i).flatten.map { |token|
146
+ formatter = case true
147
+ when Oyster.is_name?(token) : bold
148
+ when token =~ /[A-Z]/ && token.upcase == token : underline
149
+ when token =~ /[a-z]/ && token.downcase == token : bold
150
+ end
151
+ formatter ? "#{ formatter }#{ token }#{ normal }" : token
152
+ }.join('')
153
+ }.join("\n")
154
+ end
155
+
156
+ def bold
157
+ WINDOWS ? "" : "\e[1m"
158
+ end
159
+
160
+ def underline
161
+ WINDOWS ? "" : "\e[4m"
162
+ end
163
+
164
+ def normal
165
+ WINDOWS ? "" : "\e[0m"
166
+ end
167
+
168
+ end
169
+ end
170
+
@@ -0,0 +1,195 @@
1
+ require 'test/unit'
2
+ require 'oyster'
3
+
4
+ class OysterTest < Test::Unit::TestCase
5
+
6
+ def setup
7
+ @spec = Oyster.spec do
8
+ name 'oyster'
9
+ synopsis <<-EOS
10
+ oyster [OPTIONS] filename
11
+ oyster [-e PATTERN] file1 [, file2 [, file3 [, ...]]]
12
+ EOS
13
+ description <<-EOS
14
+ Oyster is a Ruby command-line option parser that doesn't hate you. It lets
15
+ you specify options using a simple DSL and parses user input into a hash to
16
+ match the options your program accepts.
17
+
18
+ class Foo < Option
19
+ def consume(list); end
20
+ end
21
+
22
+ Nothing to see here.
23
+ EOS
24
+
25
+ flag :verbose, :default => true, :desc => 'Print verbose output'
26
+ flag :all, :default => false, :desc => 'Include all files?'
27
+
28
+ shortcut :woop, '--verbose --all --files'
29
+
30
+ string :k, :default => 'Its short', :desc => 'Just a little string'
31
+
32
+ string :user
33
+
34
+ string :binary, :default => 'ruby', :desc => <<-EOS
35
+ Which binary to use. You can change the executable used to format the output
36
+ of this command, setting it to your scripting language of choice. This is just
37
+ a lot of text to make sure help formatting works.
38
+ EOS
39
+
40
+ integer :status, :default => 200,
41
+ :desc => 'Tell the program the status code to return'
42
+
43
+ float :quality, :default => 0.5,
44
+ :desc => 'Level of compression loss incurred when copying'
45
+
46
+ array :files, :desc => 'The files you want to process'
47
+
48
+ file :path, :desc => 'Path to read program input from'
49
+
50
+ notes <<-EOS
51
+ This program is free software, distributed under the MIT license.
52
+ EOS
53
+
54
+ author 'James Coglan <jcoglan@googlemail.com>'
55
+
56
+ subcommand :add do
57
+ name 'oyster-add'
58
+ synopsis <<-EOS
59
+ oyster add [--squash] file1 [file2 [...]]
60
+ EOS
61
+ description <<-EOS
62
+ oyster-add is a subcommand of oyster. This text is here
63
+ to test subcommand recognition so it probably doesn't
64
+ matter much what it says, as long it's long enough to
65
+ wrap to a few lines and it lets us tell commands apart.
66
+ EOS
67
+
68
+ glob :filelist
69
+
70
+ flag :squash, :desc => 'Squashes all the files into one string'
71
+
72
+ subcommand :nothing do
73
+ flag :something
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def test_help
80
+ @spec.parse %w(--help)
81
+ rescue Oyster::HelpRendered
82
+ end
83
+
84
+ def test_dash_length
85
+ opts = @spec.parse %w(-user me)
86
+ assert_equal '-s', opts[:user]
87
+ opts = @spec.parse %w(--u me)
88
+ assert_equal nil, opts[:user]
89
+ end
90
+
91
+ def test_flags
92
+ opts = @spec.parse %w(myfile.txt)
93
+ assert_equal true, opts[:verbose]
94
+ opts = @spec.parse %w(myfile.txt --verbose)
95
+ assert_equal true, opts[:verbose]
96
+ assert_equal 'myfile.txt', opts[:unclaimed].first
97
+ opts = @spec.parse %w(myfile.text -v)
98
+ assert_equal true, opts[:verbose]
99
+ opts = @spec.parse %w(--no-verbose)
100
+ assert_equal false, opts[:verbose]
101
+
102
+ assert_equal false, opts[:help]
103
+ end
104
+
105
+ def test_shorthand_flags
106
+ opts = @spec.parse %w(something -vau jcoglan)
107
+ assert_equal true, opts[:verbose]
108
+ assert_equal true, opts[:all]
109
+ assert_equal 'jcoglan', opts[:user]
110
+ end
111
+
112
+ def test_shortcuts
113
+ opts = @spec.parse %w(-u little-old-me --woop help.txt cmd.rb)
114
+ assert_equal 'little-old-me', opts[:user]
115
+ assert_equal true, opts[:verbose]
116
+ assert_equal true, opts[:all]
117
+ assert_equal 'help.txt, cmd.rb', opts[:files].join(', ')
118
+ end
119
+
120
+ def test_strings
121
+ opts = @spec.parse %w(-v --user jcoglan something)
122
+ assert_equal 'jcoglan', opts[:user]
123
+ assert_equal 'something', opts[:unclaimed].first
124
+ opts = @spec.parse %w(-v)
125
+ assert_equal nil, opts[:user]
126
+ opts = @spec.parse ['-u', "My name is"]
127
+ assert_equal 'My name is', opts[:user]
128
+
129
+ opts = @spec.parse %w(-b)
130
+ assert_equal 'ruby', opts[:binary]
131
+ opts = @spec.parse %w(--binary some_other_prog)
132
+ assert_equal 'some_other_prog', opts[:binary]
133
+ end
134
+
135
+ def test_single_letter_option
136
+ opts = @spec.parse %w(-k so-whats-up)
137
+ assert_equal 'so-whats-up', opts[:k]
138
+ end
139
+
140
+ def test_string_with_flag
141
+ opts = @spec.parse %w(the first --user is -v)
142
+ assert_equal 'the, first', opts[:unclaimed].join(', ')
143
+ assert_equal 'is', opts[:user]
144
+ assert_equal true, opts[:verbose]
145
+ end
146
+
147
+ def test_numerics
148
+ opts = @spec.parse %w(-q 0.99 --status 20.4)
149
+ assert_equal 0.99, opts[:quality]
150
+ assert Float === opts[:quality]
151
+ assert_equal 20, opts[:status]
152
+ assert Integer === opts[:status]
153
+ end
154
+
155
+ def test_array
156
+ opts = @spec.parse %w(--files foo bar baz -u jcoglan)
157
+ assert_equal 'foo, bar, baz', opts[:files].join(', ')
158
+ assert_equal 'jcoglan', opts[:user]
159
+ opts = @spec.parse %w(--files foo bar baz)
160
+ assert_equal 'foo, bar, baz', opts[:files].join(', ')
161
+ end
162
+
163
+ def test_stop_parsing
164
+ opts = @spec.parse %w(--files something.txt my.rb -- some more args)
165
+ assert_equal 'something.txt, my.rb', opts[:files].join(', ')
166
+ assert_equal 'some more args', opts[:unclaimed].join(' ')
167
+ end
168
+
169
+ def test_globs
170
+ opts = @spec.parse %w(add --filelist ./*.txt)
171
+ assert_equal './History.txt, ./Manifest.txt, ./README.txt', opts[:add][:filelist].sort.join(', ')
172
+ end
173
+
174
+ def test_subcommands
175
+ opts = @spec.parse %w(-v add --help)
176
+ rescue Oyster::HelpRendered
177
+ opts = @spec.parse %w(-v --user someguy thingy add -s arg1 arg2)
178
+ assert_equal true, opts[:verbose]
179
+ assert_equal 'someguy', opts[:user]
180
+ assert_equal 'thingy', opts[:unclaimed].join(', ')
181
+ assert_equal true, opts[:add][:squash]
182
+ assert_equal 'arg1, arg2', opts[:add][:unclaimed].join(', ')
183
+ opts = @spec.parse %w(-v add nothing -s)
184
+ assert_equal true, opts[:verbose]
185
+ assert_equal false, opts[:add][:squash]
186
+ assert_equal true, opts[:add][:nothing][:something]
187
+ end
188
+
189
+ def test_file
190
+ opts = @spec.parse %w(--path Rakefile)
191
+ assert opts[:path] =~ /Oyster::VERSION/
192
+ end
193
+
194
+ end
195
+
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oyster
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - James Coglan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-09-11 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.7.0
24
+ version:
25
+ description: Oyster is a command-line input parser that doesn't hate you. It provides a simple API that you use to write a spec for the user interface to your program, and it handles mapping the input to a hash for you. It supports both long and short option names, subcommands, and various types of input data.
26
+ email:
27
+ - jcoglan@googlemail.com
28
+ executables: []
29
+
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - History.txt
34
+ - Manifest.txt
35
+ - README.txt
36
+ files:
37
+ - History.txt
38
+ - Manifest.txt
39
+ - README.txt
40
+ - Rakefile
41
+ - lib/oyster.rb
42
+ - lib/oyster/specification.rb
43
+ - lib/oyster/option.rb
44
+ - lib/oyster/options/flag.rb
45
+ - lib/oyster/options/string.rb
46
+ - lib/oyster/options/integer.rb
47
+ - lib/oyster/options/float.rb
48
+ - lib/oyster/options/file.rb
49
+ - lib/oyster/options/array.rb
50
+ - lib/oyster/options/glob.rb
51
+ - lib/oyster/options/shortcut.rb
52
+ - lib/oyster/options/subcommand.rb
53
+ - test/test_oyster.rb
54
+ has_rdoc: true
55
+ homepage: http://github.com/jcoglan/oyster
56
+ post_install_message:
57
+ rdoc_options:
58
+ - --main
59
+ - README.txt
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ version:
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: "0"
73
+ version:
74
+ requirements: []
75
+
76
+ rubyforge_project: oyster
77
+ rubygems_version: 1.2.0
78
+ signing_key:
79
+ specification_version: 2
80
+ summary: Oyster is a command-line input parser that doesn't hate you
81
+ test_files:
82
+ - test/test_oyster.rb