firebase-ruby 0.2.0.1 → 0.3.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.
- checksums.yaml +5 -5
- data/.codeclimate.yml +1 -1
- data/README.md +1 -1
- data/bin/fbrb +23 -18
- data/firebase-ruby.gemspec +3 -3
- data/lib/firebase-ruby.rb +0 -1
- data/lib/firebase-ruby/auth.rb +19 -9
- data/lib/firebase-ruby/database.rb +4 -5
- data/lib/firebase-ruby/{http.rb → neko-http.rb} +84 -37
- data/lib/firebase-ruby/{trollop.rb → optimist.rb} +380 -277
- data/lib/firebase-ruby/version.rb +1 -1
- metadata +12 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c81422955349a239c73df7add0f495be525dee6df19fb99c07fcc7a97ad5ea9b
|
4
|
+
data.tar.gz: c28b48f45899cc15a405baf006addb187eb80379d144522f9412ec5c3244daac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1c81dbcaef8b12f675a85d07488c6979161d2a2bd3bdada1e5e3fd69a6b38745a396f44c80bda7e88a00b310ee7febde6f300025378c8614d4f3764b284bcf3f
|
7
|
+
data.tar.gz: 267c6bdbef1be804ddcccd6c20d534fa6f0a874751fa0479c240079933da0d9ea4cc0a22931fae3ebfb12551f6ff075f8849a0e2627ef56e150afca770372663
|
data/.codeclimate.yml
CHANGED
data/README.md
CHANGED
@@ -57,4 +57,4 @@ db.delete('/users/jack/name/last')
|
|
57
57
|
|
58
58
|
Using the given credentials, `firebase-ruby` will automatically retrieve the [access token](https://firebase.google.com/docs/reference/rest/database/user-auth) from Google's server. The token is valid for 1 hour but a new token will be fetched right before it expires.
|
59
59
|
|
60
|
-
`firebase-ruby` keeps the Google OAuth 2.0 process a black box. But for more details, see the document on Google Developers which the process is based on: [Using OAuth 2.0 for Server to Server Applications](https://developers.google.com/identity/protocols/
|
60
|
+
`firebase-ruby` keeps the Google OAuth 2.0 process a black box. But for more details, see the document on Google Developers which the process is based on: [Using OAuth 2.0 for Server to Server Applications](https://developers.google.com/identity/protocols/oauth2/service-account).
|
data/bin/fbrb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
require 'firebase-ruby/
|
2
|
+
require 'firebase-ruby/optimist'
|
3
3
|
require 'firebase-ruby'
|
4
4
|
require 'json'
|
5
5
|
|
6
6
|
|
7
|
-
opts =
|
7
|
+
opts = Optimist::options do
|
8
8
|
banner "fbrb [options] <URL>"
|
9
9
|
opt :data, 'HTTP POST data', type: :string
|
10
10
|
opt :id, 'Project ID', type: :string
|
@@ -32,29 +32,34 @@ log.debug("Command line arguments: #{opts}")
|
|
32
32
|
path = opts[:path]
|
33
33
|
path ||= ARGV.shift
|
34
34
|
|
35
|
-
|
35
|
+
Optimist::die :path, "is missing" if path.nil?
|
36
36
|
|
37
37
|
db = Firebase::Database.new()
|
38
38
|
db.set_auth_with_key(path: opts[:key])
|
39
39
|
|
40
40
|
method = opts[:request].downcase.to_sym
|
41
41
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
42
|
+
begin
|
43
|
+
case method
|
44
|
+
when :get, :delete
|
45
|
+
data = db.public_send(method, path)
|
46
|
+
when :put, :patch, :post
|
47
|
+
if opts[:data_given]
|
48
|
+
data = db.public_send(method, path, opts[:data])
|
49
|
+
else
|
50
|
+
Optimist::die :data, "is missing"
|
51
|
+
end
|
50
52
|
end
|
51
|
-
end
|
52
53
|
|
53
|
-
if data
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
54
|
+
if data
|
55
|
+
if opts[:ruby]
|
56
|
+
puts data
|
57
|
+
else
|
58
|
+
json_opts = {indent: ' ', space: ' ', object_nl: "\n", array_nl: "\n"}
|
59
|
+
puts JSON.fast_generate(data, json_opts)
|
60
|
+
end
|
59
61
|
end
|
62
|
+
rescue => e
|
63
|
+
puts e.message
|
64
|
+
exit 1
|
60
65
|
end
|
data/firebase-ruby.gemspec
CHANGED
@@ -7,8 +7,8 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.version = Firebase::Version
|
8
8
|
s.authors = ['Ken J.']
|
9
9
|
s.email = ['kenjij@gmail.com']
|
10
|
-
s.summary = %q{Pure Ruby Firebase REST library}
|
11
|
-
s.description = %q{
|
10
|
+
s.summary = %q{Pure simple Ruby based Firebase REST library}
|
11
|
+
s.description = %q{Firebase REST library written in pure Ruby without external dependancy.}
|
12
12
|
s.homepage = 'https://github.com/kenjij/firebase-ruby'
|
13
13
|
s.license = 'MIT'
|
14
14
|
|
@@ -17,5 +17,5 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.require_paths = ['lib']
|
18
18
|
|
19
19
|
s.required_ruby_version = '>= 2.0'
|
20
|
-
s.add_runtime_dependency 'jwt', '~>
|
20
|
+
s.add_runtime_dependency 'jwt', '~> 2.2'
|
21
21
|
end
|
data/lib/firebase-ruby.rb
CHANGED
data/lib/firebase-ruby/auth.rb
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
require 'jwt'
|
2
|
+
require 'firebase-ruby/neko-http'
|
2
3
|
|
3
|
-
module Firebase
|
4
4
|
|
5
|
+
module Firebase
|
5
6
|
class Auth
|
6
|
-
|
7
7
|
GOOGLE_JWT_SCOPE = 'https://www.googleapis.com/auth/firebase.database https://www.googleapis.com/auth/userinfo.email'
|
8
|
-
GOOGLE_JWT_AUD = 'https://
|
8
|
+
GOOGLE_JWT_AUD = 'https://oauth2.googleapis.com/token'
|
9
9
|
GOOGLE_ALGORITHM = 'RS256'
|
10
10
|
GOOGLE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
|
11
|
-
GOOGLE_TOKEN_URL = 'https://
|
11
|
+
GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token'
|
12
12
|
|
13
13
|
attr_reader :project_id
|
14
14
|
attr_reader :client_email
|
15
|
+
attr_reader :token_uri
|
15
16
|
attr_reader :access_token
|
16
17
|
attr_reader :expires
|
17
18
|
|
@@ -28,7 +29,7 @@ module Firebase
|
|
28
29
|
def valid_token
|
29
30
|
return access_token if access_token && !expiring?
|
30
31
|
return access_token if request_access_token
|
31
|
-
|
32
|
+
raise 'No valid access token.'
|
32
33
|
end
|
33
34
|
|
34
35
|
# If token is expiring within a minute
|
@@ -52,7 +53,11 @@ module Firebase
|
|
52
53
|
@private_key = cred[:private_key]
|
53
54
|
@project_id = cred[:project_id]
|
54
55
|
@client_email = cred[:client_email]
|
56
|
+
@token_uri = cred[:token_uri]
|
57
|
+
@token_uri ||= GOOGLE_TOKEN_URL
|
55
58
|
Firebase.logger.info('Private key loaded from JSON')
|
59
|
+
s = [:project_id, :client_email].map{ |x| "#{x}: #{self.public_send(x)}" }
|
60
|
+
Firebase.logger.debug("The key contained:\n#{s.join("\n")}")
|
56
61
|
end
|
57
62
|
|
58
63
|
# @param path [String] path to JSON file with private key
|
@@ -64,14 +69,21 @@ module Firebase
|
|
64
69
|
|
65
70
|
# Request new token from Google
|
66
71
|
def request_access_token
|
67
|
-
Firebase.logger.info('Requesting access token
|
68
|
-
|
72
|
+
Firebase.logger.info('Requesting access token...')
|
73
|
+
Firebase.logger.debug("token_uri: #{token_uri}")
|
74
|
+
res = Neko::HTTP.post_form(token_uri, jwt)
|
69
75
|
Firebase.logger.debug("HTTP response code: #{res[:code]}")
|
70
76
|
if res.class == Hash && res[:code] == 200
|
71
77
|
data = JSON.parse(res[:body], {symbolize_names: true})
|
72
78
|
@access_token = data[:access_token]
|
73
79
|
@expires = Time.now + data[:expires_in]
|
80
|
+
Firebase.logger.info('Access token acquired.')
|
81
|
+
s = "Token #{@access_token.length} bytes, expires #{@expires}"
|
82
|
+
Firebase.logger.debug(s)
|
74
83
|
return true
|
84
|
+
else
|
85
|
+
Firebase.logger.error('Access token request failed.')
|
86
|
+
Firebase.logger.debug("HTTP #{res[:code]} #{res[:message]}")
|
75
87
|
end
|
76
88
|
return false
|
77
89
|
end
|
@@ -90,7 +102,5 @@ module Firebase
|
|
90
102
|
jwt = JWT.encode payload, pkey, GOOGLE_ALGORITHM
|
91
103
|
return {grant_type: GOOGLE_GRANT_TYPE, assertion: jwt}
|
92
104
|
end
|
93
|
-
|
94
105
|
end
|
95
|
-
|
96
106
|
end
|
@@ -1,7 +1,8 @@
|
|
1
|
-
|
1
|
+
require 'firebase-ruby/neko-http'
|
2
2
|
|
3
|
-
class Database
|
4
3
|
|
4
|
+
module Firebase
|
5
|
+
class Database
|
5
6
|
FIREBASE_URL_TEMPLATE = 'https://%s.firebaseio.com/'
|
6
7
|
|
7
8
|
attr_accessor :auth, :print, :shallow
|
@@ -59,7 +60,7 @@ module Firebase
|
|
59
60
|
def http
|
60
61
|
unless @http
|
61
62
|
url = FIREBASE_URL_TEMPLATE % project_id
|
62
|
-
@http = HTTP.new(url, {'Content-Type' => 'application/json'})
|
63
|
+
@http = Neko::HTTP.new(url, {'Content-Type' => 'application/json'})
|
63
64
|
end
|
64
65
|
@http.headers['Authorization'] = "Bearer #{auth.valid_token}"
|
65
66
|
return @http
|
@@ -77,7 +78,5 @@ module Firebase
|
|
77
78
|
end
|
78
79
|
return JSON.parse(data[:body], {symbolize_names: true})
|
79
80
|
end
|
80
|
-
|
81
81
|
end
|
82
|
-
|
83
82
|
end
|
@@ -1,11 +1,22 @@
|
|
1
|
+
# NekoHTTP - Pure Ruby HTTP client using net/http
|
2
|
+
#
|
3
|
+
# v.20200629
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
require 'logger'
|
1
7
|
require 'net/http'
|
2
8
|
require 'openssl'
|
3
9
|
|
10
|
+
module Neko
|
11
|
+
def self.logger=(logger)
|
12
|
+
@logger = logger
|
13
|
+
end
|
4
14
|
|
5
|
-
|
15
|
+
def self.logger
|
16
|
+
@logger ||= NullLogger.new()
|
17
|
+
end
|
6
18
|
|
7
19
|
class HTTP
|
8
|
-
|
9
20
|
METHOD_HTTP_CLASS = {
|
10
21
|
get: Net::HTTP::Get,
|
11
22
|
put: Net::HTTP::Put,
|
@@ -14,30 +25,66 @@ module Firebase
|
|
14
25
|
delete: Net::HTTP::Delete
|
15
26
|
}
|
16
27
|
|
17
|
-
|
18
|
-
|
28
|
+
# Simple GET request
|
29
|
+
# @param url [String] full URL string
|
30
|
+
# @param params [Array, Hash] it will be converted to URL encoded query
|
31
|
+
# @param hdrs [Hash] HTTP headers
|
32
|
+
# @return [Hash] contains: :code, :headers, :body, :message
|
33
|
+
def self.get(url, params, hdrs = nil)
|
34
|
+
h = HTTP.new(url, hdrs)
|
19
35
|
data = h.get(params: params)
|
20
36
|
h.close
|
21
37
|
return data
|
22
38
|
end
|
23
39
|
|
24
|
-
|
25
|
-
|
40
|
+
# Send POST request with form data URL encoded body
|
41
|
+
# @param url [String] full URL string
|
42
|
+
# @param params [Array, Hash] it will be converted to URL encoded body
|
43
|
+
# @param hdrs [Hash] HTTP headers
|
44
|
+
# @return (see #self.get)
|
45
|
+
def self.post_form(url, params, hdrs = nil)
|
46
|
+
h = HTTP.new(url, hdrs)
|
26
47
|
data = h.post(params: params)
|
27
48
|
h.close
|
28
49
|
return data
|
29
50
|
end
|
30
51
|
|
52
|
+
# Send POST request with JSON body
|
53
|
+
# It will set the Content-Type to application/json.
|
54
|
+
# @param url [String] full URL string
|
55
|
+
# @param obj [Array, Hash, String] Array/Hash will be converted to JSON
|
56
|
+
# @param hdrs [Hash] HTTP headers
|
57
|
+
# @return (see #self.get)
|
58
|
+
def self.post_json(url, obj, hdrs = {})
|
59
|
+
hdrs['Content-Type'] = 'application/json'
|
60
|
+
h = HTTP.new(url, hdrs)
|
61
|
+
case obj
|
62
|
+
when Array, Hash
|
63
|
+
body = JSON.fast_generate(obj)
|
64
|
+
when String
|
65
|
+
body = obj
|
66
|
+
else
|
67
|
+
raise ArgumentError, 'Argument is neither Array, Hash, String'
|
68
|
+
end
|
69
|
+
data = h.post(body: body)
|
70
|
+
h.close
|
71
|
+
return data
|
72
|
+
end
|
73
|
+
|
31
74
|
attr_reader :init_uri, :http
|
32
|
-
attr_accessor :headers
|
75
|
+
attr_accessor :logger, :headers
|
33
76
|
|
77
|
+
# Instance constructor for tailored use
|
78
|
+
# @param url [String] full URL string
|
79
|
+
# @param hdrs [Hash] HTTP headers
|
34
80
|
def initialize(url, hdrs = nil)
|
81
|
+
@logger = Neko.logger
|
35
82
|
@init_uri = URI(url)
|
36
83
|
raise ArgumentError, 'Invalid URL' unless @init_uri.class <= URI::HTTP
|
37
84
|
@http = Net::HTTP.new(init_uri.host, init_uri.port)
|
38
85
|
http.use_ssl = init_uri.scheme == 'https'
|
39
86
|
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
40
|
-
|
87
|
+
@headers = hdrs
|
41
88
|
end
|
42
89
|
|
43
90
|
def get(path: nil, params: nil, query: nil)
|
@@ -72,24 +119,28 @@ module Firebase
|
|
72
119
|
when :get, :delete
|
73
120
|
if params
|
74
121
|
query = URI.encode_www_form(params)
|
75
|
-
|
122
|
+
logger.info('Created urlencoded query from params')
|
76
123
|
end
|
77
|
-
uri.query = query
|
124
|
+
uri.query = query if query
|
78
125
|
req = METHOD_HTTP_CLASS[method].new(uri)
|
79
126
|
when :put, :patch, :post
|
80
127
|
uri.query = query if query
|
81
128
|
req = METHOD_HTTP_CLASS[method].new(uri)
|
82
129
|
if params
|
83
130
|
req.form_data = params
|
84
|
-
|
131
|
+
logger.info('Created form data from params')
|
85
132
|
elsif body
|
86
133
|
req.body = body
|
87
134
|
end
|
88
135
|
else
|
89
136
|
return nil
|
90
137
|
end
|
138
|
+
if uri.userinfo
|
139
|
+
req.basic_auth(uri.user, uri.password)
|
140
|
+
logger.info('Created basic auth header from URL')
|
141
|
+
end
|
91
142
|
data = send(req)
|
92
|
-
data = redirect(method, uri
|
143
|
+
data = redirect(method, uri: data, body: req.body) if data.class <= URI::HTTP
|
93
144
|
return data
|
94
145
|
end
|
95
146
|
|
@@ -102,36 +153,34 @@ module Firebase
|
|
102
153
|
def send(req)
|
103
154
|
inject_headers_to(req)
|
104
155
|
unless http.started?
|
105
|
-
|
156
|
+
logger.info('HTTP session not started; starting now')
|
106
157
|
http.start
|
107
|
-
|
158
|
+
logger.debug("Opened connection to #{http.address}:#{http.port}")
|
108
159
|
end
|
109
|
-
|
110
|
-
|
160
|
+
logger.debug("Sending HTTP #{req.method} request to #{req.path}")
|
161
|
+
logger.debug("Body size: #{req.body.length}") if req.request_body_permitted?
|
111
162
|
res = http.request(req)
|
112
163
|
return handle_response(res)
|
113
164
|
end
|
114
165
|
|
115
166
|
def inject_headers_to(req)
|
116
167
|
return if headers.nil?
|
117
|
-
headers.each
|
118
|
-
|
119
|
-
end
|
120
|
-
Firebase.logger.info('Header injected into HTTP request header')
|
168
|
+
headers.each { |k, v| req[k] = v }
|
169
|
+
logger.info('Header injected into HTTP request header')
|
121
170
|
end
|
122
171
|
|
123
172
|
def handle_response(res)
|
124
173
|
if res.connection_close?
|
125
|
-
|
174
|
+
logger.info('HTTP response header says connection close; closing session now')
|
126
175
|
close
|
127
176
|
end
|
128
177
|
case res
|
129
178
|
when Net::HTTPRedirection
|
130
|
-
|
179
|
+
logger.info('HTTP response was a redirect')
|
131
180
|
data = URI(res['Location'])
|
132
181
|
if data.class == URI::Generic
|
133
182
|
data = uri_with_path(res['Location'])
|
134
|
-
|
183
|
+
logger.debug("Full URI object built for local redirect with path: #{data.path}")
|
135
184
|
end
|
136
185
|
# when Net::HTTPSuccess
|
137
186
|
# when Net::HTTPClientError
|
@@ -147,25 +196,23 @@ module Firebase
|
|
147
196
|
return data
|
148
197
|
end
|
149
198
|
|
150
|
-
def redirect(method, uri
|
199
|
+
def redirect(method, uri:, body: nil)
|
151
200
|
if uri.host == init_uri.host && uri.port == init_uri.port
|
152
|
-
|
153
|
-
new_http =
|
201
|
+
logger.info("Local #{method.upcase} redirect, reusing HTTP session")
|
202
|
+
new_http = self
|
154
203
|
else
|
155
|
-
|
204
|
+
logger.info("External #{method.upcase} redirect, spawning new HTTP object")
|
156
205
|
new_http = HTTP.new("#{uri.scheme}://#{uri.host}#{uri.path}", headers)
|
157
206
|
end
|
158
|
-
|
159
|
-
when :get, :delete
|
160
|
-
data = operate(method, uri, params: params, query: query)
|
161
|
-
when :put, :patch, :post
|
162
|
-
data = new_http.public_send(method, uri, params: params, body: body, query: query)
|
163
|
-
else
|
164
|
-
data = nil
|
165
|
-
end
|
166
|
-
return data
|
207
|
+
new_http.__send__(:operate, method, path: uri.path, body: body, query: uri.query)
|
167
208
|
end
|
168
|
-
|
169
209
|
end
|
170
210
|
|
211
|
+
class NullLogger < Logger
|
212
|
+
def initialize(*args)
|
213
|
+
end
|
214
|
+
|
215
|
+
def add(*args, &block)
|
216
|
+
end
|
217
|
+
end
|
171
218
|
end
|
@@ -1,17 +1,17 @@
|
|
1
|
-
# lib/
|
1
|
+
# lib/optimist.rb -- optimist command-line processing library
|
2
2
|
# Copyright (c) 2008-2014 William Morgan.
|
3
3
|
# Copyright (c) 2014 Red Hat, Inc.
|
4
|
-
#
|
4
|
+
# optimist is licensed under the MIT license.
|
5
5
|
|
6
6
|
require 'date'
|
7
7
|
|
8
|
-
module
|
8
|
+
module Optimist
|
9
9
|
# note: this is duplicated in gemspec
|
10
10
|
# please change over there too
|
11
|
-
VERSION = "
|
11
|
+
VERSION = "3.0.1"
|
12
12
|
|
13
13
|
## Thrown by Parser in the event of a commandline error. Not needed if
|
14
|
-
## you're using the
|
14
|
+
## you're using the Optimist::options entry.
|
15
15
|
class CommandlineError < StandardError
|
16
16
|
attr_reader :error_code
|
17
17
|
|
@@ -22,12 +22,12 @@ class CommandlineError < StandardError
|
|
22
22
|
end
|
23
23
|
|
24
24
|
## Thrown by Parser if the user passes in '-h' or '--help'. Handled
|
25
|
-
## automatically by
|
25
|
+
## automatically by Optimist#options.
|
26
26
|
class HelpNeeded < StandardError
|
27
27
|
end
|
28
28
|
|
29
29
|
## Thrown by Parser if the user passes in '-v' or '--version'. Handled
|
30
|
-
## automatically by
|
30
|
+
## automatically by Optimist#options.
|
31
31
|
class VersionNeeded < StandardError
|
32
32
|
end
|
33
33
|
|
@@ -38,15 +38,38 @@ FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/
|
|
38
38
|
PARAM_RE = /^-(-|\.$|[^\d\.])/
|
39
39
|
|
40
40
|
## The commandline parser. In typical usage, the methods in this class
|
41
|
-
## will be handled internally by
|
41
|
+
## will be handled internally by Optimist::options. In this case, only the
|
42
42
|
## #opt, #banner and #version, #depends, and #conflicts methods will
|
43
43
|
## typically be called.
|
44
44
|
##
|
45
45
|
## If you want to instantiate this class yourself (for more complicated
|
46
46
|
## argument-parsing logic), call #parse to actually produce the output hash,
|
47
47
|
## and consider calling it from within
|
48
|
-
##
|
48
|
+
## Optimist::with_standard_exception_handling.
|
49
49
|
class Parser
|
50
|
+
|
51
|
+
## The registry is a class-instance-variable map of option aliases to their subclassed Option class.
|
52
|
+
@registry = {}
|
53
|
+
|
54
|
+
## The Option subclasses are responsible for registering themselves using this function.
|
55
|
+
def self.register(lookup, klass)
|
56
|
+
@registry[lookup.to_sym] = klass
|
57
|
+
end
|
58
|
+
|
59
|
+
## Gets the class from the registry.
|
60
|
+
## Can be given either a class-name, e.g. Integer, a string, e.g "integer", or a symbol, e.g :integer
|
61
|
+
def self.registry_getopttype(type)
|
62
|
+
return nil unless type
|
63
|
+
if type.respond_to?(:name)
|
64
|
+
type = type.name
|
65
|
+
lookup = type.downcase.to_sym
|
66
|
+
else
|
67
|
+
lookup = type.to_sym
|
68
|
+
end
|
69
|
+
raise ArgumentError, "Unsupported argument type '#{type}', registry lookup '#{lookup}'" unless @registry.has_key?(lookup)
|
70
|
+
return @registry[lookup].new
|
71
|
+
end
|
72
|
+
|
50
73
|
INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
|
51
74
|
|
52
75
|
## The values from the commandline that were not interpreted by #parse.
|
@@ -75,7 +98,7 @@ class Parser
|
|
75
98
|
@educate_on_error = false
|
76
99
|
@synopsis = nil
|
77
100
|
@usage = nil
|
78
|
-
|
101
|
+
|
79
102
|
# instance_eval(&b) if b # can't take arguments
|
80
103
|
cloaker(&b).bind(self).call(*a) if b
|
81
104
|
end
|
@@ -90,7 +113,7 @@ class Parser
|
|
90
113
|
## [+:long+] Specify the long form of the argument, i.e. the form with two dashes. If unspecified, will be automatically derived based on the argument name by turning the +name+ option into a string, and replacing any _'s by -'s.
|
91
114
|
## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+. Use :none: to not have a short value.
|
92
115
|
## [+:type+] Require that the argument take a parameter or parameters of type +type+. For a single parameter, the value can be a member of +SINGLE_ARG_TYPES+, or a corresponding Ruby class (e.g. +Integer+ for +:int+). For multiple-argument parameters, the value can be any member of +MULTI_ARG_TYPES+ constant. If unset, the default argument type is +:flag+, meaning that the argument does not take a parameter. The specification of +:type+ is not necessary if a +:default+ is given.
|
93
|
-
## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus
|
116
|
+
## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus Optimist::options) will have a +nil+ value for this key unless the argument is given on the commandline. The argument type is derived automatically from the class of the default value given, so specifying a +:type+ is not necessary if a +:default+ is given. (But see below for an important caveat when +:multi+: is specified too.) If the argument is a flag, and the default is set to +true+, then if it is specified on the the commandline the value will be +false+.
|
94
117
|
## [+:required+] If set to +true+, the argument must be provided on the commandline.
|
95
118
|
## [+:multi+] If set to +true+, allows multiple occurrences of the option on the commandline. Otherwise, only a single instance of the option is allowed. (Note that this is different from taking multiple parameters. See below.)
|
96
119
|
##
|
@@ -116,7 +139,7 @@ class Parser
|
|
116
139
|
## There's one ambiguous case to be aware of: when +:multi+: is true and a
|
117
140
|
## +:default+ is set to an array (of something), it's ambiguous whether this
|
118
141
|
## is a multi-value argument as well as a multi-occurrence argument.
|
119
|
-
## In thise case,
|
142
|
+
## In thise case, Optimist assumes that it's not a multi-value argument.
|
120
143
|
## If you want a multi-value, multi-occurrence argument with a default
|
121
144
|
## value, you must specify +:type+ as well.
|
122
145
|
|
@@ -164,7 +187,7 @@ class Parser
|
|
164
187
|
|
165
188
|
## Marks two (or more!) options as requiring each other. Only handles
|
166
189
|
## undirected (i.e., mutual) dependencies. Directed dependencies are
|
167
|
-
## better modeled with
|
190
|
+
## better modeled with Optimist::die.
|
168
191
|
def depends(*syms)
|
169
192
|
syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
|
170
193
|
@constraints << [:depends, syms]
|
@@ -182,7 +205,7 @@ class Parser
|
|
182
205
|
## intact.
|
183
206
|
##
|
184
207
|
## A typical use case would be for subcommand support, where these
|
185
|
-
## would be set to the list of subcommands. A subsequent
|
208
|
+
## would be set to the list of subcommands. A subsequent Optimist
|
186
209
|
## invocation would then be used to parse subcommand options, after
|
187
210
|
## shifting the subcommand off of ARGV.
|
188
211
|
def stop_on(*words)
|
@@ -203,7 +226,7 @@ class Parser
|
|
203
226
|
@educate_on_error = true
|
204
227
|
end
|
205
228
|
|
206
|
-
## Parses the commandline. Typically called by
|
229
|
+
## Parses the commandline. Typically called by Optimist::options,
|
207
230
|
## but you can call it directly if you need more control.
|
208
231
|
##
|
209
232
|
## throws CommandlineError, HelpNeeded, and VersionNeeded exceptions.
|
@@ -240,7 +263,7 @@ class Parser
|
|
240
263
|
|
241
264
|
sym = nil if arg =~ /--no-/ # explicitly invalidate --no-no- arguments
|
242
265
|
|
243
|
-
next
|
266
|
+
next nil if ignore_invalid_options && !sym
|
244
267
|
raise CommandlineError, "unknown argument '#{arg}'" unless sym
|
245
268
|
|
246
269
|
if given_args.include?(sym) && !@specs[sym].multi?
|
@@ -255,7 +278,7 @@ class Parser
|
|
255
278
|
# The block returns the number of parameters taken.
|
256
279
|
num_params_taken = 0
|
257
280
|
|
258
|
-
unless params.
|
281
|
+
unless params.empty?
|
259
282
|
if @specs[sym].single_arg?
|
260
283
|
given_args[sym][:params] << params[0, 1] # take the first parameter
|
261
284
|
num_params_taken = 1
|
@@ -301,20 +324,7 @@ class Parser
|
|
301
324
|
|
302
325
|
vals["#{sym}_given".intern] = true # mark argument as specified on the commandline
|
303
326
|
|
304
|
-
|
305
|
-
when :flag
|
306
|
-
vals[sym] = (sym.to_s =~ /^no_/ ? negative_given : !negative_given)
|
307
|
-
when :int, :ints
|
308
|
-
vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } }
|
309
|
-
when :float, :floats
|
310
|
-
vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } }
|
311
|
-
when :string, :strings
|
312
|
-
vals[sym] = params.map { |pg| pg.map(&:to_s) }
|
313
|
-
when :io, :ios
|
314
|
-
vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } }
|
315
|
-
when :date, :dates
|
316
|
-
vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } }
|
317
|
-
end
|
327
|
+
vals[sym] = opts.parse(params, negative_given)
|
318
328
|
|
319
329
|
if opts.single_arg?
|
320
330
|
if opts.multi? # multiple options, each with a single parameter
|
@@ -344,41 +354,13 @@ class Parser
|
|
344
354
|
vals
|
345
355
|
end
|
346
356
|
|
347
|
-
def parse_date_parameter(param, arg) #:nodoc:
|
348
|
-
begin
|
349
|
-
require 'chronic'
|
350
|
-
time = Chronic.parse(param)
|
351
|
-
rescue LoadError
|
352
|
-
# chronic is not available
|
353
|
-
end
|
354
|
-
time ? Date.new(time.year, time.month, time.day) : Date.parse(param)
|
355
|
-
rescue ArgumentError
|
356
|
-
raise CommandlineError, "option '#{arg}' needs a date"
|
357
|
-
end
|
358
|
-
|
359
357
|
## Print the help message to +stream+.
|
360
358
|
def educate(stream = $stdout)
|
361
359
|
width # hack: calculate it now; otherwise we have to be careful not to
|
362
360
|
# call this unless the cursor's at the beginning of a line.
|
361
|
+
|
363
362
|
left = {}
|
364
|
-
@specs.each
|
365
|
-
left[name] =
|
366
|
-
(spec.short? ? "-#{spec.short}, " : "") + "--#{spec.long}" +
|
367
|
-
case spec.type
|
368
|
-
when :flag then ""
|
369
|
-
when :int then "=<i>"
|
370
|
-
when :ints then "=<i+>"
|
371
|
-
when :string then "=<s>"
|
372
|
-
when :strings then "=<s+>"
|
373
|
-
when :float then "=<f>"
|
374
|
-
when :floats then "=<f+>"
|
375
|
-
when :io then "=<filename/uri>"
|
376
|
-
when :ios then "=<filename/uri+>"
|
377
|
-
when :date then "=<date>"
|
378
|
-
when :dates then "=<date+>"
|
379
|
-
end +
|
380
|
-
(spec.flag? && spec.default ? ", --no-#{spec.long}" : "")
|
381
|
-
end
|
363
|
+
@specs.each { |name, spec| left[name] = spec.educate }
|
382
364
|
|
383
365
|
leftcol_width = left.values.map(&:length).max || 0
|
384
366
|
rightcol_start = leftcol_width + 6 # spaces
|
@@ -400,27 +382,8 @@ class Parser
|
|
400
382
|
|
401
383
|
spec = @specs[opt]
|
402
384
|
stream.printf " %-#{leftcol_width}s ", left[opt]
|
403
|
-
desc = spec.
|
404
|
-
default_s = case spec.default
|
405
|
-
when $stdout then "<stdout>"
|
406
|
-
when $stdin then "<stdin>"
|
407
|
-
when $stderr then "<stderr>"
|
408
|
-
when Array
|
409
|
-
spec.default.join(", ")
|
410
|
-
else
|
411
|
-
spec.default.to_s
|
412
|
-
end
|
385
|
+
desc = spec.description_with_default
|
413
386
|
|
414
|
-
if spec.default
|
415
|
-
if spec.desc =~ /\.$/
|
416
|
-
" (Default: #{default_s})"
|
417
|
-
else
|
418
|
-
" (default: #{default_s})"
|
419
|
-
end
|
420
|
-
else
|
421
|
-
""
|
422
|
-
end
|
423
|
-
end
|
424
387
|
stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
|
425
388
|
end
|
426
389
|
end
|
@@ -460,8 +423,9 @@ class Parser
|
|
460
423
|
end
|
461
424
|
end
|
462
425
|
|
463
|
-
## The per-parser version of
|
426
|
+
## The per-parser version of Optimist::die (see that for documentation).
|
464
427
|
def die(arg, msg = nil, error_code = nil)
|
428
|
+
msg, error_code = nil, msg if msg.kind_of?(Integer)
|
465
429
|
if msg
|
466
430
|
$stderr.puts "Error: argument --#{@specs[arg].long} #{msg}."
|
467
431
|
else
|
@@ -489,47 +453,60 @@ private
|
|
489
453
|
when /^--$/ # arg terminator
|
490
454
|
return remains += args[(i + 1)..-1]
|
491
455
|
when /^--(\S+?)=(.*)$/ # long argument with equals
|
492
|
-
yield "--#{$1}", [$2]
|
456
|
+
num_params_taken = yield "--#{$1}", [$2]
|
457
|
+
if num_params_taken.nil?
|
458
|
+
remains << args[i]
|
459
|
+
if @stop_on_unknown
|
460
|
+
return remains += args[i + 1..-1]
|
461
|
+
end
|
462
|
+
end
|
493
463
|
i += 1
|
494
464
|
when /^--(\S+)$/ # long argument
|
495
465
|
params = collect_argument_parameters(args, i + 1)
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
if @stop_on_unknown
|
503
|
-
return remains += args[i + 1..-1]
|
504
|
-
else
|
505
|
-
remains += params
|
506
|
-
end
|
466
|
+
num_params_taken = yield args[i], params
|
467
|
+
|
468
|
+
if num_params_taken.nil?
|
469
|
+
remains << args[i]
|
470
|
+
if @stop_on_unknown
|
471
|
+
return remains += args[i + 1..-1]
|
507
472
|
end
|
508
|
-
|
473
|
+
else
|
474
|
+
i += num_params_taken
|
509
475
|
end
|
476
|
+
i += 1
|
510
477
|
when /^-(\S+)$/ # one or more short arguments
|
478
|
+
short_remaining = ""
|
511
479
|
shortargs = $1.split(//)
|
512
480
|
shortargs.each_with_index do |a, j|
|
513
481
|
if j == (shortargs.length - 1)
|
514
482
|
params = collect_argument_parameters(args, i + 1)
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
return remains += args[i + 1..-1]
|
523
|
-
else
|
524
|
-
remains += params
|
525
|
-
end
|
483
|
+
|
484
|
+
num_params_taken = yield "-#{a}", params
|
485
|
+
unless num_params_taken
|
486
|
+
short_remaining << a
|
487
|
+
if @stop_on_unknown
|
488
|
+
remains << "-#{short_remaining}"
|
489
|
+
return remains += args[i + 1..-1]
|
526
490
|
end
|
527
|
-
|
491
|
+
else
|
492
|
+
i += num_params_taken
|
528
493
|
end
|
529
494
|
else
|
530
|
-
yield "-#{a}",
|
495
|
+
unless yield "-#{a}", []
|
496
|
+
short_remaining << a
|
497
|
+
if @stop_on_unknown
|
498
|
+
short_remaining += shortargs[j + 1..-1].join
|
499
|
+
remains << "-#{short_remaining}"
|
500
|
+
return remains += args[i + 1..-1]
|
501
|
+
end
|
502
|
+
end
|
531
503
|
end
|
532
504
|
end
|
505
|
+
|
506
|
+
unless short_remaining.empty?
|
507
|
+
remains << "-#{short_remaining}"
|
508
|
+
end
|
509
|
+
i += 1
|
533
510
|
else
|
534
511
|
if @stop_on_unknown
|
535
512
|
return remains += args[i..-1]
|
@@ -543,29 +520,6 @@ private
|
|
543
520
|
remains
|
544
521
|
end
|
545
522
|
|
546
|
-
def parse_integer_parameter(param, arg)
|
547
|
-
raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^-?[\d_]+$/
|
548
|
-
param.to_i
|
549
|
-
end
|
550
|
-
|
551
|
-
def parse_float_parameter(param, arg)
|
552
|
-
raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
|
553
|
-
param.to_f
|
554
|
-
end
|
555
|
-
|
556
|
-
def parse_io_parameter(param, arg)
|
557
|
-
if param =~ /^(stdin|-)$/i
|
558
|
-
$stdin
|
559
|
-
else
|
560
|
-
require 'open-uri'
|
561
|
-
begin
|
562
|
-
open param
|
563
|
-
rescue SystemCallError => e
|
564
|
-
raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}"
|
565
|
-
end
|
566
|
-
end
|
567
|
-
end
|
568
|
-
|
569
523
|
def collect_argument_parameters(args, start_at)
|
570
524
|
params = []
|
571
525
|
pos = start_at
|
@@ -621,173 +575,318 @@ private
|
|
621
575
|
end
|
622
576
|
end
|
623
577
|
|
624
|
-
## The option for each flag
|
625
578
|
class Option
|
626
|
-
## The set of values that indicate a flag option when passed as the
|
627
|
-
## +:type+ parameter of #opt.
|
628
|
-
FLAG_TYPES = [:flag, :bool, :boolean]
|
629
579
|
|
630
|
-
|
631
|
-
|
632
|
-
##
|
633
|
-
## A value of +io+ corresponds to a readable IO resource, including
|
634
|
-
## a filename, URI, or the strings 'stdin' or '-'.
|
635
|
-
SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date]
|
636
|
-
|
637
|
-
## The set of values that indicate a multiple-parameter option (i.e., that
|
638
|
-
## takes multiple space-separated values on the commandline) when passed as
|
639
|
-
## the +:type+ parameter of #opt.
|
640
|
-
MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates]
|
641
|
-
|
642
|
-
## The complete set of legal values for the +:type+ parameter of #opt.
|
643
|
-
TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES
|
644
|
-
|
645
|
-
attr_accessor :name, :opts
|
646
|
-
|
647
|
-
def initialize(name, desc="", opts={}, &b)
|
648
|
-
## fill in :type
|
649
|
-
opts[:type] = # normalize
|
650
|
-
case opts[:type]
|
651
|
-
when :boolean, :bool then :flag
|
652
|
-
when :integer then :int
|
653
|
-
when :integers then :ints
|
654
|
-
when :double then :float
|
655
|
-
when :doubles then :floats
|
656
|
-
when Class
|
657
|
-
case opts[:type].name
|
658
|
-
when 'TrueClass',
|
659
|
-
'FalseClass' then :flag
|
660
|
-
when 'String' then :string
|
661
|
-
when 'Integer' then :int
|
662
|
-
when 'Float' then :float
|
663
|
-
when 'IO' then :io
|
664
|
-
when 'Date' then :date
|
665
|
-
else
|
666
|
-
raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
|
667
|
-
end
|
668
|
-
when nil then nil
|
669
|
-
else
|
670
|
-
raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
|
671
|
-
opts[:type]
|
672
|
-
end
|
580
|
+
attr_accessor :name, :short, :long, :default
|
581
|
+
attr_writer :multi_given
|
673
582
|
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
583
|
+
def initialize
|
584
|
+
@long = nil
|
585
|
+
@short = nil
|
586
|
+
@name = nil
|
587
|
+
@multi_given = false
|
588
|
+
@hidden = false
|
589
|
+
@default = nil
|
590
|
+
@optshash = Hash.new()
|
591
|
+
end
|
592
|
+
|
593
|
+
def opts(key)
|
594
|
+
@optshash[key]
|
595
|
+
end
|
596
|
+
|
597
|
+
def opts=(o)
|
598
|
+
@optshash = o
|
599
|
+
end
|
600
|
+
|
601
|
+
## Indicates a flag option, which is an option without an argument
|
602
|
+
def flag? ; false ; end
|
603
|
+
def single_arg?
|
604
|
+
!self.multi_arg? && !self.flag?
|
605
|
+
end
|
606
|
+
|
607
|
+
def multi ; @multi_given ; end
|
608
|
+
alias multi? multi
|
609
|
+
|
610
|
+
## Indicates that this is a multivalued (Array type) argument
|
611
|
+
def multi_arg? ; false ; end
|
612
|
+
## note: Option-Types with both multi_arg? and flag? false are single-parameter (normal) options.
|
613
|
+
|
614
|
+
def array_default? ; self.default.kind_of?(Array) ; end
|
615
|
+
|
616
|
+
def short? ; short && short != :none ; end
|
617
|
+
|
618
|
+
def callback ; opts(:callback) ; end
|
619
|
+
def desc ; opts(:desc) ; end
|
620
|
+
|
621
|
+
def required? ; opts(:required) ; end
|
622
|
+
|
623
|
+
def parse(_paramlist, _neg_given)
|
624
|
+
raise NotImplementedError, "parse must be overridden for newly registered type"
|
625
|
+
end
|
626
|
+
|
627
|
+
# provide type-format string. default to empty, but user should probably override it
|
628
|
+
def type_format ; "" ; end
|
629
|
+
|
630
|
+
def educate
|
631
|
+
(short? ? "-#{short}, " : "") + "--#{long}" + type_format + (flag? && default ? ", --no-#{long}" : "")
|
632
|
+
end
|
633
|
+
|
634
|
+
## Format the educate-line description including the default-value(s)
|
635
|
+
def description_with_default
|
636
|
+
return desc unless default
|
637
|
+
default_s = case default
|
638
|
+
when $stdout then '<stdout>'
|
639
|
+
when $stdin then '<stdin>'
|
640
|
+
when $stderr then '<stderr>'
|
641
|
+
when Array
|
642
|
+
default.join(', ')
|
643
|
+
else
|
644
|
+
default.to_s
|
645
|
+
end
|
646
|
+
defword = desc.end_with?('.') ? 'Default' : 'default'
|
647
|
+
return "#{desc} (#{defword}: #{default_s})"
|
648
|
+
end
|
649
|
+
|
650
|
+
## Provide a way to register symbol aliases to the Parser
|
651
|
+
def self.register_alias(*alias_keys)
|
652
|
+
alias_keys.each do |alias_key|
|
653
|
+
# pass in the alias-key and the class
|
654
|
+
Parser.register(alias_key, self)
|
682
655
|
end
|
656
|
+
end
|
683
657
|
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
if opts[:default].empty?
|
695
|
-
if opts[:type]
|
696
|
-
raise ArgumentError, "multiple argument type must be plural" unless MULTI_ARG_TYPES.include?(opts[:type])
|
697
|
-
nil
|
698
|
-
else
|
699
|
-
raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'"
|
700
|
-
end
|
701
|
-
else
|
702
|
-
case opts[:default][0] # the first element determines the types
|
703
|
-
when Integer then :ints
|
704
|
-
when Numeric then :floats
|
705
|
-
when String then :strings
|
706
|
-
when IO then :ios
|
707
|
-
when Date then :dates
|
708
|
-
else
|
709
|
-
raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'"
|
710
|
-
end
|
711
|
-
end
|
712
|
-
when nil then nil
|
713
|
-
else
|
714
|
-
raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
|
715
|
-
end
|
658
|
+
## Factory class methods ...
|
659
|
+
|
660
|
+
# Determines which type of object to create based on arguments passed
|
661
|
+
# to +Optimist::opt+. This is trickier in Optimist, than other cmdline
|
662
|
+
# parsers (e.g. Slop) because we allow the +default:+ to be able to
|
663
|
+
# set the option's type.
|
664
|
+
def self.create(name, desc="", opts={}, settings={})
|
665
|
+
|
666
|
+
opttype = Optimist::Parser.registry_getopttype(opts[:type])
|
667
|
+
opttype_from_default = get_klass_from_default(opts, opttype)
|
716
668
|
|
717
|
-
raise ArgumentError, ":type specification and default type don't match (default type is #{
|
669
|
+
raise ArgumentError, ":type specification and default type don't match (default type is #{opttype_from_default.class})" if opttype && opttype_from_default && (opttype.class != opttype_from_default.class)
|
718
670
|
|
719
|
-
|
671
|
+
opt_inst = (opttype || opttype_from_default || Optimist::BooleanOption.new)
|
720
672
|
|
721
673
|
## fill in :long
|
722
|
-
|
723
|
-
opts[:long] = case opts[:long]
|
724
|
-
when /^--([^-].*)$/ then $1
|
725
|
-
when /^[^-]/ then opts[:long]
|
726
|
-
else raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
|
727
|
-
end
|
674
|
+
opt_inst.long = handle_long_opt(opts[:long], name)
|
728
675
|
|
729
676
|
## fill in :short
|
730
|
-
|
731
|
-
opts[:short] = case opts[:short]
|
732
|
-
when /^-(.)$/ then $1
|
733
|
-
when nil, :none, /^.$/ then opts[:short]
|
734
|
-
else raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
|
735
|
-
end
|
677
|
+
opt_inst.short = handle_short_opt(opts[:short])
|
736
678
|
|
737
|
-
|
738
|
-
|
739
|
-
|
679
|
+
## fill in :multi
|
680
|
+
multi_given = opts[:multi] || false
|
681
|
+
opt_inst.multi_given = multi_given
|
740
682
|
|
741
683
|
## fill in :default for flags
|
742
|
-
|
684
|
+
defvalue = opts[:default] || opt_inst.default
|
743
685
|
|
744
686
|
## autobox :default for :multi (multi-occurrence) arguments
|
745
|
-
|
687
|
+
defvalue = [defvalue] if defvalue && multi_given && !defvalue.kind_of?(Array)
|
688
|
+
opt_inst.default = defvalue
|
689
|
+
opt_inst.name = name
|
690
|
+
opt_inst.opts = opts
|
691
|
+
opt_inst
|
692
|
+
end
|
746
693
|
|
747
|
-
|
748
|
-
opts[:multi] ||= false
|
694
|
+
private
|
749
695
|
|
750
|
-
|
751
|
-
|
696
|
+
def self.get_type_from_disdef(optdef, opttype, disambiguated_default)
|
697
|
+
if disambiguated_default.is_a? Array
|
698
|
+
return(optdef.first.class.name.downcase + "s") if !optdef.empty?
|
699
|
+
if opttype
|
700
|
+
raise ArgumentError, "multiple argument type must be plural" unless opttype.multi_arg?
|
701
|
+
return nil
|
702
|
+
else
|
703
|
+
raise ArgumentError, "multiple argument type cannot be deduced from an empty array"
|
704
|
+
end
|
705
|
+
end
|
706
|
+
return disambiguated_default.class.name.downcase
|
752
707
|
end
|
753
708
|
|
754
|
-
def
|
755
|
-
|
709
|
+
def self.get_klass_from_default(opts, opttype)
|
710
|
+
## for options with :multi => true, an array default doesn't imply
|
711
|
+
## a multi-valued argument. for that you have to specify a :type
|
712
|
+
## as well. (this is how we disambiguate an ambiguous situation;
|
713
|
+
## see the docs for Parser#opt for details.)
|
714
|
+
|
715
|
+
disambiguated_default = if opts[:multi] && opts[:default].is_a?(Array) && opttype.nil?
|
716
|
+
opts[:default].first
|
717
|
+
else
|
718
|
+
opts[:default]
|
719
|
+
end
|
720
|
+
|
721
|
+
return nil if disambiguated_default.nil?
|
722
|
+
type_from_default = get_type_from_disdef(opts[:default], opttype, disambiguated_default)
|
723
|
+
return Optimist::Parser.registry_getopttype(type_from_default)
|
756
724
|
end
|
757
725
|
|
758
|
-
def
|
759
|
-
|
760
|
-
|
761
|
-
|
726
|
+
def self.handle_long_opt(lopt, name)
|
727
|
+
lopt = lopt ? lopt.to_s : name.to_s.gsub("_", "-")
|
728
|
+
lopt = case lopt
|
729
|
+
when /^--([^-].*)$/ then $1
|
730
|
+
when /^[^-]/ then lopt
|
731
|
+
else raise ArgumentError, "invalid long option name #{lopt.inspect}"
|
732
|
+
end
|
762
733
|
end
|
763
734
|
|
764
|
-
def
|
765
|
-
|
735
|
+
def self.handle_short_opt(sopt)
|
736
|
+
sopt = sopt.to_s if sopt && sopt != :none
|
737
|
+
sopt = case sopt
|
738
|
+
when /^-(.)$/ then $1
|
739
|
+
when nil, :none, /^.$/ then sopt
|
740
|
+
else raise ArgumentError, "invalid short option name '#{sopt.inspect}'"
|
741
|
+
end
|
766
742
|
|
767
|
-
|
768
|
-
|
743
|
+
if sopt
|
744
|
+
raise ArgumentError, "a short option name can't be a number or a dash" if sopt =~ ::Optimist::Parser::INVALID_SHORT_ARG_REGEX
|
745
|
+
end
|
746
|
+
return sopt
|
769
747
|
end
|
770
748
|
|
771
|
-
|
772
|
-
#? def multi_default ; opts.default || opts.multi && [] ; end
|
773
|
-
def array_default? ; opts[:default].kind_of?(Array) ; end
|
749
|
+
end
|
774
750
|
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
def
|
779
|
-
|
780
|
-
|
781
|
-
|
751
|
+
# Flag option. Has no arguments. Can be negated with "no-".
|
752
|
+
class BooleanOption < Option
|
753
|
+
register_alias :flag, :bool, :boolean, :trueclass, :falseclass
|
754
|
+
def initialize
|
755
|
+
super()
|
756
|
+
@default = false
|
757
|
+
end
|
758
|
+
def flag? ; true ; end
|
759
|
+
def parse(_paramlist, neg_given)
|
760
|
+
return(self.name.to_s =~ /^no_/ ? neg_given : !neg_given)
|
761
|
+
end
|
762
|
+
end
|
763
|
+
|
764
|
+
# Floating point number option class.
|
765
|
+
class FloatOption < Option
|
766
|
+
register_alias :float, :double
|
767
|
+
def type_format ; "=<f>" ; end
|
768
|
+
def parse(paramlist, _neg_given)
|
769
|
+
paramlist.map do |pg|
|
770
|
+
pg.map do |param|
|
771
|
+
raise CommandlineError, "option '#{self.name}' needs a floating-point number" unless param.is_a?(Numeric) || param =~ FLOAT_RE
|
772
|
+
param.to_f
|
773
|
+
end
|
774
|
+
end
|
775
|
+
end
|
776
|
+
end
|
777
|
+
|
778
|
+
# Integer number option class.
|
779
|
+
class IntegerOption < Option
|
780
|
+
register_alias :int, :integer, :fixnum
|
781
|
+
def type_format ; "=<i>" ; end
|
782
|
+
def parse(paramlist, _neg_given)
|
783
|
+
paramlist.map do |pg|
|
784
|
+
pg.map do |param|
|
785
|
+
raise CommandlineError, "option '#{self.name}' needs an integer" unless param.is_a?(Numeric) || param =~ /^-?[\d_]+$/
|
786
|
+
param.to_i
|
787
|
+
end
|
788
|
+
end
|
789
|
+
end
|
790
|
+
end
|
791
|
+
|
792
|
+
# Option class for handling IO objects and URLs.
|
793
|
+
# Note that this will return the file-handle, not the file-name
|
794
|
+
# in the case of file-paths given to it.
|
795
|
+
class IOOption < Option
|
796
|
+
register_alias :io
|
797
|
+
def type_format ; "=<filename/uri>" ; end
|
798
|
+
def parse(paramlist, _neg_given)
|
799
|
+
paramlist.map do |pg|
|
800
|
+
pg.map do |param|
|
801
|
+
if param =~ /^(stdin|-)$/i
|
802
|
+
$stdin
|
803
|
+
else
|
804
|
+
require 'open-uri'
|
805
|
+
begin
|
806
|
+
open param
|
807
|
+
rescue SystemCallError => e
|
808
|
+
raise CommandlineError, "file or url for option '#{self.name}' cannot be opened: #{e.message}"
|
809
|
+
end
|
810
|
+
end
|
811
|
+
end
|
812
|
+
end
|
813
|
+
end
|
814
|
+
end
|
782
815
|
|
783
|
-
|
816
|
+
# Option class for handling Strings.
|
817
|
+
class StringOption < Option
|
818
|
+
register_alias :string
|
819
|
+
def type_format ; "=<s>" ; end
|
820
|
+
def parse(paramlist, _neg_given)
|
821
|
+
paramlist.map { |pg| pg.map(&:to_s) }
|
822
|
+
end
|
823
|
+
end
|
784
824
|
|
785
|
-
|
786
|
-
|
825
|
+
# Option for dates. Uses Chronic if it exists.
|
826
|
+
class DateOption < Option
|
827
|
+
register_alias :date
|
828
|
+
def type_format ; "=<date>" ; end
|
829
|
+
def parse(paramlist, _neg_given)
|
830
|
+
paramlist.map do |pg|
|
831
|
+
pg.map do |param|
|
832
|
+
next param if param.is_a?(Date)
|
833
|
+
begin
|
834
|
+
begin
|
835
|
+
require 'chronic'
|
836
|
+
time = Chronic.parse(param)
|
837
|
+
rescue LoadError
|
838
|
+
# chronic is not available
|
839
|
+
end
|
840
|
+
time ? Date.new(time.year, time.month, time.day) : Date.parse(param)
|
841
|
+
rescue ArgumentError
|
842
|
+
raise CommandlineError, "option '#{self.name}' needs a date"
|
843
|
+
end
|
844
|
+
end
|
845
|
+
end
|
787
846
|
end
|
788
847
|
end
|
789
848
|
|
790
|
-
|
849
|
+
### MULTI_OPT_TYPES :
|
850
|
+
## The set of values that indicate a multiple-parameter option (i.e., that
|
851
|
+
## takes multiple space-separated values on the commandline) when passed as
|
852
|
+
## the +:type+ parameter of #opt.
|
853
|
+
|
854
|
+
# Option class for handling multiple Integers
|
855
|
+
class IntegerArrayOption < IntegerOption
|
856
|
+
register_alias :fixnums, :ints, :integers
|
857
|
+
def type_format ; "=<i+>" ; end
|
858
|
+
def multi_arg? ; true ; end
|
859
|
+
end
|
860
|
+
|
861
|
+
# Option class for handling multiple Floats
|
862
|
+
class FloatArrayOption < FloatOption
|
863
|
+
register_alias :doubles, :floats
|
864
|
+
def type_format ; "=<f+>" ; end
|
865
|
+
def multi_arg? ; true ; end
|
866
|
+
end
|
867
|
+
|
868
|
+
# Option class for handling multiple Strings
|
869
|
+
class StringArrayOption < StringOption
|
870
|
+
register_alias :strings
|
871
|
+
def type_format ; "=<s+>" ; end
|
872
|
+
def multi_arg? ; true ; end
|
873
|
+
end
|
874
|
+
|
875
|
+
# Option class for handling multiple dates
|
876
|
+
class DateArrayOption < DateOption
|
877
|
+
register_alias :dates
|
878
|
+
def type_format ; "=<date+>" ; end
|
879
|
+
def multi_arg? ; true ; end
|
880
|
+
end
|
881
|
+
|
882
|
+
# Option class for handling Files/URLs via 'open'
|
883
|
+
class IOArrayOption < IOOption
|
884
|
+
register_alias :ios
|
885
|
+
def type_format ; "=<filename/uri+>" ; end
|
886
|
+
def multi_arg? ; true ; end
|
887
|
+
end
|
888
|
+
|
889
|
+
## The easy, syntactic-sugary entry method into Optimist. Creates a Parser,
|
791
890
|
## passes the block to it, then parses +args+ with it, handling any errors or
|
792
891
|
## requests for help or version information appropriately (and then exiting).
|
793
892
|
## Modifies +args+ in place. Returns a hash of option values.
|
@@ -804,8 +903,8 @@ end
|
|
804
903
|
##
|
805
904
|
## Example:
|
806
905
|
##
|
807
|
-
## require '
|
808
|
-
## opts =
|
906
|
+
## require 'optimist'
|
907
|
+
## opts = Optimist::options do
|
809
908
|
## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
|
810
909
|
## opt :name, "Monkey name", :type => :string # a string --name <s>, defaulting to nil
|
811
910
|
## opt :num_limbs, "Number of limbs", :default => 4 # an integer --num-limbs <i>, defaulting to 4
|
@@ -817,13 +916,13 @@ end
|
|
817
916
|
## ## if called with --monkey
|
818
917
|
## p opts # => {:monkey=>true, :name=>nil, :num_limbs=>4, :help=>false, :monkey_given=>true}
|
819
918
|
##
|
820
|
-
## See more examples at http://
|
919
|
+
## See more examples at http://optimist.rubyforge.org.
|
821
920
|
def options(args = ARGV, *a, &b)
|
822
921
|
@last_parser = Parser.new(*a, &b)
|
823
922
|
with_standard_exception_handling(@last_parser) { @last_parser.parse args }
|
824
923
|
end
|
825
924
|
|
826
|
-
## If
|
925
|
+
## If Optimist::options doesn't do quite what you want, you can create a Parser
|
827
926
|
## object and call Parser#parse on it. That method will throw CommandlineError,
|
828
927
|
## HelpNeeded and VersionNeeded exceptions when necessary; if you want to
|
829
928
|
## have these handled for you in the standard manner (e.g. show the help
|
@@ -834,15 +933,15 @@ end
|
|
834
933
|
##
|
835
934
|
## Usage example:
|
836
935
|
##
|
837
|
-
## require '
|
838
|
-
## p =
|
936
|
+
## require 'optimist'
|
937
|
+
## p = Optimist::Parser.new do
|
839
938
|
## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
|
840
939
|
## opt :goat, "Use goat mode", :default => true # a flag --goat, defaulting to true
|
841
940
|
## end
|
842
941
|
##
|
843
|
-
## opts =
|
942
|
+
## opts = Optimist::with_standard_exception_handling p do
|
844
943
|
## o = p.parse ARGV
|
845
|
-
## raise
|
944
|
+
## raise Optimist::HelpNeeded if ARGV.empty? # show help screen
|
846
945
|
## o
|
847
946
|
## end
|
848
947
|
##
|
@@ -877,12 +976,16 @@ end
|
|
877
976
|
## opt :whatever # ...
|
878
977
|
## end
|
879
978
|
##
|
880
|
-
##
|
979
|
+
## Optimist::die "need at least one filename" if ARGV.empty?
|
980
|
+
##
|
981
|
+
## An exit code can be provide if needed
|
982
|
+
##
|
983
|
+
## Optimist::die "need at least one filename", -2 if ARGV.empty?
|
881
984
|
def die(arg, msg = nil, error_code = nil)
|
882
985
|
if @last_parser
|
883
986
|
@last_parser.die arg, msg, error_code
|
884
987
|
else
|
885
|
-
raise ArgumentError, "
|
988
|
+
raise ArgumentError, "Optimist::die can only be called after Optimist::options"
|
886
989
|
end
|
887
990
|
end
|
888
991
|
|
@@ -897,13 +1000,13 @@ end
|
|
897
1000
|
## EOS
|
898
1001
|
## end
|
899
1002
|
##
|
900
|
-
##
|
1003
|
+
## Optimist::educate if ARGV.empty?
|
901
1004
|
def educate
|
902
1005
|
if @last_parser
|
903
1006
|
@last_parser.educate
|
904
1007
|
exit
|
905
1008
|
else
|
906
|
-
raise ArgumentError, "
|
1009
|
+
raise ArgumentError, "Optimist::educate can only be called after Optimist::options"
|
907
1010
|
end
|
908
1011
|
end
|
909
1012
|
|