shellopts 1.0.1 → 2.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
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: