shellopts 2.0.0.pre.13 → 2.0.0.pre.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -48,7 +48,7 @@ module ShellOpts
48
48
 
49
49
  private
50
50
  def inoa(message = nil)
51
- raise ShellOpts::UserError, message || "Illegal number of arguments"
51
+ raise Error.new(nil), message || "Illegal number of arguments"
52
52
  end
53
53
  end
54
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
+
@@ -0,0 +1,21 @@
1
+
2
+ module ShellOpts
3
+ class CompileError < StandardError; end
4
+
5
+ class ShellOptsError < RuntimeError; end
6
+
7
+ class Error < ShellOptsError
8
+ attr_reader :subject
9
+
10
+ def initialize(subject = nil)
11
+ super()
12
+ @subject = subject
13
+ end
14
+ end
15
+
16
+ class Fail < ShellOptsError; end
17
+ end
18
+
19
+ class NotYet < NotImplementedError; end
20
+ class NotThis < ScriptError; end
21
+ class NotHere < ScriptError; end
@@ -0,0 +1,125 @@
1
+
2
+ require 'ext/algorithm'
3
+
4
+ require 'stringio'
5
+
6
+ module ShellOpts
7
+ class Formatter
8
+ # Return string describing usage of command
9
+ def self.usage_string(command, levels: 1, margin: "")
10
+ elements(command, levels: levels, help: false).map { |line|
11
+ "#{margin}#{line}"
12
+ }.join("\n")
13
+ end
14
+
15
+ # Return string with help for the given command
16
+ def self.help_string(command, levels: 10, margin: "", tab: " ")
17
+ io = StringIO.new
18
+ elements(command, levels: levels, help: true).each { |head, texts, options|
19
+ io.puts "#{margin}#{head}"
20
+ texts.each { |text| io.puts "#{margin}#{tab}#{text}" }
21
+ options.each { |opt_head, opt_texts|
22
+ io.puts
23
+ io.puts "#{margin}#{tab}#{opt_head}"
24
+ opt_texts.each { |text| io.puts "#{margin}#{tab*2}#{text}" }
25
+ }
26
+ io.puts
27
+ }
28
+ io.string[0..-2]
29
+ end
30
+
31
+ private
32
+ def self.elements(command, levels: 1, help: false)
33
+ result = []
34
+ recursive_elements(result, command, levels: levels, help: help)
35
+ result
36
+ end
37
+
38
+ def self.recursive_elements(acc, command, levels: 1, help: false)
39
+ cmds = (command.virtual? ? command.cmds : [command])
40
+ cmds.each { |cmd|
41
+ if levels == 1 || cmd.cmds.empty?
42
+ usage = (
43
+ path_elements(cmd) +
44
+ option_elements(cmd) +
45
+ subcommand_element(cmd) +
46
+ argument_elements(cmd)
47
+ ).compact.join(" ")
48
+ if help
49
+ opts = []
50
+ cmd.opts.each { |opt|
51
+ next if opt.text.empty?
52
+ opts << [option_help(opt), opt.text]
53
+ }
54
+ acc << [usage, cmd.text, opts]
55
+ else
56
+ acc << usage
57
+ end
58
+ else
59
+ cmd.cmds.each { |subcmd|
60
+ recursive_elements(acc, subcmd, levels: levels - 1, help: help)
61
+ }
62
+ end
63
+ }
64
+ end
65
+
66
+ # Return command line usage string
67
+ def self.command(cmd)
68
+ (path_elements(cmd) + option_elements(cmd) + argument_elements(cmd)).compact.join(" ")
69
+ end
70
+
71
+ def self.path_elements(cmd)
72
+ Algorithm.follow(cmd, :parent).map { |parent| parent.name }.reverse
73
+ end
74
+
75
+ def self.option_elements(cmd)
76
+ elements = []
77
+ collapsable_opts, other_opts = cmd.opts.partition { |opt| opt.shortname && !opt.argument? }
78
+
79
+ if !collapsable_opts.empty?
80
+ elements << "-" + collapsable_opts.map(&:shortname).join
81
+ end
82
+
83
+ elements + other_opts.map { |opt|
84
+ if opt.shortname
85
+ "-#{opt.shortname} #{opt.argument_name}" # We know opt has an argument
86
+ elsif opt.argument?
87
+ "--#{opt.longname}=#{opt.argument_name}"
88
+ else
89
+ "--#{opt.longname}"
90
+ end
91
+ }
92
+ end
93
+
94
+ def self.option_help(opt)
95
+ result = opt.names.map { |name|
96
+ if name.size == 1
97
+ "-#{name}"
98
+ else
99
+ "--#{name}"
100
+ end
101
+ }.join(", ")
102
+ if opt.argument?
103
+ if opt.longname
104
+ result += "=#{opt.argument_name}"
105
+ else
106
+ result += " #{opt.argument_name}"
107
+ end
108
+ end
109
+ result
110
+ end
111
+
112
+ def self.subcommand_element(cmd)
113
+ !cmd.cmds.empty? ? [cmd.cmds.map(&:name).join("|")] : []
114
+ end
115
+
116
+ def self.argument_elements(cmd)
117
+ cmd.args
118
+ end
119
+
120
+ def self.help_element(cmd)
121
+ text.map { |l| l.sub(/^\s*# /, "").rstrip }.join(" ")
122
+ end
123
+ end
124
+ end
125
+