Class: Aspera::Cli::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/aspera/cli/parser.rb

Overview

parse command line options arguments options start with '-', others are commands resolves on extended value syntax

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(program_name, argv = nil) ⇒ Parser

Returns a new instance of Parser.

Parameters:

  • Name of the program

  • (defaults to: nil)

    Command line arguments to parse



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'lib/aspera/cli/parser.rb', line 328

def initialize(program_name, argv = nil)
  # Option descriptions: maps option symbol to its OptionValue descriptor
  # @type [Hash{Symbol => OptionValue}]
  @declared_options = {}
  # do we ask missing options and arguments to user ?
  @ask_missing_mandatory = false # STDIN.isatty
  # ask optional options if not provided and in interactive
  @ask_missing_optional = false
  # get_option fails if a mandatory parameter is asked
  @fail_on_missing_mandatory = true
  # set to true when --help / -h is parsed
  @help_requested = false
  # options can also be provided by env vars : --param-name -> ASCLI_PARAM_NAME
  @option_pairs_batch = {}
  @option_pairs_env = {}
  # Short option char -> option symbol, e.g. {'h' => :help, 'v' => :version}
  @short_options = {}
  # Current help section group name, set by #group
  @current_group = 'global'
  env_prefix = program_name.upcase + OPTION_SEP_SYMBOL
  ENV.each do |k, v|
    @option_pairs_env[k.delete_prefix(env_prefix).downcase.to_sym] = v if k.start_with?(env_prefix)
  end
  Log.log.debug{"env=#{@option_pairs_env}".red}
  # command line values starting with at least one '-'
  @unprocessed_cmd_line_options = []
  # command line values *not* starting with '-'
  @unprocessed_cmd_line_arguments = []
  # a copy of all initial options
  @initial_cli_options = []
  # For each option string: list (one entry per occurrence) of the number of positional args
  # that appear before it in original argv. Used by `@:` in option values to skip preceding args.
  # @type [Hash{String => Array<Integer>}]
  @args_before_option = {}
  return if argv.nil?
  # true until `--` is found (stop options)
  process_options = true
  arg_count = 0
  argv.each do |value|
    if process_options && value.start_with?('-')
      Log.log.trace1{"opt: #{value}"}
      if value.eql?(OPTIONS_STOP)
        process_options = false
      else
        @unprocessed_cmd_line_options.push(value)
        (@args_before_option[value] ||= []).push(arg_count)
      end
    else
      Log.log.trace1{"arg: #{value}"}
      @unprocessed_cmd_line_arguments.push(value)
      arg_count += 1
    end
  end
  # Total positional args at parse time - used in args_as_extended to compute how many to skip.
  @arg_total_count = @unprocessed_cmd_line_arguments.length
  # Number of original positional args before the option currently being parsed (nil = positional context).
  @current_option_args_offset = nil
  @initial_cli_options = @unprocessed_cmd_line_options.dup.freeze
  Log.log.trace1{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
  declare(:interactive, description: 'Use interactive input of missing params', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :ask_missing_mandatory})
  declare(:ask_options, description: 'Ask even optional options', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :ask_missing_optional})
  # do not parse options yet, let's wait for option `-h` to be overridden
end

Instance Attribute Details

#ask_missing_mandatoryObject

Returns the value of attribute ask_missing_mandatory.



323
324
325
# File 'lib/aspera/cli/parser.rb', line 323

def ask_missing_mandatory
  @ask_missing_mandatory
end

#ask_missing_optionalObject

Returns the value of attribute ask_missing_optional.



323
324
325
# File 'lib/aspera/cli/parser.rb', line 323

def ask_missing_optional
  @ask_missing_optional
end

#declared_optionsHash{Symbol => OptionValue} (readonly)

Returns all declared options (read-only view).

Returns:

  • all declared options (read-only view)



554
555
556
# File 'lib/aspera/cli/parser.rb', line 554

def declared_options
  @declared_options
end

#fail_on_missing_mandatory=(value) ⇒ Object (writeonly)

Sets the attribute fail_on_missing_mandatory

Parameters:

  • the value to set the attribute fail_on_missing_mandatory to.



324
325
326
# File 'lib/aspera/cli/parser.rb', line 324

def fail_on_missing_mandatory=(value)
  @fail_on_missing_mandatory = value
end

#help_requestedObject

Returns the value of attribute help_requested.



323
324
325
# File 'lib/aspera/cli/parser.rb', line 323

def help_requested
  @help_requested
end

Class Method Details

.get_from_list(short_value, descr, allowed_values) ⇒ Object

Find shortened string value in allowed symbol list

Raises:



274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/aspera/cli/parser.rb', line 274

def get_from_list(short_value, descr, allowed_values)
  Aspera.assert_type(short_value, String)
  # we accept shortcuts
  matching_exact = allowed_values.select{ |i| i.to_s.eql?(short_value)}
  return matching_exact.first if matching_exact.length == 1
  matching = allowed_values.select{ |i| i.to_s.start_with?(short_value)}
  raise BadArgument, "Identifier '#{short_value}' used where a #{descr} is expected: place the identifier after the command" if matching.empty? && short_value.match?(REGEX_LOOKUP_ID_BY_FIELD)
  Aspera.assert(!matching.empty?, multi_choice_assert_msg("unknown value for #{descr}: #{short_value}", allowed_values), type: BadArgument)
  Aspera.assert(matching.length.eql?(1), multi_choice_assert_msg("ambiguous shortcut for #{descr}: #{short_value}", matching), type: BadArgument)
  return BoolValue.true?(matching.first) if allowed_values.eql?(BoolValue::ALL)
  matching.first
end

.match_prefix(short_value, allowed_values) ⇒ Object?

Find a key in a list by exact match or unique prefix match

Returns:

  • the matching key, or nil if none or ambiguous



289
290
291
292
293
# File 'lib/aspera/cli/parser.rb', line 289

def match_prefix(short_value, allowed_values)
  return short_value if allowed_values.include?(short_value)
  matches = allowed_values.select{ |k| k.to_s.start_with?(short_value.to_s)}
  matches.length == 1 ? matches.first : nil
end

.multi_choice_assert_msg(error_msg, accept_list) ⇒ Object

Generates error message with list of allowed values

Parameters:

  • Error message

  • List of allowed values



298
299
300
# File 'lib/aspera/cli/parser.rb', line 298

def multi_choice_assert_msg(error_msg, accept_list)
  [error_msg, 'Use:', *accept_list.map{ |choice| "- #{choice}"}.sort].join("\n")
end

.option_line_to_name(name) ⇒ String

Change option name with dash to name with underscore

Parameters:

  • option name with dash separators

Returns:

  • option name with underscore separators



305
306
307
# File 'lib/aspera/cli/parser.rb', line 305

def option_line_to_name(name)
  name.gsub(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
end

.option_name_to_line(name) ⇒ Object



309
310
311
# File 'lib/aspera/cli/parser.rb', line 309

def option_name_to_line(name)
  "#{OPTION_PREFIX}#{name.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)}"
end

.percent_selector(identifier) ⇒ Hash{Symbol => String}?

Returns {field:,value:} if identifier is a percent selector, else nil.

Returns:

  • {field:,value:} if identifier is a percent selector, else nil



314
315
316
317
318
319
320
# File 'lib/aspera/cli/parser.rb', line 314

def percent_selector(identifier)
  Aspera.assert_type(identifier, String)
  if (m = identifier.match(REGEX_LOOKUP_ID_BY_FIELD))
    return {field: m[1], value: ExtendedValue.instance.evaluate(m[2], context: "percent selector: #{m[1]}")}
  end
  nil
end

Instance Method Details

#add_option_preset(preset_hash, where, override: true) ⇒ Object

Adds each of the keys of specified hash as an option

Parameters:

  • Options to add

  • Where the value comes from

  • (defaults to: true)

    Override if already present



629
630
631
632
633
634
635
636
# File 'lib/aspera/cli/parser.rb', line 629

def add_option_preset(preset_hash, where, override: true)
  Aspera.assert_type(preset_hash, Hash)
  Log.log.debug{"add_option_preset: #{preset_hash}, #{where}, #{override}"}
  preset_hash.each do |k, v|
    option_symbol = k.to_sym
    @option_pairs_batch[option_symbol] = v if override || !@option_pairs_batch.key?(option_symbol)
  end
end

#add_types_info(types) ⇒ String

Add a type to the message if not special types

Parameters:

  • types to add

Returns:

  • Types if relevant



395
396
397
398
# File 'lib/aspera/cli/parser.rb', line 395

def add_types_info(types)
  return '' if !types || types.empty? || types.eql?(Allowed::TYPES_ENUM) || types.eql?(Allowed::TYPES_BOOLEAN) || types.eql?(Allowed::TYPES_STRING)
  " (#{types.map(&:name).join(', ')})"
end

#args_as_extended(end_marker) ⇒ Hash, Array

Read remaining args and build an Array or Hash

Parameters:

  • Argument to @: extended value

Returns:

  • Object representing dot-path values



801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
# File 'lib/aspera/cli/parser.rb', line 801

def args_as_extended(end_marker)
  # This extended value does not take args (`@:`)
  # ExtendedValue.assert_no_value(end_marker, :p)
  end_marker = SpecialValues::EOA if end_marker.empty?
  # When called from an option value, skip positional args that appear before the option in argv.
  # @current_option_args_offset holds the number of original args before the option (nil = positional context).
  # The number to actually skip = args_before_option - args_already_consumed (clamped to 0).
  skip_count = if @current_option_args_offset
    [@current_option_args_offset - (@arg_total_count - @unprocessed_cmd_line_arguments.length), 0].max
  else
    0
  end
  skipped = skip_count.positive? ? @unprocessed_cmd_line_arguments.shift(skip_count) : []
  Log.log.trace1{"args_as_extended: skipping #{skipped.length} args before option: #{skipped}"} unless skipped.empty?
  result = nil
  get_next_argument('args', multiple: end_marker).each do |argument|
    Aspera.assert(argument.include?(OPTION_VALUE_SEPARATOR)){"Positional argument: #{argument} does not include #{OPTION_VALUE_SEPARATOR}"}
    path, value = argument.split(OPTION_VALUE_SEPARATOR, 2)
    result = DotContainer.dotted_to_container(path.split(DotContainer::SEPARATOR), smart_convert(value), result)
  end
  # Restore skipped args so they remain available for command dispatching
  @unprocessed_cmd_line_arguments.unshift(*skipped) unless skipped.empty?
  result
end

#clear_option(option_symbol) ⇒ Object

Set option to nil



608
609
610
611
# File 'lib/aspera/cli/parser.rb', line 608

def clear_option(option_symbol)
  Aspera.assert_type(option_symbol, Symbol)
  option_def(option_symbol).clear
end

#command_or_arg_empty?Boolean

Check if there were unprocessed values to generate error

Returns:



644
645
646
# File 'lib/aspera/cli/parser.rb', line 644

def command_or_arg_empty?
  @unprocessed_cmd_line_arguments.empty?
end

#declare(option_symbol, description: nil, short: nil, allowed: nil, default: nil, handler: nil, deprecation: nil, schema: nil, &block) ⇒ Object

Declare an option

Parameters:

  • option name

  • (defaults to: nil)

    description for help; if nil, derived from schema

  • (defaults to: nil)

    short option name

  • (defaults to: nil)

    Allowed values, see OptionValue

  • (defaults to: nil)

    default value

  • (defaults to: nil)

    handler for option value: keys: :o(object) and :m(method)

  • (defaults to: nil)

    deprecation

  • (defaults to: nil)

    Definition of schema for Hash parameters

  • Block to execute when option is found



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'lib/aspera/cli/parser.rb', line 410

def declare(option_symbol, description: nil, short: nil, allowed: nil, default: nil, handler: nil, deprecation: nil, schema: nil, &block)
  Aspera.assert_type(option_symbol, Symbol)
  Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
  Aspera.assert_type(handler, Hash) if handler
  Aspera.assert(handler.keys.sort.eql?(%i[m o]), 'handler must have keys :m and :o') if handler
  option_attrs = @declared_options[option_symbol] = OptionValue.new(
    option:      option_symbol,
    description: description,
    allowed:     allowed,
    handler:     handler,
    deprecation: deprecation,
    schema:      schema
  )
  option_attrs.group = @current_group
  description = option_attrs.description
  Aspera.assert(!description.nil?){"#{option_symbol}: no description and no schema to derive one from"}
  Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
  Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with an uppercase"}
  Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :allowed instead of hash/extended value in option description"}
  set_option(option_symbol, default, where: 'default') unless default.nil?
  case option_attrs.types
  when Allowed::TYPES_ENUM, Allowed::TYPES_BOOLEAN
    # This option value must be a symbol (or array of symbols)
    set_option(option_symbol, BoolValue.true?(default), where: 'default') if option_attrs.values.eql?(BoolValue::ALL) && !default.nil?
  when Allowed::TYPES_NONE
    Aspera.assert_type(block, Proc){"missing execution block for #{option_symbol}"}
    option_attrs.block = block
  end
  @short_options[short] = option_symbol unless short.nil?
  Log.log.trace1{"declare: #{option_symbol}, group: #{@current_group}, short: #{short}"}
end

#final_errorsObject

Unprocessed options or arguments ?



649
650
651
652
653
654
# File 'lib/aspera/cli/parser.rb', line 649

def final_errors
  result = []
  result.push("unprocessed options: #{@unprocessed_cmd_line_options}") unless @unprocessed_cmd_line_options.empty?
  result.push("unprocessed values: #{@unprocessed_cmd_line_arguments}") unless @unprocessed_cmd_line_arguments.empty?
  result
end

#get_interactive(descr, check_option: false, multiple: false, accept_list: nil, schema: nil) ⇒ String

Prompt user for input in a list of symbols

Parameters:

  • description for help

  • (defaults to: false)

    Check attributes of option with name=descr

  • (defaults to: false)

    true if multiple values expected

  • (defaults to: nil)

    List of expected values

Returns:

  • user input



772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
# File 'lib/aspera/cli/parser.rb', line 772

def get_interactive(descr, check_option: false, multiple: false, accept_list: nil, schema: nil)
  option_attrs = @declared_options[descr.to_sym]
  what = option_attrs ? 'option' : 'argument'
  default_prompt = "#{what}: #{descr}"
  if !@ask_missing_mandatory
    message = "Missing #{default_prompt}"
    message = self.class.multi_choice_assert_msg(message, accept_list) if accept_list
    message += "\n#{TerminalFormatter::HINT}Give `#{HELP}` as argument to retrieve the schema of the missing argument." if schema
    raise Cli::MissingArgument, message
  end
  # ask interactively
  result = []
  puts(' (one per line, end with empty line)') if multiple
  loop do
    prompt = default_prompt
    prompt = "#{accept_list.join(' ')}\n#{default_prompt}" if accept_list
    entry = prompt_user_input(prompt, sensitive: option_attrs&.sensitive)
    break if entry.empty? && multiple
    entry = ExtendedValue.instance.evaluate(entry, context: 'interactive input')
    entry = self.class.get_from_list(entry, descr, accept_list) if accept_list
    return entry unless multiple
    result.push(entry)
  end
  result
end

#get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: Allowed::TYPES_STRING, aliases: nil, default: nil, schema: nil) ⇒ Object, ...

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Low-level positional argument reader. Prefer Base#resolve_argument from action methods. Direct calls from outside Parser are legacy exceptions documented in ST12/ST13 (mixins without DSL: sync_actions, ascp_actions; setup callbacks: aoc.rb).

Parameters:

  • description for help

  • (defaults to: true)

    true: raise error no more argument

  • (defaults to: false)

    true: return all remaining arguments (Array). String: until marker

  • (defaults to: nil)

    list of allowed values

  • (defaults to: Allowed::TYPES_STRING)

    Accepted value type(s) or list of Symbols

  • (defaults to: nil)

    map of aliases: key = alias, value = real value

  • (defaults to: nil)

    default value

Returns:

  • one value, list or nil (if optional and no default)

API:

  • private



469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
# File 'lib/aspera/cli/parser.rb', line 469

def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: Allowed::TYPES_STRING, aliases: nil, default: nil, schema: nil)
  Aspera.assert_array_all(accept_list, Symbol) unless accept_list.nil?
  Aspera.assert_hash_all(aliases, Symbol, Symbol) unless aliases.nil?
  validation = Symbol unless accept_list.nil?
  validation = [validation] unless validation.is_a?(Array) || validation.nil?
  Aspera.assert_array_all(validation, Class){'validation'} unless validation.nil?
  descr = "#{descr}#{add_types_info(validation)}"
  result =
    if !@unprocessed_cmd_line_arguments.empty?
      case multiple
      when true
        values = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length)
      when false
        values = [@unprocessed_cmd_line_arguments.shift]
      when String
        index = @unprocessed_cmd_line_arguments.index(multiple)
        if index
          values = @unprocessed_cmd_line_arguments.shift(index)
          @unprocessed_cmd_line_arguments.shift # remove end marker
        else
          values = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length)
        end
      else Aspera.error_unexpected_value(multiple){'multiple'}
      end
      values = values.map{ |v| ExtendedValue.instance.evaluate(v, context: "argument: #{descr}", allowed: validation)}
      # If expecting list and only one arg of type array : it is the list
      values = values.first if multiple && values.length.eql?(1) && values.first.is_a?(Array)
      if accept_list
        allowed_values = [].concat(accept_list)
        allowed_values.concat(aliases.keys) unless aliases.nil?
        values = values.map{ |v| self.class.get_from_list(v, descr, allowed_values)}
      end
      multiple ? values : values.first
    elsif !default.nil? then default
      # no value provided, either get value interactively, or exception
    elsif mandatory then get_interactive(descr, multiple: multiple, accept_list: accept_list, schema: schema)
    end
  if result.is_a?(String) && validation&.eql?(Allowed::TYPES_INTEGER)
    int_result = Integer(result, exception: false)
    raise Cli::BadArgument, "Invalid integer: #{result}" if int_result.nil?
    result = int_result
  end
  Log.log.trace1{"#{descr}=#{result}"}
  result = aliases[result] if aliases&.key?(result)
  # if value comes from JSON/YAML, it may come as Integer
  result = result.to_s if result.is_a?(Integer) && validation&.eql?(Allowed::TYPES_STRING)
  if validation && (mandatory || !result.nil?)
    value_list = multiple ? result : [result]
    value_list.each do |value|
      raise SchemaRequest.new(:argument, descr, schema) if validation.include?(Hash) && value.eql?(HELP)
      raise Cli::BadArgument,
        "Argument #{descr} is a #{value.class} but must be #{'one of: ' if validation.length > 1}#{validation.map(&:name).join(', ')}" unless validation.any?{ |t| value.is_a?(t)}
    end
  end
  result
end

#get_next_command(command_list, aliases: nil) ⇒ Object



544
# File 'lib/aspera/cli/parser.rb', line 544

def get_next_command(command_list, aliases: nil); get_next_argument('command', accept_list: command_list, aliases: aliases); end

#get_option(option_symbol, mandatory: false, schema: nil) ⇒ Object

Get an option value by name either return value or calls handler, can return nil ask interactively if requested/required

Parameters:

  • name of the option to retrieve

  • (defaults to: false)

    if true, raise error if option not set

  • (defaults to: nil)

    contextual schema path override; when set, raises SchemaRequest if the option value is 'help' (used for --query whose schema depends on the current command)

Raises:



572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
# File 'lib/aspera/cli/parser.rb', line 572

def get_option(option_symbol, mandatory: false, schema: nil)
  Aspera.assert_type(option_symbol, Symbol)
  option_attrs = option_def(option_symbol)
  result = option_attrs.value
  # Contextual schema: raise SchemaRequest when value is 'help'
  raise SchemaRequest.new(:option, option_symbol.to_s, schema) if schema && result.eql?(HELP)
  # Do not fail for manual generation if option mandatory but not set
  return :skip_missing_mandatory if result.nil? && mandatory && !@fail_on_missing_mandatory
  if result.nil?
    if !@ask_missing_mandatory
      Aspera.assert(!mandatory, type: Cli::BadArgument){"Missing mandatory option: #{option_symbol}"}
    elsif @ask_missing_optional || mandatory
      # ask_missing_mandatory
      result = get_interactive(option_symbol.to_s, check_option: true, accept_list: option_attrs.values, schema: option_attrs.schema)
      set_option(option_symbol, result, where: 'interactive')
    end
  end
  result
end

#group(name) ⇒ Object

Set the current help section group name for subsequent declarations

Parameters:

  • group name, shown as section header in help text



444
445
446
# File 'lib/aspera/cli/parser.rb', line 444

def group(name)
  @current_group = name
end

#help_text(banner: nil) ⇒ String

Generate help text for all declared options, grouped by section.

Parameters:

  • (defaults to: nil)

    Optional banner text to prepend

Returns:

  • Formatted help text



829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
# File 'lib/aspera/cli/parser.rb', line 829

def help_text(banner: nil)
  rows = []
  current_group = nil
  @declared_options.each do |sym, opt|
    if opt.group != current_group
      current_group = opt.group
      rows << [{value: "OPTIONS: #{current_group}", colspan: 2}]
    end
    short_char = @short_options.key(sym)
    short_part = short_char ? "-#{short_char}, " : '    '
    flag = "#{short_part}#{symbol_to_option(sym, option_display_value(opt))}"
    rows << [flag, opt.description]
  end
  table = Terminal::Table.new(rows: rows, style: {border: HELP_BORDER, padding_left: 0, padding_right: 2})
  banner.nil? ? table.to_s : "#{banner}\n#{table}"
end

#instance_identifier(description: 'identifier', &block) {|field, value| ... } ⇒ String+

Resource identifier as positional parameter

Parameters:

  • (defaults to: 'identifier')

    description of the identifier

  • block to search for identifier based on attribute value

Yield Parameters:

  • field (String)

    The field name from percent selector

  • value (String)

    The value from percent selector

Yield Returns:

  • (String)

    Resolved identifier

Returns:

  • identifier or list of IDs (if bulk option is set)



534
535
536
537
538
539
540
541
542
# File 'lib/aspera/cli/parser.rb', line 534

def instance_identifier(description: 'identifier', &block)
  res_id = get_next_argument(description, multiple: get_option(:bulk))
  # Can be an Array
  if res_id.is_a?(String) && (m = Parser.percent_selector(res_id))
    Aspera.assert(block_given?, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
    res_id = yield(m[:field], m[:value])
  end
  res_id
end

#known_options(only_defined: false) ⇒ Hash

Returns options as taken from config file and command line just before command execution.

Parameters:

  • (defaults to: false)

    if true, only return options that were defined

Returns:

  • options as taken from config file and command line just before command execution



677
678
679
680
681
682
683
684
685
686
# File 'lib/aspera/cli/parser.rb', line 677

def known_options(only_defined: false)
  result = {}
  @declared_options.each_key do |option_symbol|
    v = get_option(option_symbol)
    result[option_symbol] = v unless only_defined && v.nil?
  rescue => e
    result[option_symbol] = e.to_s
  end
  result
end

#option_declared?(option_symbol) ⇒ Boolean

Check whether an option has already been declared in this manager

Parameters:

  • name of the option

Returns:



549
550
551
# File 'lib/aspera/cli/parser.rb', line 549

def option_declared?(option_symbol)
  @declared_options.key?(option_symbol)
end

#option_def(option_symbol) ⇒ OptionValue

Get an option definition by name

Parameters:

  • name of the option

Returns:

  • Option definition

Raises:

  • if option not found



560
561
562
563
# File 'lib/aspera/cli/parser.rb', line 560

def option_def(option_symbol)
  Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument){"Unknown option: #{option_symbol}"}
  @declared_options[option_symbol]
end

#parse_options!Object

Removes already known options from the list



689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
# File 'lib/aspera/cli/parser.rb', line 689

def parse_options!
  Log.log.trace1('parse_options!'.red)
  # First options from conf file
  @option_pairs_batch = consume_option_pairs(@option_pairs_batch, 'set')
  # Then, env var (to override)
  @option_pairs_env = consume_option_pairs(@option_pairs_env, 'env')
  # Then, command line override: process one option at a time so that @current_option_args_offset
  # can be set before each option is evaluated (used by `@:` extended value in option values).
  unknown_options = []
  Log.log.trace1('Before parse')
  Log.dump(:unprocessed_cmd_line_options, @unprocessed_cmd_line_options, level: :trace1)
  until @unprocessed_cmd_line_options.empty?
    opt = @unprocessed_cmd_line_options.shift
    # Expose args_before for this option so `args_as_extended` can skip args preceding it.
    # Peek (first) without consuming - consumed only if this option is processed (not deferred).
    @current_option_args_offset = @args_before_option[opt]&.first
    if opt.start_with?(OPTION_PREFIX)
      # Long option: --name or --name=value
      name_raw, raw_value = opt.delete_prefix(OPTION_PREFIX).split(OPTION_VALUE_SEPARATOR, 2)
      option_sym = self.class.option_line_to_name(name_raw).to_sym
      resolved_sym = self.class.match_prefix(option_sym, @declared_options.keys)
      if resolved_sym
        dispatch_option(resolved_sym, raw_value)
        @args_before_option[opt]&.shift # consumed: advance to next occurrence
      else
        # Dotted notation: --a.b.c=d does: a={"b":{"c":ext_val(d)}}
        Log.log.trace1{"Unknown long option: #{opt}".red}
        if !raw_value.nil?
          path = name_raw.split(DotContainer::SEPARATOR)
          root_sym = self.class.option_line_to_name(path.shift).to_sym
          if @declared_options.key?(root_sym)
            set_option(root_sym, DotContainer.dotted_to_container(path, smart_convert(raw_value), get_option(root_sym)), where: 'dotted')
            @args_before_option[opt]&.shift # consumed: advance to next occurrence
            next
          end
        end
        # Unknown option: defer to next parse_options! round, do not consume the recorded offset
        unknown_options.push(opt)
      end
    elsif opt.start_with?('-') && (option_sym = @short_options[opt[1]])
      # Short option: -h, -v, or -Pvalue (value glued to flag)
      dispatch_option(option_sym, opt.length > 2 ? opt[2..] : nil)
      @args_before_option[opt]&.shift # consumed: advance to next occurrence
    else
      unknown_options.push(opt)
    end
  end
  @current_option_args_offset = nil
  Log.log.trace1('After parse')
  Log.log.trace1{"remains: #{unknown_options}"}
  # Set unprocessed options for next time
  @unprocessed_cmd_line_options = unknown_options
end

#prompt_user_input(prompt, sensitive: false) ⇒ Object



743
744
745
746
747
748
749
# File 'lib/aspera/cli/parser.rb', line 743

def prompt_user_input(prompt, sensitive: false)
  return $stdin.getpass("#{prompt}> ") if sensitive
  print("#{prompt}> ")
  line = $stdin.gets
  Aspera.assert_type(line, String){'Unexpected end of standard input'}
  line.chomp
end

#prompt_user_input_in_list(prompt, sym_list) ⇒ Symbol

prompt user for input in a list of symbols

Parameters:

  • prompt to display

  • list of symbols to select from

Returns:

  • selected symbol



755
756
757
758
759
760
761
762
763
764
# File 'lib/aspera/cli/parser.rb', line 755

def prompt_user_input_in_list(prompt, sym_list)
  loop do
    input = prompt_user_input(prompt).to_sym
    if sym_list.any?{ |a| a.eql?(input)}
      return input
    else
      $stderr.puts("No such #{prompt}: #{input}, select one of: #{sym_list.join(', ')}") # rubocop:disable Style/StderrPuts
    end
  end
end

#rename_current_group(name) ⇒ Object

Rename all options currently tagged with @current_group to a new name, then update @current_group. Used by add_manual_header when a plugin declares its options before its group name is known (e.g. Plugins::Config).

Parameters:

  • new group name



452
453
454
455
# File 'lib/aspera/cli/parser.rb', line 452

def rename_current_group(name)
  @declared_options.each_value{ |opt| opt.group = name if opt.group.eql?(@current_group)}
  @current_group = name
end

#set_handler(option_symbol, object:, method:) ⇒ nil

Bind (or re-bind) a runtime handler to an already-declared option. Called from plugin initialize() for Category C handlers whose target object (e.g. @gen_options) is created after class-load time.

Parameters:

  • name of the already-declared option

  • the target object for get/set delegation

  • accessor method name on object

Returns:



620
621
622
623
# File 'lib/aspera/cli/parser.rb', line 620

def set_handler(option_symbol, object:, method:)
  Aspera.assert_type(option_symbol, Symbol)
  option_def(option_symbol).bind_handler(o: object, m: method)
end

#set_option(option_symbol, value, where: 'code override') ⇒ Object

Set an option value by name, either store value or call handler String is given to extended value

Parameters:

  • option name

  • Value to set

  • (defaults to: 'code override')

    Where the value comes from

Raises:



597
598
599
600
601
602
603
604
605
# File 'lib/aspera/cli/parser.rb', line 597

def set_option(option_symbol, value, where: 'code override')
  Aspera.assert_type(option_symbol, Symbol)
  option = option_def(option_symbol)
  # Raise immediately only when the option has a static schema: the schema is known at parse time.
  # When schema is nil (e.g. --query), 'help' is stored as-is and SchemaRequest is raised later
  # in get_option() with the contextual schema provided by the calling command.
  raise SchemaRequest.new(:option, option.option, option.schema) if option.types&.include?(Hash) && value.eql?(HELP) && option.schema
  option.assign_value(value, where: where)
end

#unprocessed_options_with_valueHash

Get all original options on command line used to generate a config in config file

Returns:

  • options as taken from config file and command line just before command execution



658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# File 'lib/aspera/cli/parser.rb', line 658

def unprocessed_options_with_value
  result = {}
  @initial_cli_options.each do |option_argument|
    # ignore short options
    next unless option_argument.start_with?(OPTION_PREFIX)
    name, value = option_argument.delete_prefix(OPTION_PREFIX).split(OPTION_VALUE_SEPARATOR, 2)
    # ignore options without value
    next if value.nil?
    Log.log.debug{"option #{name}=#{value}"}
    path = name.split(DotContainer::SEPARATOR)
    path[0] = self.class.option_line_to_name(path[0])
    DotContainer.dotted_to_container(path, smart_convert(value), result)
    @unprocessed_cmd_line_options.delete(option_argument)
  end
  result
end

#unshift_next_argument(argument) ⇒ Object

Allows a plugin to add an argument as next argument to process



639
640
641
# File 'lib/aspera/cli/parser.rb', line 639

def unshift_next_argument(argument)
  @unprocessed_cmd_line_arguments.unshift(argument)
end