firebase-ruby 0.2.0.1 → 0.3.0

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