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.
- checksums.yaml +4 -4
- data/README.md +10 -0
- data/TODO +17 -1
- 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/lib/shellopts.rb +116 -202
- data/rs +40 -0
- data/shellopts.gemspec +1 -1
- metadata +12 -6
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/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
|
+
|
data/shellopts.gemspec
CHANGED
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
|
|
32
32
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
33
33
|
spec.require_paths = ["lib"]
|
34
34
|
|
35
|
-
spec.add_development_dependency "bundler", "~>
|
35
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
36
36
|
spec.add_development_dependency "rake", ">= 12.3.3"
|
37
37
|
spec.add_development_dependency "rspec", "~> 3.0"
|
38
38
|
spec.add_development_dependency "indented_io"
|
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:
|
11
|
+
date: 2020-07-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: '1.16'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: '1.16'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -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:
|