shellopts 1.0.0 → 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.
- checksums.yaml +4 -4
- data/README.md +10 -0
- data/TODO +17 -1
- data/lib/shellopts.rb +116 -202
- data/lib/shellopts/ast/node.rb +1 -1
- data/lib/shellopts/compiler.rb +7 -7
- data/lib/shellopts/generator.rb +15 -0
- data/lib/shellopts/grammar/command.rb +27 -9
- data/lib/shellopts/grammar/option.rb +11 -1
- data/lib/shellopts/grammar/program.rb +2 -2
- data/lib/shellopts/idr.rb +209 -0
- data/lib/shellopts/messenger.rb +71 -0
- data/lib/shellopts/option_struct.rb +245 -0
- data/lib/shellopts/shellopts.rb +98 -0
- data/lib/shellopts/version.rb +1 -1
- data/rs +40 -0
- metadata +10 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e60db3cf5de50dcd106cb0fca007d064e18354278a65207bb167bf8aa63d3433
|
4
|
+
data.tar.gz: 85b0108262357d6e5654e725ab12293f51fd2579beb13fa0d910be2200c32310
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 375fe97651622560d786b288217e1a8581d12bbc497dee55b36848597d9410d393a127e86088cfe4ac8f949270d9f6797d6f135f65492f15bb9d4a84bff1ffbc
|
7
|
+
data.tar.gz: c69a813e4672d87c6a4da69e0e8086d8acfe54c8506cda64ce6dc36dbc12669cc79c29ffd480c6871937d87491139f4e7448fd35a6d6a7d333451addd6c634a6
|
data/README.md
CHANGED
@@ -378,6 +378,16 @@ release a new version, update the version number in `version.rb`, and then run
|
|
378
378
|
git commits and tags, and push the `.gem` file to
|
379
379
|
[rubygems.org](https://rubygems.org).
|
380
380
|
|
381
|
+
## Implementation
|
382
|
+
|
383
|
+
FIXME
|
384
|
+
# ShellOpts is a library for parsing command line options and commands. It
|
385
|
+
# consists of the interface module {ShellOpts}, the implementation class
|
386
|
+
# {ShellOpts::ShellOpts} and the representation classes
|
387
|
+
# {ShellOpts::OptionsHash} and {ShellOpts::OptionsStruct}.
|
388
|
+
# {ShellOpts::Messenger} is used for error messages
|
389
|
+
|
390
|
+
|
381
391
|
## Contributing
|
382
392
|
|
383
393
|
Bug reports and pull requests are welcome on GitHub at
|
data/TODO
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
|
2
2
|
TODO
|
3
|
+
o Add validation block to ShellOpts class methods
|
4
|
+
o Get rid of key_name. Define #name on Grammar::Node instead
|
5
|
+
o Define #name to the string name of the option/command without prefixed '--'
|
6
|
+
for options. This can cause collisions but they can be avoided using aliases
|
7
|
+
o Clean-up
|
8
|
+
o Grammar::options -> Grammar::option_multihash
|
9
|
+
o Clean-up identifiers etc.
|
10
|
+
o Un-multi-izing Grammar::option_multihash and turn it into a regular hash from key to option
|
11
|
+
o subcommand vs. command consistency
|
12
|
+
o Implement ObjectStruct#key! and ObjectStruct#value! (?)
|
13
|
+
o Allow command_alias == nil to suppress the method
|
14
|
+
o Raise on non-existing names/keys. Only return nil for declared names/keys that are not present
|
15
|
+
o Use hash_tree
|
3
16
|
o Also allow assignment to usage string for ShellOpts::ShellOpts objects
|
4
17
|
o Create a ShellOpts.args method? It would be useful when processing commands:
|
5
18
|
case opt
|
@@ -9,7 +22,10 @@ TODO
|
|
9
22
|
ShellOpts.args would be a shorthand for ShellOpts.shellopts.args
|
10
23
|
Another option would be to create an argument-processing method:
|
11
24
|
shellopts.argv(2) -> call error if not exactly two arguments else return elements
|
12
|
-
|
25
|
+
o Add a ShellOpts.option method:
|
26
|
+
file = ShellOpts.option("--file")
|
27
|
+
This will only work for options on the outermost level... maybe:
|
28
|
+
file = ShellOpts.option("load! --file")
|
13
29
|
o Check on return value from #process block to see if all options was handled:
|
14
30
|
case opt
|
15
31
|
when '-v'; verbose = true # Return value 'true' is ok
|
data/lib/shellopts.rb
CHANGED
@@ -2,231 +2,145 @@ require "shellopts/version"
|
|
2
2
|
|
3
3
|
require 'shellopts/compiler.rb'
|
4
4
|
require 'shellopts/parser.rb'
|
5
|
+
require 'shellopts/generator.rb'
|
6
|
+
require 'shellopts/option_struct.rb'
|
7
|
+
require 'shellopts/messenger.rb'
|
5
8
|
require 'shellopts/utils.rb'
|
6
9
|
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
+
# Name of program. Defined as the basename of the program file
|
11
|
+
PROGRAM = File.basename($PROGRAM_NAME)
|
12
|
+
|
13
|
+
# ShellOpts main Module
|
14
|
+
#
|
15
|
+
# This module contains methods to process command line options and arguments.
|
16
|
+
# ShellOpts keeps a reference in ShellOpts.shellopts to the result of the last
|
17
|
+
# command that was processed through its interface and use it as the implicit
|
18
|
+
# object of many of its methods. This matches the typical use case where only
|
19
|
+
# one command line is ever processed and makes it possible to create class
|
20
|
+
# methods that knows about the command like #error and #fail
|
21
|
+
#
|
22
|
+
# For example; the following process and convert a command line into a struct
|
23
|
+
# representation and also sets ShellOpts.shellopts object so that the #error
|
24
|
+
# method can print a relevant usage string:
|
25
|
+
#
|
26
|
+
# USAGE = "a,all f,file=FILE -- ARG1 ARG2"
|
27
|
+
# opts, args = ShellOpts.as_struct(USAGE, ARGV)
|
28
|
+
# File.exist?(opts.file) or error "Can't find #{opts.file}"
|
29
|
+
#
|
30
|
+
# The command line is processed through one of the methods #process, #as_array,
|
31
|
+
# #as_hash, or #as_struct that returns a [data, args] tuple. The data type
|
32
|
+
# depends on the method: #process yields a Idr object that internally serves as
|
33
|
+
# the base for the #as_array and #as_hash and #as_struct that converts it into
|
34
|
+
# an Array, Hash, or ShellOpts::OptionStruct object. For example:
|
35
|
+
#
|
36
|
+
# USAGE = "..."
|
37
|
+
# ShellOpts.process(USAGE, ARGV)
|
38
|
+
# program, args = ShellOpts.as_program(USAGE, ARGV)
|
39
|
+
# array, args = ShellOpts.as_array(USAGE, ARGV)
|
40
|
+
# hash, args = ShellOpts.as_hash(USAGE, ARGV)
|
41
|
+
# struct, args = ShellOpts.as_struct(USAGE, ARGV)
|
10
42
|
#
|
11
|
-
# ShellOpts
|
43
|
+
# ShellOpts can raise the exception CompilerError is there is an error in the
|
44
|
+
# USAGE string. If there is an error in the user supplied command line, #error
|
45
|
+
# is called instead and the program terminates with exit code 1. ShellOpts
|
46
|
+
# raises ConversionError is there is a name collision when converting to the
|
47
|
+
# hash or struct representations. Note that CompilerError and ConversionError
|
48
|
+
# are caused by misuse of the library and the problem should be corrected by
|
49
|
+
# the developer
|
50
|
+
#
|
51
|
+
# ShellOpts injects the constant PROGRAM into the global scope. It contains the
|
12
52
|
# name of the program
|
13
53
|
#
|
14
54
|
module ShellOpts
|
15
|
-
#
|
16
|
-
|
17
|
-
@shellopts
|
18
|
-
end
|
55
|
+
# Base class for ShellOpts exceptions
|
56
|
+
class Error < RuntimeError; end
|
19
57
|
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
def self.usage=(usage) @usage = usage end
|
26
|
-
|
27
|
-
# Process command line options and arguments. #process takes a usage string
|
28
|
-
# defining the options and the array of command line arguments to be parsed
|
29
|
-
# as arguments
|
30
|
-
#
|
31
|
-
# If called with a block, the block is called with name and value of each
|
32
|
-
# option or command and #process returns a list of remaining command line
|
33
|
-
# arguments. If called without a block a ShellOpts::ShellOpts object is
|
34
|
-
# returned
|
35
|
-
#
|
36
|
-
# The value of an option is its argument, the value of a command is an array
|
37
|
-
# of name/value pairs of options and subcommands. Option values are converted
|
38
|
-
# to the target type (String, Integer, Float) if specified
|
39
|
-
#
|
40
|
-
# Example
|
41
|
-
#
|
42
|
-
# # Define options
|
43
|
-
# USAGE = 'a,all g,global +v,verbose h,help save! snapshot f,file=FILE h,help'
|
44
|
-
#
|
45
|
-
# # Define defaults
|
46
|
-
# all = false
|
47
|
-
# global = false
|
48
|
-
# verbose = 0
|
49
|
-
# save = false
|
50
|
-
# snapshot = false
|
51
|
-
# file = nil
|
52
|
-
#
|
53
|
-
# # Process options
|
54
|
-
# argv = ShellOpts.process(USAGE, ARGV) do |name, value|
|
55
|
-
# case name
|
56
|
-
# when '-a', '--all'; all = true
|
57
|
-
# when '-g', '--global'; global = value
|
58
|
-
# when '-v', '--verbose'; verbose += 1
|
59
|
-
# when '-h', '--help'; print_help(); exit(0)
|
60
|
-
# when 'save'
|
61
|
-
# save = true
|
62
|
-
# value.each do |name, value|
|
63
|
-
# case name
|
64
|
-
# when '--snapshot'; snapshot = true
|
65
|
-
# when '-f', '--file'; file = value
|
66
|
-
# when '-h', '--help'; print_save_help(); exit(0)
|
67
|
-
# end
|
68
|
-
# end
|
69
|
-
# else
|
70
|
-
# raise "Not a user error. The developer forgot or misspelled an option"
|
71
|
-
# end
|
72
|
-
# end
|
73
|
-
#
|
74
|
-
# # Process remaining arguments
|
75
|
-
# argv.each { |arg| ... }
|
76
|
-
#
|
77
|
-
# If an error is encountered while compiling the usage string, a
|
78
|
-
# +ShellOpts::Compiler+ exception is raised. If the error happens while
|
79
|
-
# parsing the command line arguments, the program prints an error message and
|
80
|
-
# exits with status 1. Failed assertions raise a +ShellOpts::InternalError+
|
81
|
-
# exception
|
82
|
-
#
|
83
|
-
# Note that you can't process more than one command line at a time because
|
84
|
-
# #process saves a hidden {ShellOpts::ShellOpts} class variable used by the
|
85
|
-
# class methods #error and #fail. Call #reset to clear the global object if
|
86
|
-
# you really need to parse more than one command line. Alternatively you can
|
87
|
-
# create +ShellOpts::ShellOpts+ objects yourself and also use the object methods
|
88
|
-
# #error and #fail:
|
89
|
-
#
|
90
|
-
# shellopts = ShellOpts::ShellOpts.new(USAGE, ARGS)
|
91
|
-
# shellopts.each { |name, value| ... }
|
92
|
-
# shellopts.args.each { |arg| ... }
|
93
|
-
# shellopts.error("Something went wrong")
|
94
|
-
#
|
95
|
-
# Use #shellopts to get the hidden +ShellOpts::ShellOpts+ object
|
96
|
-
#
|
97
|
-
def self.process(usage, argv, program_name: PROGRAM, &block)
|
98
|
-
if !block_given?
|
99
|
-
ShellOpts.new(usage, argv, program_name: program_name)
|
100
|
-
else
|
101
|
-
@shellopts.nil? or raise InternalError, "ShellOpts class variable already initialized"
|
102
|
-
@shellopts = ShellOpts.new(usage, argv, program_name: program_name)
|
103
|
-
@shellopts.each(&block)
|
104
|
-
@shellopts.args
|
58
|
+
# Raised when a syntax error is detected in the usage string
|
59
|
+
class CompilerError < Error
|
60
|
+
def initialize(start, message)
|
61
|
+
super(message)
|
62
|
+
set_backtrace(caller(start))
|
105
63
|
end
|
106
64
|
end
|
107
65
|
|
108
|
-
#
|
109
|
-
#
|
110
|
-
|
111
|
-
@shellopts = nil
|
112
|
-
@usage = nil
|
113
|
-
end
|
66
|
+
# Raised when an error is detected during conversion from the Idr to array,
|
67
|
+
# hash, or struct
|
68
|
+
class ConversionError < Error; end
|
114
69
|
|
115
|
-
#
|
116
|
-
|
117
|
-
# response to user-errors (eg. specifying an illegal option)
|
118
|
-
#
|
119
|
-
# If there is no current ShellOpts object +error+ will look for USAGE to make
|
120
|
-
# it possible to use +error+ before the command line is processed and also as
|
121
|
-
# a stand-alone error reporting method
|
122
|
-
def self.error(*msgs)
|
123
|
-
program = @shellopts&.program_name || PROGRAM
|
124
|
-
usage_string = usage || (defined?(USAGE) && USAGE ? Grammar.compile(PROGRAM, USAGE).usage : nil)
|
125
|
-
emit_and_exit(program, @usage.nil?, usage_string, *msgs)
|
126
|
-
end
|
70
|
+
# Raised when an internal error is detected
|
71
|
+
class InternalError < Error; end
|
127
72
|
|
128
|
-
#
|
129
|
-
|
130
|
-
# user-errors but system errors (like disk full)
|
131
|
-
def self.fail(*msgs)
|
132
|
-
program = @shellopts&.program_name || PROGRAM
|
133
|
-
emit_and_exit(program, false, nil, *msgs)
|
134
|
-
end
|
73
|
+
# The current compilation object. It is set by #process
|
74
|
+
def self.shellopts() @shellopts end
|
135
75
|
|
136
|
-
#
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
def usage() @grammar.usage end
|
143
|
-
|
144
|
-
# The grammar compiled from the usage string. If #ast is defined, it's
|
145
|
-
# equal to ast.grammar
|
146
|
-
attr_reader :grammar
|
147
|
-
|
148
|
-
# The AST resulting from parsing the command line arguments
|
149
|
-
attr_reader :ast
|
150
|
-
|
151
|
-
# List of remaining non-option command line arguments. Shorthand for ast.arguments
|
152
|
-
def args() @ast.arguments end
|
153
|
-
|
154
|
-
# Compile a usage string into a grammar and use that to parse command line
|
155
|
-
# arguments
|
156
|
-
#
|
157
|
-
# +usage+ is the usage string, and +argv+ the command line (typically the
|
158
|
-
# global ARGV array). +program_name+ is the name of the program and is
|
159
|
-
# used in error messages. It defaults to the basename of the program
|
160
|
-
#
|
161
|
-
# Errors in the usage string raise a CompilerError exception. Errors in the
|
162
|
-
# argv arguments terminates the program with an error message
|
163
|
-
def initialize(usage, argv, program_name: File.basename($0))
|
164
|
-
@program_name = program_name
|
165
|
-
begin
|
166
|
-
@grammar = Grammar.compile(program_name, usage)
|
167
|
-
@ast = Ast.parse(@grammar, argv)
|
168
|
-
rescue Grammar::Compiler::Error => ex
|
169
|
-
raise CompilerError.new(5, ex.message)
|
170
|
-
rescue Ast::Parser::Error => ex
|
171
|
-
error(ex.message)
|
172
|
-
end
|
173
|
-
end
|
76
|
+
# Process command line and set and return the shellopts compile object
|
77
|
+
def self.process(usage, argv, name: self.name, message: nil)
|
78
|
+
@shellopts.nil? or reset
|
79
|
+
messenger = message && Messenger.new(name, message, format: :custom)
|
80
|
+
@shellopts = ShellOpts.new(usage, argv, name: name, messenger: messenger)
|
81
|
+
end
|
174
82
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
83
|
+
# Return the internal data representation of the command line (Idr::Program).
|
84
|
+
# Note that #as_program that the remaning arguments are accessible through
|
85
|
+
# the returned object
|
86
|
+
def self.as_program(usage, argv, name: self.name, message: nil)
|
87
|
+
process(usage, argv, name: name, message: message)
|
88
|
+
[shellopts.idr, shellopts.args]
|
89
|
+
end
|
179
90
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
end
|
188
|
-
end
|
91
|
+
# Process command line, set current shellopts object, and return a [array, argv]
|
92
|
+
# tuple. Returns the representation of the current object if not given any
|
93
|
+
# arguments
|
94
|
+
def self.as_array(usage, argv, name: self.name, message: nil)
|
95
|
+
process(usage, argv, name: name, message: message)
|
96
|
+
[shellopts.to_a, shellopts.args]
|
97
|
+
end
|
189
98
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
99
|
+
# Process command line, set current shellopts object, and return a [hash, argv]
|
100
|
+
# tuple. Returns the representation of the current object if not given any
|
101
|
+
# arguments
|
102
|
+
def self.as_hash(usage, argv, name: self.name, message: nil, use: ShellOpts::DEFAULT_USE, aliases: {})
|
103
|
+
process(usage, argv, name: name, message: message)
|
104
|
+
[shellopts.to_hash(use: use, aliases: aliases), shellopts.args]
|
105
|
+
end
|
196
106
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
107
|
+
# Process command line, set current shellopts object, and return a [struct, argv]
|
108
|
+
# tuple. Returns the representation of the current object if not given any
|
109
|
+
# arguments
|
110
|
+
def self.as_struct(usage, argv, name: self.name, message: nil, use: ShellOpts::DEFAULT_USE, aliases: {})
|
111
|
+
process(usage, argv, name: name, message: message)
|
112
|
+
[shellopts.to_struct(use: use, aliases: aliases), shellopts.args]
|
202
113
|
end
|
203
114
|
|
204
|
-
#
|
205
|
-
|
115
|
+
# Process command line, set current shellopts object, and then iterate
|
116
|
+
# options and commands as an array. Returns an enumerator to the array
|
117
|
+
# representation of the current shellopts object if not given a block
|
118
|
+
# argument
|
119
|
+
def self.each(usage = nil, argv = nil, name: self.name, message: nil, &block)
|
120
|
+
process(usage, argv, name: name, message: message)
|
121
|
+
shellopts.each(&block)
|
122
|
+
end
|
206
123
|
|
207
|
-
#
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
124
|
+
# Print error message and usage string and exit with status 1. This method
|
125
|
+
# should be called in response to user-errors (eg. specifying an illegal
|
126
|
+
# option)
|
127
|
+
def self.error(*msgs)
|
128
|
+
raise "Oops" if shellopts.nil?
|
129
|
+
shellopts.error(*msgs)
|
213
130
|
end
|
214
131
|
|
215
|
-
#
|
216
|
-
|
132
|
+
# Print error message and exit with status 1. This method should not be
|
133
|
+
# called in response to system errors (eg. disk full)
|
134
|
+
def self.fail(*msgs)
|
135
|
+
raise "Oops" if shellopts.nil?
|
136
|
+
shellopts.fail(*msgs)
|
137
|
+
end
|
217
138
|
|
218
139
|
private
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
$stderr.puts "#{program}: #{msgs.join}"
|
223
|
-
if use_usage
|
224
|
-
$stderr.puts "Usage: #{program} #{usage}" if usage
|
225
|
-
else
|
226
|
-
$stderr.puts usage if usage
|
227
|
-
end
|
228
|
-
exit 1
|
140
|
+
# Reset state variables
|
141
|
+
def self.reset()
|
142
|
+
@shellopts = nil
|
229
143
|
end
|
230
|
-
end
|
231
144
|
|
232
|
-
|
145
|
+
@shellopts = nil
|
146
|
+
end
|
data/lib/shellopts/ast/node.rb
CHANGED
@@ -23,7 +23,7 @@ module ShellOpts
|
|
23
23
|
end
|
24
24
|
|
25
25
|
# Return either a value (option value), an array of values (command), or
|
26
|
-
# nil (option without a value).
|
26
|
+
# nil (option without a value). It must be defined in sub-classes of Ast::Node
|
27
27
|
def values() raise end
|
28
28
|
|
29
29
|
# :nocov:
|
data/lib/shellopts/compiler.rb
CHANGED
@@ -8,12 +8,12 @@ require 'shellopts/grammar/program.rb'
|
|
8
8
|
module ShellOpts
|
9
9
|
module Grammar
|
10
10
|
# Compiles an option definition string and returns a Grammar::Program
|
11
|
-
# object.
|
11
|
+
# object. name is the name of the program and source is the
|
12
12
|
# option definition string
|
13
|
-
def self.compile(
|
14
|
-
|
13
|
+
def self.compile(name, source)
|
14
|
+
name.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{name.class}"
|
15
15
|
source.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{source.class}"
|
16
|
-
Compiler.new(
|
16
|
+
Compiler.new(name, source).call
|
17
17
|
end
|
18
18
|
|
19
19
|
# Service object for compiling an option definition string. Returns a
|
@@ -26,8 +26,8 @@ module ShellOpts
|
|
26
26
|
class Error < RuntimeError; end
|
27
27
|
|
28
28
|
# Initialize a Compiler object. source is the option definition string
|
29
|
-
def initialize(
|
30
|
-
@
|
29
|
+
def initialize(name, source)
|
30
|
+
@name, @tokens = name, source.split(/\s+/).reject(&:empty?)
|
31
31
|
|
32
32
|
# @commands_by_path is an hash from command-path to Command or Program
|
33
33
|
# object. The top level Program object has nil as its path.
|
@@ -54,7 +54,7 @@ module ShellOpts
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def compile_program
|
57
|
-
program = @commands_by_path[nil] = Grammar::Program.new(@
|
57
|
+
program = @commands_by_path[nil] = Grammar::Program.new(@name, compile_options)
|
58
58
|
while curr_token && curr_token != "--"
|
59
59
|
compile_command
|
60
60
|
end
|
@@ -11,12 +11,8 @@ module ShellOpts
|
|
11
11
|
# Name of command (String). Name doesn't include the exclamation point ('!')
|
12
12
|
attr_reader :name
|
13
13
|
|
14
|
-
#
|
15
|
-
|
16
|
-
attr_reader :options
|
17
|
-
|
18
|
-
# Sub-commands of this command. Is a hash from sub-command name to command object
|
19
|
-
attr_reader :commands
|
14
|
+
# Same as #name. TODO Define in Grammar::Node instead
|
15
|
+
alias :key_name :name
|
20
16
|
|
21
17
|
# List of options in declaration order
|
22
18
|
attr_reader :option_list
|
@@ -24,6 +20,23 @@ module ShellOpts
|
|
24
20
|
# List of commands in declaration order
|
25
21
|
attr_reader :command_list
|
26
22
|
|
23
|
+
# Multihash from option key or names (both short and long names) to option. This
|
24
|
+
# means an option can occur more than once as the hash value
|
25
|
+
def options()
|
26
|
+
@option_multihash ||= @option_list.flat_map { |option|
|
27
|
+
option.identifiers.map { |ident| [ident, option] }
|
28
|
+
}.to_h
|
29
|
+
end
|
30
|
+
|
31
|
+
# Sub-commands of this command. Is a multihash from sub-command key or
|
32
|
+
# name to command object. Lazily constructed because subcommands are added
|
33
|
+
# after initialization
|
34
|
+
def commands()
|
35
|
+
@command_multihash ||= @command_list.flat_map { |command|
|
36
|
+
command.identifiers.map { |name| [name, command] }
|
37
|
+
}.to_h
|
38
|
+
end
|
39
|
+
|
27
40
|
# Initialize a Command object. parent is the parent Command object or nil
|
28
41
|
# if this is the root object. name is the name of the command (without
|
29
42
|
# the exclamation mark), and option_list a list of Option objects
|
@@ -32,11 +45,17 @@ module ShellOpts
|
|
32
45
|
@name = name
|
33
46
|
parent.attach(self) if parent
|
34
47
|
@option_list = option_list
|
35
|
-
@options = @option_list.flat_map { |opt| opt.names.map { |name| [name, opt] } }.to_h
|
36
|
-
@commands = {}
|
37
48
|
@command_list = []
|
38
49
|
end
|
39
50
|
|
51
|
+
# Return key for the identifier
|
52
|
+
def identifier2key(ident)
|
53
|
+
options[ident]&.key || commands[ident]&.key
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return list of identifiers for the command
|
57
|
+
def identifiers() [key, name] end
|
58
|
+
|
40
59
|
# :nocov:
|
41
60
|
def dump(&block)
|
42
61
|
puts "#{key.inspect}"
|
@@ -55,7 +74,6 @@ module ShellOpts
|
|
55
74
|
protected
|
56
75
|
def attach(command)
|
57
76
|
command.instance_variable_set(:@parent, self)
|
58
|
-
@commands[command.name] = command
|
59
77
|
@command_list << command
|
60
78
|
end
|
61
79
|
end
|
@@ -10,6 +10,9 @@ module ShellOpts
|
|
10
10
|
# List of long names (incl. '--')
|
11
11
|
attr_reader :long_names
|
12
12
|
|
13
|
+
# Name of the key attribute (eg. if key is :all then key_name is '--all'
|
14
|
+
attr_reader :key_name
|
15
|
+
|
13
16
|
# List of flags (Symbol)
|
14
17
|
def flags() @flags.keys end
|
15
18
|
|
@@ -23,7 +26,8 @@ module ShellOpts
|
|
23
26
|
# there's no :string flag, it's status is inferred. label is the optional
|
24
27
|
# informal name of the option argument (eg. 'FILE') or nil if not present
|
25
28
|
def initialize(short_names, long_names, flags, label = nil)
|
26
|
-
|
29
|
+
@key_name = long_names.first || short_names.first
|
30
|
+
super(@key_name.sub(/^-+/, "").to_sym)
|
27
31
|
@short_names, @long_names = short_names, long_names
|
28
32
|
@flags = flags.map { |flag| [flag, true] }.to_h
|
29
33
|
@label = label
|
@@ -32,6 +36,12 @@ module ShellOpts
|
|
32
36
|
# Array of option names with short names first and then the long names
|
33
37
|
def names() @short_names + @long_names end
|
34
38
|
|
39
|
+
# Array of names and the key
|
40
|
+
def identifiers() names + [key] end
|
41
|
+
|
42
|
+
# Return true if +ident+ is equal to any name or to key
|
43
|
+
def match?(ident) names.include?(ident) || ident == key end
|
44
|
+
|
35
45
|
# Flag query methods. Returns true if the flag is present and otherwise nil
|
36
46
|
def repeated?() @flags[:repeated] || false end
|
37
47
|
def argument?() @flags[:argument] || false end
|
@@ -17,7 +17,7 @@ module ShellOpts
|
|
17
17
|
def usage
|
18
18
|
(
|
19
19
|
render_options(option_list) +
|
20
|
-
|
20
|
+
command_list.map { |cmd| render_command(cmd) } +
|
21
21
|
args
|
22
22
|
).flatten.join(" ")
|
23
23
|
end
|
@@ -34,7 +34,7 @@ module ShellOpts
|
|
34
34
|
private
|
35
35
|
def render_command(command)
|
36
36
|
[command.name] + render_options(command.option_list) +
|
37
|
-
command.
|
37
|
+
command.command_list.map { |cmd| render_command(cmd) }.flatten
|
38
38
|
end
|
39
39
|
|
40
40
|
def render_options(options)
|
@@ -0,0 +1,209 @@
|
|
1
|
+
|
2
|
+
module ShellOpts
|
3
|
+
# Idr models the Internal Data Representation of a program. It is the native
|
4
|
+
# representation of a command
|
5
|
+
#
|
6
|
+
# The IDR should ideally be completely detached from the compile-time grammar
|
7
|
+
# and AST but they are only hidden from view in this implementation. Create
|
8
|
+
# a Shellopts object instead to access the compiler data
|
9
|
+
#
|
10
|
+
module Idr
|
11
|
+
# Base class for the Idr class hierarchy. It is constructed from an Ast
|
12
|
+
# object by #generate. Node is modelled as an element of a hash with a key
|
13
|
+
# and a value. Options have their (optional) argument as value while
|
14
|
+
# commands use +self+ as value
|
15
|
+
class Node
|
16
|
+
# Unique key (within context) for the option or command. nil for the
|
17
|
+
# top-level Program object
|
18
|
+
#
|
19
|
+
# It is usually the first long option if present and else the first short
|
20
|
+
# option turned into a Symbol by first removing prefixed dashed, eg.
|
21
|
+
# '--all' becomes :all
|
22
|
+
attr_reader :key
|
23
|
+
|
24
|
+
# Name of command and option as used on the command line
|
25
|
+
attr_reader :name
|
26
|
+
|
27
|
+
# Value of node. This can be a simple value (String, Integer, or Float),
|
28
|
+
# an Array of values, or a Idr::Command object. Note that the value of a
|
29
|
+
# Command object is the object itself
|
30
|
+
#
|
31
|
+
# Repeated options are implemented as an Array with one element for each
|
32
|
+
# use of the option. The element is nil if the option doesn't take
|
33
|
+
# arguments or if an optional argument is missing.
|
34
|
+
attr_reader :value
|
35
|
+
|
36
|
+
protected
|
37
|
+
# Copy arguments into instance variables
|
38
|
+
def initialize(ast, key, name, value)
|
39
|
+
@ast, @key, @name, @value = ast, key, name, value
|
40
|
+
end
|
41
|
+
|
42
|
+
# The AST node for this Idr object
|
43
|
+
attr_reader :ast
|
44
|
+
|
45
|
+
# Shorthand to the grammar node for this Idr object
|
46
|
+
def grammar() @ast.grammar end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Base class for Options
|
50
|
+
class Option < Node
|
51
|
+
end
|
52
|
+
|
53
|
+
class SimpleOption < Option
|
54
|
+
protected
|
55
|
+
# Initialize with defauls from the Ast. +value+ is set to true if option
|
56
|
+
# doesn't take an argument
|
57
|
+
def initialize(ast)
|
58
|
+
value = ast.grammar.argument? ? ast.value : true
|
59
|
+
super(ast, ast.key, ast.name, value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# An OptionGroup models repeated options collapsed into a single key. The
|
64
|
+
# name of the group should be set to the name of the key (eg. '--all' if
|
65
|
+
# the key is :all)
|
66
|
+
class OptionGroup < Option
|
67
|
+
# Array of names of the options
|
68
|
+
attr_reader :names
|
69
|
+
|
70
|
+
# Array of values of the options
|
71
|
+
alias :values :value
|
72
|
+
|
73
|
+
# Name is set to the key name and value to an array of option values
|
74
|
+
def initialize(key, name, options)
|
75
|
+
@names = options.map(&:name)
|
76
|
+
super(nil, key, name, options.map(&:value))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Command < Node
|
81
|
+
# Hash from key to options with repeated option_list collapsed into a
|
82
|
+
# option group. It also include an entry for the subcommand. Options are
|
83
|
+
# ordered by first use on the command line. The command entry will always
|
84
|
+
# be last
|
85
|
+
attr_reader :options
|
86
|
+
|
87
|
+
# List of command line options in the same order as on the command line
|
88
|
+
attr_reader :option_list
|
89
|
+
|
90
|
+
# Subcommand object. Possibly nil
|
91
|
+
attr_reader :subcommand
|
92
|
+
|
93
|
+
# True if ident is declared
|
94
|
+
def declared?(ident) option?(ident) || command?(ident) end
|
95
|
+
|
96
|
+
# True if ident is declared as an option
|
97
|
+
def option?(ident) grammar.options.key?(ident) end
|
98
|
+
|
99
|
+
# True if ident is declared as a command
|
100
|
+
def command?(ident) grammar.commands.key?(ident) end
|
101
|
+
|
102
|
+
# True if ident is present
|
103
|
+
def key?(ident)
|
104
|
+
declared?(ident) or raise InternalError, "Undefined identifier: #{ident.inspect}"
|
105
|
+
key = grammar.identifier2key(ident)
|
106
|
+
@options.key?(key)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Value of ident. Repeated options are collapsed into an OptionGroup object
|
110
|
+
def [](ident)
|
111
|
+
declared?(ident) or raise InternalError, "Undefined identifier: #{ident.inspect}"
|
112
|
+
key = grammar.identifier2key(ident)
|
113
|
+
if @options.key?(key)
|
114
|
+
@options[key].value
|
115
|
+
elsif option?(key)
|
116
|
+
false
|
117
|
+
else
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Apply defaults recursively. Values can be lambdas that will be evaluated to
|
123
|
+
# get the default value
|
124
|
+
def apply(defaults = {}) end
|
125
|
+
|
126
|
+
# Return options and command as an array
|
127
|
+
def to_a() @ast.values end
|
128
|
+
|
129
|
+
# Return options and command as a hash. The hash also define the
|
130
|
+
# singleton method #subcommand that returns the key of the subcommand
|
131
|
+
#
|
132
|
+
# +key+ controls the type of keys used: +:key+ (the default) use the
|
133
|
+
# symbolic key, +:name+ use key_name. Note that using +:name+ can cause name collisions between
|
134
|
+
# option and command names and that #to_s raises an exception if it detects a collision
|
135
|
+
#
|
136
|
+
# +aliases+ maps from key to replacement key (which could be any object).
|
137
|
+
# +aliases+ can be used to avoid name collisions between options and
|
138
|
+
# commands
|
139
|
+
#
|
140
|
+
# IDEA: Make subcommand _not_ follow the +key+ setting so that setting key to
|
141
|
+
# IDEA: Add a singleton method #subcommand to the hash
|
142
|
+
#
|
143
|
+
def to_h(use: :key, aliases: {})
|
144
|
+
value = {}
|
145
|
+
value.define_singleton_method(:subcommand) { nil }
|
146
|
+
options.values.each { |opt|
|
147
|
+
ident = aliases[opt.key] || (use == :key ? opt.key : opt.ast.grammar.key_name)
|
148
|
+
!value.key?(ident) or raise ConversionError, "Duplicate key: #{ident.inspect}"
|
149
|
+
case opt
|
150
|
+
when Option
|
151
|
+
value[ident] = opt.value
|
152
|
+
when Command
|
153
|
+
value[ident] = opt.value.to_h
|
154
|
+
value.define_singleton_method(:subcommand) { ident } # Redefine
|
155
|
+
else
|
156
|
+
raise InternalError, "Oops"
|
157
|
+
end
|
158
|
+
}
|
159
|
+
value
|
160
|
+
end
|
161
|
+
|
162
|
+
# Return options and command as a struct
|
163
|
+
def to_struct(key = :key, aliases = {}) OptionStruct.new(self, key, aliases) end
|
164
|
+
|
165
|
+
protected
|
166
|
+
# Initialize an Idr::Command object and all dependent objects
|
167
|
+
def initialize(ast)
|
168
|
+
super(ast, ast.key, ast.name, self)
|
169
|
+
@option_list = ast.options.map { |node| SimpleOption.new(node) }
|
170
|
+
@subcommand = Command.new(ast.command) if ast.command
|
171
|
+
@options = @option_list.group_by { |option| option.key }.map { |key, option_list|
|
172
|
+
option =
|
173
|
+
if ast.grammar.options[key].repeated?
|
174
|
+
OptionGroup.new(key, ast.grammar.options[key].key_name, option_list)
|
175
|
+
else
|
176
|
+
option_list.first
|
177
|
+
end
|
178
|
+
[key, option]
|
179
|
+
}.to_h
|
180
|
+
@options[subcommand.key] = @subcommand if @subcommand
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
class Program < Command
|
185
|
+
# #key is nil for the top-level Program object
|
186
|
+
def key() nil end
|
187
|
+
|
188
|
+
# Remaining command line arguments
|
189
|
+
def args() @ast.arguments end
|
190
|
+
|
191
|
+
# Messenger object that is used to emit error messages. It should
|
192
|
+
# implement #error(*args) and #fail(*args)
|
193
|
+
attr_reader :messenger
|
194
|
+
|
195
|
+
# Initialize the top-level Idr::Program object
|
196
|
+
def initialize(ast, messenger)
|
197
|
+
@messenger = messenger
|
198
|
+
super(ast)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Emit error message and a usage description before exiting with status 1
|
202
|
+
def error(*args) messenger.error(*error_messages) end
|
203
|
+
|
204
|
+
# Emit error message before exiting with status 1
|
205
|
+
def fail(*args) messenger.fail(*error_messages) end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
@@ -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,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
|
+
|
data/lib/shellopts/version.rb
CHANGED
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
|
+
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shellopts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0.pre.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Claus Rasmussen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-07-
|
11
|
+
date: 2020-07-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -111,13 +111,19 @@ files:
|
|
111
111
|
- lib/shellopts/ast/option.rb
|
112
112
|
- lib/shellopts/ast/program.rb
|
113
113
|
- lib/shellopts/compiler.rb
|
114
|
+
- lib/shellopts/generator.rb
|
114
115
|
- lib/shellopts/grammar/command.rb
|
115
116
|
- lib/shellopts/grammar/node.rb
|
116
117
|
- lib/shellopts/grammar/option.rb
|
117
118
|
- lib/shellopts/grammar/program.rb
|
119
|
+
- lib/shellopts/idr.rb
|
120
|
+
- lib/shellopts/messenger.rb
|
121
|
+
- lib/shellopts/option_struct.rb
|
118
122
|
- lib/shellopts/parser.rb
|
123
|
+
- lib/shellopts/shellopts.rb
|
119
124
|
- lib/shellopts/utils.rb
|
120
125
|
- lib/shellopts/version.rb
|
126
|
+
- rs
|
121
127
|
- shellopts.gemspec
|
122
128
|
homepage: http://github.com/clrgit/shellopts
|
123
129
|
licenses: []
|
@@ -134,9 +140,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
134
140
|
version: '0'
|
135
141
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
142
|
requirements:
|
137
|
-
- - "
|
143
|
+
- - ">"
|
138
144
|
- !ruby/object:Gem::Version
|
139
|
-
version:
|
145
|
+
version: 1.3.1
|
140
146
|
requirements: []
|
141
147
|
rubygems_version: 3.0.8
|
142
148
|
signing_key:
|