class ICalPal::Options
Options can be abbreviated as long as they are unique.
options which require a single hyphen.
icalpal requires two hyphens for all options, except single-letter<br>icalBuddy. Note that
Many options are intentionally copied from
* Command-line arguments
* Configuration file
* Environment variables
* Defaults
Handle program options from all sources:
def initialize
def initialize # prologue @op = OptionParser.new @op.summary_width = 23 @op.banner += ' [-c] COMMAND' @op.version = VERSION @op.accept(RDT) { |s| 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('--norc', 'Ignore ICALPAL and ICALPAL_CONFIG environment variables') @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', RDT, 'List events starting on or after DATE') @op.on('--to=DATE', 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('')) @op.on_tail('') note = 'Do not quote or escape values.' note += ' Options set in ICALPAL override ICALPAL_CONFIG.' note += ' Options on the command line override ICALPAL.' @op.on_tail("#{@op.summary_indent}#{note}") end
def pad(t)
-
(Array
- Array containing +summary_indent+, +t+,)
Parameters:
-
t
(String
) -- Text on the left side
def pad(t) [ @op.summary_indent, t, ' ' * (@op.summary_width - t.length) ] end
def parse_options
-
(Hash)
- All options loaded from defaults, environment
def parse_options begin cli = {} env = {} cf = {} # Load from CLI @op.parse!(into: cli) # Environment variable needs special parsing. # OptionParser.parse doesn't handle whitespace in a # comma-separated value. begin o = [] ENV['ICALPAL'].gsub(/^-/, ' -').split(' -').each do |e| a = e.split(' ', 2) if a[0] o.push("-#{a[0]}") o.push(a[1]) if a[1] end end @op.parse!(o, into: env) end if ENV['ICALPAL'] && !cli[:norc] # Configuration file needs special parsing for the same reason begin o = [] cli[:cf] ||= ENV['ICALPAL_CONFIG'] || $defaults[:common][:cf] File.read(File.expand_path(cli[:cf])).split("\n").each do |line| a = line.split(' ', 2) if a[0] && a[0][0] != '#' o.push(a[0]) o.push(a[1]) if a[1] end end @op.parse!(o, into: cf) rescue StandardError end unless cli[:norc] 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[:now] = true if m[1] == 'Now' cli[:days] = (m[1] == 'Today')? m[2].to_i : 1 cli[:from] = $today cli[:to] = $today + cli[:days] if 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[:to] = RDT.new(*opts[:to].to_a[0..2] + [ 23, 59, 59 ]) opts[:days] ||= Integer(opts[:to] - opts[:from]) opts[:from] = $now if opts[:n] end # Sorting opts[:sort] = 'due_date' if opts[:std] || 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