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

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