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.
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
- # ShellOpts is a library for parsing command line options and sub-commands. The
8
- # library API consists of the methods {ShellOpts.process}, {ShellOpts.error},
9
- # and {ShellOpts.fail} and the result class {ShellOpts::ShellOpts}
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 inject the constant PROGRAM into the global scope. It contains the
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
- # Return the hidden +ShellOpts::ShellOpts+ object (see .process)
16
- def self.shellopts()
17
- @shellopts
18
- end
55
+ # Base class for ShellOpts exceptions
56
+ class Error < RuntimeError; end
19
57
 
20
- # Prettified usage string used by #error and #fail. Default is +usage+ of
21
- # the current +ShellOpts::ShellOpts+ object
22
- def self.usage() @usage || @shellopts&.usage end
23
-
24
- # Set the usage string
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
- # Reset the hidden +ShellOpts::ShellOpts+ class variable so that you can process
109
- # another command line
110
- def self.reset()
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
- # Print error message and usage string and exit with status 1. It use the
116
- # current ShellOpts object if defined. This method should be called in
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
- # Print error message and exit with status 1. It use the current ShellOpts
129
- # object if defined. This method should not be called in response to
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
- # The compilation object
137
- class ShellOpts
138
- # Name of program
139
- attr_reader :program_name
140
-
141
- # Prettified usage string used by #error and #fail. Shorthand for +grammar.usage+
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
- # Unroll the AST into a nested array
176
- def to_a
177
- @ast.values
178
- end
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
- # Iterate the result as name/value pairs. See {ShellOpts.process} for a
181
- # detailed description
182
- def each(&block)
183
- if block_given?
184
- to_a.each { |*args| yield(*args) }
185
- else
186
- to_a # FIXME: Iterator
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
- # Print error message and usage string and exit with status 1. This method
191
- # should be called in response to user-errors (eg. specifying an illegal
192
- # option)
193
- def error(*msgs)
194
- ::ShellOpts.emit_and_exit(program_name, true, usage, msgs)
195
- end
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
- # Print error message and exit with status 1. This method should not be
198
- # called in response to user-errors but system errors (like disk full)
199
- def fail(*msgs)
200
- ::ShellOpts.emit_and_exit(program_name, false, nil, msgs)
201
- end
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
- # Base class for ShellOpts exceptions
205
- class Error < RuntimeError; end
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
- # Raised when an error is detected in the usage string
208
- class CompilerError < Error
209
- def initialize(start, message)
210
- super(message)
211
- set_backtrace(caller(start))
212
- end
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
- # Raised when an internal error is detected
216
- class InternalError < Error; end
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
- @shellopts = nil
220
-
221
- def self.emit_and_exit(program, use_usage, usage, *msgs)
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
- PROGRAM = File.basename($PROGRAM_NAME)
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", "~> 2.2.10"
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: 1.0.1
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: 2021-08-18 00:00:00.000000000 Z
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: 2.2.10
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: 2.2.10
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: '0'
145
+ version: 1.3.1
140
146
  requirements: []
141
147
  rubygems_version: 3.0.8
142
148
  signing_key: