Class: ICalPal::Options

Inherits:
Object
  • Object
show all
Defined in:
lib/options.rb

Overview

Handle program options from all sources:

  • Defaults

  • Environment variables

  • Configuration file

  • Command-line arguments

Many options are intentionally copied from icalBuddy. Note that icalpal requires two hyphens for all options, except single-letter options which require a single hyphen.

Options can be abbreviated as long as they are unique.

Constant Summary collapse

COMMANDS =

Commands that can be run

%w{events eventsToday eventsNow tasks datedTasks undatedTasks calendars accounts stores}
OUTFORMATS =

Supported output formats

%w{ansi csv default hash html json md rdoc remind toc xml yaml}

Instance Method Summary collapse

Constructor Details

#initializeOptions

Define the OptionParser



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/options.rb', line 22

def initialize
  # prologue
  @op = OptionParser.new
  @op.summary_width = 23
  @op.banner += " [-c] COMMAND"
  @op.version = ICalPal::VERSION

  @op.accept(ICalPal::RDT) { |s| ICalPal::RDT.conv(s) }

  # head
  @op.on_head("\nCOMMAND must be one of the following:\n\n")

  @op.on("%s%s %sPrint events" % pad('events'))
  @op.on("%s%s %sPrint tasks" % pad('tasks'))
  @op.on("%s%s %sPrint calendars" % pad('calendars'))
  @op.on("%s%s %sPrint accounts" % pad('accounts'))

  @op.separator('')
  @op.on("%s%s %sPrint events occurring today" % pad('eventsToday'))
  @op.on("%s%s %sPrint events occurring between today and NUM days into the future" % pad('eventsToday+NUM'))
  @op.on("%s%s %sPrint events occurring at present time" % pad('eventsNow'))
  @op.on("%s%s %sPrint tasks with a due date" % pad('datedTasks'))
  @op.on("%s%s %sPrint tasks with no due date" % pad('undatedTasks'))

  # global
  @op.separator("\nGlobal options:\n\n")

  @op.on('-c=COMMAND', '--cmd=COMMAND', COMMANDS, 'Command to run')
  @op.on('--db=DB', "Use DB file instead of Calendar",
         "(default: #{$defaults[:common][:db]}",
         'For the tasks commands this should be a directory containing .sqlite files',
         "(default: #{$defaults[:tasks][:db]})")
  @op.on('--cf=FILE', "Set config file path (default: #{$defaults[:common][:cf]})")
  @op.on('-o', '--output=FORMAT', OUTFORMATS,
        "Print as FORMAT (default: #{$defaults[:common][:output]})", "[#{OUTFORMATS.join(', ')}]")
  
  # include/exclude
  @op.separator("\nIncluding/excluding accounts, calendars, items:\n\n")

  @op.on('--is=ACCOUNTS', Array, 'List of accounts to include')
  @op.on('--es=ACCOUNTS', Array, 'List of accounts to exclude')

  @op.separator('')
  @op.on('--it=TYPES', Array, 'List of calendar types to include')
  @op.on('--et=TYPES', Array, 'List of calendar types to exclude',
        "[#{EventKit::EKSourceType.map { |i| i[:name] }.join(', ') }]")

  @op.separator('')
  @op.on('--ic=CALENDARS', Array, 'List of calendars to include')
  @op.on('--ec=CALENDARS', Array, 'List of calendars to exclude')

  @op.separator('')
  @op.on('--il=LISTS', Array, 'List of reminder lists to include')
  @op.on('--el=LISTS', Array, 'List of reminder lists to exclude')

  @op.separator('')
  @op.on('--match=FIELD=REGEXP', String, 'Include only items whose FIELD matches REGEXP (ignoring case)')

  # dates
  @op.separator("\nChoosing dates:\n\n")

  @op.on('--from=DATE', ICalPal::RDT, 'List events starting on or after DATE')
  @op.on('--to=DATE', ICalPal::RDT, 'List events starting on or before DATE',
        'DATE can be yesterday, today, tomorrow, +N, -N, or anything accepted by DateTime.parse()',
        'See https://ruby-doc.org/stdlib-2.6.1/libdoc/date/rdoc/DateTime.html#method-c-parse')
  @op.separator('')
  @op.on('-n', 'Include only events from now on')
  @op.on('--days=N',  OptionParser::DecimalInteger,
        'Show N days of events, including start date')
  @op.on('--sed', 'Show empty dates with --sd')
  @op.on('--ia', 'Include only all-day events')
  @op.on('--ea', 'Exclude all-day events')

  # properties
  @op.separator("\nChoose properties to include in the output:\n\n")

  @op.on('--iep=PROPERTIES', Array, 'List of properties to include')
  @op.on('--eep=PROPERTIES', Array, 'List of properties to exclude')
  @op.on('--aep=PROPERTIES', Array, 'List of properties to include in addition to the default list')
  @op.separator('')
  @op.on('--itp=PROPERTIES', Array, 'List of task properties to include')
  @op.on('--etp=PROPERTIES', Array, 'List of task properties to exclude')
  @op.on('--atp=PROPERTIES', Array, 'List of task properties to include in addition to the default list',
        'Included for backwards compatability, these are aliases for --iep, --eep, and --aep')
  @op.separator('')

  @op.on('--uid', 'Show event UIDs')
  @op.on('--eed', 'Exclude end datetimes')

  @op.separator('')
  @op.on('--nc', 'No calendar names')
  @op.on('--npn', 'No property names')
  @op.on('--nrd', 'No relative dates')

  @op.separator('')
  @op.separator(@op.summary_indent + 'Properties are listed in the order specified')
  @op.separator('')
  @op.separator(@op.summary_indent +
               "Use 'all' for PROPERTIES to include all available properties (except any listed in --eep)")
  @op.separator(@op.summary_indent +
               "Use 'list' for PROPERTIES to list all available properties and exit")

  # formatting
  @op.separator("\nFormatting the output:\n\n")

  @op.on('--li=N', OptionParser::DecimalInteger, 'Show at most N items (default: 0 for no limit)')

  @op.separator('')
  @op.on('--sc', 'Separate by calendar')
  @op.on('--sd', 'Separate by date')
  @op.on('--sp', 'Separate by priority')
  @op.on('--sep=PROPERTY', 'Separate by PROPERTY')
  @op.separator('')
  @op.on('--sort=PROPERTY', 'Sort by PROPERTY')
  @op.on('--std', 'Sort tasks by due date (same as --sort=due_date)')
  @op.on('--stda', 'Sort tasks by due date (ascending) (same as --sort=due_date -r)')
  @op.on('-r', '--reverse', 'Sort in reverse')

  @op.separator('')
  @op.on('--ps=SEPARATORS', Array, 'List of property separators')
  @op.on('--ss=SEPARATOR', String, 'Set section separator')

  @op.separator('')
  @op.on('--df=FORMAT', String, 'Set date format')
  @op.on('--tf=FORMAT', String, 'Set time format',
        'See https://ruby-doc.org/stdlib-2.6.1/libdoc/date/rdoc/DateTime.html#method-i-strftime for details')

  @op.separator('')
  @op.on('-b', '--bullet=STRING', String, 'Use STRING for bullets')
  @op.on('--ab=STRING', String, 'Use STRING for alert bullets')
  @op.on('--nb', 'Do not use bullets')
  @op.on('--nnr=SEPARATOR', String, 'Set replacement for newlines within notes')

  @op.separator('')
  @op.on('-f', 'Format output using standard ANSI colors')
  @op.on('--color', 'Format output using a larger color palette')

  # help
  @op.separator("\nHelp:\n\n")

  @op.on('-h', '--help', 'Show this message') { @op.abort(@op.help) }
  @op.on('-V', '-v', '--version', "Show version and exit (#{@op.version})") { @op.abort(@op.version)  }
  @op.on('-d', '--debug=LEVEL', /#{Regexp.union(Logger::SEV_LABEL[0..-2]).source}/i,
        "Set the logging level (default: #{Logger::SEV_LABEL[$defaults[:common][:debug]].downcase})",
        "[#{Logger::SEV_LABEL[0..-2].join(', ').downcase}]")

  # environment variables
  @op.on_tail("\nEnvironment variables:\n\n")

  @op.on_tail("%s%s %sAdditional arguments" % pad('ICALPAL'))
  @op.on_tail("%s%s %sAdditional arguments from a file" % pad('ICALPAL_CONFIG'))
  @op.on_tail("%s%s %s(default: #{$defaults[:common][:cf]})" % pad(''))
end

Instance Method Details

#parse_optionsHash

Parse options from the CLI and merge them with other sources

Returns:

  • (Hash)

    All options loaded from defaults, environment variables, configuration file, and the command line



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/options.rb', line 180

def parse_options
  begin
    cli = {}
    env = {}
    cf = {}

    # Load from CLI, environment, configuration file
    @op.parse!(into: cli)
    @op.parse!(ENV['ICALPAL'].split, into: env) rescue nil
    cli[:cf] ||= ENV['ICALPAL_CONFIG'] || $defaults[:common][:cf]
    @op.parse!(File.read(File.expand_path(cli[:cf])).split, into: cf) rescue nil

    cli[:cmd] ||= @op.default_argv[0]
    cli[:cmd] ||= env[:cmd] if env[:cmd]
    cli[:cmd] ||= cf[:cmd] if cf[:cmd]
    cli[:cmd] = 'stores' if cli[:cmd] == 'accounts'

    # Parse eventsNow and eventsToday commands
    cli[:cmd].match('events(Now|Today)(\+[0-9]+)?') do |m|
      cli[:n] = true if m[1] == 'Now'
      cli[:days] = (m[1] == 'Today')? m[2].to_i + 1 : 1

      cli[:from] = $today
      cli[:to] = $today + cli[:days]
      cli[:days] = Integer(cli[:to] - cli[:from])

      cli[:cmd] = 'events'
    end if cli[:cmd]

    # Must have a valid command
    raise(OptionParser::MissingArgument, 'COMMAND is required') unless cli[:cmd]
    raise(OptionParser::InvalidArgument, "Unknown COMMAND #{cli[:cmd]}") unless (COMMANDS.any? cli[:cmd])

    # Merge options
    opts = $defaults[:common]
      .merge($defaults[cli[:cmd].to_sym])
      .merge(cf)
      .merge(env)
      .merge(cli)

    # datedTasks and undatedTasks
    opts[:cmd] = "tasks" if opts[:cmd] == "datedTasks"
    opts[:cmd] = "tasks" if opts[:cmd] == "undatedTasks"

    # Make sure opts[:db] and opts[:tasks] are Arrays
    opts[:db] = [ opts[:db] ] unless opts[:db].is_a?(Array)
    opts[:tasks] = [ opts[:tasks] ] unless opts[:db].is_a?(Array)

    # All kids love log!
    $log.level = opts[:debug]

    # For posterity
    opts[:ruby] = RUBY_VERSION
    opts[:version] = @op.version

    # From the Department of Redundancy Department
    opts[:iep] += opts[:itp] if opts[:itp]
    opts[:eep] += opts[:etp] if opts[:etp]
    opts[:aep] += opts[:atp] if opts[:atp]
    opts[:props] = (opts[:iep] + opts[:aep] - opts[:eep]).uniq

    # From, to, days
    if opts[:from]
      opts[:to] += 1 if opts[:to]
      opts[:to] ||= opts[:from] + 1 if opts[:from]
      opts[:to] = opts[:from] + opts[:days] if opts[:days]
      opts[:days] ||= Integer(opts[:to] - opts[:from])
      opts[:from] = $now if opts[:n]
    end

    # Sorting
    opts[:sort] = 'due_date' if opts[:std] or opts[:stda]
    opts[:reverse] = true if opts[:std]

    # Colors
    opts[:palette] = 8 if opts[:f]
    opts[:palette] = 24 if opts[:color]

    # Sections
    unless opts[:sep]
      opts[:sep] = 'calendar' if opts[:sc]
      opts[:sep] = 'sday' if opts[:sd]
      opts[:sep] = 'long_priority' if opts[:sp]
    end
    opts[:nc] = true if opts[:sc]

    # Sanity checks
    raise(OptionParser::InvalidArgument, '--li cannot be negative') if opts[:li].negative?
    raise(OptionParser::InvalidOption, 'Start date must be before end date') if opts[:from] && opts[:from] > opts[:to]
    raise(OptionParser::MissingArgument, 'No properties to display') if opts[:props].empty?

  rescue StandardError => e
    @op.abort("#{e}\n\n#{@op.help}\n#{e}")
  end

  opts.sort.to_h
end