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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+