shellopts 0.9.7 → 2.0.0.pre.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,71 @@
1
+
2
+ module ShellOpts
3
+ # Service object for output of messages
4
+ #
5
+ # Messages are using the common command line formats
6
+ #
7
+ class Messenger
8
+ # Name of the program. When assigning to +name+ prefixed and suffixed
9
+ # whitespaces are removed
10
+ attr_accessor :name
11
+
12
+ # :nodoc:
13
+ def name=(name) @name = name.strip end
14
+ # :nodoc:
15
+
16
+ # Usage string. If not nil the usage string is printed by #error. When
17
+ # assigning to +usage+ suffixed whitespaces are removed and the format
18
+ # automatically set to +:custom+
19
+ attr_accessor :usage
20
+
21
+ # :nodoc:
22
+ def usage=(usage)
23
+ @format = :custom
24
+ @usage = usage&.rstrip
25
+ end
26
+ # :nodoc:
27
+
28
+ # Format of the usage string: +:default+ prefixes the +usage+ with 'Usage:
29
+ # #{name} ' before printing. +:custom+ prints +usage+ as is
30
+ attr_accessor :format
31
+
32
+ # Initialize a Messenger object. +name+ is the name of the name and +usage+
33
+ # is a short description of the options (eg. '-a -b') or a longer multiline
34
+ # explanation. The +:format+ option selects bewtween the two: +short+ (the
35
+ # default) or :long. Note that
36
+ #
37
+ def initialize(name, usage, format: :default)
38
+ @name = name
39
+ @usage = usage
40
+ @format = format
41
+ end
42
+
43
+ # Print error message and usage string and exit with status 1. Output is
44
+ # using the following format
45
+ #
46
+ # <name name>: <message>
47
+ # Usage: <name name> <options and arguments>
48
+ #
49
+ def error(*msgs)
50
+ $stderr.print "#{name}: #{msgs.join}\n"
51
+ if usage
52
+ $stderr.print "Usage: #{name} " if format == :default
53
+ $stderr.print "#{usage}\n"
54
+ end
55
+ exit 1
56
+ end
57
+
58
+ # Print error message and exit with status 1. It use the current ShellOpts
59
+ # object if defined. This method should not be called in response to
60
+ # user-errors but system errors (like disk full). Output is using the
61
+ # following format:
62
+ #
63
+ # <name name>: <message>
64
+ #
65
+ def fail(*msgs)
66
+ $stderr.puts "#{name}: #{msgs.join}"
67
+ exit 1
68
+ end
69
+ end
70
+ end
71
+
@@ -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,100 @@
1
+
2
+ require "shellopts"
3
+
4
+ require "shellopts/args.rb"
5
+
6
+ # TODO
7
+ #
8
+ # PROCESSING
9
+ # 1. Compile usage string and yield a grammar
10
+ # 2. Parse the options using the grammar and yield an AST
11
+ # 3. Construct the Program model from the AST
12
+ # 4. Apply defaults to the model
13
+ # 6. Run validations on the model
14
+ # 5. Create representation from the model
15
+ #
16
+
17
+ module ShellOpts
18
+ # The command line processing object
19
+ class ShellOpts
20
+ # One of :key, :name, :option
21
+ #
22
+ # Option Command
23
+ # :key key #command! (no collision)
24
+ # :name name #command (possible collision)
25
+ # :option --option #command (multihash, no collision) (TODO)
26
+ #
27
+ DEFAULT_USE = :key
28
+
29
+ # Name of program
30
+ attr_reader :name
31
+
32
+ # The grammar compiled from the usage string
33
+ attr_reader :grammar
34
+
35
+ # The AST parsed from the command line arguments
36
+ attr_reader :ast
37
+
38
+ # The IDR generated from the Ast
39
+ attr_reader :idr
40
+
41
+ # Object for error & fail messages. Default is to write a message on
42
+ # standard error and exit with status 1
43
+ attr_accessor :messenger
44
+
45
+ # Compile a usage string into a grammar and use that to parse command line
46
+ # arguments
47
+ #
48
+ # +usage+ is the usage string, and +argv+ the command line (typically the
49
+ # global ARGV array). +name+ is the name of the program and defaults to the
50
+ # basename of the program
51
+ #
52
+ # Syntax errors in the usage string are caused by the developer and raise a
53
+ # +ShellOpts::CompilerError+ exception. Errors in the +argv+ arguments are
54
+ # caused by the user and terminates the program with an error message and a
55
+ # short description of its usage
56
+ def initialize(usage, argv, name: PROGRAM, messenger: nil)
57
+ @name = name
58
+ begin
59
+ @grammar = Grammar.compile(name, usage)
60
+ @messenger = messenger || Messenger.new(name, @grammar.usage)
61
+ @ast = Ast.parse(@grammar, argv)
62
+ @idr = Idr.generate(@ast, @messenger)
63
+ rescue Grammar::Compiler::Error => ex
64
+ raise CompilerError.new(5, ex.message)
65
+ rescue Ast::Parser::Error => ex
66
+ error(ex.message)
67
+ end
68
+ end
69
+
70
+ # Return an array representation of options and commands in the same order
71
+ # as on the command line. Each option or command is represented by a [name,
72
+ # value] pair. The value of an option is be nil if the option didn't have
73
+ # an argument and else either a String, Integer, or Float. The value of a
74
+ # command is an array of its options and commands
75
+ def to_a() idr.to_a end
76
+
77
+ # Return a hash representation of the options. See {ShellOpts::OptionsHash}
78
+ def to_h(use: :key, aliases: {}) @idr.to_h(use: use, aliases: aliases) end
79
+
80
+ # Return a struct representation of the options. See {ShellOpts::OptionStruct}
81
+ def to_struct(use: :key, aliases: {}) @idr.to_struct(use: use, aliases: aliases) end
82
+
83
+ # List of remaining non-option command line arguments. Returns a Argv object
84
+ def args() Args.new(self, ast&.arguments) end
85
+
86
+ # Iterate options and commands as name/value pairs. Same as +to_a.each+
87
+ def each(&block) to_a.each(&block) end
88
+
89
+ # Print error messages and usage string and exit with status 1. This method
90
+ # should be called in response to user-errors (eg. specifying an illegal
91
+ # option)
92
+ def error(*msgs) @messenger.error(*msgs) end
93
+
94
+ # Print error message and exit with status 1. This method should called in
95
+ # response to system errors (like disk full)
96
+ def fail(*msgs) @messenger.fail(*msgs) end
97
+ end
98
+ end
99
+
100
+
@@ -1,3 +1,3 @@
1
1
  module Shellopts
2
- VERSION = "0.9.7"
2
+ VERSION = "2.0.0-4"
3
3
  end
data/rs ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/bash
2
+
3
+ PROGRAM=$(basename $0)
4
+ USAGE="SOURCE-FILE"
5
+
6
+ function error() {
7
+ echo "$PROGRAM: $@"
8
+ echo "Usage: $PROGRAM $USAGE"
9
+ exit 1
10
+ } >&2
11
+
12
+ [ $# = 1 ] || error "Illegal number of arguments"
13
+ SOURCE_NAME=${1%.rb}.rb
14
+
15
+ GEM_FILE=$(ls *.gemspec 2>/dev/null)
16
+ [ -n "$GEM_FILE" ] || error "Can't find gemspec file"
17
+ GEM_NAME=${GEM_FILE%.gemspec}
18
+
19
+ if [ -f lib/$SOURCE_NAME ]; then
20
+ SOURCE_FILE=lib/$SOURCE_NAME
21
+ elif [ -f lib/$GEM_NAME/$SOURCE_NAME ]; then
22
+ SOURCE_FILE=lib/$GEM_NAME/$SOURCE_NAME
23
+ else
24
+ SOURCE_FILE=$(find lib/$GEM_NAME -type f -path $SOURCE_NAME | head -1)
25
+ if [ -z "$SOURCE_FILE" ]; then
26
+ SOURCE_FILE=lib/$GEM_NAME/$SOURCE_NAME
27
+ fi
28
+ fi
29
+
30
+ SPEC_FILE=spec/${SOURCE_NAME%.rb}_spec.rb
31
+ [ -f $SPEC_FILE ] || error "Can't find spec file '$SPEC_FILE'"
32
+
33
+ rspec --fail-fast $SPEC_FILE || {
34
+ # rcov forgets a newline when rspec fails
35
+ status=$?; echo; exit $status;
36
+ }
37
+
38
+
39
+
40
+