pagy 5.7.5 → 8.6.2

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.
Files changed (153) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/apps/calendar.ru +745 -0
  4. data/apps/demo.ru +435 -0
  5. data/apps/rails.ru +213 -0
  6. data/apps/repro.ru +177 -0
  7. data/apps/tmp/calendar.sqlite3 +0 -0
  8. data/apps/tmp/calendar.sqlite3-shm +0 -0
  9. data/apps/tmp/calendar.sqlite3-wal +0 -0
  10. data/apps/tmp/local_secret.txt +1 -0
  11. data/bin/pagy +100 -0
  12. data/{lib/config → config}/pagy.rb +72 -96
  13. data/javascripts/pagy-module.js +100 -0
  14. data/javascripts/pagy.d.ts +5 -0
  15. data/javascripts/pagy.js +4 -0
  16. data/javascripts/pagy.min.js +4 -0
  17. data/javascripts/pagy.min.js.map +10 -0
  18. data/javascripts/pagy.mjs +100 -0
  19. data/lib/optimist.rb +1022 -0
  20. data/lib/pagy/backend.rb +16 -4
  21. data/lib/pagy/calendar/day.rb +22 -10
  22. data/lib/pagy/calendar/month.rb +35 -9
  23. data/lib/pagy/calendar/quarter.rb +36 -10
  24. data/lib/pagy/calendar/unit.rb +106 -0
  25. data/lib/pagy/calendar/week.rb +16 -16
  26. data/lib/pagy/calendar/year.rb +15 -9
  27. data/lib/pagy/calendar.rb +60 -68
  28. data/lib/pagy/console.rb +3 -3
  29. data/lib/pagy/countless.rb +17 -12
  30. data/lib/pagy/exceptions.rb +1 -1
  31. data/lib/pagy/extras/arel.rb +2 -2
  32. data/lib/pagy/extras/array.rb +2 -2
  33. data/lib/pagy/extras/bootstrap.rb +57 -53
  34. data/lib/pagy/extras/bulma.rb +53 -57
  35. data/lib/pagy/extras/calendar.rb +54 -37
  36. data/lib/pagy/extras/countless.rb +3 -3
  37. data/lib/pagy/extras/elasticsearch_rails.rb +12 -11
  38. data/lib/pagy/extras/foundation.rb +59 -54
  39. data/lib/pagy/extras/gearbox.rb +32 -19
  40. data/lib/pagy/extras/headers.rb +11 -11
  41. data/lib/pagy/extras/i18n.rb +5 -5
  42. data/lib/pagy/extras/items.rb +34 -24
  43. data/lib/pagy/extras/{frontend_helpers.rb → js_tools.rb} +24 -24
  44. data/lib/pagy/extras/jsonapi.rb +79 -0
  45. data/lib/pagy/extras/materialize.rb +63 -47
  46. data/lib/pagy/extras/meilisearch.rb +26 -22
  47. data/lib/pagy/extras/metadata.rb +13 -9
  48. data/lib/pagy/extras/overflow.rb +13 -10
  49. data/lib/pagy/extras/pagy.rb +82 -0
  50. data/lib/pagy/extras/searchkick.rb +12 -11
  51. data/lib/pagy/extras/semantic.rb +57 -45
  52. data/lib/pagy/extras/size.rb +40 -0
  53. data/lib/pagy/extras/standalone.rb +12 -16
  54. data/lib/pagy/extras/trim.rb +13 -13
  55. data/lib/pagy/extras/uikit.rb +60 -46
  56. data/lib/pagy/frontend.rb +62 -45
  57. data/lib/pagy/i18n.rb +4 -3
  58. data/lib/pagy/url_helpers.rb +13 -25
  59. data/lib/pagy.rb +61 -59
  60. data/locales/ar.yml +28 -0
  61. data/locales/be.yml +25 -0
  62. data/locales/bg.yml +21 -0
  63. data/locales/bs.yml +25 -0
  64. data/locales/ca.yml +21 -0
  65. data/locales/ckb.yml +18 -0
  66. data/locales/cs.yml +23 -0
  67. data/locales/da.yml +21 -0
  68. data/locales/de.yml +21 -0
  69. data/locales/en.yml +21 -0
  70. data/locales/es.yml +21 -0
  71. data/locales/fr.yml +21 -0
  72. data/locales/hr.yml +25 -0
  73. data/locales/id.yml +19 -0
  74. data/locales/it.yml +21 -0
  75. data/locales/ja.yml +19 -0
  76. data/locales/km.yml +19 -0
  77. data/locales/ko.yml +17 -0
  78. data/locales/nb.yml +21 -0
  79. data/locales/nl.yml +21 -0
  80. data/locales/nn.yml +21 -0
  81. data/locales/pl.yml +25 -0
  82. data/locales/pt-BR.yml +21 -0
  83. data/locales/pt.yml +21 -0
  84. data/locales/ru.yml +25 -0
  85. data/locales/sr.yml +25 -0
  86. data/locales/sv-SE.yml +21 -0
  87. data/locales/sv.yml +21 -0
  88. data/locales/sw.yml +23 -0
  89. data/locales/ta.yml +23 -0
  90. data/locales/tr.yml +19 -0
  91. data/locales/uk.yml +25 -0
  92. data/locales/vi.yml +17 -0
  93. data/locales/zh-CN.yml +19 -0
  94. data/locales/zh-HK.yml +19 -0
  95. data/locales/zh-TW.yml +19 -0
  96. data/stylesheets/pagy.css +46 -0
  97. data/stylesheets/pagy.scss +48 -0
  98. data/stylesheets/pagy.tailwind.css +21 -0
  99. metadata +79 -66
  100. data/lib/javascripts/pagy-dev.js +0 -118
  101. data/lib/javascripts/pagy-module.d.ts +0 -30
  102. data/lib/javascripts/pagy-module.js +0 -117
  103. data/lib/javascripts/pagy.js +0 -1
  104. data/lib/locales/ar.yml +0 -26
  105. data/lib/locales/bg.yml +0 -22
  106. data/lib/locales/bs.yml +0 -24
  107. data/lib/locales/ca.yml +0 -22
  108. data/lib/locales/cs.yml +0 -22
  109. data/lib/locales/da.yml +0 -22
  110. data/lib/locales/de.yml +0 -22
  111. data/lib/locales/en.yml +0 -22
  112. data/lib/locales/es.yml +0 -22
  113. data/lib/locales/fr.yml +0 -22
  114. data/lib/locales/hr.yml +0 -24
  115. data/lib/locales/id.yml +0 -20
  116. data/lib/locales/it.yml +0 -22
  117. data/lib/locales/ja.yml +0 -20
  118. data/lib/locales/km.yml +0 -19
  119. data/lib/locales/ko.yml +0 -20
  120. data/lib/locales/nb.yml +0 -22
  121. data/lib/locales/nl.yml +0 -22
  122. data/lib/locales/pl.yml +0 -24
  123. data/lib/locales/pt-BR.yml +0 -22
  124. data/lib/locales/pt.yml +0 -22
  125. data/lib/locales/ru.yml +0 -24
  126. data/lib/locales/sr.yml +0 -23
  127. data/lib/locales/sv-SE.yml +0 -23
  128. data/lib/locales/sv.yml +0 -23
  129. data/lib/locales/sw.yml +0 -22
  130. data/lib/locales/ta.yml +0 -22
  131. data/lib/locales/tr.yml +0 -20
  132. data/lib/locales/uk.yml +0 -24
  133. data/lib/locales/zh-CN.yml +0 -20
  134. data/lib/locales/zh-HK.yml +0 -20
  135. data/lib/locales/zh-TW.yml +0 -20
  136. data/lib/pagy/calendar/month_mixin.rb +0 -49
  137. data/lib/pagy/extras/navs.rb +0 -63
  138. data/lib/pagy/extras/support.rb +0 -54
  139. data/lib/templates/bootstrap_nav.html.erb +0 -24
  140. data/lib/templates/bootstrap_nav.html.haml +0 -34
  141. data/lib/templates/bootstrap_nav.html.slim +0 -34
  142. data/lib/templates/bulma_nav.html.erb +0 -24
  143. data/lib/templates/bulma_nav.html.haml +0 -32
  144. data/lib/templates/bulma_nav.html.slim +0 -32
  145. data/lib/templates/foundation_nav.html.erb +0 -24
  146. data/lib/templates/foundation_nav.html.haml +0 -34
  147. data/lib/templates/foundation_nav.html.slim +0 -34
  148. data/lib/templates/nav.html.erb +0 -22
  149. data/lib/templates/nav.html.haml +0 -30
  150. data/lib/templates/nav.html.slim +0 -29
  151. data/lib/templates/uikit_nav.html.erb +0 -15
  152. data/lib/templates/uikit_nav.html.haml +0 -28
  153. data/lib/templates/uikit_nav.html.slim +0 -28
data/lib/optimist.rb ADDED
@@ -0,0 +1,1022 @@
1
+ # lib/optimist.rb -- optimist command-line processing library
2
+ # Copyright (c) 2008-2014 William Morgan.
3
+ # Copyright (c) 2014 Red Hat, Inc.
4
+ # optimist is licensed under the MIT license.
5
+
6
+ require 'date'
7
+
8
+ module Optimist
9
+ VERSION = "3.1.0"
10
+
11
+ ## Thrown by Parser in the event of a commandline error. Not needed if
12
+ ## you're using the Optimist::options entry.
13
+ class CommandlineError < StandardError
14
+ attr_reader :error_code
15
+
16
+ def initialize(msg, error_code = nil)
17
+ super(msg)
18
+ @error_code = error_code
19
+ end
20
+ end
21
+
22
+ ## Thrown by Parser if the user passes in '-h' or '--help'. Handled
23
+ ## automatically by Optimist#options.
24
+ class HelpNeeded < StandardError
25
+ end
26
+
27
+ ## Thrown by Parser if the user passes in '-v' or '--version'. Handled
28
+ ## automatically by Optimist#options.
29
+ class VersionNeeded < StandardError
30
+ end
31
+
32
+ ## Regex for floating point numbers
33
+ FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/
34
+
35
+ ## Regex for parameters
36
+ PARAM_RE = /^-(-|\.$|[^\d\.])/
37
+
38
+ ## The commandline parser. In typical usage, the methods in this class
39
+ ## will be handled internally by Optimist::options. In this case, only the
40
+ ## #opt, #banner and #version, #depends, and #conflicts methods will
41
+ ## typically be called.
42
+ ##
43
+ ## If you want to instantiate this class yourself (for more complicated
44
+ ## argument-parsing logic), call #parse to actually produce the output hash,
45
+ ## and consider calling it from within
46
+ ## Optimist::with_standard_exception_handling.
47
+ class Parser
48
+
49
+ ## The registry is a class-instance-variable map of option aliases to their subclassed Option class.
50
+ @registry = {}
51
+
52
+ ## The Option subclasses are responsible for registering themselves using this function.
53
+ def self.register(lookup, klass)
54
+ @registry[lookup.to_sym] = klass
55
+ end
56
+
57
+ ## Gets the class from the registry.
58
+ ## Can be given either a class-name, e.g. Integer, a string, e.g "integer", or a symbol, e.g :integer
59
+ def self.registry_getopttype(type)
60
+ return nil unless type
61
+ if type.respond_to?(:name)
62
+ type = type.name
63
+ lookup = type.downcase.to_sym
64
+ else
65
+ lookup = type.to_sym
66
+ end
67
+ raise ArgumentError, "Unsupported argument type '#{type}', registry lookup '#{lookup}'" unless @registry.has_key?(lookup)
68
+ return @registry[lookup].new
69
+ end
70
+
71
+ INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
72
+
73
+ ## The values from the commandline that were not interpreted by #parse.
74
+ attr_reader :leftovers
75
+
76
+ ## The complete configuration hashes for each option. (Mainly useful
77
+ ## for testing.)
78
+ attr_reader :specs
79
+
80
+ ## A flag that determines whether or not to raise an error if the parser is passed one or more
81
+ ## options that were not registered ahead of time. If 'true', then the parser will simply
82
+ ## ignore options that it does not recognize.
83
+ attr_accessor :ignore_invalid_options
84
+
85
+ ## Initializes the parser, and instance-evaluates any block given.
86
+ def initialize(*a, &b)
87
+ @version = nil
88
+ @leftovers = []
89
+ @specs = {}
90
+ @long = {}
91
+ @short = {}
92
+ @order = []
93
+ @constraints = []
94
+ @stop_words = []
95
+ @stop_on_unknown = false
96
+ @educate_on_error = false
97
+ @synopsis = nil
98
+ @usage = nil
99
+
100
+ # instance_eval(&b) if b # can't take arguments
101
+ cloaker(&b).bind(self).call(*a) if b
102
+ end
103
+
104
+ ## Define an option. +name+ is the option name, a unique identifier
105
+ ## for the option that you will use internally, which should be a
106
+ ## symbol or a string. +desc+ is a string description which will be
107
+ ## displayed in help messages.
108
+ ##
109
+ ## Takes the following optional arguments:
110
+ ##
111
+ ## [+: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.
112
+ ## [+: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.
113
+ ## [+: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.
114
+ ## [+: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+.
115
+ ## [+:required+] If set to +true+, the argument must be provided on the commandline.
116
+ ## [+: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.)
117
+ ##
118
+ ## Note that there are two types of argument multiplicity: an argument
119
+ ## can take multiple values, e.g. "--arg 1 2 3". An argument can also
120
+ ## be allowed to occur multiple times, e.g. "--arg 1 --arg 2".
121
+ ##
122
+ ## Arguments that take multiple values should have a +:type+ parameter
123
+ ## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+
124
+ ## value of an array of the correct type (e.g. [String]). The
125
+ ## value of this argument will be an array of the parameters on the
126
+ ## commandline.
127
+ ##
128
+ ## Arguments that can occur multiple times should be marked with
129
+ ## +:multi+ => +true+. The value of this argument will also be an array.
130
+ ## In contrast with regular non-multi options, if not specified on
131
+ ## the commandline, the default value will be [], not nil.
132
+ ##
133
+ ## These two attributes can be combined (e.g. +:type+ => +:strings+,
134
+ ## +:multi+ => +true+), in which case the value of the argument will be
135
+ ## an array of arrays.
136
+ ##
137
+ ## There's one ambiguous case to be aware of: when +:multi+: is true and a
138
+ ## +:default+ is set to an array (of something), it's ambiguous whether this
139
+ ## is a multi-value argument as well as a multi-occurrence argument.
140
+ ## In this case, Optimist assumes that it's not a multi-value argument.
141
+ ## If you want a multi-value, multi-occurrence argument with a default
142
+ ## value, you must specify +:type+ as well.
143
+
144
+ def opt(name, desc = "", opts = {}, &b)
145
+ opts[:callback] ||= b if block_given?
146
+ opts[:desc] ||= desc
147
+
148
+ o = Option.create(name, desc, opts)
149
+
150
+ raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? o.name
151
+ raise ArgumentError, "long option name #{o.long.inspect} is already taken; please specify a (different) :long" if @long[o.long]
152
+ raise ArgumentError, "short option name #{o.short.inspect} is already taken; please specify a (different) :short" if @short[o.short]
153
+ @long[o.long] = o.name
154
+ @short[o.short] = o.name if o.short?
155
+ @specs[o.name] = o
156
+ @order << [:opt, o.name]
157
+ end
158
+
159
+ ## Sets the version string. If set, the user can request the version
160
+ ## on the commandline. Should probably be of the form "<program name>
161
+ ## <version number>".
162
+ def version(s = nil)
163
+ s ? @version = s : @version
164
+ end
165
+
166
+ ## Sets the usage string. If set the message will be printed as the
167
+ ## first line in the help (educate) output and ending in two new
168
+ ## lines.
169
+ def usage(s = nil)
170
+ s ? @usage = s : @usage
171
+ end
172
+
173
+ ## Adds a synopsis (command summary description) right below the
174
+ ## usage line, or as the first line if usage isn't specified.
175
+ def synopsis(s = nil)
176
+ s ? @synopsis = s : @synopsis
177
+ end
178
+
179
+ ## Adds text to the help display. Can be interspersed with calls to
180
+ ## #opt to build a multi-section help page.
181
+ def banner(s)
182
+ @order << [:text, s]
183
+ end
184
+ alias_method :text, :banner
185
+
186
+ ## Marks two (or more!) options as requiring each other. Only handles
187
+ ## undirected (i.e., mutual) dependencies. Directed dependencies are
188
+ ## better modeled with Optimist::die.
189
+ def depends(*syms)
190
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
191
+ @constraints << [:depends, syms]
192
+ end
193
+
194
+ ## Marks two (or more!) options as conflicting.
195
+ def conflicts(*syms)
196
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
197
+ @constraints << [:conflicts, syms]
198
+ end
199
+
200
+ ## Marks two (or more!) options as required but mutually exclusive.
201
+ def either(*syms)
202
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
203
+ @constraints << [:conflicts, syms]
204
+ @constraints << [:either, syms]
205
+ end
206
+
207
+ ## Defines a set of words which cause parsing to terminate when
208
+ ## encountered, such that any options to the left of the word are
209
+ ## parsed as usual, and options to the right of the word are left
210
+ ## intact.
211
+ ##
212
+ ## A typical use case would be for subcommand support, where these
213
+ ## would be set to the list of subcommands. A subsequent Optimist
214
+ ## invocation would then be used to parse subcommand options, after
215
+ ## shifting the subcommand off of ARGV.
216
+ def stop_on(*words)
217
+ @stop_words = [*words].flatten
218
+ end
219
+
220
+ ## Similar to #stop_on, but stops on any unknown word when encountered
221
+ ## (unless it is a parameter for an argument). This is useful for
222
+ ## cases where you don't know the set of subcommands ahead of time,
223
+ ## i.e., without first parsing the global options.
224
+ def stop_on_unknown
225
+ @stop_on_unknown = true
226
+ end
227
+
228
+ ## Instead of displaying "Try --help for help." on an error
229
+ ## display the usage (via educate)
230
+ def educate_on_error
231
+ @educate_on_error = true
232
+ end
233
+
234
+ ## Parses the commandline. Typically called by Optimist::options,
235
+ ## but you can call it directly if you need more control.
236
+ ##
237
+ ## throws CommandlineError, HelpNeeded, and VersionNeeded exceptions.
238
+ def parse(cmdline = ARGV)
239
+ vals = {}
240
+ required = {}
241
+
242
+ opt :version, "Print version and exit" if @version && ! (@specs[:version] || @long["version"])
243
+ opt :help, "Show this message" unless @specs[:help] || @long["help"]
244
+
245
+ @specs.each do |sym, opts|
246
+ required[sym] = true if opts.required?
247
+ vals[sym] = opts.default
248
+ vals[sym] = [] if opts.multi && !opts.default # multi arguments default to [], not nil
249
+ end
250
+
251
+ resolve_default_short_options!
252
+
253
+ ## resolve symbols
254
+ given_args = {}
255
+ @leftovers = each_arg cmdline do |arg, params|
256
+ ## handle --no- forms
257
+ arg, negative_given = if arg =~ /^--no-([^-]\S*)$/
258
+ ["--#{$1}", true]
259
+ else
260
+ [arg, false]
261
+ end
262
+
263
+ sym = case arg
264
+ when /^-([^-])$/ then @short[$1]
265
+ when /^--([^-]\S*)$/ then @long[$1] || @long["no-#{$1}"]
266
+ else raise CommandlineError, "invalid argument syntax: '#{arg}'"
267
+ end
268
+
269
+ sym = nil if arg =~ /--no-/ # explicitly invalidate --no-no- arguments
270
+
271
+ next nil if ignore_invalid_options && !sym
272
+ raise CommandlineError, "unknown argument '#{arg}'" unless sym
273
+
274
+ if given_args.include?(sym) && !@specs[sym].multi?
275
+ raise CommandlineError, "option '#{arg}' specified multiple times"
276
+ end
277
+
278
+ given_args[sym] ||= {}
279
+ given_args[sym][:arg] = arg
280
+ given_args[sym][:negative_given] = negative_given
281
+ given_args[sym][:params] ||= []
282
+
283
+ # The block returns the number of parameters taken.
284
+ num_params_taken = 0
285
+
286
+ unless params.empty?
287
+ if @specs[sym].single_arg?
288
+ given_args[sym][:params] << params[0, 1] # take the first parameter
289
+ num_params_taken = 1
290
+ elsif @specs[sym].multi_arg?
291
+ given_args[sym][:params] << params # take all the parameters
292
+ num_params_taken = params.size
293
+ end
294
+ end
295
+
296
+ num_params_taken
297
+ end
298
+
299
+ ## check for version and help args
300
+ raise VersionNeeded if given_args.include? :version
301
+ raise HelpNeeded if given_args.include? :help
302
+
303
+ ## check constraint satisfaction
304
+ @constraints.each do |type, syms|
305
+ constraint_sym = syms.find { |sym| given_args[sym] }
306
+
307
+ case type
308
+ when :depends
309
+ next unless constraint_sym
310
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym].long} requires --#{@specs[sym].long}" unless given_args.include? sym }
311
+ when :conflicts
312
+ next unless constraint_sym
313
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym].long} conflicts with --#{@specs[sym].long}" if given_args.include?(sym) && (sym != constraint_sym) }
314
+ when :either
315
+ raise CommandlineError, "one of #{syms.map { |sym| "--#{@specs[sym].long}" }.join(', ') } is required" if (syms & given_args.keys).size != 1
316
+ end
317
+ end
318
+
319
+ required.each do |sym, val|
320
+ raise CommandlineError, "option --#{@specs[sym].long} must be specified" unless given_args.include? sym
321
+ end
322
+
323
+ ## parse parameters
324
+ given_args.each do |sym, given_data|
325
+ arg, params, negative_given = given_data.values_at :arg, :params, :negative_given
326
+
327
+ opts = @specs[sym]
328
+ if params.empty? && !opts.flag?
329
+ raise CommandlineError, "option '#{arg}' needs a parameter" unless opts.default
330
+ params << (opts.array_default? ? opts.default.clone : [opts.default])
331
+ end
332
+
333
+ vals["#{sym}_given".intern] = true # mark argument as specified on the commandline
334
+
335
+ vals[sym] = opts.parse(params, negative_given)
336
+
337
+ if opts.single_arg?
338
+ if opts.multi? # multiple options, each with a single parameter
339
+ vals[sym] = vals[sym].map { |p| p[0] }
340
+ else # single parameter
341
+ vals[sym] = vals[sym][0][0]
342
+ end
343
+ elsif opts.multi_arg? && !opts.multi?
344
+ vals[sym] = vals[sym][0] # single option, with multiple parameters
345
+ end
346
+ # else: multiple options, with multiple parameters
347
+
348
+ opts.callback.call(vals[sym]) if opts.callback
349
+ end
350
+
351
+ ## modify input in place with only those
352
+ ## arguments we didn't process
353
+ cmdline.clear
354
+ @leftovers.each { |l| cmdline << l }
355
+
356
+ ## allow openstruct-style accessors
357
+ class << vals
358
+ def method_missing(m, *_args)
359
+ self[m] || self[m.to_s]
360
+ end
361
+ end
362
+ vals
363
+ end
364
+
365
+ ## Print the help message to +stream+.
366
+ def educate(stream = $stdout)
367
+ width # hack: calculate it now; otherwise we have to be careful not to
368
+ # call this unless the cursor's at the beginning of a line.
369
+
370
+ left = {}
371
+ @specs.each { |name, spec| left[name] = spec.educate }
372
+
373
+ leftcol_width = left.values.map(&:length).max || 0
374
+ rightcol_start = leftcol_width + 6 # spaces
375
+
376
+ unless @order.size > 0 && @order.first.first == :text
377
+ command_name = File.basename($0).gsub(/\.[^.]+$/, '')
378
+ stream.puts "Usage: #{command_name} #{@usage}\n" if @usage
379
+ stream.puts "#{@synopsis}\n" if @synopsis
380
+ stream.puts if @usage || @synopsis
381
+ stream.puts "#{@version}\n" if @version
382
+ stream.puts "Options:"
383
+ end
384
+
385
+ @order.each do |what, opt|
386
+ if what == :text
387
+ stream.puts wrap(opt)
388
+ next
389
+ end
390
+
391
+ spec = @specs[opt]
392
+ stream.printf " %-#{leftcol_width}s ", left[opt]
393
+ desc = spec.description_with_default
394
+
395
+ stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
396
+ end
397
+ end
398
+
399
+ def width #:nodoc:
400
+ @width ||= if $stdout.tty?
401
+ begin
402
+ require 'io/console'
403
+ w = IO.console.winsize.last
404
+ w.to_i > 0 ? w : 80
405
+ rescue LoadError, NoMethodError, Errno::ENOTTY, Errno::EBADF, Errno::EINVAL
406
+ legacy_width
407
+ end
408
+ else
409
+ 80
410
+ end
411
+ end
412
+
413
+ def legacy_width
414
+ # Support for older Rubies where io/console is not available
415
+ `tput cols`.to_i
416
+ rescue Errno::ENOENT
417
+ 80
418
+ end
419
+ private :legacy_width
420
+
421
+ def wrap(str, opts = {}) # :nodoc:
422
+ if str == ""
423
+ [""]
424
+ else
425
+ inner = false
426
+ str.split("\n").map do |s|
427
+ line = wrap_line s, opts.merge(:inner => inner)
428
+ inner = true
429
+ line
430
+ end.flatten
431
+ end
432
+ end
433
+
434
+ ## The per-parser version of Optimist::die (see that for documentation).
435
+ def die(arg, msg = nil, error_code = nil)
436
+ msg, error_code = nil, msg if msg.kind_of?(Integer)
437
+ if msg
438
+ $stderr.puts "Error: argument --#{@specs[arg].long} #{msg}."
439
+ else
440
+ $stderr.puts "Error: #{arg}."
441
+ end
442
+ if @educate_on_error
443
+ $stderr.puts
444
+ educate $stderr
445
+ else
446
+ $stderr.puts "Try --help for help."
447
+ end
448
+ exit(error_code || -1)
449
+ end
450
+
451
+ private
452
+
453
+ ## yield successive arg, parameter pairs
454
+ def each_arg(args)
455
+ remains = []
456
+ i = 0
457
+
458
+ until i >= args.length
459
+ return remains += args[i..-1] if @stop_words.member? args[i]
460
+ case args[i]
461
+ when /^--$/ # arg terminator
462
+ return remains += args[(i + 1)..-1]
463
+ when /^--(\S+?)=(.*)$/ # long argument with equals
464
+ num_params_taken = yield "--#{$1}", [$2]
465
+ if num_params_taken.nil?
466
+ remains << args[i]
467
+ if @stop_on_unknown
468
+ return remains += args[i + 1..-1]
469
+ end
470
+ end
471
+ i += 1
472
+ when /^--(\S+)$/ # long argument
473
+ params = collect_argument_parameters(args, i + 1)
474
+ num_params_taken = yield args[i], params
475
+
476
+ if num_params_taken.nil?
477
+ remains << args[i]
478
+ if @stop_on_unknown
479
+ return remains += args[i + 1..-1]
480
+ end
481
+ else
482
+ i += num_params_taken
483
+ end
484
+ i += 1
485
+ when /^-(\S+)$/ # one or more short arguments
486
+ short_remaining = ""
487
+ shortargs = $1.split(//)
488
+ shortargs.each_with_index do |a, j|
489
+ if j == (shortargs.length - 1)
490
+ params = collect_argument_parameters(args, i + 1)
491
+
492
+ num_params_taken = yield "-#{a}", params
493
+ unless num_params_taken
494
+ short_remaining << a
495
+ if @stop_on_unknown
496
+ remains << "-#{short_remaining}"
497
+ return remains += args[i + 1..-1]
498
+ end
499
+ else
500
+ i += num_params_taken
501
+ end
502
+ else
503
+ unless yield "-#{a}", []
504
+ short_remaining << a
505
+ if @stop_on_unknown
506
+ short_remaining += shortargs[j + 1..-1].join
507
+ remains << "-#{short_remaining}"
508
+ return remains += args[i + 1..-1]
509
+ end
510
+ end
511
+ end
512
+ end
513
+
514
+ unless short_remaining.empty?
515
+ remains << "-#{short_remaining}"
516
+ end
517
+ i += 1
518
+ else
519
+ if @stop_on_unknown
520
+ return remains += args[i..-1]
521
+ else
522
+ remains << args[i]
523
+ i += 1
524
+ end
525
+ end
526
+ end
527
+
528
+ remains
529
+ end
530
+
531
+ def collect_argument_parameters(args, start_at)
532
+ params = []
533
+ pos = start_at
534
+ while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do
535
+ params << args[pos]
536
+ pos += 1
537
+ end
538
+ params
539
+ end
540
+
541
+ def resolve_default_short_options!
542
+ @order.each do |type, name|
543
+ opts = @specs[name]
544
+ next if type != :opt || opts.short
545
+
546
+ c = opts.long.split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) }
547
+ if c # found a character to use
548
+ opts.short = c
549
+ @short[c] = name
550
+ end
551
+ end
552
+ end
553
+
554
+ def wrap_line(str, opts = {})
555
+ prefix = opts[:prefix] || 0
556
+ width = opts[:width] || (self.width - 1)
557
+ start = 0
558
+ ret = []
559
+ until start > str.length
560
+ nextt =
561
+ if start + width >= str.length
562
+ str.length
563
+ else
564
+ x = str.rindex(/\s/, start + width)
565
+ x = str.index(/\s/, start) if x && x < start
566
+ x || str.length
567
+ end
568
+ ret << ((ret.empty? && !opts[:inner]) ? "" : " " * prefix) + str[start...nextt]
569
+ start = nextt + 1
570
+ end
571
+ ret
572
+ end
573
+
574
+ ## instance_eval but with ability to handle block arguments
575
+ ## thanks to _why: http://redhanded.hobix.com/inspect/aBlockCostume.html
576
+ def cloaker(&b)
577
+ (class << self; self; end).class_eval do
578
+ define_method :cloaker_, &b
579
+ meth = instance_method :cloaker_
580
+ remove_method :cloaker_
581
+ meth
582
+ end
583
+ end
584
+ end
585
+
586
+ class Option
587
+
588
+ attr_accessor :name, :short, :long, :default
589
+ attr_writer :multi_given
590
+
591
+ def initialize
592
+ @long = nil
593
+ @short = nil
594
+ @name = nil
595
+ @multi_given = false
596
+ @hidden = false
597
+ @default = nil
598
+ @optshash = Hash.new()
599
+ end
600
+
601
+ def opts(key)
602
+ @optshash[key]
603
+ end
604
+
605
+ def opts=(o)
606
+ @optshash = o
607
+ end
608
+
609
+ ## Indicates a flag option, which is an option without an argument
610
+ def flag? ; false ; end
611
+ def single_arg?
612
+ !self.multi_arg? && !self.flag?
613
+ end
614
+
615
+ def multi ; @multi_given ; end
616
+ alias multi? multi
617
+
618
+ ## Indicates that this is a multivalued (Array type) argument
619
+ def multi_arg? ; false ; end
620
+ ## note: Option-Types with both multi_arg? and flag? false are single-parameter (normal) options.
621
+
622
+ def array_default? ; self.default.kind_of?(Array) ; end
623
+
624
+ def short? ; short && short != :none ; end
625
+
626
+ def callback ; opts(:callback) ; end
627
+ def desc ; opts(:desc) ; end
628
+
629
+ def required? ; opts(:required) ; end
630
+
631
+ def parse(_paramlist, _neg_given)
632
+ raise NotImplementedError, "parse must be overridden for newly registered type"
633
+ end
634
+
635
+ # provide type-format string. default to empty, but user should probably override it
636
+ def type_format ; "" ; end
637
+
638
+ def educate
639
+ (short? ? "-#{short}, " : "") + "--#{long}" + type_format + (flag? && default ? ", --no-#{long}" : "")
640
+ end
641
+
642
+ ## Format the educate-line description including the default-value(s)
643
+ def description_with_default
644
+ return desc unless default
645
+ default_s = case default
646
+ when $stdout then '<stdout>'
647
+ when $stdin then '<stdin>'
648
+ when $stderr then '<stderr>'
649
+ when Array
650
+ default.join(', ')
651
+ else
652
+ default.to_s
653
+ end
654
+ defword = desc.end_with?('.') ? 'Default' : 'default'
655
+ return "#{desc} (#{defword}: #{default_s})"
656
+ end
657
+
658
+ ## Provide a way to register symbol aliases to the Parser
659
+ def self.register_alias(*alias_keys)
660
+ alias_keys.each do |alias_key|
661
+ # pass in the alias-key and the class
662
+ Parser.register(alias_key, self)
663
+ end
664
+ end
665
+
666
+ ## Factory class methods ...
667
+
668
+ # Determines which type of object to create based on arguments passed
669
+ # to +Optimist::opt+. This is trickier in Optimist, than other cmdline
670
+ # parsers (e.g. Slop) because we allow the +default:+ to be able to
671
+ # set the option's type.
672
+ def self.create(name, desc="", opts={}, settings={})
673
+
674
+ opttype = Optimist::Parser.registry_getopttype(opts[:type])
675
+ opttype_from_default = get_klass_from_default(opts, opttype)
676
+
677
+ 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)
678
+
679
+ opt_inst = (opttype || opttype_from_default || Optimist::BooleanOption.new)
680
+
681
+ ## fill in :long
682
+ opt_inst.long = handle_long_opt(opts[:long], name)
683
+
684
+ ## fill in :short
685
+ opt_inst.short = handle_short_opt(opts[:short])
686
+
687
+ ## fill in :multi
688
+ multi_given = opts[:multi] || false
689
+ opt_inst.multi_given = multi_given
690
+
691
+ ## fill in :default for flags
692
+ defvalue = opts[:default] || opt_inst.default
693
+
694
+ ## autobox :default for :multi (multi-occurrence) arguments
695
+ defvalue = [defvalue] if defvalue && multi_given && !defvalue.kind_of?(Array)
696
+ opt_inst.default = defvalue
697
+ opt_inst.name = name
698
+ opt_inst.opts = opts
699
+ opt_inst
700
+ end
701
+
702
+ private
703
+
704
+ def self.get_type_from_disdef(optdef, opttype, disambiguated_default)
705
+ if disambiguated_default.is_a? Array
706
+ return(optdef.first.class.name.downcase + "s") if !optdef.empty?
707
+ if opttype
708
+ raise ArgumentError, "multiple argument type must be plural" unless opttype.multi_arg?
709
+ return nil
710
+ else
711
+ raise ArgumentError, "multiple argument type cannot be deduced from an empty array"
712
+ end
713
+ end
714
+ return disambiguated_default.class.name.downcase
715
+ end
716
+
717
+ def self.get_klass_from_default(opts, opttype)
718
+ ## for options with :multi => true, an array default doesn't imply
719
+ ## a multi-valued argument. for that you have to specify a :type
720
+ ## as well. (this is how we disambiguate an ambiguous situation;
721
+ ## see the docs for Parser#opt for details.)
722
+
723
+ disambiguated_default = if opts[:multi] && opts[:default].is_a?(Array) && opttype.nil?
724
+ opts[:default].first
725
+ else
726
+ opts[:default]
727
+ end
728
+
729
+ return nil if disambiguated_default.nil?
730
+ type_from_default = get_type_from_disdef(opts[:default], opttype, disambiguated_default)
731
+ return Optimist::Parser.registry_getopttype(type_from_default)
732
+ end
733
+
734
+ def self.handle_long_opt(lopt, name)
735
+ lopt = lopt ? lopt.to_s : name.to_s.gsub("_", "-")
736
+ lopt = case lopt
737
+ when /^--([^-].*)$/ then $1
738
+ when /^[^-]/ then lopt
739
+ else raise ArgumentError, "invalid long option name #{lopt.inspect}"
740
+ end
741
+ end
742
+
743
+ def self.handle_short_opt(sopt)
744
+ sopt = sopt.to_s if sopt && sopt != :none
745
+ sopt = case sopt
746
+ when /^-(.)$/ then $1
747
+ when nil, :none, /^.$/ then sopt
748
+ else raise ArgumentError, "invalid short option name '#{sopt.inspect}'"
749
+ end
750
+
751
+ if sopt
752
+ raise ArgumentError, "a short option name can't be a number or a dash" if sopt =~ ::Optimist::Parser::INVALID_SHORT_ARG_REGEX
753
+ end
754
+ return sopt
755
+ end
756
+
757
+ end
758
+
759
+ # Flag option. Has no arguments. Can be negated with "no-".
760
+ class BooleanOption < Option
761
+ register_alias :flag, :bool, :boolean, :trueclass, :falseclass
762
+ def initialize
763
+ super()
764
+ @default = false
765
+ end
766
+ def flag? ; true ; end
767
+ def parse(_paramlist, neg_given)
768
+ return(self.name.to_s =~ /^no_/ ? neg_given : !neg_given)
769
+ end
770
+ end
771
+
772
+ # Floating point number option class.
773
+ class FloatOption < Option
774
+ register_alias :float, :double
775
+ def type_format ; "=<f>" ; end
776
+ def parse(paramlist, _neg_given)
777
+ paramlist.map do |pg|
778
+ pg.map do |param|
779
+ raise CommandlineError, "option '#{self.name}' needs a floating-point number" unless param.is_a?(Numeric) || param =~ FLOAT_RE
780
+ param.to_f
781
+ end
782
+ end
783
+ end
784
+ end
785
+
786
+ # Integer number option class.
787
+ class IntegerOption < Option
788
+ register_alias :int, :integer, :fixnum
789
+ def type_format ; "=<i>" ; end
790
+ def parse(paramlist, _neg_given)
791
+ paramlist.map do |pg|
792
+ pg.map do |param|
793
+ raise CommandlineError, "option '#{self.name}' needs an integer" unless param.is_a?(Numeric) || param =~ /^-?[\d_]+$/
794
+ param.to_i
795
+ end
796
+ end
797
+ end
798
+ end
799
+
800
+ # Option class for handling IO objects and URLs.
801
+ # Note that this will return the file-handle, not the file-name
802
+ # in the case of file-paths given to it.
803
+ class IOOption < Option
804
+ register_alias :io
805
+ def type_format ; "=<filename/uri>" ; end
806
+ def parse(paramlist, _neg_given)
807
+ paramlist.map do |pg|
808
+ pg.map do |param|
809
+ if param =~ /^(stdin|-)$/i
810
+ $stdin
811
+ else
812
+ require 'open-uri'
813
+ begin
814
+ open param
815
+ rescue SystemCallError => e
816
+ raise CommandlineError, "file or url for option '#{self.name}' cannot be opened: #{e.message}"
817
+ end
818
+ end
819
+ end
820
+ end
821
+ end
822
+ end
823
+
824
+ # Option class for handling Strings.
825
+ class StringOption < Option
826
+ register_alias :string
827
+ def type_format ; "=<s>" ; end
828
+ def parse(paramlist, _neg_given)
829
+ paramlist.map { |pg| pg.map(&:to_s) }
830
+ end
831
+ end
832
+
833
+ # Option for dates. Uses Chronic if it exists.
834
+ class DateOption < Option
835
+ register_alias :date
836
+ def type_format ; "=<date>" ; end
837
+ def parse(paramlist, _neg_given)
838
+ paramlist.map do |pg|
839
+ pg.map do |param|
840
+ next param if param.is_a?(Date)
841
+ begin
842
+ begin
843
+ require 'chronic'
844
+ time = Chronic.parse(param)
845
+ rescue LoadError
846
+ # chronic is not available
847
+ end
848
+ time ? Date.new(time.year, time.month, time.day) : Date.parse(param)
849
+ rescue ArgumentError
850
+ raise CommandlineError, "option '#{self.name}' needs a date"
851
+ end
852
+ end
853
+ end
854
+ end
855
+ end
856
+
857
+ ### MULTI_OPT_TYPES :
858
+ ## The set of values that indicate a multiple-parameter option (i.e., that
859
+ ## takes multiple space-separated values on the commandline) when passed as
860
+ ## the +:type+ parameter of #opt.
861
+
862
+ # Option class for handling multiple Integers
863
+ class IntegerArrayOption < IntegerOption
864
+ register_alias :fixnums, :ints, :integers
865
+ def type_format ; "=<i+>" ; end
866
+ def multi_arg? ; true ; end
867
+ end
868
+
869
+ # Option class for handling multiple Floats
870
+ class FloatArrayOption < FloatOption
871
+ register_alias :doubles, :floats
872
+ def type_format ; "=<f+>" ; end
873
+ def multi_arg? ; true ; end
874
+ end
875
+
876
+ # Option class for handling multiple Strings
877
+ class StringArrayOption < StringOption
878
+ register_alias :strings
879
+ def type_format ; "=<s+>" ; end
880
+ def multi_arg? ; true ; end
881
+ end
882
+
883
+ # Option class for handling multiple dates
884
+ class DateArrayOption < DateOption
885
+ register_alias :dates
886
+ def type_format ; "=<date+>" ; end
887
+ def multi_arg? ; true ; end
888
+ end
889
+
890
+ # Option class for handling Files/URLs via 'open'
891
+ class IOArrayOption < IOOption
892
+ register_alias :ios
893
+ def type_format ; "=<filename/uri+>" ; end
894
+ def multi_arg? ; true ; end
895
+ end
896
+
897
+ ## The easy, syntactic-sugary entry method into Optimist. Creates a Parser,
898
+ ## passes the block to it, then parses +args+ with it, handling any errors or
899
+ ## requests for help or version information appropriately (and then exiting).
900
+ ## Modifies +args+ in place. Returns a hash of option values.
901
+ ##
902
+ ## The block passed in should contain zero or more calls to +opt+
903
+ ## (Parser#opt), zero or more calls to +text+ (Parser#text), and
904
+ ## probably a call to +version+ (Parser#version).
905
+ ##
906
+ ## The returned block contains a value for every option specified with
907
+ ## +opt+. The value will be the value given on the commandline, or the
908
+ ## default value if the option was not specified on the commandline. For
909
+ ## every option specified on the commandline, a key "<option
910
+ ## name>_given" will also be set in the hash.
911
+ ##
912
+ ## Example:
913
+ ##
914
+ ## require 'optimist'
915
+ ## opts = Optimist::options do
916
+ ## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
917
+ ## opt :name, "Monkey name", :type => :string # a string --name <s>, defaulting to nil
918
+ ## opt :num_limbs, "Number of limbs", :default => 4 # an integer --num-limbs <i>, defaulting to 4
919
+ ## end
920
+ ##
921
+ ## ## if called with no arguments
922
+ ## p opts # => {:monkey=>false, :name=>nil, :num_limbs=>4, :help=>false}
923
+ ##
924
+ ## ## if called with --monkey
925
+ ## p opts # => {:monkey=>true, :name=>nil, :num_limbs=>4, :help=>false, :monkey_given=>true}
926
+ ##
927
+ ## See more examples at http://optimist.rubyforge.org.
928
+ def options(args = ARGV, *a, &b)
929
+ @last_parser = Parser.new(*a, &b)
930
+ with_standard_exception_handling(@last_parser) { @last_parser.parse args }
931
+ end
932
+
933
+ ## If Optimist::options doesn't do quite what you want, you can create a Parser
934
+ ## object and call Parser#parse on it. That method will throw CommandlineError,
935
+ ## HelpNeeded and VersionNeeded exceptions when necessary; if you want to
936
+ ## have these handled for you in the standard manner (e.g. show the help
937
+ ## and then exit upon an HelpNeeded exception), call your code from within
938
+ ## a block passed to this method.
939
+ ##
940
+ ## Note that this method will call System#exit after handling an exception!
941
+ ##
942
+ ## Usage example:
943
+ ##
944
+ ## require 'optimist'
945
+ ## p = Optimist::Parser.new do
946
+ ## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
947
+ ## opt :goat, "Use goat mode", :default => true # a flag --goat, defaulting to true
948
+ ## end
949
+ ##
950
+ ## opts = Optimist::with_standard_exception_handling p do
951
+ ## o = p.parse ARGV
952
+ ## raise Optimist::HelpNeeded if ARGV.empty? # show help screen
953
+ ## o
954
+ ## end
955
+ ##
956
+ ## Requires passing in the parser object.
957
+
958
+ def with_standard_exception_handling(parser)
959
+ yield
960
+ rescue CommandlineError => e
961
+ parser.die(e.message, nil, e.error_code)
962
+ rescue HelpNeeded
963
+ parser.educate
964
+ exit
965
+ rescue VersionNeeded
966
+ puts parser.version
967
+ exit
968
+ end
969
+
970
+ ## Informs the user that their usage of 'arg' was wrong, as detailed by
971
+ ## 'msg', and dies. Example:
972
+ ##
973
+ ## options do
974
+ ## opt :volume, :default => 0.0
975
+ ## end
976
+ ##
977
+ ## die :volume, "too loud" if opts[:volume] > 10.0
978
+ ## die :volume, "too soft" if opts[:volume] < 0.1
979
+ ##
980
+ ## In the one-argument case, simply print that message, a notice
981
+ ## about -h, and die. Example:
982
+ ##
983
+ ## options do
984
+ ## opt :whatever # ...
985
+ ## end
986
+ ##
987
+ ## Optimist::die "need at least one filename" if ARGV.empty?
988
+ ##
989
+ ## An exit code can be provide if needed
990
+ ##
991
+ ## Optimist::die "need at least one filename", -2 if ARGV.empty?
992
+ def die(arg, msg = nil, error_code = nil)
993
+ if @last_parser
994
+ @last_parser.die arg, msg, error_code
995
+ else
996
+ raise ArgumentError, "Optimist::die can only be called after Optimist::options"
997
+ end
998
+ end
999
+
1000
+ ## Displays the help message and dies. Example:
1001
+ ##
1002
+ ## options do
1003
+ ## opt :volume, :default => 0.0
1004
+ ## banner <<-EOS
1005
+ ## Usage:
1006
+ ## #$0 [options] <name>
1007
+ ## where [options] are:
1008
+ ## EOS
1009
+ ## end
1010
+ ##
1011
+ ## Optimist::educate if ARGV.empty?
1012
+ def educate
1013
+ if @last_parser
1014
+ @last_parser.educate
1015
+ exit
1016
+ else
1017
+ raise ArgumentError, "Optimist::educate can only be called after Optimist::options"
1018
+ end
1019
+ end
1020
+
1021
+ module_function :options, :die, :educate, :with_standard_exception_handling
1022
+ end # module