shellopts 2.0.0.pre.13 → 2.0.0.pre.14

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ac8ce6815e283630cb66195b0069a0d1772d8234dd580833e49b38cb0717fe8
4
- data.tar.gz: e6fc6e96d47db9078edad643ce9ee4fabdeaf3450a5ab61fd330b701bfadf244
3
+ metadata.gz: f1e9e2f138c476cb6af3184dd5e43f8a0f82108d05e4170491c5c46dfb547627
4
+ data.tar.gz: 9990bee10ffe5c6c5db537a416f366ed70c4577a8a3a07658281d9485225d438
5
5
  SHA512:
6
- metadata.gz: f77988efda7870d95b7693125e0e4b9753dbbe637cfc126d92559654bd171f17f37698ac7588ce9d3fc0521ba00156b862e763f1ae95190d77d89cc5e500bf5a
7
- data.tar.gz: 98d96929cd577649796e5f699e3c748f7ef3c72cb8ad3dd6036d2e656bae15229a414263a3701a4a027daacb869de2ceb52b1d3095fd1715c064d6befbaedbd0
6
+ metadata.gz: 5a57589ccaf3b2f516727cd6e7b066044d13a49d2c5280a478de162e42574e1eca03c2b013133d9b0ce50b4ac037fef15d12a01f5893b32ec6132d03c121e08f
7
+ data.tar.gz: bc338066a99d6470bd434d26cf994159ec926c4eb7f3a7d1d8b358303d5850bcdf058b56447ebf6c581bb2295de2d5f57f5637db5ab82fbd47ba618b8988afe6
data/TODO CHANGED
@@ -1,136 +1,16 @@
1
-
2
- TODO
3
- o Rethink #error and #fail <- The use-case is one-file ruby scripts. Idea: Only use in main exe file?
4
- o Create exceptions: UserError SystemFail and allow them to be used instead of #error and #fail
5
- ? Remove ! from OptionStruct#subcommand return value. We know we're
6
- processing commands so there is no need to have a distinct name and it
7
- feels a lot more intuitive without it
8
- o Add validation block to ShellOpts class methods
9
- o Get rid of key_name. Define #name on Grammar::Node instead
10
- o Define #name to the string name of the option/command without prefixed '--'
11
- for options. This can cause collisions but they can be avoided using aliases
12
- o Clean-up
13
- o Grammar::options -> Grammar::option_multihash
14
- o Clean-up identifiers etc.
15
- o Un-multi-izing Grammar::option_multihash and turn it into a regular hash from key to option
16
- o subcommand vs. command consistency
17
- o Implement ObjectStruct#key! and ObjectStruct#value! (?)
18
- o Allow command_alias == nil to suppress the method
19
- o Raise on non-existing names/keys. Only return nil for declared names/keys that are not present
20
- o Use hash_tree
21
- o Also allow assignment to usage string for ShellOpts::ShellOpts objects
22
- o Create a ShellOpts.args method? It would be useful when processing commands:
23
- case opt
24
- when "command"
25
- call_command_method(ShellOpts.args[1], ShellOpts.args[2])
26
- end
27
- ShellOpts.args would be a shorthand for ShellOpts.shellopts.args
28
- Another option would be to create an argument-processing method:
29
- shellopts.argv(2) -> call error if not exactly two arguments else return elements
30
- o Add a ShellOpts.option method:
31
- file = ShellOpts.option("--file")
32
- This will only work for options on the outermost level... maybe:
33
- file = ShellOpts.option("load! --file")
34
- o Check on return value from #process block to see if all options was handled:
35
- case opt
36
- when '-v'; verbose = true # Return value 'true' is ok
37
- # Unhandled option means return value is nil
38
- end
39
- o Consolidate some of the 3 variations of #error and #fail
40
- o Add a option flag for solitary options (--help)
41
- o Make a #to_yaml
42
- o Make an official dump method for debug
43
- o Make a note that all options are processed at once and not as-you-go
44
- o Test that arguments with spaces work
45
- o Long version usage strings (major release)
46
- o Doc: Example of processing of sub-commands and sub-sub-commands
47
-
48
- + Add a 'mandatory' argument to #subcommand
49
- + More tests
50
- + More doc
51
- + Implement value-name-before-flags rule
52
- + Kill option array values
53
- + Kill key forms
54
- + Rename Option#opt to Option#name
55
- + Have all Ast objects to be on [key, name, value] form
56
- + Change #=>i, $=>f and introduce b (boolean)
57
- + Unshift program name to usage definition string before compiling
58
- + Rename to UsageCompiler and ArgvParser
59
- + Make usage-string handle commands
60
- + Change !cmd to cmd!
61
- + Clean-up terminology: Option-name is used for names with and without the prefixed dashes
62
- + Rename Option#has_argument? and #optional? to something else
63
- + Fix location reporting of compiler errors
64
- + Allow '--' in usage so that everything after can be used as USAGE in error messages
65
- + Handle pretty-printing of usage string in handling of ParserError
66
- + Compiler.new.compile(usage), Parser.new(compiled_program).parse(argv)
67
- + Check for duplicate option in the parser
68
- + Handle CompilerError
69
- + Use nil value as the name of the top 'command'
70
- + Refactor compilation to avoid having the Command objects throw CompilerErrors
71
- + Change to 'parser.parse' / 'parser.parse3'
72
- + Use first long option as symbolic key
73
- + Use full option names everywhere (eg. '--all' instead of 'all')
74
-
75
- - Revert change from '#' -> 'i'
76
- - Guard against reserved 'object_id' name in OpenStruct
77
- - Default value ('=' -> ':')
78
- Default values are better handled in the calling program
79
-
80
- ? More specialized exceptions: "MissingArgumentError" etc.
81
-
82
- LATER
83
- o Allow '-a' and '--aa' in usage
84
- o Allow single-line comments
85
- o Allow multi-line comments
86
- o Regex as option value spec
87
- o "FILE", "DIR", "NEWFILE", "NEWDIR" as keyword in option value spec
88
- RFILE, RDIR
89
- WFILE, WDIR
90
- EFILE, EDIR
91
- o Octal and hexadecimal integers
92
- o Escape of separator in lists
93
- o Handle output of subcommand usage like "cmd1 cmd1.cmd2 cmd2"
94
- o Command-specific arguments: clone! o,opt ++ ARG1 ARG2...
95
- o Hostname and email as basic types
96
-
97
- ON TO_H BRANCH
98
- ShellOpts.process(usage, argv) { |opt,val| ... } => args
99
- ShellOpts.process(usage, argv) { |key,opt,val| ... } => args
100
-
101
- opts = ShellOpts.new(usage, argv, defaults = {})
102
- opts = ShellOpts.new(usage, argv, defaults = OpenStruct.new)
103
-
104
- opts.args
105
- opts.to_a
106
- opts.to_h
107
- opts.to_openstruct
108
-
109
- opts.each { |opt,val| ... }
110
- opts.each { |key,opt,val| ... }
111
-
112
- LONG FORMAT
113
-
114
- PROGRAM = File.basename(ARGV.first)
115
- USAGE = "-a -f FILE -lvh FILE..."
116
- DESCR = %(
117
- Short description
118
-
119
- Longer description
120
- )
121
- OPTIONS = %(
122
- -a,--all
123
- Process all files
124
-
125
- -f, --file=FILE
126
- Process file
127
-
128
- !command
129
- This is a command
130
-
131
- --this-is-a-command-option
132
- Options for commands are nested
133
-
134
- ...
135
- )
1
+ o Somehow escape comments where a line starts with an option name
2
+ o 'help' should list commands in declaration order
3
+ o 'help' should use all levels by default
4
+ o 'help' should always include top-level options (try setting levels: 10 and
5
+ see top-level options are gone
6
+ o Special handling of --help arguments so that '--help command' is possible
7
+ o Support for paging of help:
8
+ begin
9
+ file = Tempfile.new("prick")
10
+ file.puts HELP.split("\n").map { |l| l.sub(/^ /, "") }
11
+ file.flush
12
+ system "less #{file.path}"
13
+ ensure
14
+ file.close
15
+ end
136
16
 
@@ -0,0 +1,14 @@
1
+
2
+ module Algorithm
3
+ def follow(object, sym = nil, &block)
4
+ sym.nil? == block_given? or raise "Can't use both symbol and block"
5
+ a = []
6
+ while object
7
+ a << object
8
+ object = block_given? ? yield(object) : object.send(sym)
9
+ end
10
+ a
11
+ end
12
+
13
+ module_function :follow
14
+ end
@@ -0,0 +1,8 @@
1
+
2
+ if File.directory?(File.join(File.dirname(File.dirname(File.dirname(__FILE__))), "spec"))
3
+ RUBY_ENV = "development"
4
+ else
5
+ RUBY_ENV = "production"
6
+ end
7
+
8
+
data/lib/shellopts.rb CHANGED
@@ -1,257 +1,119 @@
1
1
  require "shellopts/version"
2
2
 
3
- require 'shellopts/compiler.rb'
4
- require 'shellopts/parser.rb'
5
- require 'shellopts/generator.rb'
6
- require 'shellopts/option_struct.rb'
7
- require 'shellopts/main.rb'
3
+ require "ext/algorithm.rb"
4
+ require "ext/ruby_env.rb"
8
5
 
9
- # Name of program. Defined as the basename of the program file
10
- #PROGRAM = File.basename($PROGRAM_NAME)
6
+ require "shellopts/constants.rb"
7
+ require "shellopts/exceptions.rb"
11
8
 
12
- # ShellOpts main Module
13
- #
14
- # This module contains methods to process command line options and arguments.
15
- # ShellOpts keeps a reference in ShellOpts.shellopts to the result of the last
16
- # command that was processed through its interface and use it as the implicit
17
- # object of many of its methods. This matches the typical use case where only
18
- # one command line is ever processed and makes it possible to create class
19
- # methods that knows about the command like #error and #fail
20
- #
21
- # For example; the following process and convert a command line into a struct
22
- # representation and also sets ShellOpts.shellopts object so that the #error
23
- # method can print a relevant spec string:
24
- #
25
- # USAGE = "a,all f,file=FILE -- ARG1 ARG2"
26
- # opts, args = ShellOpts.as_struct(USAGE, ARGV)
27
- # File.exist?(opts.file) or error "Can't find #{opts.file}"
28
- #
29
- # The command line is processed through one of the methods #process, #as_array,
30
- # #as_hash, or #as_struct that returns a [data, args] tuple. The data type
31
- # depends on the method: #process yields a Idr object that internally serves as
32
- # the base for the #as_array and #as_hash and #as_struct that converts it into
33
- # an Array, Hash, or ShellOpts::OptionStruct object. For example:
34
- #
35
- # USAGE = "..."
36
- # ShellOpts.process(USAGE, ARGV)
37
- # program, args = ShellOpts.as_program(USAGE, ARGV)
38
- # array, args = ShellOpts.as_array(USAGE, ARGV)
39
- # hash, args = ShellOpts.as_hash(USAGE, ARGV)
40
- # struct, args = ShellOpts.as_struct(USAGE, ARGV)
41
- #
42
- # +args+ is a ShellOpts::Argv object containing the the remaning command line
43
- # arguments. Argv is derived from Array
44
- #
45
- # ShellOpts can raise the exception CompilerError is there is an error in the
46
- # USAGE string. If there is an error in the user supplied command line, #error
47
- # is called instead and the program terminates with exit code 1. ShellOpts
48
- # raises ConversionError is there is a name collision when converting to the
49
- # hash or struct representations. Note that CompilerError and ConversionError
50
- # are caused by misuse of the library and the problem should be corrected by
51
- # the developer
52
- #
53
- # ShellOpts injects the constant PROGRAM into the global scope. It contains the
54
- # name of the program
55
- #
56
- # INCLUDING SHELLOPTS
57
- #
58
- # ShellOpts can optionally be included in your shell application main file but
59
- # it is not supposed to be included anywhere else
60
- #
61
- # Some behind the scenes magic happen if you include the ShellOpts module in your
62
- # main exe file
63
- #
64
- module ShellOpts
65
- def self.default_name()
66
- @default_name || defined?(PROGRAM) ? PROGRAM : File.basename($0)
67
- end
68
-
69
- def self.default_name=(name)
70
- @default_name = name
71
- end
72
-
73
- def self.default_usage()
74
- @default_usage || defined?(USAGE) ? USAGE : nil
75
- end
9
+ require "shellopts/grammar/analyzer.rb"
10
+ require "shellopts/grammar/lexer.rb"
11
+ require "shellopts/grammar/parser.rb"
12
+ require "shellopts/grammar/command.rb"
13
+ require "shellopts/grammar/option.rb"
76
14
 
77
- def self.default_usage=(usage)
78
- @default_usage = usage
79
- end
15
+ require "shellopts/ast/parser.rb"
16
+ require "shellopts/ast/command.rb"
17
+ require "shellopts/ast/option.rb"
80
18
 
81
- def self.default_key_type()
82
- @default_key_type || ::ShellOpts::DEFAULT_KEY_TYPE
83
- end
84
-
85
- def self.default_key_type=(type)
86
- @default_key_type = type
87
- end
19
+ require "shellopts/args.rb"
20
+ require "shellopts/formatter.rb"
88
21
 
89
- # Result of the last as_* command
90
- def self.opts() @opts end
91
- def self.args() @args end
22
+ if RUBY_ENV == "development"
23
+ require "shellopts/grammar/dump.rb"
24
+ require "shellopts/ast/dump.rb"
25
+ end
92
26
 
93
- # Base class for ShellOpts exceptions
94
- class Error < RuntimeError; end
27
+ $verb = nil
28
+ $quiet = nil
29
+ $shellopts = nil
95
30
 
96
- # Raised when a syntax error is detected in the spec string
97
- class CompilerError < Error
98
- def initialize(start, usage)
99
- super(usage)
100
- set_backtrace(caller(start))
31
+ module ShellOpts
32
+ class ShellOpts
33
+ attr_reader :name # Name of program. Defaults to the name of the executable
34
+ attr_reader :spec
35
+ attr_reader :argv
36
+
37
+ attr_reader :grammar
38
+ attr_reader :program
39
+ attr_reader :arguments
40
+
41
+ def initialize(spec, argv, name: nil)
42
+ @name = name || File.basename($PROGRAM_NAME)
43
+ @spec, @argv = spec, argv.dup
44
+ exprs = Grammar::Lexer.lex(@spec)
45
+ commands = Grammar::Parser.parse(@name, exprs)
46
+ @grammar = Grammar::Analyzer.analyze(commands)
47
+
48
+ begin
49
+ @program, @arguments = Ast::Parser.parse(@grammar, @argv)
50
+ rescue Error => ex
51
+ error(ex.subject, ex.message)
52
+ end
101
53
  end
102
- end
103
-
104
- # Raised when an error is detected in the command line
105
- class ParserError < Error; end
106
-
107
- # Raised when the command line error is caused by the user. It is raised by
108
- # the parser but can also be used by the application if the command line
109
- # fails a semantic check
110
- class UserError < ParserError; end
111
-
112
- # Raised when the error is caused by a failed assumption about the system. It
113
- # is not raised by the ShellOpts library as it only concerns itself with
114
- # command line syntax but can be used by the application to report a failure
115
- # through ShellOpts#fail method when the ShellOpts module is included
116
- class SystemFail < Error; end
117
-
118
- # Raised when an error is detected during conversion from the Idr to array,
119
- # hash, or struct
120
- class ConversionError < Error; end
121
-
122
- # Raised when an internal error is detected
123
- class InternalError < Error; end
124
-
125
- # The current compilation object. It is set by #process
126
- def self.shellopts() @shellopts end
127
54
 
128
- # Name of program
129
- def program_name() shellopts!.name end
130
- def program_name=(name) shellopts!.name = name end
131
-
132
- # Usage string
133
- def usage() shellopts!.spec end
134
- def usage=(spec) shellopts!.spec = spec end
135
-
136
- # Process command line, set current shellopts object, and return it.
137
- # Remaining arguments from the command line can be accessed through
138
- # +shellopts.args+
139
- def self.process(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
140
- @shellopts.nil? or reset
141
- @shellopts = ShellOpts.new(spec, argv, name: name, usage: usage)
142
- @shellopts.process
143
- end
55
+ def error(subject = nil, message)
56
+ $stderr.puts "#{name}: #{message}"
57
+ usage(subject, device: $stderr)
58
+ exit 1
59
+ end
144
60
 
145
- # Process command line, set current shellopts object, and return a
146
- # [Idr::Program, argv] tuple. Automatically includes the ShellOpts module
147
- # if called from the main Ruby object (ie. your executable)
148
- def self.as_program(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
149
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
150
- process(spec, argv, name: name, usage: usage)
151
- @opts = shellopts.idr
152
- @args = shellopts.args
153
- [@opts, @args]
154
- end
61
+ def fail(message)
62
+ $stderr.puts "#{name}: #{message}"
63
+ exit 1
64
+ end
155
65
 
156
- # Process command line, set current shellopts object, and return a [array,
157
- # argv] tuple. Automatically includes the ShellOpts module if called from the
158
- # main Ruby object (ie. your executable)
159
- def self.as_array(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
160
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
161
- process(spec, argv, name: name, usage: usage)
162
- @opts = shellopts.to_a
163
- @args = shellopts.args
164
- [@opts, @args]
165
- end
66
+ def usage(subject = nil, device: $stdout, levels: 1, margin: "")
67
+ subject = find_subject(subject)
68
+ device.puts Formatter.usage_string(subject, levels: levels, margin: margin)
69
+ end
166
70
 
167
- # Process command line, set current shellopts object, and return a [hash,
168
- # argv] tuple. Automatically includes the ShellOpts module if called from the
169
- # main Ruby object (ie. your executable)
170
- def self.as_hash(
171
- spec, argv,
172
- name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
173
- key_type: ::ShellOpts.default_key_type,
174
- aliases: {})
175
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
176
- process(spec, argv, name: name, usage: usage)
177
- @opts = shellopts.to_h(key_type: key_type, aliases: aliases)
178
- @args = shellopts.args
179
- [@opts, @args]
180
- end
71
+ def help(subject = nil, device: $stdout, levels: 10, margin: "", tab: " ")
72
+ subject = find_subject(subject)
73
+ device.puts Formatter.help_string(subject, levels: levels, margin: margin, tab: tab)
74
+ end
181
75
 
182
- # Process command line, set current shellopts object, and return a [struct,
183
- # argv] tuple. Automatically includes the ShellOpts module if called from the
184
- # main Ruby object (ie. your executable)
185
- def self.as_struct(
186
- spec, argv,
187
- name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
188
- aliases: {})
189
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
190
- process(spec, argv, name: name, usage: usage)
191
- @opts = shellopts.to_struct(aliases: aliases)
192
- @args = shellopts.args
193
- [@opts, @args]
194
- end
76
+ private
77
+ def lookup(name)
78
+ a = name.split(".")
79
+ cmd = grammar
80
+ while element = a.shift
81
+ cmd = cmd.commands[element]
82
+ end
83
+ cmd
84
+ end
195
85
 
196
- # Process command line, set current shellopts object, and then iterate
197
- # options and commands as an array. Returns an enumerator to the array
198
- # representation of the current shellopts object if not given a block
199
- # argument. Automatically includes the ShellOpts module if called from the
200
- # main Ruby object (ie. your executable)
201
- def self.each(spec = nil, argv = nil, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage, &block)
202
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
203
- process(spec, argv, name: name, usage: usage)
204
- @opts = shellopts.to_a
205
- @args = shellopts.args
206
- shellopts.each(&block)
86
+ def find_subject(obj)
87
+ case obj
88
+ when String; lookup(obj)
89
+ when Ast::Command; Command.grammar(obj)
90
+ when Grammar::Command; obj
91
+ when NilClass; grammar
92
+ else
93
+ raise Internal, "Illegal object: #{obj.class}"
94
+ end
95
+ end
207
96
  end
208
97
 
209
- # Print error usage and spec string and exit with status 1. This method
210
- # should be called in response to user-errors (eg. specifying an illegal
211
- # option)
212
- def self.error(*msgs, exit: true)
213
- shellopts!.error(msgs, exit: exit)
98
+ def self.process(spec, argv, name: nil)
99
+ $shellopts = ShellOpts.new(spec, argv, name: name)
100
+ [$shellopts.program, $shellopts.arguments]
214
101
  end
215
102
 
216
- # Print error usage and exit with status 1. This method should not be
217
- # called in response to system errors (eg. disk full)
218
- def self.fail(*msgs, exit: true)
219
- shellopts!.fail(*msgs, exit: exit)
103
+ def self.error(subject = nil, message)
104
+ $shellopts.error(subject, message)
220
105
  end
221
106
 
222
- def self.included(base)
223
- # base.equal?(Object) is only true when included in main (we hope)
224
- if !@is_included_in_main && base.equal?(Object)
225
- @is_included_in_main = true
226
- at_exit do
227
- case $!
228
- when ShellOpts::UserError
229
- ::ShellOpts.error($!.message, exit: false)
230
- exit!(1)
231
- when ShellOpts::SystemFail
232
- ::ShellOpts.fail($!.message)
233
- exit!(1)
234
- end
235
- end
236
- end
237
- super
107
+ def self.fail(message)
108
+ $shellopts.fail(message)
238
109
  end
239
110
 
240
- private
241
- # Default default key type
242
- DEFAULT_KEY_TYPE = :name
243
-
244
- # Reset state variables
245
- def self.reset()
246
- @shellopts = nil
111
+ def self.help(subject = nil, device: $stdout, levels: 10, margin: "", tab: " ")
112
+ $shellopts.help(subject, device: device, levels: levels, margin: margin, tab: tab)
247
113
  end
248
114
 
249
- # (shorthand) Raise an InternalError if shellopts is nil. Return shellopts
250
- def self.shellopts!
251
- ::ShellOpts.shellopts or raise UserError, "No ShellOpts.shellopts object"
115
+ def self.usage(subject = nil, device: $stdout, levels: 1, margin: "")
116
+ $shellopts.usage(subject, device: device, levels: levels, margin: margin)
252
117
  end
253
-
254
- @shellopts = nil
255
- @is_included_in_main = false
256
118
  end
257
119