shellopts 2.0.0.pre.7 → 2.0.0.pre.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,21 +2,23 @@
2
2
  module ShellOpts
3
3
  # Specialization of Array for arguments lists. Args extends Array with a
4
4
  # #extract and an #expect method to extract elements from the array. The
5
- # methods call #error() in response to errors
5
+ # methods raise a ShellOpts::UserError exception in case of errors
6
6
  class Args < Array
7
7
  def initialize(shellopts, *args)
8
8
  @shellopts = shellopts
9
9
  super(*args)
10
10
  end
11
11
 
12
- # Remove and return elements from beginning of the array. If
13
- # +count_or_range+ is a number, that number of elements will be returned.
14
- # If the count is one, a simple value is returned instead of an array. If
15
- # the count is negative, the elements will be removed from the end of the
16
- # array. If +count_or_range+ is a range, the number of elements returned
17
- # will be in that range. The range can't contain negative numbers #expect
18
- # calls #error() if there's is not enough elements in the array to satisfy
19
- # the request
12
+ # Remove and return elements from beginning of the array
13
+ #
14
+ # If +count_or_range+ is a number, that number of elements will be
15
+ # returned. If the count is one, a simple value is returned instead of an
16
+ # array. If the count is negative, the elements will be removed from the
17
+ # end of the array. If +count_or_range+ is a range, the number of elements
18
+ # returned will be in that range. The range can't contain negative numbers
19
+ #
20
+ # #extract raise a ShellOpts::UserError exception if there's is not enough
21
+ # elements in the array to satisfy the request
20
22
  def extract(count_or_range, message = nil)
21
23
  if count_or_range.is_a?(Range)
22
24
  range = count_or_range
@@ -24,17 +26,21 @@ module ShellOpts
24
26
  n_extract = [self.size, range.max].min
25
27
  n_extend = range.max > self.size ? range.max - self.size : 0
26
28
  r = self.shift(n_extract) + Array.new(n_extend)
29
+ range.max <= 1 ? r.first : r
27
30
  else
28
31
  count = count_or_range
29
32
  self.size >= count.abs or inoa(message)
30
33
  start = count >= 0 ? 0 : size + count
31
34
  r = slice!(start, count.abs)
32
- r.size == 0 ? nil : (r.size == 1 ? r.first : r)
35
+ r.size <= 0 ? nil : (r.size == 1 ? r.first : r)
33
36
  end
34
37
  end
35
38
 
36
- # As extract except it doesn't allow negative counts and that the array is
39
+ # As #extract except it doesn't allow negative counts and that the array is
37
40
  # expect to be emptied by the operation
41
+ #
42
+ # #expect raise a ShellOpts::UserError exception if the array is not emptied
43
+ # by the operation
38
44
  def expect(count_or_range, message = nil)
39
45
  count_or_range === self.size or inoa(message)
40
46
  extract(count_or_range) # Can't fail
@@ -42,7 +48,7 @@ module ShellOpts
42
48
 
43
49
  private
44
50
  def inoa(message = nil)
45
- raise ShellOpts::UserError, message || "Illegal number of arguments"
51
+ raise Error.new(nil), message || "Illegal number of arguments"
46
52
  end
47
53
  end
48
54
  end
@@ -1,41 +1,112 @@
1
1
  module ShellOpts
2
2
  module Ast
3
- class Command < Node
4
- # Array of options (Ast::Option). Initially empty but filled out by the
5
- # parser
6
- attr_reader :options
7
-
8
- # Optional sub-command (Ast::Command). Initially nil but assigned by the
9
- # parser
10
- attr_accessor :subcommand
11
-
12
- def initialize(grammar, name)
13
- super(grammar, name)
14
- @options = []
3
+ # Note that Command is derived from BasicObject to minimize the number of
4
+ # reserved names
5
+ class Command < BasicObject
6
+ def initialize(grammar)
7
+ @grammar = grammar
8
+ @options_list = []
9
+ @options_hash = {}
15
10
  @subcommand = nil
11
+ @subcommands_hash = {} # have at most one element
12
+
13
+ @grammar.opts.each { |opt|
14
+ if opt.argument?
15
+ self.instance_eval("def #{opt.ident}() @options_hash[:#{opt.ident}] end")
16
+ end
17
+ self.instance_eval("def #{opt.ident}?() @options_hash.key?(:#{opt.ident}) end")
18
+ }
19
+
20
+ @grammar.cmds.each { |cmd|
21
+ self.instance_eval("def #{cmd.ident}!() @subcommands_hash[:#{cmd.ident}] end")
22
+ }
16
23
  end
17
24
 
18
- # Array of option or command tuples
19
- def values
20
- (options + (Array(subcommand || []))).map { |node| node.to_tuple }
25
+ # Return true if the option was used. Defined in #initialize for each option
26
+ # def <option>?() end
27
+
28
+ # Return the value of the option. Note that repeated options have their
29
+ # values aggregated into an array. Defined in #initialize for each option
30
+ # def <option>() end
31
+
32
+ # List of Ast::Option objects in the same order as on the command line
33
+ def options() @options_list end
34
+
35
+ # Hash from option identifier to option value. Note that repeated options
36
+ # have their values aggregated into an array
37
+ def [](ident) @options_hash[ident].argument end
38
+
39
+ # Return the sub-command Command object or nil if not present. Defined in
40
+ # #initialize for each sub-command
41
+ # def <command>!() end
42
+
43
+ # The sub-command identifier or nil if not present
44
+ def subcommand() @subcommand && Command.grammar(@subcommand).ident end
45
+
46
+ # The sub-command Command object or nil if not present
47
+ def subcommand!() @subcommand end
48
+
49
+ # Class-level accessor methods
50
+ def self.program?(command) command.__send__(:__is_program__) end
51
+ def self.grammar(command) command.__send__(:__get_grammar__) end
52
+
53
+ # Class-level mutating methods
54
+ def self.add_option(command, option) command.__send__(:__add_option__, option) end
55
+ def self.add_command(command, subcommand) command.__send__(:__add_command__, subcommand) end
56
+
57
+ private
58
+ # True if this is a Program object
59
+ def __is_program__() false end
60
+
61
+ # Get grammar
62
+ def __get_grammar__()
63
+ @grammar
21
64
  end
22
65
 
23
- # :nocov:
24
- def dump(&block)
25
- super {
26
- yield if block_given?
27
- puts "options:"
28
- indent { options.each { |opt| opt.dump } }
29
- print "subcommand:"
30
- if subcommand
31
- puts
32
- indent { subcommand.dump }
33
- else
34
- puts "nil"
35
- end
36
- }
66
+ # Add an option. Only used from the parser
67
+ def __add_option__(option)
68
+ @options_list << option
69
+ if option.grammar.repeatable?
70
+ (@options_hash[option.grammar.ident] ||= []) << option.argument
71
+ else
72
+ @options_hash[option.grammar.ident] = option.argument
73
+ end
74
+ end
75
+
76
+ # Set sub-command. Only used from the parser
77
+ def __add_command__(command)
78
+ ident = Command.grammar(command).ident
79
+ @subcommand = command
80
+ @subcommands_hash[ident] = command
37
81
  end
38
- # :nocov:
82
+ end
83
+
84
+ class Program < Command
85
+ def __is_program__() true end
39
86
  end
40
87
  end
41
88
  end
89
+
90
+ # # TODO: Create class-level methods for access
91
+ # private
92
+ # # Return class of object. #class is not defined for BasicObjects so this
93
+ # # method provides an alternative way of getting the class
94
+ # def self.class_of(object)
95
+ # # https://stackoverflow.com/a/18621313/2130986
96
+ # ::Kernel.instance_method(:class).bind(object).call
97
+ # end
98
+ #
99
+ # # Class method implementation of ObjectStruct#instance_variable_set that is
100
+ # # not defined in a BasicObject
101
+ # def self.set_variable(this, var, value)
102
+ # # https://stackoverflow.com/a/18621313/2130986
103
+ # ::Kernel.instance_method(:instance_variable_set).bind(this).call(var, value)
104
+ # end
105
+ #
106
+ # # Class method implementation of ObjectStruct#instance_variable_get that is
107
+ # # not defined in a BasicObject
108
+ # def self.get_variable(this, var)
109
+ # # https://stackoverflow.com/a/18621313/2130986
110
+ # ::Kernel.instance_method(:instance_variable_get).bind(this).call(var)
111
+ # end
112
+
@@ -0,0 +1,28 @@
1
+ module ShellOpts
2
+ module Ast
3
+ class Command < BasicObject
4
+ def dump
5
+ klass = __is_program__ ? "Program" : "Command"
6
+ ::Kernel.puts "#{@grammar.ident.inspect} (#{klass})"
7
+ ::Kernel.indent {
8
+ if !options.empty?
9
+ options.map(&:dump)
10
+ end
11
+ if subcommand
12
+ subcommand!.dump
13
+ end
14
+ }
15
+ end
16
+ end
17
+
18
+ class Option
19
+ def dump
20
+ puts "#{grammar.ident.inspect} (Option)"
21
+ indent {
22
+ puts "name: #{name.inspect}"
23
+ puts "argument: #{argument.inspect}"
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,21 +1,15 @@
1
1
  module ShellOpts
2
2
  module Ast
3
- class Option < Node
4
- # Optional value. Can be a String, Integer, or Float
5
- attr_reader :value
3
+ class Option
4
+ attr_reader :grammar
5
+ attr_reader :name # The actual name used on the command line
6
+ attr_reader :argument
6
7
 
7
- def initialize(grammar, name, value)
8
- super(grammar, name)
9
- @value = value
8
+ def initialize(grammar, name, argument)
9
+ @grammar = grammar
10
+ @name = name
11
+ @argument = argument
10
12
  end
11
-
12
- def values() value end
13
-
14
- # :nocov:
15
- def dump
16
- super { puts "values: #{values.inspect}" }
17
- end
18
- # :nocov:
19
13
  end
20
14
  end
21
15
  end
@@ -0,0 +1,106 @@
1
+ module ShellOpts
2
+ module Ast
3
+ # Parse a subcommand
4
+ class Parser
5
+ def initialize(grammar, argv)
6
+ @grammar, @argv = grammar, argv.dup
7
+ @seen_options = {} # Used to keep track of repeated options
8
+ @current = nil # Current command
9
+ end
10
+
11
+ def call
12
+ @current = program = Program.new(@grammar)
13
+ parse_command(program)
14
+ cmd = Command.grammar(@current)
15
+ !cmd.virtual? or error("'%s' command requires a sub-command")
16
+ [program, Args.new(cmd, @argv)]
17
+ end
18
+
19
+ def self.parse(grammar, argv)
20
+ self.new(grammar, argv).call
21
+ end
22
+
23
+ private
24
+ def error(message)
25
+ grammar = Command.grammar(@current)
26
+ raise Error.new(grammar), message % grammar.name
27
+ end
28
+
29
+ def parse_command(command)
30
+ @seen_options = {} # Every new command resets the seen options
31
+ while arg = @argv.first
32
+ if arg == "--"
33
+ @argv.shift
34
+ break
35
+ elsif arg.start_with?("-")
36
+ parse_option(command)
37
+ elsif cmd = Command.grammar(command).commands[arg]
38
+ @argv.shift
39
+ @current = subcommand = Ast::Command.new(cmd)
40
+ Command.add_command(command, subcommand)
41
+ parse_command(subcommand)
42
+ break
43
+ else
44
+ break
45
+ end
46
+ end
47
+ end
48
+
49
+ def parse_option(command)
50
+ # Split into name and argument
51
+ case @argv.first
52
+ when /^--(.+?)(?:=(.*))?$/
53
+ name, arg, short = $1, $2, false
54
+ opt_name = "--#{name}"
55
+ when /^-(.)(.+)?$/
56
+ name, arg, short = $1, $2, true
57
+ opt_name = "-#{name}"
58
+ end
59
+ @argv.shift
60
+
61
+ option = Command.grammar(command).options[name] or error "Unknown option '#{opt_name}'"
62
+ !@seen_options.key?(option.ident) || option.repeatable? or error "Duplicate option '#{opt_name}'"
63
+ @seen_options[option.ident] = true
64
+
65
+ # Parse (optional) argument
66
+ if option.argument?
67
+ if arg.nil? && !option.optional?
68
+ if !@argv.empty?
69
+ arg = @argv.shift
70
+ else
71
+ error "Missing argument for option '#{opt_name}'"
72
+ end
73
+ end
74
+ arg &&= parse_option_arg(option, name, arg)
75
+ elsif arg && short
76
+ @argv.unshift("-#{arg}")
77
+ arg = nil
78
+ elsif !arg.nil?
79
+ error "No argument allowed for option '#{opt_name}'"
80
+ end
81
+
82
+ Command.add_option(command, Option.new(option, name, arg))
83
+ end
84
+
85
+ def parse_option_arg(option, name, arg)
86
+ if option.string?
87
+ arg
88
+ elsif arg == ""
89
+ nil
90
+ elsif option.integer?
91
+ arg =~ /^-?\d+$/ or error "Illegal integer in '#{name}' argument: '#{arg}'"
92
+ arg.to_i
93
+ else # option.float?
94
+ # https://stackoverflow.com/a/21891705/2130986
95
+ arg =~ /^[+-]?(?:0|[1-9]\d*)(?:\.(?:\d*[1-9]|0))?$/ or
96
+ error "Illegal float in '#{name}' argument: '#{arg}'"
97
+ arg.to_f
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+
105
+
106
+
@@ -0,0 +1,88 @@
1
+
2
+ module ShellOpts
3
+ # FIXME: An option group is -abcd, an option list is a,b,c,d
4
+ module Constants
5
+ # Short and long option names
6
+ SHORT_OPTION_NAME_SUB_RE = /[a-zA-Z0-9]/
7
+ LONG_OPTION_NAME_SUB_RE = /[a-z](?:[\w-]*\w)/
8
+ OPTION_NAME_SUB_RE = /#{SHORT_OPTION_NAME_SUB_RE}|#{LONG_OPTION_NAME_SUB_RE}/
9
+
10
+ # Initial option in a group
11
+ INITIAL_SHORT_OPTION_SUB_RE = /[-+]#{SHORT_OPTION_NAME_SUB_RE}/
12
+ INITIAL_LONG_OPTION_SUB_RE = /(?:--|\+\+)#{LONG_OPTION_NAME_SUB_RE}/
13
+ INITIAL_OPTION_SUB_RE = /#{INITIAL_SHORT_OPTION_SUB_RE}|#{INITIAL_LONG_OPTION_SUB_RE}/
14
+
15
+ # A list of short and long options
16
+ OPTION_GROUP_SUB_RE = /#{INITIAL_OPTION_SUB_RE}(?:,#{OPTION_NAME_SUB_RE})*/
17
+
18
+ # Option argument
19
+ OPTION_ARG_SUB_RE = /[A-Z](?:[A-Z0-9_-]*[A-Z0-9])?/
20
+
21
+ # Matches option flags and argument. It defines the following captures
22
+ #
23
+ # $1 - Argument flag ('=')
24
+ # $2 - Type flag ('#' or '$')
25
+ # $3 - Argument name
26
+ # $4 - Optional flag ('?')
27
+ #
28
+ OPTION_FLAGS_SUB_RE = /(=)(#|\$)?(#{OPTION_ARG_SUB_RE})?(\?)?/
29
+
30
+ # Matches a declaration of an option. The RE defines the following captures:
31
+ #
32
+ # $1 - Option group
33
+ # $2 - Argument flag ('=')
34
+ # $3 - Type flag ('#' or '$')
35
+ # $4 - Argument name
36
+ # $5 - Optional flag ('?')
37
+ #
38
+ OPTION_SUB_RE = /(#{OPTION_GROUP_SUB_RE})#{OPTION_FLAGS_SUB_RE}?/
39
+
40
+ # Command and command paths
41
+ COMMAND_IDENT_SUB_RE = /[a-z](?:[a-z0-9_-]*[a-z0-9])?/
42
+ COMMAND_SUB_RE = /#{COMMAND_IDENT_SUB_RE}!/
43
+ COMMAND_PATH_SUB_RE = /#{COMMAND_IDENT_SUB_RE}(?:\.#{COMMAND_IDENT_SUB_RE})*!/
44
+
45
+ # Command argument
46
+ ARGUMENT_SUB_RE = /[A-Z][A-Z0-9_-]*[A-Z0-9](?:\.\.\.)?/
47
+ ARGUMENT_EXPR_SUB_RE = /\[?#{ARGUMENT_SUB_RE}(?:#{ARGUMENT_SUB_RE}|[\[\]\|\s])*/
48
+
49
+ # Matches a line starting with a command or an option
50
+ SCAN_RE = /^(?:#{COMMAND_PATH_SUB_RE}|#{OPTION_SUB_RE})(?:\s+.*)?$/
51
+
52
+
53
+ # Create anchored REs for all SUB_REs
54
+ self.constants.each { |c|
55
+ next if c.to_s !~ /_SUB_RE$/
56
+ sub_re = self.const_get(c)
57
+ next if !sub_re.is_a?(Regexp)
58
+ re = /^#{sub_re}$/
59
+ name = c.to_s.sub(/_SUB_RE$/, "_RE")
60
+ self.const_set(name, re)
61
+ }
62
+
63
+ # Method names reserved by the BasicObject class
64
+ BASIC_OBJECT_RESERVED_WORDS = %w(
65
+ ! != == __id__ __send__ equal? instance_eval instance_exec method_missing
66
+ singleton_method_added singleton_method_removed singleton_method_undefined)
67
+
68
+ # Method names reserved by the Ast::Command class
69
+ AST_COMMAND_RESERVED_WORDS = %w(
70
+ initialize options subcommand __is_program__ __get_grammar__
71
+ __add_option__ __add_command__)
72
+
73
+ # Reserved option names
74
+ OPTION_RESERVED_WORDS =
75
+ (BASIC_OBJECT_RESERVED_WORDS + AST_COMMAND_RESERVED_WORDS).grep(OPTION_NAME_RE)
76
+
77
+ # Reserved command names
78
+ COMMAND_RESERVED_WORDS = %w(subcommand)
79
+ end
80
+
81
+ include Constants
82
+ end
83
+
84
+
85
+
86
+
87
+
88
+