Class: Aspera::Cli::Parser
- Inherits:
-
Object
- Object
- Aspera::Cli::Parser
- 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
-
#ask_missing_mandatory ⇒ Object
Returns the value of attribute ask_missing_mandatory.
-
#ask_missing_optional ⇒ Object
Returns the value of attribute ask_missing_optional.
-
#declared_options ⇒ Hash{Symbol => OptionValue}
readonly
All declared options (read-only view).
-
#fail_on_missing_mandatory ⇒ Object
writeonly
Sets the attribute fail_on_missing_mandatory.
-
#help_requested ⇒ Object
Returns the value of attribute help_requested.
Class Method Summary collapse
-
.get_from_list(short_value, descr, allowed_values) ⇒ Symbol, Boolean
Find shortened string value in allowed symbol list.
-
.match_prefix(short_value, allowed_values) ⇒ Object?
Find a key in a list by exact match or unique prefix match.
-
.multi_choice_assert_msg(error_msg, accept_list, aliases: nil) ⇒ Object
Generates error message with list of allowed values.
-
.option_line_to_name(name) ⇒ String
Change option name with dash to name with underscore.
-
.option_name_to_line(name) ⇒ String
Convert option symbol to CLI line flag format.
-
.percent_selector(identifier) ⇒ Hash{Symbol => String}?
Parse percent-selector string into field name and value (extended value is parsed in value).
-
.smart_convert(value) ⇒ Boolean, ...
Using dotted hash notation, convert value to bool, int, float or extended value.
Instance Method Summary collapse
-
#add_option_preset(preset_hash, where, override: true) ⇒ Object
Adds each of the keys of specified hash as an option.
-
#add_types_info(types) ⇒ String
Add a type to the message if not special types.
-
#args_as_extended(end_marker) ⇒ Hash, Array
Read remaining args and build an
ArrayorHash. -
#clear_option(option_symbol) ⇒ nil
Set option to
nil. -
#command_or_arg_empty? ⇒ Boolean
Check if there are no pending positional arguments.
-
#declare(option_symbol, description: nil, short: nil, allowed: nil, default: nil, handler: nil, deprecation: nil, schema: nil, &block) ⇒ Object
Declare an option.
-
#extract_argument_tokens(multiple) ⇒ Array<String>
Extract raw string tokens from @argv_tokens (Argument objects) according to
multiple. -
#final_errors ⇒ Array<String>
Check for unprocessed options or arguments error messages.
-
#get_interactive(descr, check_option: false, multiple: false, accept_list: nil, aliases: nil, schema: nil) ⇒ String
Prompt user for input in a list of symbols.
-
#get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: Type::STRING, aliases: nil, default: nil, schema: nil) ⇒ Object, ...
private
Low-level positional argument reader.
-
#get_next_command(command_list, aliases: nil) ⇒ Symbol
Get next positional command argument from accepted list.
-
#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.
-
#group(name) ⇒ Object
Set the current help section group name for subsequent declarations.
-
#help_text(banner: nil) ⇒ String
Generate help text for all declared options, grouped by section.
-
#initialize(program_name, argv = nil) ⇒ Parser
constructor
A new instance of Parser.
-
#instance_identifier(description: 'identifier', &block) {|field, value| ... } ⇒ String+
Resource identifier as positional parameter.
-
#known_options(only_defined: false) ⇒ Hash
Options as taken from config file and command line just before command execution.
-
#option_declared?(option_symbol) ⇒ Boolean
Check whether an option has already been declared in this manager.
-
#option_def(option_symbol) ⇒ OptionValue
Get an option definition by name.
-
#parse_options! ⇒ Object
Removes already known options from the list.
-
#prompt_user_input(prompt, sensitive: false) ⇒ String
Prompt user for console input.
-
#prompt_user_input_in_list(prompt, sym_list) ⇒ Symbol
prompt user for input in a list of symbols.
-
#rename_current_group(name) ⇒ Object
Rename all options currently tagged with @current_group to a new name, then update @current_group.
-
#set_handler(option_symbol, object:, method:) ⇒ nil
Bind (or re-bind) a runtime handler to an already-declared option.
-
#set_option(option_symbol, value, where: 'code override', warn_deprecation: true) ⇒ Object
Set an option value by name, either store value or call handler String is given to extended value.
-
#unprocessed_options_with_value ⇒ Hash
Get all original options on command line used to generate a config in config file.
-
#unshift_next_argument(argument) ⇒ Array<Option, Argument>
Allows a plugin to add an argument as next argument to process.
-
#validate_argument(value, validation:, descr:, schema:) ⇒ Object
Validate and coerce a single argument value.
Constructor Details
#initialize(program_name, argv = nil) ⇒ Parser
Returns a new instance of Parser.
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 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 |
# File 'lib/aspera/cli/parser.rb', line 495 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 explicitly reset to nil from CLI (e.g. --opt=@none:); preset injection skips these @explicitly_cleared = {} # 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::NAME_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.dump(:env, @option_pairs_env) # Ordered list of all CLI tokens after `--` splitting. # Single source of truth for CLI parsing — pending_arguments and pending_options are derived views. # @type [Array<Option, Argument>] @argv_tokens = [] # Frozen snapshot of @argv_tokens used by unprocessed_options_with_value. @initial_argv_tokens = [].freeze # Number of original positional args before the option currently being parsed (nil = positional context). @current_option_args_offset = nil # Current index in @argv_tokens during parse_options! loop (used by shift_next_argument_token). @current_parse_idx = nil return if argv.nil? # true until `--` is found (stop options) = true argv.each do |value| if && value.start_with?('-') Log.log.trace1 { "opt: #{value}" } if value.eql?(OPTIONS_STOP) = false else @argv_tokens.push(value.start_with?(Option::PREFIX) ? Option.from_long(value) : Option.from_short(value)) end else Log.log.trace1 { "arg: #{value}" } @argv_tokens.push(Argument.new(value)) end end @initial_argv_tokens = @argv_tokens.dup.freeze Log.log.trace1 { "add_cmd_line_options:arguments=#{pending_arguments},options=#{}".red } declare(:interactive, description: 'Use interactive input of missing params', allowed: Type::BOOLEAN, handler: {o: self, m: :ask_missing_mandatory}) declare(:ask_options, description: 'Ask even optional options', allowed: Type::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_mandatory ⇒ Object
Returns the value of attribute ask_missing_mandatory.
490 491 492 |
# File 'lib/aspera/cli/parser.rb', line 490 def ask_missing_mandatory @ask_missing_mandatory end |
#ask_missing_optional ⇒ Object
Returns the value of attribute ask_missing_optional.
490 491 492 |
# File 'lib/aspera/cli/parser.rb', line 490 def ask_missing_optional @ask_missing_optional end |
#declared_options ⇒ Hash{Symbol => OptionValue} (readonly)
Returns all declared options (read-only view).
707 708 709 |
# File 'lib/aspera/cli/parser.rb', line 707 def @declared_options end |
#fail_on_missing_mandatory=(value) ⇒ Object (writeonly)
Sets the attribute fail_on_missing_mandatory
491 492 493 |
# File 'lib/aspera/cli/parser.rb', line 491 def fail_on_missing_mandatory=(value) @fail_on_missing_mandatory = value end |
#help_requested ⇒ Object
Returns the value of attribute help_requested.
490 491 492 |
# File 'lib/aspera/cli/parser.rb', line 490 def help_requested @help_requested end |
Class Method Details
.get_from_list(short_value, descr, allowed_values) ⇒ Symbol, Boolean
Find shortened string value in allowed symbol list
413 414 415 416 417 418 419 420 421 422 423 424 |
# File 'lib/aspera/cli/parser.rb', line 413 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
428 429 430 431 432 |
# File 'lib/aspera/cli/parser.rb', line 428 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, aliases: nil) ⇒ Object
Generates error message with list of allowed values
438 439 440 441 442 443 444 445 446 447 448 |
# File 'lib/aspera/cli/parser.rb', line 438 def multi_choice_assert_msg(error_msg, accept_list, aliases: nil) # Build reverse map: id → [alias, ...] for annotation reverse = aliases&.each_with_object({}) do |(ali, id), h| (h[id] ||= []) << ali end lines = accept_list.map do |choice| suffix = reverse&.key?(choice) ? " (alias: #{reverse[choice].join(', ')})" : '' "- #{choice}#{suffix}" end [error_msg, 'Use:', *lines.sort].join("\n") end |
.option_line_to_name(name) ⇒ String
Change option name with dash to name with underscore
453 454 455 |
# File 'lib/aspera/cli/parser.rb', line 453 def option_line_to_name(name) name.gsub(Option::NAME_SEP_LINE, Option::NAME_SEP_SYMBOL) end |
.option_name_to_line(name) ⇒ String
Convert option symbol to CLI line flag format
460 461 462 |
# File 'lib/aspera/cli/parser.rb', line 460 def option_name_to_line(name) "#{Option::PREFIX}#{name.to_s.gsub(Option::NAME_SEP_SYMBOL, Option::NAME_SEP_LINE)}" end |
.percent_selector(identifier) ⇒ Hash{Symbol => String}?
Parse percent-selector string into field name and value (extended value is parsed in value)
467 468 469 470 471 472 473 |
# File 'lib/aspera/cli/parser.rb', line 467 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 |
.smart_convert(value) ⇒ Boolean, ...
Using dotted hash notation, convert value to bool, int, float or extended value
478 479 480 481 482 483 484 485 486 487 |
# File 'lib/aspera/cli/parser.rb', line 478 def smart_convert(value) case value when 'true' then true when 'false' then false else Integer(value, exception: false) || Float(value, exception: false) || ExtendedValue.instance.evaluate(value, context: 'dotted expression') end end |
Instance Method Details
#add_option_preset(preset_hash, where, override: true) ⇒ Object
Adds each of the keys of specified hash as an option
784 785 786 787 788 789 790 791 792 793 794 795 |
# File 'lib/aspera/cli/parser.rb', line 784 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| # Ignore comment/meta keys (e.g. _comment, _description) next if k.to_s.start_with?(PresetManager::Key::META_PREFIX) option_symbol = k.to_sym # Never restore an option that was explicitly cleared from the CLI (e.g. --opt=@none:) next if @explicitly_cleared.key?(option_symbol) @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
557 558 559 560 |
# File 'lib/aspera/cli/parser.rb', line 557 def add_types_info(types) return '' if !types || types.empty? || types.eql?(Type::ENUM) || types.eql?(Type::BOOLEAN) || types.eql?(Type::STRING) " (#{types.map(&:name).join(', ')})" end |
#args_as_extended(end_marker) ⇒ Hash, Array
Read remaining args and build an Array or Hash
1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 |
# File 'lib/aspera/cli/parser.rb', line 1043 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 is set by parse_options! to the number of args in # @unprocessed_cmd_line_arguments that preceded this option; nil when called from a positional context. skip_count = @current_option_args_offset || 0 # Skip leading positional args that precede this option in original argv order. # Grab the first `skip_count` Argument tokens and temporarily remove them. arg_tokens = @argv_tokens.grep(Argument) skipped_tokens = skip_count.positive? ? arg_tokens.first(skip_count) : [] skipped_tokens.each { |t| @argv_tokens.delete(t) } Log.log.trace1 { "args_as_extended: skipping #{skipped_tokens.length} args before option: #{skipped_tokens.map(&:value)}" } unless skipped_tokens.empty? result = nil get_next_argument('args', multiple: end_marker).each do |argument| Aspera.assert(argument.include?(Option::VALUE_SEP)) { "Positional argument: #{argument} does not include #{Option::VALUE_SEP}" } path, value = argument.split(Option::VALUE_SEP, 2) result = DotContainer.dotted_to_container(path.split(DotContainer::SEPARATOR), Parser.smart_convert(value), result) end # Restore skipped tokens so they remain available for command dispatching skipped_tokens.reverse_each { |t| @argv_tokens.unshift(t) } result end |
#clear_option(option_symbol) ⇒ nil
Set option to nil
763 764 765 766 |
# File 'lib/aspera/cli/parser.rb', line 763 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 are no pending positional arguments
806 807 808 |
# File 'lib/aspera/cli/parser.rb', line 806 def command_or_arg_empty? pending_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
577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 |
# File 'lib/aspera/cli/parser.rb', line 577 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', warn_deprecation: false) unless default.nil? case option_attrs.types when Type::ENUM, Type::BOOLEAN # This option value must be a symbol (or array of symbols) set_option(option_symbol, BoolValue.true?(default), where: 'default', warn_deprecation: false) if option_attrs.values.eql?(BoolValue::ALL) && !default.nil? when Type::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 |
#extract_argument_tokens(multiple) ⇒ Array<String>
Extract raw string tokens from @argv_tokens (Argument objects) according to multiple.
Consumed Argument tokens are removed from @argv_tokens.
946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 |
# File 'lib/aspera/cli/parser.rb', line 946 def extract_argument_tokens(multiple) arg_tokens = @argv_tokens.grep(Argument) case multiple when false tok = arg_tokens.first @argv_tokens.delete(tok) [tok.value] when true arg_tokens.each { |t| @argv_tokens.delete(t) } arg_tokens.map(&:value) when String idx = arg_tokens.index { |t| t.value.eql?(multiple) } consumed = idx ? arg_tokens[0, idx] : arg_tokens marker = idx ? arg_tokens[idx] : nil (consumed + [marker].compact).each { |t| @argv_tokens.delete(t) } consumed.map(&:value) else Aspera.error_unexpected_value(multiple) { 'multiple' } end end |
#final_errors ⇒ Array<String>
Check for unprocessed options or arguments error messages
812 813 814 815 816 817 |
# File 'lib/aspera/cli/parser.rb', line 812 def final_errors result = [] result.push("unprocessed options: #{}") unless .empty? result.push("unprocessed values: #{pending_arguments}") unless pending_arguments.empty? result end |
#get_interactive(descr, check_option: false, multiple: false, accept_list: nil, aliases: nil, schema: nil) ⇒ String
Prompt user for input in a list of symbols
1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 |
# File 'lib/aspera/cli/parser.rb', line 1014 def get_interactive(descr, check_option: false, multiple: false, accept_list: nil, aliases: nil, schema: nil) option_attrs = @declared_options[descr.to_sym] what = option_attrs ? 'option' : 'argument' default_prompt = "#{what}: #{descr}" if !@ask_missing_mandatory = "Missing #{default_prompt}" = self.class.multi_choice_assert_msg(, accept_list, aliases: aliases) if accept_list += "\n#{TerminalFormatter::HINT}Give `#{HELP}` as argument to retrieve the schema of the missing argument." if schema raise Cli::MissingArgument, 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: Type::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).
636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 |
# File 'lib/aspera/cli/parser.rb', line 636 def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: Type::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 !pending_arguments.empty? values = extract_argument_tokens(multiple) 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 elsif mandatory then get_interactive(descr, multiple: multiple, accept_list: accept_list, aliases: aliases, schema: schema) 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?(Type::STRING) # Integer coercion: a String argument for an INTEGER option must be parseable if result.is_a?(String) && validation&.eql?(Type::INTEGER) int_result = Integer(result, exception: false) raise Cli::BadArgument, "Invalid integer: #{result}" if int_result.nil? result = int_result end if validation && (mandatory || !result.nil?) value_list = multiple ? result : [result] value_list.each { |value| validate_argument(value, validation: validation, descr: descr, schema: schema) } end result end |
#get_next_command(command_list, aliases: nil) ⇒ Symbol
Get next positional command argument from accepted list
697 |
# File 'lib/aspera/cli/parser.rb', line 697 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
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 |
# File 'lib/aspera/cli/parser.rb', line 725 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
611 612 613 |
# File 'lib/aspera/cli/parser.rb', line 611 def group(name) @current_group = name end |
#help_text(banner: nil) ⇒ String
Generate help text for all declared options, grouped by section.
1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 |
# File 'lib/aspera/cli/parser.rb', line 1071 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))}" desc = opt.deprecation ? "#{opt.description} (deprecated: #{opt.deprecation})" : opt.description rows << [flag, desc] end table = Terminal::Table.new(rows: rows, style: {border: HELP_BORDER, padding_left: 0, padding_right: 2}) .nil? ? table.to_s : "#{}\n#{table}" end |
#instance_identifier(description: 'identifier', &block) {|field, value| ... } ⇒ String+
Resource identifier as positional parameter
683 684 685 686 687 688 689 690 691 |
# File 'lib/aspera/cli/parser.rb', line 683 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.
840 841 842 843 844 845 846 847 848 849 |
# File 'lib/aspera/cli/parser.rb', line 840 def (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
702 703 704 |
# File 'lib/aspera/cli/parser.rb', line 702 def option_declared?(option_symbol) @declared_options.key?(option_symbol) end |
#option_def(option_symbol) ⇒ OptionValue
Get an option definition by name
713 714 715 716 |
# File 'lib/aspera/cli/parser.rb', line 713 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
852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 |
# File 'lib/aspera/cli/parser.rb', line 852 def 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. # Iterate @argv_tokens in order so that --opt val and -s val can consume the next argument # token directly, without any secondary index. # Process one option at a time so that @current_option_args_offset can be set before each # option is evaluated (used by `@:` extended value). deferred_tokens = [] Log.log.trace1('Before parse') Log.dump(:argv_tokens, @argv_tokens, level: :trace1) # Iterate with an index so that Argument tokens stay in @argv_tokens (not shifted). # Only Option tokens are removed; Arguments remain until consumed by extract_argument_tokens. @current_parse_idx = 0 while @current_parse_idx < @argv_tokens.length tok = @argv_tokens[@current_parse_idx] if tok.is_a?(Argument) @current_parse_idx += 1 next end # tok is an Option — remove it from @argv_tokens @argv_tokens.delete_at(@current_parse_idx) # @current_option_args_offset = number of positional Argument tokens that appear BEFORE # this option in @argv_tokens (i.e. at indices 0...@current_parse_idx after the delete). # Used by args_as_extended to skip those leading args when collecting @: values. @current_option_args_offset = @argv_tokens[0...@current_parse_idx].count { |t| t.is_a?(Argument) } if tok.short_char # Short option: -X or -Xvalue option_sym = @short_options[tok.short_char] if option_sym raw_value = tok.value # No inline value and option expects a value: consume the next :argument token raw_value = shift_next_argument_token \ if !tok.has_value && !@declared_options[option_sym].types.eql?(Type::NONE) dispatch_option(option_sym, raw_value) else deferred_tokens.push(tok) end elsif tok.dot_path # Dotted notation: --a.b.c=val or --a.b.c val (always takes priority over plain option lookup) Log.log.trace1 { "Dotted option: #{tok.raw}".red } if tok.has_value raw_value = tok.value value_from_next_token = false else raw_value = shift_next_argument_token value_from_next_token = true end if @declared_options.key?(tok.name.to_sym) set_option(tok.name.to_sym, DotContainer.dotted_to_container(tok.dot_path, Parser.smart_convert(raw_value), get_option(tok.name.to_sym)), where: 'dotted') else # Only re-inject if value was space-separated (consumed from next token); inline values stay in tok.value @argv_tokens.unshift(Argument.new(raw_value)) if raw_value && value_from_next_token deferred_tokens.push(tok) end elsif (resolved_sym = self.class.match_prefix(tok.name.to_sym, @declared_options.keys)) # Known long option (plain, no dot-path) raw_value = tok.value # No inline `=` and option expects a value: consume the next :argument token raw_value = shift_next_argument_token \ if !tok.has_value && !@declared_options[resolved_sym].types.eql?(Type::NONE) dispatch_option(resolved_sym, raw_value) else Log.log.trace1 { "Unknown long option: #{tok.raw}".red } deferred_tokens.push(tok) end end @current_option_args_offset = nil @current_parse_idx = nil Log.log.trace1('After parse') Log.log.trace1 { "deferred: #{deferred_tokens}" } # @argv_tokens now contains only: remaining Arguments + deferred Options (already in correct order). # Re-insert deferred Options at their original positions by rebuilding from @initial_argv_tokens. deferred_ids = deferred_tokens.map(&:object_id).to_set remaining_arg_ids = @argv_tokens.grep(Argument).map(&:object_id).to_set injected = @argv_tokens.grep(Argument).reject { |t| @initial_argv_tokens.include?(t) } @argv_tokens = @initial_argv_tokens.select do |t| (t.is_a?(Option) && deferred_ids.include?(t.object_id)) || (t.is_a?(Argument) && remaining_arg_ids.include?(t.object_id)) end # Prepend arguments injected at runtime (e.g. via unshift_next_argument) not in @initial_argv_tokens. @argv_tokens.unshift(*injected) end |
#prompt_user_input(prompt, sensitive: false) ⇒ String
Prompt user for console input
985 986 987 988 989 990 991 |
# File 'lib/aspera/cli/parser.rb', line 985 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
997 998 999 1000 1001 1002 1003 1004 1005 1006 |
# File 'lib/aspera/cli/parser.rb', line 997 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).
619 620 621 622 |
# File 'lib/aspera/cli/parser.rb', line 619 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.
775 776 777 778 |
# File 'lib/aspera/cli/parser.rb', line 775 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', warn_deprecation: true) ⇒ Object
Set an option value by name, either store value or call handler String is given to extended value
750 751 752 753 754 755 756 757 758 |
# File 'lib/aspera/cli/parser.rb', line 750 def set_option(option_symbol, value, where: 'code override', warn_deprecation: true) 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, warn_deprecation: warn_deprecation) end |
#unprocessed_options_with_value ⇒ Hash
Get all original options on command line used to generate a config in config file
821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 |
# File 'lib/aspera/cli/parser.rb', line 821 def result = {} @initial_argv_tokens.each_with_index do |tok, idx| next unless tok.is_a?(Option) && tok.short_char.nil? # For space-separated form: value is the immediately following :argument token (if any) value = tok.value || @initial_argv_tokens[idx + 1]&.then { |t| t.value if t.is_a?(Argument) } # ignore options without value next if value.nil? name = tok.dot_path ? [tok.name, *tok.dot_path].join(DotContainer::SEPARATOR) : tok.name Log.log.debug { "option #{name}=#{value}" } path = [tok.name, *(tok.dot_path || [])] DotContainer.dotted_to_container(path, Parser.smart_convert(value), result) @argv_tokens.reject! { |t| t.is_a?(Option) && t.raw.eql?(tok.raw) } end result end |
#unshift_next_argument(argument) ⇒ Array<Option, Argument>
Allows a plugin to add an argument as next argument to process
800 801 802 |
# File 'lib/aspera/cli/parser.rb', line 800 def unshift_next_argument(argument) @argv_tokens.unshift(Argument.new(argument)) end |
#validate_argument(value, validation:, descr:, schema:) ⇒ Object
Validate and coerce a single argument value. Raises SchemaRequest when the value is 'help' and validation includes Hash. Raises BadArgument when the value is an Integer on a STRING validation (coerced upstream). Raises BadArgument when the value's type is not in the validation list.
974 975 976 977 978 979 |
# File 'lib/aspera/cli/parser.rb', line 974 def validate_argument(value, validation:, descr:, schema:) 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 |