timesheet 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,242 @@
1
+ require 'time'
2
+ require 'chronic'
3
+ require 'rich_units'
4
+
5
+ class TimesheetParser
6
+
7
+ TIME_PERIODS = [:today, :current_week, :current_month, :yesterday, :last_week, :last_month]
8
+ SUB_COMMANDS = %w(add list edit delete report)
9
+ WEEK_START = "monday"
10
+ USAGE = <<-EOS
11
+ Timesheet is a script for keeping track of time spent on various projects.
12
+
13
+ Usage:
14
+
15
+ timesheet [OPTIONS] COMMAND [ARGS]
16
+
17
+ COMMAND is any of the following:
18
+ add
19
+ edit
20
+ delete
21
+ list
22
+ report
23
+
24
+ see 'timesheet COMMAND --help' for more information on a specific command.
25
+
26
+ Note: your timesheet data will be stored in a hidden directory under your user account. Timesheet
27
+ figures out where this is by referencing the "HOME" environment variable. The default location is
28
+ therefore: /Users/someuser/.timesheet/store.yaml
29
+
30
+ You may override this location to any specific location you want by setting the "TIMESHEET_DATA_FILE"
31
+ environment variable. This should be the full path to where you want the data stored. Including filename
32
+ and extension. You only need to set this if you are unsatisfied with the default location.
33
+
34
+ OPTIONS are:
35
+
36
+ EOS
37
+
38
+ def self.parse(args, outstream=STDOUT)
39
+
40
+ global_opts = Trollop::options(args) do
41
+ version "Timesheet #{Timesheet::VERSION} (c) 2009 John F. Schank III"
42
+ banner USAGE
43
+ stop_on SUB_COMMANDS
44
+
45
+ opt :debug, "Show debugging information while processing"
46
+ end
47
+
48
+ if (args.nil? || args.empty?)
49
+ Trollop::die "No parameters on command-line, use -h for help"
50
+ end
51
+
52
+ command = args.shift
53
+ command_opts = case command
54
+ when "add"
55
+ add_options = Trollop::options(args) do
56
+ banner <<-EOS
57
+ timesheet add [OPTIONS]
58
+
59
+ Adds an entry to the database.
60
+ If there is an overlap, it should present a choice to split the entry, or abort.
61
+
62
+ OPTIONS are:
63
+
64
+ EOS
65
+ opt :project, "Project name", {:type => :string, :required => true}
66
+ opt :start, "Start date-time", {:type => :time, :required => true}
67
+ opt :end, "End date-time", {:type => :time, :required => true}
68
+ opt :comment, "comment", {:type => :string}
69
+ depends :start, :end
70
+ end
71
+ add_options[:command] = :add
72
+ add_options
73
+
74
+ when "edit"
75
+ edit_options = Trollop::options(args) do
76
+ banner <<-EOS
77
+ timesheet edit [OPTIONS]
78
+
79
+ Allows editing of existing entries
80
+
81
+ OPTIONS are:
82
+
83
+ EOS
84
+ opt :record_number, "Record Number", {:type => :int, :required => true}
85
+ opt :project, "Project name", {:type => :string}
86
+ opt :start, "Start date-time", {:type => :time}
87
+ opt :end, "End date-time", {:type => :time}
88
+ opt :comment, "comment", {:type => :string}
89
+ end
90
+
91
+ edit_options[:command] = :edit
92
+ edit_options
93
+
94
+ when "delete"
95
+ delete_options = Trollop::options(args) do
96
+ banner <<-EOS
97
+ timesheet delete [OPTIONS]
98
+
99
+ Allows deletion of existing entries
100
+
101
+ OPTIONS are:
102
+
103
+
104
+ EOS
105
+ opt :record_number, "Record Number", {:type => :int, :required => true}
106
+ end
107
+
108
+ delete_options[:command] = :delete
109
+ delete_options
110
+
111
+ when "list"
112
+ list_options = Trollop::options(args) do
113
+ banner <<-EOS
114
+ timesheet list [OPTIONS]
115
+
116
+ Prints all matching entries. Any entry where the given date is covered by the entry.
117
+ Each entry's record number is displayed
118
+
119
+ OPTIONS are:
120
+
121
+
122
+ EOS
123
+ opt :today, "Current Day"
124
+ opt :current_week, "Current Week"
125
+ opt :current_month, "Current Month"
126
+ opt :yesterday, "Yesterday"
127
+ opt :last_week, "Last Week"
128
+ opt :last_month, "Last Month"
129
+ opt :start, "Start date-time", {:type => :time}
130
+ opt :end, "End date-time", {:type => :time}
131
+ depends :start, :end
132
+ conflicts :today, :current_week, :current_month, :yesterday, :last_week, :last_month, :start
133
+ end
134
+
135
+ # a list is just a detailed report.
136
+ list_options[:command] = :report
137
+ list_options[:detail] = true
138
+ TIME_PERIODS.each do |period|
139
+ list_options.merge!(convert_to_start_end(period)) if list_options[period]
140
+ list_options.delete(period)
141
+ end
142
+ list_options.merge!(convert_to_start_end(:today)) unless list_options[:start] # add default period
143
+ list_options
144
+
145
+ when "report"
146
+ report_options = Trollop::options(args) do
147
+ banner <<-EOS
148
+ timesheet report [OPTIONS]
149
+
150
+ displays total hours by project. For entries in the period.
151
+
152
+ summary report shows hours spent on top level projects, and excludes comments
153
+ detail shows hours for each item.
154
+ byday shows hours for each day, with entries and comments. (daily breakout)
155
+
156
+ defaults: --summary, and --today
157
+
158
+ OPTIONS are:
159
+
160
+
161
+ EOS
162
+ opt :summary, "Summary report"
163
+ opt :detail, "Detailed report"
164
+ opt :byday, "By Day report"
165
+ opt :today, "Current Day"
166
+ opt :current_week, "Current Week"
167
+ opt :current_month, "Current Month"
168
+ opt :yesterday, "Yesterday"
169
+ opt :last_week, "Last Week"
170
+ opt :last_month, "Last Month"
171
+ opt :start, "Start date-time", {:type => :time}
172
+ opt :end, "End date-time", {:type => :time}
173
+ conflicts :summary, :detail, :byday
174
+ conflicts :today, :current_week, :current_month, :yesterday, :last_week, :last_month, :start
175
+ depends :start, :end
176
+ end
177
+ report_options[:command] = :report
178
+ TIME_PERIODS.each do |period|
179
+ report_options.merge!(convert_to_start_end(period)) if report_options[period]
180
+ report_options.delete(period)
181
+ end
182
+ report_options.merge!(convert_to_start_end(:today)) unless report_options[:start] # add default period
183
+ report_options.merge!({:summary => true}) unless report_options[:summary] || report_options[:detail] || report_options[:byday]
184
+ report_options
185
+
186
+ else
187
+ Trollop::die "unknown subcommand #{command.inspect}"
188
+ end
189
+
190
+ opts = {}
191
+ opts.merge! global_opts
192
+ opts.merge! command_opts
193
+ opts.merge!({:remainder => args})
194
+
195
+ if (opts[:debug] )
196
+ outstream.puts "Command-line options hash..."
197
+ outstream.puts opts.inspect
198
+ end
199
+
200
+ raise ArgumentError.new("Command was not fully understood. Use --debug flag to diagnose.") unless opts[:remainder].empty?
201
+
202
+ opts
203
+
204
+ end
205
+
206
+ def self.convert_to_start_end(period)
207
+ periods = {}
208
+ case period
209
+ when :today
210
+ periods[:start] = Chronic.parse('today at 12 am')
211
+ periods[:end] = periods[:start] + 1.day
212
+
213
+ when :current_week
214
+ periods[:start] = Chronic.parse("this week #{WEEK_START} at 12 am")
215
+ periods[:end] = periods[:start] + 7.days
216
+
217
+ when :current_month
218
+ today = Date.today
219
+ next_month = today >> 1
220
+ periods[:start] = Time.local(today.year, today.month, 1, 0, 0, 0)
221
+ periods[:end] = Time.local(next_month.year, next_month.month, 1, 0, 0, 0)
222
+
223
+ when :yesterday
224
+ periods[:start] = Chronic.parse('yesterday at 12 am')
225
+ periods[:end] = periods[:start] + 1.day
226
+
227
+ when :last_week
228
+ periods[:start] = Chronic.parse("last week #{WEEK_START} at 12 am")
229
+ periods[:end] = periods[:start] + 7.days
230
+
231
+ when :last_month
232
+ today = Date.today
233
+ last_month = today << 1
234
+ periods[:start] = Time.local(last_month.year, last_month.month, 1, 0, 0, 0)
235
+ periods[:end] = Time.local(today.year, today.month, 1, 0, 0, 0)
236
+
237
+ else raise "Unknown time period #{period.to_s}"
238
+ end
239
+ periods
240
+ end
241
+
242
+ end
@@ -0,0 +1,761 @@
1
+ ## lib/trollop.rb -- trollop command-line processing library
2
+ ## Author:: William Morgan (mailto: wmorgan-trollop@masanjin.net)
3
+ ## Copyright:: Copyright 2007 William Morgan
4
+ ## License:: GNU GPL version 2
5
+
6
+ require 'date'
7
+ require 'time'
8
+
9
+ module Trollop
10
+
11
+ VERSION = "1.15"
12
+
13
+ ## Thrown by Parser in the event of a commandline error. Not needed if
14
+ ## you're using the Trollop::options entry.
15
+ class CommandlineError < StandardError; end
16
+
17
+ ## Thrown by Parser if the user passes in '-h' or '--help'. Handled
18
+ ## automatically by Trollop#options.
19
+ class HelpNeeded < StandardError; end
20
+
21
+ ## Thrown by Parser if the user passes in '-h' or '--version'. Handled
22
+ ## automatically by Trollop#options.
23
+ class VersionNeeded < StandardError; end
24
+
25
+ ## Regex for floating point numbers
26
+ FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))$/
27
+
28
+ ## Regex for parameters
29
+ PARAM_RE = /^-(-|\.$|[^\d\.])/
30
+
31
+ ## The commandline parser. In typical usage, the methods in this class
32
+ ## will be handled internally by Trollop::options. In this case, only the
33
+ ## #opt, #banner and #version, #depends, and #conflicts methods will
34
+ ## typically be called.
35
+ ##
36
+ ## If it's necessary to instantiate this class (for more complicated
37
+ ## argument-parsing situations), be sure to call #parse to actually
38
+ ## produce the output hash.
39
+ #noinspection ALL
40
+ class Parser
41
+
42
+ ## The set of values that indicate a flag option when passed as the
43
+ ## +:type+ parameter of #opt.
44
+ FLAG_TYPES = [:flag, :bool, :boolean]
45
+
46
+ ## The set of values that indicate a single-parameter (normal) option when
47
+ ## passed as the +:type+ parameter of #opt.
48
+ ##
49
+ ## A value of +io+ corresponds to a readable IO resource, including
50
+ ## a filename, URI, or the strings 'stdin' or '-'.
51
+ SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date, :time]
52
+
53
+ ## The set of values that indicate a multiple-parameter option (i.e., that
54
+ ## takes multiple space-separated values on the commandline) when passed as
55
+ ## the +:type+ parameter of #opt.
56
+ MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates, :times]
57
+
58
+ ## The complete set of legal values for the +:type+ parameter of #opt.
59
+ TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES
60
+
61
+ INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
62
+
63
+ ## The values from the commandline that were not interpreted by #parse.
64
+ attr_reader :leftovers
65
+
66
+ ## The complete configuration hashes for each option. (Mainly useful
67
+ ## for testing.)
68
+ attr_reader :specs
69
+
70
+ ## Initializes the parser, and instance-evaluates any block given.
71
+ def initialize *a, &b
72
+ @version = nil
73
+ @leftovers = []
74
+ @specs = {}
75
+ @long = {}
76
+ @short = {}
77
+ @order = []
78
+ @constraints = []
79
+ @stop_words = []
80
+ @stop_on_unknown = false
81
+
82
+ #instance_eval(&b) if b # can't take arguments
83
+ cloaker(&b).bind(self).call(*a) if b
84
+ end
85
+
86
+ ## Define an option. +name+ is the option name, a unique identifier
87
+ ## for the option that you will use internally, which should be a
88
+ ## symbol or a string. +desc+ is a string description which will be
89
+ ## displayed in help messages.
90
+ ##
91
+ ## Takes the following optional arguments:
92
+ ##
93
+ ## [+: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.
94
+ ## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+.
95
+ ## [+: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.
96
+ ## [+: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+.
97
+ ## [+:required+] If set to +true+, the argument must be provided on the commandline.
98
+ ## [+: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.)
99
+ ##
100
+ ## Note that there are two types of argument multiplicity: an argument
101
+ ## can take multiple values, e.g. "--arg 1 2 3". An argument can also
102
+ ## be allowed to occur multiple times, e.g. "--arg 1 --arg 2".
103
+ ##
104
+ ## Arguments that take multiple values should have a +:type+ parameter
105
+ ## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+
106
+ ## value of an array of the correct type (e.g. [String]). The
107
+ ## value of this argument will be an array of the parameters on the
108
+ ## commandline.
109
+ ##
110
+ ## Arguments that can occur multiple times should be marked with
111
+ ## +:multi+ => +true+. The value of this argument will also be an array.
112
+ ## In contrast with regular non-multi options, if not specified on
113
+ ## the commandline, the default value will be [], not nil.
114
+ ##
115
+ ## These two attributes can be combined (e.g. +:type+ => +:strings+,
116
+ ## +:multi+ => +true+), in which case the value of the argument will be
117
+ ## an array of arrays.
118
+ ##
119
+ ## There's one ambiguous case to be aware of: when +:multi+: is true and a
120
+ ## +:default+ is set to an array (of something), it's ambiguous whether this
121
+ ## is a multi-value argument as well as a multi-occurrence argument.
122
+ ## In thise case, Trollop assumes that it's not a multi-value argument.
123
+ ## If you want a multi-value, multi-occurrence argument with a default
124
+ ## value, you must specify +:type+ as well.
125
+
126
+ def opt name, desc="", opts={}
127
+ raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name
128
+
129
+ ## fill in :type
130
+ opts[:type] = # normalize
131
+ case opts[:type]
132
+ when :boolean, :bool; :flag
133
+ when :integer; :int
134
+ when :integers; :ints
135
+ when :double; :float
136
+ when :doubles; :floats
137
+ when Class
138
+ case opts[:type].name
139
+ when 'TrueClass', 'FalseClass'; :flag
140
+ when 'String'; :string
141
+ when 'Integer'; :int
142
+ when 'Float'; :float
143
+ when 'IO'; :io
144
+ when 'Date'; :date
145
+ when 'Time'; :time
146
+ else
147
+ raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
148
+ end
149
+ when nil; nil
150
+ else
151
+ raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
152
+ opts[:type]
153
+ end
154
+
155
+ ## for options with :multi => true, an array default doesn't imply
156
+ ## a multi-valued argument. for that you have to specify a :type
157
+ ## as well. (this is how we disambiguate an ambiguous situation;
158
+ ## see the docs for Parser#opt for details.)
159
+ disambiguated_default =
160
+ if opts[:multi] && opts[:default].is_a?(Array) && !opts[:type]
161
+ opts[:default].first
162
+ else
163
+ opts[:default]
164
+ end
165
+
166
+ type_from_default =
167
+ case disambiguated_default
168
+ when Integer; :int
169
+ when Numeric; :float
170
+ when TrueClass, FalseClass; :flag
171
+ when String; :string
172
+ when IO; :io
173
+ when Date; :date
174
+ when Time; :time
175
+ when Array
176
+ if opts[:default].empty?
177
+ raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'"
178
+ end
179
+ case opts[:default][0] # the first element determines the types
180
+ when Integer; :ints
181
+ when Numeric; :floats
182
+ when String; :strings
183
+ when IO; :ios
184
+ when Date; :dates
185
+ when Time; :times
186
+ else
187
+ raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'"
188
+ end
189
+ when nil; nil
190
+ else
191
+ raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
192
+ end
193
+
194
+ 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
195
+
196
+ opts[:type] = opts[:type] || type_from_default || :flag
197
+
198
+ ## fill in :long
199
+ opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
200
+ opts[:long] =
201
+ case opts[:long]
202
+ when /^--([^-].*)$/
203
+ $1
204
+ when /^[^-]/
205
+ opts[:long]
206
+ else
207
+ raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
208
+ end
209
+ raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]
210
+
211
+ ## fill in :short
212
+ opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
213
+ opts[:short] = case opts[:short]
214
+ when /^-(.)$/; $1
215
+ when nil, :none, /^.$/; opts[:short]
216
+ else raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
217
+ end
218
+
219
+ if opts[:short]
220
+ raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
221
+ raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX
222
+ end
223
+
224
+ ## fill in :default for flags
225
+ opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
226
+
227
+ ## autobox :default for :multi (multi-occurrence) arguments
228
+ opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array)
229
+
230
+ ## fill in :multi
231
+ opts[:multi] ||= false
232
+
233
+ opts[:desc] ||= desc
234
+ @long[opts[:long]] = name
235
+ @short[opts[:short]] = name if opts[:short] && opts[:short] != :none
236
+ @specs[name] = opts
237
+ @order << [:opt, name]
238
+ end
239
+
240
+ ## Sets the version string. If set, the user can request the version
241
+ ## on the commandline. Should probably be of the form "<program name>
242
+ ## <version number>".
243
+ def version s=nil; @version = s if s; @version end
244
+
245
+ ## Adds text to the help display. Can be interspersed with calls to
246
+ ## #opt to build a multi-section help page.
247
+ def banner s; @order << [:text, s] end
248
+ alias :text :banner
249
+
250
+ ## Marks two (or more!) options as requiring each other. Only handles
251
+ ## undirected (i.e., mutual) dependencies. Directed dependencies are
252
+ ## better modeled with Trollop::die.
253
+ def depends *syms
254
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
255
+ @constraints << [:depends, syms]
256
+ end
257
+
258
+ ## Marks two (or more!) options as conflicting.
259
+ def conflicts *syms
260
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
261
+ @constraints << [:conflicts, syms]
262
+ end
263
+
264
+ ## Defines a set of words which cause parsing to terminate when
265
+ ## encountered, such that any options to the left of the word are
266
+ ## parsed as usual, and options to the right of the word are left
267
+ ## intact.
268
+ ##
269
+ ## A typical use case would be for subcommand support, where these
270
+ ## would be set to the list of subcommands. A subsequent Trollop
271
+ ## invocation would then be used to parse subcommand options, after
272
+ ## shifting the subcommand off of ARGV.
273
+ def stop_on *words
274
+ @stop_words = [*words].flatten
275
+ end
276
+
277
+ ## Similar to #stop_on, but stops on any unknown word when encountered
278
+ ## (unless it is a parameter for an argument). This is useful for
279
+ ## cases where you don't know the set of subcommands ahead of time,
280
+ ## i.e., without first parsing the global options.
281
+ def stop_on_unknown
282
+ @stop_on_unknown = true
283
+ end
284
+
285
+ ## Parses the commandline. Typically called by Trollop::options.
286
+ def parse cmdline=ARGV
287
+ vals = {}
288
+ required = {}
289
+
290
+ opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"]
291
+ opt :help, "Show this message" unless @specs[:help] || @long["help"]
292
+
293
+ @specs.each do |sym, opts|
294
+ required[sym] = true if opts[:required]
295
+ vals[sym] = opts[:default]
296
+ vals[sym] = [] if opts[:multi] && !opts[:default] # multi arguments default to [], not nil
297
+ end
298
+
299
+ resolve_default_short_options
300
+
301
+ ## resolve symbols
302
+ given_args = {}
303
+ @leftovers = each_arg cmdline do |arg, params|
304
+ sym = case arg
305
+ when /^-([^-])$/
306
+ @short[$1]
307
+ when /^--([^-]\S*)$/
308
+ @long[$1]
309
+ else
310
+ raise CommandlineError, "invalid argument syntax: '#{arg}'"
311
+ end
312
+ raise CommandlineError, "unknown argument '#{arg}'" unless sym
313
+
314
+ if given_args.include?(sym) && !@specs[sym][:multi]
315
+ raise CommandlineError, "option '#{arg}' specified multiple times"
316
+ end
317
+
318
+ given_args[sym] ||= {}
319
+
320
+ given_args[sym][:arg] = arg
321
+ given_args[sym][:params] ||= []
322
+
323
+ # The block returns the number of parameters taken.
324
+ num_params_taken = 0
325
+
326
+ unless params.nil?
327
+ if SINGLE_ARG_TYPES.include?(@specs[sym][:type])
328
+ given_args[sym][:params] << params[0, 1] # take the first parameter
329
+ num_params_taken = 1
330
+ elsif MULTI_ARG_TYPES.include?(@specs[sym][:type])
331
+ given_args[sym][:params] << params # take all the parameters
332
+ num_params_taken = params.size
333
+ end
334
+ end
335
+
336
+ num_params_taken
337
+ end
338
+
339
+ ## check for version and help args
340
+ raise VersionNeeded if given_args.include? :version
341
+ raise HelpNeeded if given_args.include? :help
342
+
343
+ ## check constraint satisfaction
344
+ @constraints.each do |type, syms|
345
+ constraint_sym = syms.find { |sym| given_args[sym] }
346
+ next unless constraint_sym
347
+
348
+ case type
349
+ when :depends
350
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym }
351
+ when :conflicts
352
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) }
353
+ end
354
+ end
355
+
356
+ required.each do |sym, val|
357
+ raise CommandlineError, "option --#{@specs[sym][:long]} must be specified" unless given_args.include? sym
358
+ end
359
+
360
+ ## parse parameters
361
+ given_args.each do |sym, given_data|
362
+ arg = given_data[:arg]
363
+ params = given_data[:params]
364
+
365
+ opts = @specs[sym]
366
+ raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag
367
+
368
+ vals["#{sym}_given".intern] = true # mark argument as specified on the commandline
369
+
370
+ case opts[:type]
371
+ when :flag
372
+ vals[sym] = !opts[:default]
373
+ when :int, :ints
374
+ vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } }
375
+ when :float, :floats
376
+ vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } }
377
+ when :string, :strings
378
+ vals[sym] = params.map { |pg| pg.map { |p| p.to_s } }
379
+ when :io, :ios
380
+ vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } }
381
+ when :date, :dates
382
+ vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } }
383
+ when :time, :times
384
+ vals[sym] = params.map { |pg| pg.map { |p| parse_time_parameter p, arg } }
385
+ end
386
+
387
+ if SINGLE_ARG_TYPES.include?(opts[:type])
388
+ unless opts[:multi] # single parameter
389
+ vals[sym] = vals[sym][0][0]
390
+ else # multiple options, each with a single parameter
391
+ vals[sym] = vals[sym].map { |p| p[0] }
392
+ end
393
+ elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi]
394
+ vals[sym] = vals[sym][0] # single option, with multiple parameters
395
+ end
396
+ # else: multiple options, with multiple parameters
397
+ end
398
+
399
+ ## allow openstruct-style accessors
400
+ class << vals
401
+ def method_missing(m, *args)
402
+ self[m] || self[m.to_s]
403
+ end
404
+ end
405
+ vals
406
+ end
407
+
408
+ def parse_date_parameter param, arg #:nodoc:
409
+ begin
410
+ begin
411
+ time = Chronic.parse(param)
412
+ rescue NameError
413
+ # chronic is not available
414
+ end
415
+ time ? Date.new(time.year, time.month, time.day) : Date.parse(param)
416
+ rescue ArgumentError => e
417
+ raise CommandlineError, "option '#{arg}' needs a date"
418
+ end
419
+ end
420
+
421
+ def parse_time_parameter param, arg #:nodoc:
422
+ begin
423
+ begin
424
+ time = Chronic.parse(param)
425
+ rescue NameError
426
+ # chronic is not available
427
+ end
428
+ time ? Time.local(time.year, time.month, time.day, time.hour, time.min, time.sec) : Time.parse(param)
429
+ rescue ArgumentError => e
430
+ raise CommandlineError, "option '#{arg}' needs a date"
431
+ end
432
+ end
433
+
434
+ ## Print the help message to +stream+.
435
+ def educate stream=$stdout
436
+ width # just calculate it now; otherwise we have to be careful not to
437
+ # call this unless the cursor's at the beginning of a line.
438
+
439
+ left = {}
440
+ @specs.each do |name, spec|
441
+ left[name] = "--#{spec[:long]}" +
442
+ (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") +
443
+ case spec[:type]
444
+ when :flag; ""
445
+ when :int; " <i>"
446
+ when :ints; " <i+>"
447
+ when :string; " <s>"
448
+ when :strings; " <s+>"
449
+ when :float; " <f>"
450
+ when :floats; " <f+>"
451
+ when :io; " <filename/uri>"
452
+ when :ios; " <filename/uri+>"
453
+ when :date; " <date>"
454
+ when :dates; " <date+>"
455
+ when :time; " <time>"
456
+ when :times; " <time+>"
457
+ end
458
+ end
459
+
460
+ leftcol_width = left.values.map { |s| s.length }.max || 0
461
+ rightcol_start = leftcol_width + 6 # spaces
462
+
463
+ unless @order.size > 0 && @order.first.first == :text
464
+ stream.puts "#@version\n" if @version
465
+ stream.puts "Options:"
466
+ end
467
+
468
+ @order.each do |what, opt|
469
+ if what == :text
470
+ stream.puts wrap(opt)
471
+ next
472
+ end
473
+
474
+ spec = @specs[opt]
475
+ stream.printf " %#{leftcol_width}s: ", left[opt]
476
+ desc = spec[:desc] + begin
477
+ default_s = case spec[:default]
478
+ when $stdout; "<stdout>"
479
+ when $stdin; "<stdin>"
480
+ when $stderr; "<stderr>"
481
+ when Array
482
+ spec[:default].join(", ")
483
+ else
484
+ spec[:default].to_s
485
+ end
486
+
487
+ if spec[:default]
488
+ if spec[:desc] =~ /\.$/
489
+ " (Default: #{default_s})"
490
+ else
491
+ " (default: #{default_s})"
492
+ end
493
+ else
494
+ ""
495
+ end
496
+ end
497
+ stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
498
+ end
499
+ end
500
+
501
+ def width #:nodoc:
502
+ @width ||= if $stdout.tty?
503
+ begin
504
+ require 'curses'
505
+ Curses::init_screen
506
+ x = Curses::cols
507
+ Curses::close_screen
508
+ x
509
+ rescue Exception
510
+ 80
511
+ end
512
+ else
513
+ 80
514
+ end
515
+ end
516
+
517
+ def wrap str, opts={} # :nodoc:
518
+ if str == ""
519
+ [""]
520
+ else
521
+ str.split("\n").map { |s| wrap_line s, opts }.flatten
522
+ end
523
+ end
524
+
525
+ private
526
+
527
+ ## yield successive arg, parameter pairs
528
+ def each_arg args
529
+ remains = []
530
+ i = 0
531
+
532
+ until i >= args.length
533
+ if @stop_words.member? args[i]
534
+ remains += args[i .. -1]
535
+ return remains
536
+ end
537
+ case args[i]
538
+ when /^--$/ # arg terminator
539
+ remains += args[(i + 1) .. -1]
540
+ return remains
541
+ when /^--(\S+?)=(.*)$/ # long argument with equals
542
+ yield "--#{$1}", [$2]
543
+ i += 1
544
+ when /^--(\S+)$/ # long argument
545
+ params = collect_argument_parameters(args, i + 1)
546
+ unless params.empty?
547
+ num_params_taken = yield args[i], params
548
+ unless num_params_taken
549
+ if @stop_on_unknown
550
+ remains += args[i + 1 .. -1]
551
+ return remains
552
+ else
553
+ remains += params
554
+ end
555
+ end
556
+ i += 1 + num_params_taken
557
+ else # long argument no parameter
558
+ yield args[i], nil
559
+ i += 1
560
+ end
561
+ when /^-(\S+)$/ # one or more short arguments
562
+ shortargs = $1.split(//)
563
+ shortargs.each_with_index do |a, j|
564
+ if j == (shortargs.length - 1)
565
+ params = collect_argument_parameters(args, i + 1)
566
+ unless params.empty?
567
+ num_params_taken = yield "-#{a}", params
568
+ unless num_params_taken
569
+ if @stop_on_unknown
570
+ remains += args[i + 1 .. -1]
571
+ return remains
572
+ else
573
+ remains += params
574
+ end
575
+ end
576
+ i += 1 + num_params_taken
577
+ else # argument no parameter
578
+ yield "-#{a}", nil
579
+ i += 1
580
+ end
581
+ else
582
+ yield "-#{a}", nil
583
+ end
584
+ end
585
+ else
586
+ if @stop_on_unknown
587
+ remains += args[i .. -1]
588
+ return remains
589
+ else
590
+ remains << args[i]
591
+ i += 1
592
+ end
593
+ end
594
+ end
595
+
596
+ remains
597
+ end
598
+
599
+ def parse_integer_parameter param, arg
600
+ raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
601
+ param.to_i
602
+ end
603
+
604
+ def parse_float_parameter param, arg
605
+ raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
606
+ param.to_f
607
+ end
608
+
609
+ def parse_io_parameter param, arg
610
+ case param
611
+ when /^(stdin|-)$/i; $stdin
612
+ else
613
+ require 'open-uri'
614
+ begin
615
+ open param
616
+ rescue SystemCallError => e
617
+ raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}"
618
+ end
619
+ end
620
+ end
621
+
622
+ def collect_argument_parameters args, start_at
623
+ params = []
624
+ pos = start_at
625
+ while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do
626
+ params << args[pos]
627
+ pos += 1
628
+ end
629
+ params
630
+ end
631
+
632
+ def resolve_default_short_options
633
+ @order.each do |type, name|
634
+ next unless type == :opt
635
+ opts = @specs[name]
636
+ next if opts[:short]
637
+
638
+ c = opts[:long].split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) }
639
+ if c # found a character to use
640
+ opts[:short] = c
641
+ @short[c] = name
642
+ end
643
+ end
644
+ end
645
+
646
+ def wrap_line str, opts={}
647
+ prefix = opts[:prefix] || 0
648
+ width = opts[:width] || (self.width - 1)
649
+ start = 0
650
+ ret = []
651
+ until start > str.length
652
+ nextt =
653
+ if start + width >= str.length
654
+ str.length
655
+ else
656
+ x = str.rindex(/\s/, start + width)
657
+ x = str.index(/\s/, start) if x && x < start
658
+ x || str.length
659
+ end
660
+ ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt]
661
+ start = nextt + 1
662
+ end
663
+ ret
664
+ end
665
+
666
+ ## instance_eval but with ability to handle block arguments
667
+ ## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html
668
+ def cloaker &b
669
+ (class << self; self; end).class_eval do
670
+ define_method :cloaker_, &b
671
+ meth = instance_method :cloaker_
672
+ remove_method :cloaker_
673
+ meth
674
+ end
675
+ end
676
+ end
677
+
678
+ ## The top-level entry method into Trollop. Creates a Parser object,
679
+ ## passes the block to it, then parses +args+ with it, handling any
680
+ ## errors or requests for help or version information appropriately (and
681
+ ## then exiting). Modifies +args+ in place. Returns a hash of option
682
+ ## values.
683
+ ##
684
+ ## The block passed in should contain zero or more calls to +opt+
685
+ ## (Parser#opt), zero or more calls to +text+ (Parser#text), and
686
+ ## probably a call to +version+ (Parser#version).
687
+ ##
688
+ ## The returned block contains a value for every option specified with
689
+ ## +opt+. The value will be the value given on the commandline, or the
690
+ ## default value if the option was not specified on the commandline. For
691
+ ## every option specified on the commandline, a key "<option
692
+ ## name>_given" will also be set in the hash.
693
+ ##
694
+ ## Example:
695
+ ##
696
+ ## require 'trollop'
697
+ ## opts = Trollop::options do
698
+ ## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
699
+ ## opt :goat, "Use goat mode", :default => true # a flag --goat, defaulting to true
700
+ ## opt :num_limbs, "Number of limbs", :default => 4 # an integer --num-limbs <i>, defaulting to 4
701
+ ## opt :num_thumbs, "Number of thumbs", :type => :int # an integer --num-thumbs <i>, defaulting to nil
702
+ ## end
703
+ ##
704
+ ## ## if called with no arguments
705
+ ## p opts # => { :monkey => false, :goat => true, :num_limbs => 4, :num_thumbs => nil }
706
+ ##
707
+ ## ## if called with --monkey
708
+ ## p opts # => {:monkey_given=>true, :monkey=>true, :goat=>true, :num_limbs=>4, :help=>false, :num_thumbs=>nil}
709
+ ##
710
+ ## See more examples at http://trollop.rubyforge.org.
711
+ def options args = ARGV, *a, &b
712
+ @p = Parser.new(*a, &b)
713
+ begin
714
+ vals = @p.parse args
715
+ args.clear
716
+ @p.leftovers.each { |l| args << l }
717
+ vals
718
+ rescue CommandlineError => e
719
+ $stderr.puts "Error: #{e.message}."
720
+ $stderr.puts "Try --help for help."
721
+ exit(-1)
722
+ rescue HelpNeeded
723
+ @p.educate
724
+ exit
725
+ rescue VersionNeeded
726
+ puts @p.version
727
+ exit
728
+ end
729
+ end
730
+
731
+ ## Informs the user that their usage of 'arg' was wrong, as detailed by
732
+ ## 'msg', and dies. Example:
733
+ ##
734
+ ## options do
735
+ ## opt :volume, :default => 0.0
736
+ ## end
737
+ ##
738
+ ## die :volume, "too loud" if opts[:volume] > 10.0
739
+ ## die :volume, "too soft" if opts[:volume] < 0.1
740
+ ##
741
+ ## In the one-argument case, simply print that message, a notice
742
+ ## about -h, and die. Example:
743
+ ##
744
+ ## options do
745
+ ## opt :whatever # ...
746
+ ## end
747
+ ##
748
+ ## Trollop::die "need at least one filename" if ARGV.empty?
749
+ def die arg, msg=nil
750
+ if msg
751
+ $stderr.puts "Error: argument --#{@p.specs[arg][:long]} #{msg}."
752
+ else
753
+ $stderr.puts "Error: #{arg}."
754
+ end
755
+ $stderr.puts "Try --help for help."
756
+ exit(-1)
757
+ end
758
+
759
+ module_function :options, :die
760
+
761
+ end # module