oyster 0.9.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/History.txt +6 -0
- data/Manifest.txt +17 -0
- data/README.txt +214 -0
- data/Rakefile +11 -0
- data/lib/oyster.rb +40 -0
- data/lib/oyster/option.rb +43 -0
- data/lib/oyster/options/array.rb +20 -0
- data/lib/oyster/options/file.rb +18 -0
- data/lib/oyster/options/flag.rb +23 -0
- data/lib/oyster/options/float.rb +14 -0
- data/lib/oyster/options/glob.rb +18 -0
- data/lib/oyster/options/integer.rb +14 -0
- data/lib/oyster/options/shortcut.rb +20 -0
- data/lib/oyster/options/string.rb +18 -0
- data/lib/oyster/options/subcommand.rb +21 -0
- data/lib/oyster/specification.rb +170 -0
- data/test/test_oyster.rb +195 -0
- metadata +82 -0
data/History.txt
ADDED
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
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,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,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,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
|
+
|
data/test/test_oyster.rb
ADDED
@@ -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
|