Class: Aargs

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

Overview

Basic aargs parser

Constant Summary collapse

DEFAULT =
Object.new

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(prologue: DEFAULT, flag_config: DEFAULT, flag_configs: nil, epilogue: DEFAULT, aliases: {}, program: nil) ⇒ Aargs

Returns a new instance of Aargs.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/aargs.rb', line 165

def initialize(
  prologue: DEFAULT,
  flag_config: DEFAULT,
  flag_configs: nil,
  epilogue: DEFAULT,
  aliases: {},
  program: nil)
  @program = program || begin
    %r{^(?:.*/)?(?<file>[^/]+):\d+:in} =~ caller.first
    file
  end
  @aliases = aliases.freeze
  prologue_set = prologue && prologue != DEFAULT
  flag_configs_set = flag_configs && flag_configs != DEFAULT
  epilogue_set = epilogue && epilogue != DEFAULT
  prologue = epilogue_set || flag_configs_set ? false : true if prologue == DEFAULT
  initialize_prologue(prologue)
  flag_config = flag_configs_set ? false : true if flag_config == DEFAULT
  @flag_configs = Hash.new(flag_config).merge(flag_configs || {}).freeze
  epilogue = prologue_set || flag_configs_set ? false : true if epilogue == DEFAULT
  initialize_epilogue(epilogue)
  @valid = false
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(sym, *_) ⇒ Object



359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/aargs.rb', line 359

def method_missing(sym, *_)
  return super unless @parsed

  /^(?<key>.*?)(?:(?<boolean>\?))?$/ =~ sym
  key = key.to_sym
  return super unless api_key?(key)

  value = @values[key]
  return !(!value) if boolean

  value
end

Instance Attribute Details

#aliasesObject (readonly)

Returns Hash.

Returns:

  • Hash



152
153
154
# File 'lib/aargs.rb', line 152

def aliases
  @aliases
end

#epilogue_keyObject (readonly)

Returns the value of attribute epilogue_key.



161
162
163
# File 'lib/aargs.rb', line 161

def epilogue_key
  @epilogue_key
end

#flag_configsObject (readonly)

Returns the value of attribute flag_configs.



158
159
160
# File 'lib/aargs.rb', line 158

def flag_configs
  @flag_configs
end

#optional_epilogueObject (readonly)

Returns the value of attribute optional_epilogue.



160
161
162
# File 'lib/aargs.rb', line 160

def optional_epilogue
  @optional_epilogue
end

#optional_prologueObject (readonly)

Returns the value of attribute optional_prologue.



156
157
158
# File 'lib/aargs.rb', line 156

def optional_prologue
  @optional_prologue
end

#prologue_keyObject (readonly)

Returns the value of attribute prologue_key.



157
158
159
# File 'lib/aargs.rb', line 157

def prologue_key
  @prologue_key
end

#required_epilogueObject (readonly)

Returns the value of attribute required_epilogue.



159
160
161
# File 'lib/aargs.rb', line 159

def required_epilogue
  @required_epilogue
end

#required_prologueObject (readonly)

Returns the value of attribute required_prologue.



155
156
157
# File 'lib/aargs.rb', line 155

def required_prologue
  @required_prologue
end

Class Method Details

.boolean?(sym, flag_configs:) ⇒ Boolean

Returns:

  • (Boolean)


259
260
261
# File 'lib/aargs.rb', line 259

def self.boolean?(sym, flag_configs:)
  flag_type(sym, flag_configs: flag_configs) == :boolean
end

.flag_config(sym, flag_configs:) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/aargs.rb', line 238

def self.flag_config(sym, flag_configs:)
  flag_config = flag_configs[sym]
  case flag_config
  when true
    { type: :anything }
  when Symbol
    { type: flag_config }
  when nil
    nil
  when String
    { help: flag_config }
  else
    flag_config
  end
end

.flag_type(sym, flag_configs:) ⇒ Object



254
255
256
257
# File 'lib/aargs.rb', line 254

def self.flag_type(sym, flag_configs:)
  config = flag_config(sym, flag_configs: flag_configs)
  config[:type] if config
end

.flagify_arg(arg) ⇒ Object



15
16
17
18
19
20
21
22
23
24
# File 'lib/aargs.rb', line 15

def self.flagify_arg(arg)
  case arg
  when Symbol
    "--#{kebab(arg)}"
  when Hash
    arg.map(&method(:flagify_kwarg)).flatten
  else
    arg
  end
end

.flagify_kwarg(arg, value) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/aargs.rb', line 26

def self.flagify_kwarg(arg, value)
  case value
  when TrueClass
    "--#{kebab(arg)}"
  when FalseClass
    "--no-#{kebab(arg)}"
  when Array
    value.map { |v| "--#{kebab(arg)}=#{v}" }
  else
    "--#{kebab(arg)}=#{value}"
  end
end

.kebab(sym) ⇒ Object



7
8
9
# File 'lib/aargs.rb', line 7

def self.kebab(sym)
  sym.to_s.gsub(/[^[:alnum:]]/, '-')
end

.parse(args_or_argv, aliases: {}, flag_configs: {}) ⇒ Object



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
# File 'lib/aargs.rb', line 46

def self.parse(args_or_argv, aliases: {}, flag_configs: {})
  argv = to_argv(*args_or_argv)

  literal_only = false
  prologue = []
  epilogue = []
  flags = {}
  last_sym = nil
  last_sym_pending = nil

  resolve = lambda do |src|
    raise "Missing value after '#{last_sym_pending}'" if last_sym_pending

    sym = underscore(src)
    aliases[sym] || sym
  end

  argv.each do |arg|
    if literal_only
      epilogue << arg
      next
    end
    case arg
    when /^--$/
      literal_only = true
      last_sym = nil
    when /^-([[:alnum:]])$/
      last_sym = sym = resolve.call(Regexp.last_match(1))
      case flags[sym]
      when true
        flags[sym] = 2
      when Integer
        flags[sym] += 1
      when nil
        flags[sym] = true
      else
        raise "Unexpected boolean '#{arg}' after set to value #{flags[sym].inspect}"
      end

    when /^--(?<no>no-)?(?<flag>[[:alnum:]-]+)(?:=(?<value>.*))?$/
      flag = Regexp.last_match[:flag]
      value = Regexp.last_match[:value]
      no = Regexp.last_match[:no]
      sym = resolve.call(flag)
      boolean = boolean?(sym, flag_configs: flag_configs)
      if no
        raise "Unexpected value specified with no- prefix: #{arg}" unless value.nil?

        flags[sym] = false
        last_sym = nil
      elsif value.nil?
        last_sym = boolean ? nil : sym
        case flags[sym]
        when true
          flags[sym] = 2
        when Integer
          flags[sym] += 1
        when nil, false
          flags[sym] = true
        else
          last_sym_pending = arg
        end
      else
        raise "Unexpected value for #{inspect_flag(arg)}: #{value.inspect}" if boolean

        last_sym = nil
        case flags[sym]
        when nil
          flags[sym] = value
        when Array
          flags[sym] << value
        else
          flags[sym] = [flags[sym], value]
        end
      end

    else
      if last_sym
        case flags[last_sym]
        when true
          flags[last_sym] = arg
        when Array
          flags[last_sym] << arg
        else
          flags[last_sym] = [flags[last_sym], arg]
        end
        last_sym_pending = nil
      elsif flags.empty?
        prologue << arg
      else # first non-switch after switches + values
        literal_only = true
        epilogue << arg
      end
    end
    next if arg.nil?
  end
  raise "Missing value after '#{last_sym_pending}'" if last_sym_pending

  result = {}
  result[:prologue] = prologue unless prologue.empty?
  result[:flags] = flags unless flags.empty?
  result[:epilogue] = epilogue unless epilogue.empty?
  result unless result.empty?
end

.to_argv(*args) ⇒ Object

Convert symbolic arguments and keyword-arguments into an equivalent ‘ARGV`. Non-symbol argments remain unchanged. Note that to generate a epilogue portion of an ARGV you need to pass keyword arguments as explicit hashes followed by non-hash, non-symbol values.



42
43
44
# File 'lib/aargs.rb', line 42

def self.to_argv(*args)
  args.map(&method(:flagify_arg)).flatten
end

.underscore(src) ⇒ Object



11
12
13
# File 'lib/aargs.rb', line 11

def self.underscore(src)
  src.gsub(/[^[:alnum:]]/, '_').to_sym
end

Instance Method Details

#api_key?(key) ⇒ Boolean

Returns if the given key is a known flag that should appear as part of the object’s API.

Returns:

  • (Boolean)

    if the given key is a known flag that should appear as part of the object’s API



346
347
348
# File 'lib/aargs.rb', line 346

def api_key?(key)
  @values.member?(key) || @optional_prologue.member?(key) || @flag_configs.member?(key)
end

#boolean?(sym) ⇒ Boolean

Returns:

  • (Boolean)


271
272
273
# File 'lib/aargs.rb', line 271

def boolean?(sym)
  Aargs.boolean?(sym, flag_configs: flag_configs)
end

#flag_config(sym) ⇒ Object



263
264
265
# File 'lib/aargs.rb', line 263

def flag_config(sym)
  Aargs.flag_config(sym, flag_configs: flag_configs)
end

#flag_type(sym) ⇒ Object



267
268
269
# File 'lib/aargs.rb', line 267

def flag_type(sym)
  Aargs.flag_type(sym, flag_configs: flag_configs)
end

#helpObject



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/aargs.rb', line 298

def help
  prologue_keys = [required_prologue, optional_prologue, prologue_key ? prologue_key : nil].map(&method(:Array)).flatten
  epilogue_keys = [required_epilogue, optional_epilogue, epilogue_key ? epilogue_key : nil].map(&method(:Array)).flatten
  flag_keys = flag_configs.keys
  flag_keys << :any_key if flag_configs[:any_key]
  all_flags = prologue_keys + (flag_keys - prologue_keys) + epilogue_keys
  usage = "Usage: #{@program} #{all_flags.map(&method(:inspect_flag)).join(' ')}"
  any_real_help = false
  lines = all_flags.map do |flag|
    config = flag_config(flag)
    next unless config

    real_help = config[:help]
    any_real_help ||= real_help
    flag_help = real_help || case config[:type]
                             when :boolean
                               '(switch)'
                             else
                               "(#{config[:type]})"
                             end
    [inspect_flag(flag), flag_help] if flag_help
  end.compact
  return [usage] if lines.empty? || !any_real_help

  width = lines.map(&:first).map(&:length).max
  lines.map! { |(flag, help)| format("  %<flag>-#{width}s : %<help>s", flag: flag, help: help) }
  [usage, nil] + lines
end

#inspect_flag(sym) ⇒ Object



287
288
289
290
291
292
293
294
295
296
# File 'lib/aargs.rb', line 287

def inspect_flag(sym)
  arg = Aargs.kebab(sym)
  return "#{arg.upcase}" if required?(sym)
  return "[#{arg.upcase}]" if optional?(sym)
  return "[aargs]" if sym == :any_key
  return "[#{arg.to_s.upcase} ... [#{arg.to_s.upcase}]]" if splat?(sym)
  return "--[no-]#{arg}" if boolean?(sym)

  "--#{arg}=VALUE"
end

#optional?(sym) ⇒ Boolean

Returns:

  • (Boolean)


279
280
281
# File 'lib/aargs.rb', line 279

def optional?(sym)
  [optional_prologue, optional_epilogue].map(&method(:Array)).flatten.member?(sym)
end

#parse(*args) ⇒ Object



331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/aargs.rb', line 331

def parse(*args)
  raise 'Aargs are frozen once parsed' if @valid

  @parsed = Aargs.parse(args, aliases: aliases, flag_configs: flag_configs) || {}
  @values = @parsed[:flags] || {}
  parsed_prologue = @parsed[:prologue] || []

  validate_sufficient_prologue(parsed_prologue)
  consumed_prologue = apply_prologue(parsed_prologue)
  apply_epilogue(parsed_prologue, consumed_prologue)
  @valid = true
  self
end

#required?(sym) ⇒ Boolean

Returns:

  • (Boolean)


275
276
277
# File 'lib/aargs.rb', line 275

def required?(sym)
  [required_prologue, required_epilogue].map(&method(:Array)).flatten.member?(sym)
end

#respond_to_missing?(sym, *_) ⇒ Boolean

Returns:

  • (Boolean)


350
351
352
353
354
355
356
357
# File 'lib/aargs.rb', line 350

def respond_to_missing?(sym, *_)
  /^(?<key>.*?)(?:(?<_boolean>\?))?$/ =~ sym
  key = key.to_sym
  # puts(sym: sym, key: key, values: @values)
  return super unless api_key?(key)

  true
end

#splat?(sym) ⇒ Boolean

Returns:

  • (Boolean)


283
284
285
# File 'lib/aargs.rb', line 283

def splat?(sym)
  [prologue_key, epilogue_key].member?(sym)
end

#valid?Boolean

Returns:

  • (Boolean)


327
328
329
# File 'lib/aargs.rb', line 327

def valid?
  @valid
end