shellopts 1.0.1 → 2.0.0.pre.1

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.
@@ -0,0 +1,245 @@
1
+
2
+ require 'shellopts/shellopts.rb'
3
+ require 'shellopts/idr'
4
+
5
+ module ShellOpts
6
+ class OptionStruct < BasicObject
7
+ # +key=:name+ cause command methods to be named without the exclamation
8
+ # mark. It doesn't change how options are named
9
+ def self.new(idr, key = :key, aliases = {})
10
+ ast = idr.instance_variable_get("@ast")
11
+ grammar = ast.grammar
12
+ instance = allocate
13
+
14
+ # Generate option accessor methods
15
+ grammar.option_list.each { |option|
16
+ key = alias_key(option.key, aliases)
17
+ instance.instance_eval("def #{key}() @#{key} end")
18
+ present = set_variable(instance, "@#{key}", idr[option.key])
19
+ instance.instance_eval("def #{key}?() #{present} end")
20
+ }
21
+
22
+ # Generate #subcommand default methods
23
+ if !idr.subcommand
24
+ instance.instance_eval("def subcommand() nil end")
25
+ instance.instance_eval("def subcommand?() false end")
26
+ instance.instance_eval("def subcommand!() nil end")
27
+ end
28
+
29
+ # Generate subcommand methods
30
+ grammar.command_list.each { |command|
31
+ key = alias_key(command.key, aliases)
32
+ if command.key == idr.subcommand&.key
33
+ struct = OptionStruct.new(idr.subcommand, aliases[idr.subcommand.key] || {})
34
+ set_variable(instance, "@subcommand", struct)
35
+ instance.instance_eval("def #{key}() @subcommand end")
36
+ instance.instance_eval("def subcommand() :#{key} end")
37
+ instance.instance_eval("def subcommand?() true end")
38
+ instance.instance_eval("def subcommand!() @subcommand end")
39
+ else
40
+ instance.instance_eval("def #{key}() nil end")
41
+ end
42
+ }
43
+
44
+ instance
45
+ end
46
+
47
+ private
48
+ # Return class of object. #class is not defined for BasicObjects so this
49
+ # method provides an alternative way of getting the class
50
+ def self.class_of(object)
51
+ # https://stackoverflow.com/a/18621313/2130986
52
+ ::Kernel.instance_method(:class).bind(object).call
53
+ end
54
+
55
+ # Replace key with alias and check against the list of reserved words
56
+ def self.alias_key(internal_key, aliases)
57
+ key = aliases[internal_key] || internal_key
58
+ !RESERVED_WORDS.include?(key.to_s) or
59
+ raise ::ShellOpts::ConversionError, "Can't create struct: '#{key}' is a reserved word"
60
+ key
61
+ end
62
+
63
+ # Shorthand helper method. Substitutes the undefined ObjectStruct#instance_variable_set
64
+ def self.set_variable(this, var, value)
65
+ # https://stackoverflow.com/a/18621313/2130986
66
+ ::Kernel.instance_method(:instance_variable_set).bind(this).call(var, value)
67
+ end
68
+
69
+ BASIC_OBJECT_RESERVED_WORDS = %w(
70
+ __id__ __send__ instance_eval instance_exec method_missing
71
+ singleton_method_added singleton_method_removed
72
+ singleton_method_undefined)
73
+ OPTIONS_STRUCT_RESERVED_WORDS = %w(subcommand)
74
+ RESERVED_WORDS = BASIC_OBJECT_RESERVED_WORDS + OPTIONS_STRUCT_RESERVED_WORDS
75
+ end
76
+ end
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+ __END__
86
+
87
+ module ShellOpts
88
+ # Struct representation of options. Usually created by ShellOpts::to_struct
89
+ #
90
+ # OptionStruct objects give easy access to configuration option values but
91
+ # meta data are more circuitously accessed through class methods with an
92
+ # explicit instance argument
93
+ #
94
+ # Option values are accessed through a member methods named after the key of
95
+ # the option. Repeated options have an Array value with one element (possibly
96
+ # nil) for each use of the option. A query method with a '?' suffixed to the
97
+ # name returns true or false depending on whether the option was used or not
98
+ #
99
+ # option - Value of option. Either an object or an Array if the option can
100
+ # be repeated
101
+ # option? - True iff option was given
102
+ #
103
+ # Command methods return a nested OptionStruct object while the special
104
+ # #command method returns the key of actual command (if any). Use
105
+ # +strukt.send(strukt.command)+ to get the subcommand of a OptionStruct. It
106
+ # is possible to rename #command method to avoid name collisions
107
+ #
108
+ # name! - Command. An OptionStruct or nil if not given on the command line
109
+ # subcommand - Key of command. Can be renamed
110
+ #
111
+ # ---------------------------------
112
+ # name! - Command. An OptionStruct or nil if not given on the command line
113
+ #
114
+ # key! - Key of command
115
+ # value! - Value of command (a subcommand). Can be renamed
116
+ #
117
+ # Note: There is no command query method because option and command names
118
+ # live in seperate namespaces and could cause colllisions. Check +name!+ for
119
+ # nil to detect if a command was given
120
+ #
121
+ # Meta data are extracted through class methods to avoid polluting the object
122
+ # namespace. OptionStruct use an OptionsHash object internally and
123
+ # implements a subset of its meta methods by forwarding to it. The
124
+ # OptionsHash object can be accessed through the #options_hash method
125
+ #
126
+ # Note that #command is defined as both an instance method and a class
127
+ # method. Use the class method to make the code work with all OptionStruct
128
+ # objects even if #command has been renamed
129
+ #
130
+ # +ShellOpts+ is derived from +BascicObject+ that reserves some words for
131
+ # internal use (+__id__+, +__send__+, +instance_eval+, +instance_exec+,
132
+ # +method_missing+, +singleton_method_added+, +singleton_method_removed+,
133
+ # +singleton_method_undefined+). ShellOpts also define two reserved words of
134
+ # its own (+__options_hash__+ and +__command__+). ShellOpts raise an
135
+ # ShellOpts::ConversionError if an option collides with one of the
136
+ # reserved words or with the special #command method
137
+ #
138
+ class OptionStruct < BasicObject
139
+ # Create a new OptionStruct instance from an AST. The optional
140
+ # +options_hash+ argument is used to create subcommands without creating a
141
+ # new options_hash argument. It is not meant for end-users. The
142
+ # +command_alias+ names the method holding the key for the subcommand (if
143
+ # any)
144
+ def self.new(ast, options_hash = OptionsHash.new(ast), command_alias: :command)
145
+ instance = allocate
146
+ set_variable(instance, "@__options_hash__", options_hash)
147
+
148
+ # Check for reserved words and +command_alias+
149
+ options_hash.keys.each { |key|
150
+ !RESERVED_WORDS.include?(key.to_s) or
151
+ raise ::ShellOpts::ConversionError, "Can't create struct: '#{key}' is a reserved word"
152
+ key != command_alias or
153
+ raise ::ShellOpts::ConversionError, "Can't create struct: '#{key}' is the command alias"
154
+ }
155
+
156
+ # Create accessor methods
157
+ ast.grammar.option_list.each { |option|
158
+ instance.instance_eval("def #{option.key}() @#{option.key} end")
159
+ instance.instance_eval("def #{option.key}?() false end")
160
+ }
161
+ ast.grammar.command_list.each { |command|
162
+ instance.instance_eval("def #{command.key}() nil end")
163
+ }
164
+
165
+ # Assign values
166
+ options_hash.each { |key, value|
167
+ if value.is_a?(OptionsHash)
168
+ set_variable(instance, "@__command__", OptionStruct.new(value.ast, value))
169
+ instance.instance_eval("def #{key}() @__command__ end")
170
+ else
171
+ set_variable(instance, "@#{key}", value)
172
+ instance.instance_eval("def #{key}?() true end")
173
+ end
174
+ }
175
+
176
+ # Command accessor method
177
+ instance.instance_eval("def #{command_alias}() @__options_hash__.command end")
178
+
179
+ instance
180
+ end
181
+
182
+ # Return the OptionsHash object from the instance
183
+ def self.options_hash(instance)
184
+ get_variable(instance, "@__options_hash__")
185
+ end
186
+
187
+ # Return class of object. #class is not defined for BasicObjects so this
188
+ # method provides an alternative way of getting the class a BasicObject
189
+ def self.class_of(object)
190
+ # https://stackoverflow.com/a/18621313/2130986
191
+ ::Kernel.instance_method(:class).bind(object).call
192
+ end
193
+
194
+ # Return the number of options and commands
195
+ def self.size(instance)
196
+ options_hash(instance).size
197
+ end
198
+
199
+ # Return the option and command keys. The keys are in order of occurrence
200
+ # on the command line. A subcommand will always be the last element
201
+ def self.keys(instance)
202
+ options_hash(instance).keys
203
+ end
204
+
205
+ # Return the actual option name used on the command line for +name+. Use
206
+ # +index+ to select between repeated options. Return the name of the
207
+ # program/subcommand if key is nil
208
+ def self.name(struct, key = nil, index = nil)
209
+ options_hash(struct).name(key, index)
210
+ end
211
+
212
+ # Return the AST node for the option key or the AST node for the
213
+ # OptionStruct if key is nil. Use +index+ to select between repeated
214
+ # options. Raise InternalError if key doesn't exists
215
+ def self.node(struct, key = nil, index = nil)
216
+ options_hash(struct).node(key, index)
217
+ end
218
+
219
+ # Return key of the command of the struct (possibly nil)
220
+ def self.command(struct)
221
+ options_hash(struct).command
222
+ end
223
+
224
+ private
225
+ BASIC_OBJECT_RESERVED_WORDS = %w(
226
+ __id__ __send__ instance_eval instance_exec method_missing
227
+ singleton_method_added singleton_method_removed
228
+ singleton_method_undefined)
229
+ OPTIONS_STRUCT_RESERVED_WORDS = %w(__options_hash__ __command__)
230
+ RESERVED_WORDS = BASIC_OBJECT_RESERVED_WORDS + OPTIONS_STRUCT_RESERVED_WORDS
231
+
232
+ # Shorthand helper method. Substitutes the undefined ObjectStruct#instance_variable_set
233
+ def self.set_variable(this, var, value)
234
+ # https://stackoverflow.com/a/18621313/2130986
235
+ ::Kernel.instance_method(:instance_variable_set).bind(this).call(var, value)
236
+ end
237
+
238
+ # Shorthand helper method: Substitutes the undefined ObjectStruct#instance_variable_get
239
+ def self.get_variable(this, var)
240
+ # https://stackoverflow.com/a/18621313/2130986
241
+ ::Kernel.instance_method(:instance_variable_get).bind(this).call(var)
242
+ end
243
+ end
244
+ end
245
+
@@ -0,0 +1,98 @@
1
+
2
+ require "shellopts"
3
+
4
+ # TODO
5
+ #
6
+ # PROCESSING
7
+ # 1. Compile usage string and yield a grammar
8
+ # 2. Parse the options using the grammar and yield an AST
9
+ # 3. Construct the Program model from the AST
10
+ # 4. Apply defaults to the model
11
+ # 6. Run validations on the model
12
+ # 5. Create representation from the model
13
+ #
14
+
15
+ module ShellOpts
16
+ # The command line processing object
17
+ class ShellOpts
18
+ # One of :key, :name, :option
19
+ #
20
+ # Option Command
21
+ # :key key #command! (no collision)
22
+ # :name name #command (possible collision)
23
+ # :option --option #command (multihash, no collision) (TODO)
24
+ #
25
+ DEFAULT_USE = :key
26
+
27
+ # Name of program
28
+ attr_reader :name
29
+
30
+ # The grammar compiled from the usage string
31
+ attr_reader :grammar
32
+
33
+ # The AST parsed from the command line arguments
34
+ attr_reader :ast
35
+
36
+ # The IDR generated from the Ast
37
+ attr_reader :idr
38
+
39
+ # Object for error & fail messages. Default is to write a message on
40
+ # standard error and exit with status 1
41
+ attr_accessor :messenger
42
+
43
+ # Compile a usage string into a grammar and use that to parse command line
44
+ # arguments
45
+ #
46
+ # +usage+ is the usage string, and +argv+ the command line (typically the
47
+ # global ARGV array). +name+ is the name of the program and defaults to the
48
+ # basename of the program
49
+ #
50
+ # Syntax errors in the usage string are caused by the developer and raise a
51
+ # +ShellOpts::CompilerError+ exception. Errors in the +argv+ arguments are
52
+ # caused by the user and terminates the program with an error message and a
53
+ # short description of its usage
54
+ def initialize(usage, argv, name: PROGRAM, messenger: nil)
55
+ @name = name
56
+ begin
57
+ @grammar = Grammar.compile(name, usage)
58
+ @messenger = messenger || Messenger.new(name, @grammar.usage)
59
+ @ast = Ast.parse(@grammar, argv)
60
+ @idr = Idr.generate(@ast, @messenger)
61
+ rescue Grammar::Compiler::Error => ex
62
+ raise CompilerError.new(5, ex.message)
63
+ rescue Ast::Parser::Error => ex
64
+ error(ex.message)
65
+ end
66
+ end
67
+
68
+ # Return an array representation of options and commands in the same order
69
+ # as on the command line. Each option or command is represented by a [name,
70
+ # value] pair. The value of an option is be nil if the option didn't have
71
+ # an argument and else either a String, Integer, or Float. The value of a
72
+ # command is an array of its options and commands
73
+ def to_a() idr.to_a end
74
+
75
+ # Return a hash representation of the options. See {ShellOpts::OptionsHash}
76
+ def to_h(use: :key, aliases: {}) @idr.to_h(use: use, aliases: aliases) end
77
+
78
+ # Return a struct representation of the options. See {ShellOpts::OptionStruct}
79
+ def to_struct(use: :key, aliases: {}) @idr.to_struct(use: use, aliases: aliases) end
80
+
81
+ # List of remaining non-option command line arguments. Shorthand for +ast&.arguments+
82
+ def args() @ast&.arguments end
83
+
84
+ # Iterate options and commands as name/value pairs. Same as +to_a.each+
85
+ def each(&block) to_a.each(&block) end
86
+
87
+ # Print error messages and usage string and exit with status 1. This method
88
+ # should be called in response to user-errors (eg. specifying an illegal
89
+ # option)
90
+ def error(*msgs) @messenger.error(*msgs) end
91
+
92
+ # Print error message and exit with status 1. This method should called in
93
+ # response to system errors (like disk full)
94
+ def fail(*msgs) @messenger.fail(*msgs) end
95
+ end
96
+ end
97
+
98
+
@@ -1,3 +1,3 @@
1
1
  module Shellopts
2
- VERSION = "1.0.1"
2
+ VERSION = "2.0.0-1"
3
3
  end