shellopts 2.0.0.pre.13 → 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.
@@ -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
+