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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 9ed31533488ea96baffad5a57685f0270a38e3c4
4
- data.tar.gz: 21e8b7d9747ef1f43a614de297a54182781ccb0f
2
+ SHA256:
3
+ metadata.gz: c81422955349a239c73df7add0f495be525dee6df19fb99c07fcc7a97ad5ea9b
4
+ data.tar.gz: c28b48f45899cc15a405baf006addb187eb80379d144522f9412ec5c3244daac
5
5
  SHA512:
6
- metadata.gz: bfb8b6e5680a6be288405e3f3b69f074afbcb91849403d2b816c37985f079b6759e79cb35b06ded436c422d390b48db0f4406e1cf67b04d6e0981cfadd36e499
7
- data.tar.gz: ef0cfe88733e0f8f6d17f5dd5f05b26e9bfcbe8e530b2f3ecc8ff0e3f15c23202685c031f68a35ae119f88d34ccc025220fd22eada415f36174f958098285421
6
+ metadata.gz: 1c81dbcaef8b12f675a85d07488c6979161d2a2bd3bdada1e5e3fd69a6b38745a396f44c80bda7e88a00b310ee7febde6f300025378c8614d4f3764b284bcf3f
7
+ data.tar.gz: 267c6bdbef1be804ddcccd6c20d534fa6f0a874751fa0479c240079933da0d9ea4cc0a22931fae3ebfb12551f6ff075f8849a0e2627ef56e150afca770372663
data/.codeclimate.yml CHANGED
@@ -15,4 +15,4 @@ ratings:
15
15
  - "**"
16
16
 
17
17
  exclude_paths:
18
- - lib/firebase-ruby/trollop.rb
18
+ - lib/firebase-ruby/optimist.rb
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/OAuth2ServiceAccount).
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/trollop'
2
+ require 'firebase-ruby/optimist'
3
3
  require 'firebase-ruby'
4
4
  require 'json'
5
5
 
6
6
 
7
- opts = Trollop::options do
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
- Trollop::die :path, "is missing" if path.nil?
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
- case method
43
- when :get, :delete
44
- data = db.public_send(method, path)
45
- when :put, :patch, :post
46
- if opts[:data_given]
47
- data = db.public_send(method, path, opts[:data])
48
- else
49
- Trollop::die :data, "is missing"
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
- if opts[:ruby]
55
- puts data
56
- else
57
- json_opts = {indent: ' ', space: ' ', object_nl: "\n", array_nl: "\n"}
58
- puts JSON.fast_generate(data, json_opts)
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
@@ -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{A simple Firebase Realtime Database REST API library in pure Ruby.}
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', '~> 1.5'
20
+ s.add_runtime_dependency 'jwt', '~> 2.2'
21
21
  end
data/lib/firebase-ruby.rb CHANGED
@@ -1,4 +1,3 @@
1
1
  require 'firebase-ruby/logger'
2
2
  require 'firebase-ruby/database'
3
3
  require 'firebase-ruby/auth'
4
- require 'firebase-ruby/http'
@@ -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://www.googleapis.com/oauth2/v4/token'
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://www.googleapis.com/oauth2/v4/token'
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
- return nil
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 to Google')
68
- res = HTTP.post_form(GOOGLE_TOKEN_URL, jwt)
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
- module Firebase
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
- module Firebase
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
- def self.get(url, params)
18
- h = HTTP.new(url)
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
- def self.post_form(url, params)
25
- h = HTTP.new(url)
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
- self.headers = hdrs
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
- Firebase.logger.info('Created urlencoded query from params')
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
- Firebase.logger.info('Created form data from params')
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, params: params, body: body, query: query) if data.class <= URI::HTTP
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
- Firebase.logger.info('HTTP session not started; starting now')
156
+ logger.info('HTTP session not started; starting now')
106
157
  http.start
107
- Firebase.logger.debug("Opened connection to #{http.address}:#{http.port}")
158
+ logger.debug("Opened connection to #{http.address}:#{http.port}")
108
159
  end
109
- Firebase.logger.debug("Sending HTTP #{req.method} request to #{req.path}")
110
- Firebase.logger.debug("Body size: #{req.body.length}") if req.request_body_permitted?
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 do |k, v|
118
- req[k] = v
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
- Firebase.logger.info('HTTP response header says connection close; closing session now')
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
- Firebase.logger.info('HTTP response was a redirect')
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
- Firebase.logger.debug("Full URI object built for local redirect with path: #{data.path}")
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, params: nil, body: nil, query: nil)
199
+ def redirect(method, uri:, body: nil)
151
200
  if uri.host == init_uri.host && uri.port == init_uri.port
152
- Firebase.logger.info("Local #{method.upcase} redirect, reusing HTTP session")
153
- new_http = http
201
+ logger.info("Local #{method.upcase} redirect, reusing HTTP session")
202
+ new_http = self
154
203
  else
155
- Firebase.logger.info("External #{method.upcase} redirect, spawning new HTTP object")
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
- case method
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/trollop.rb -- trollop command-line processing library
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
- # trollop is licensed under the MIT license.
4
+ # optimist is licensed under the MIT license.
5
5
 
6
6
  require 'date'
7
7
 
8
- module Trollop
8
+ module Optimist
9
9
  # note: this is duplicated in gemspec
10
10
  # please change over there too
11
- VERSION = "2.1.2"
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 Trollop::options entry.
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 Trollop#options.
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 Trollop#options.
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 Trollop::options. In this case, only the
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
- ## Trollop::with_standard_exception_handling.
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 Trollop::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+.
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, Trollop assumes that it's not a multi-value argument.
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 Trollop::die.
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 Trollop
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 Trollop::options,
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 0 if ignore_invalid_options && !sym
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.nil?
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
- case opts.type
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 do |name, spec|
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.desc + begin
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 Trollop::die (see that for documentation).
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
- if params.empty?
497
- yield args[i], nil
498
- i += 1
499
- else
500
- num_params_taken = yield args[i], params
501
- unless num_params_taken
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
- i += 1 + num_params_taken
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
- if params.empty?
516
- yield "-#{a}", nil
517
- i += 1
518
- else
519
- num_params_taken = yield "-#{a}", params
520
- unless num_params_taken
521
- if @stop_on_unknown
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
- i += 1 + num_params_taken
491
+ else
492
+ i += num_params_taken
528
493
  end
529
494
  else
530
- yield "-#{a}", nil
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
- ## The set of values that indicate a single-parameter (normal) option when
631
- ## passed as the +:type+ parameter of #opt.
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
- ## for options with :multi => true, an array default doesn't imply
675
- ## a multi-valued argument. for that you have to specify a :type
676
- ## as well. (this is how we disambiguate an ambiguous situation;
677
- ## see the docs for Parser#opt for details.)
678
- disambiguated_default = if opts[:multi] && opts[:default].kind_of?(Array) && !opts[:type]
679
- opts[:default].first
680
- else
681
- opts[:default]
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
- type_from_default =
685
- case disambiguated_default
686
- when Integer then :int
687
- when Numeric then :float
688
- when TrueClass,
689
- FalseClass then :flag
690
- when String then :string
691
- when IO then :io
692
- when Date then :date
693
- when Array
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 #{type_from_default})" if opts[:type] && type_from_default && opts[:type] != type_from_default
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
- opts[:type] = opts[:type] || type_from_default || :flag
671
+ opt_inst = (opttype || opttype_from_default || Optimist::BooleanOption.new)
720
672
 
721
673
  ## fill in :long
722
- opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
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
- opts[:short] = opts[:short].to_s if opts[:short] && opts[:short] != :none
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
- if opts[:short]
738
- raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ ::Trollop::Parser::INVALID_SHORT_ARG_REGEX
739
- end
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
- opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
684
+ defvalue = opts[:default] || opt_inst.default
743
685
 
744
686
  ## autobox :default for :multi (multi-occurrence) arguments
745
- opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].kind_of?(Array)
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
- ## fill in :multi
748
- opts[:multi] ||= false
694
+ private
749
695
 
750
- self.name = name
751
- self.opts = opts
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 key?(name)
755
- opts.key?(name)
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 type ; opts[:type] ; end
759
- def flag? ; type == :flag ; end
760
- def single_arg?
761
- SINGLE_ARG_TYPES.include?(type)
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 multi ; opts[:multi] ; end
765
- alias multi? multi
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
- def multi_arg?
768
- MULTI_ARG_TYPES.include?(type)
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
- def default ; opts[:default] ; end
772
- #? def multi_default ; opts.default || opts.multi && [] ; end
773
- def array_default? ; opts[:default].kind_of?(Array) ; end
749
+ end
774
750
 
775
- def short ; opts[:short] ; end
776
- def short? ; short && short != :none ; end
777
- # not thrilled about this
778
- def short=(val) ; opts[:short] = val ; end
779
- def long ; opts[:long] ; end
780
- def callback ; opts[:callback] ; end
781
- def desc ; opts[:desc] ; end
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
- def required? ; opts[:required] ; end
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
- def self.create(name, desc="", opts={})
786
- new(name, desc, opts)
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
- ## The easy, syntactic-sugary entry method into Trollop. Creates a Parser,
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 'trollop'
808
- ## opts = Trollop::options do
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://trollop.rubyforge.org.
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 Trollop::options doesn't do quite what you want, you can create a Parser
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 'trollop'
838
- ## p = Trollop::Parser.new do
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 = Trollop::with_standard_exception_handling p do
942
+ ## opts = Optimist::with_standard_exception_handling p do
844
943
  ## o = p.parse ARGV
845
- ## raise Trollop::HelpNeeded if ARGV.empty? # show help screen
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
- ## Trollop::die "need at least one filename" if ARGV.empty?
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, "Trollop::die can only be called after Trollop::options"
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
- ## Trollop::educate if ARGV.empty?
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, "Trollop::educate can only be called after Trollop::options"
1009
+ raise ArgumentError, "Optimist::educate can only be called after Optimist::options"
907
1010
  end
908
1011
  end
909
1012