oyster 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|