shellopts 2.0.0.pre.7 → 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: c7ac01e4c6feb1897f74056a36c74db0cf78802d13437908aa18631ead7c97c5
4
- data.tar.gz: 5443ff421ceb38fcf07aaf87ae7b3740fd804679ccdec377fb195c688c50b57b
3
+ metadata.gz: f1e9e2f138c476cb6af3184dd5e43f8a0f82108d05e4170491c5c46dfb547627
4
+ data.tar.gz: 9990bee10ffe5c6c5db537a416f366ed70c4577a8a3a07658281d9485225d438
5
5
  SHA512:
6
- metadata.gz: 0d8180a2acc6dac8e234567db9409cd9d01cebe8d4551f6cfa24488ce5d1fb848ec8b42b21d40e7d8f6990bf61e4dc8d482370e9cb4ceb409e3f18fdb70a877c
7
- data.tar.gz: f07d22ab7976d5efa317356fc796f8bbe20b8c6a8ee609eb24106f60e8e56d9874181dcab4e873268b1aea5a1aa27ffd1253bb10499052edd995894c6eec9d88
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,242 +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'
8
-
9
- # Name of program. Defined as the basename of the program file
10
- #PROGRAM = File.basename($PROGRAM_NAME)
11
-
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
3
+ require "ext/algorithm.rb"
4
+ require "ext/ruby_env.rb"
68
5
 
69
- def self.default_name=(name)
70
- @default_name = name
71
- end
6
+ require "shellopts/constants.rb"
7
+ require "shellopts/exceptions.rb"
72
8
 
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
19
+ require "shellopts/args.rb"
20
+ require "shellopts/formatter.rb"
84
21
 
85
- def self.default_key_type=(type)
86
- @default_key_type = type
87
- end
22
+ if RUBY_ENV == "development"
23
+ require "shellopts/grammar/dump.rb"
24
+ require "shellopts/ast/dump.rb"
25
+ end
88
26
 
89
- # Base class for ShellOpts exceptions
90
- class Error < RuntimeError; end
27
+ $verb = nil
28
+ $quiet = nil
29
+ $shellopts = nil
91
30
 
92
- # Raised when a syntax error is detected in the spec string
93
- class CompilerError < Error
94
- def initialize(start, usage)
95
- super(usage)
96
- 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
97
53
  end
98
- end
99
-
100
- # Raised when an error is detected in the command line
101
- class ParserError < Error; end
102
-
103
- # Raised when the command line error is caused by the user. It is raised by
104
- # the parser but can also be used by the application if the command line
105
- # fails a semantic check
106
- class UserError < ParserError; end
107
-
108
- # Raised when the error is caused by a failed assumption about the system. It
109
- # is not raised by the ShellOpts library as it only concerns itself with
110
- # command line syntax but can be used by the application to report a failure
111
- # through ShellOpts#fail method when the ShellOpts module is included
112
- class SystemFail < Error; end
113
-
114
- # Raised when an error is detected during conversion from the Idr to array,
115
- # hash, or struct
116
- class ConversionError < Error; end
117
-
118
- # Raised when an internal error is detected
119
- class InternalError < Error; end
120
-
121
- # The current compilation object. It is set by #process
122
- def self.shellopts() @shellopts end
123
-
124
- # Name of program
125
- def program_name() shellopts!.name end
126
- def program_name=(name) shellopts!.name = name end
127
54
 
128
- # Usage string
129
- def usage() shellopts!.spec end
130
- def usage=(spec) shellopts!.spec = spec end
131
-
132
- # Process command line, set current shellopts object, and return it.
133
- # Remaining arguments from the command line can be accessed through
134
- # +shellopts.args+
135
- def self.process(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
136
- @shellopts.nil? or reset
137
- @shellopts = ShellOpts.new(spec, argv, name: name, usage: usage)
138
- end
55
+ def error(subject = nil, message)
56
+ $stderr.puts "#{name}: #{message}"
57
+ usage(subject, device: $stderr)
58
+ exit 1
59
+ end
139
60
 
140
- # Process command line, set current shellopts object, and return a
141
- # [Idr::Program, argv] tuple. Automatically includes the ShellOpts module
142
- # if called from the main Ruby object (ie. your executable)
143
- def self.as_program(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
144
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
145
- process(spec, argv, name: name, usage: usage)
146
- [shellopts.idr, shellopts.args]
147
- end
61
+ def fail(message)
62
+ $stderr.puts "#{name}: #{message}"
63
+ exit 1
64
+ end
148
65
 
149
- # Process command line, set current shellopts object, and return a [array,
150
- # argv] tuple. Automatically includes the ShellOpts module if called from the
151
- # main Ruby object (ie. your executable)
152
- def self.as_array(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
153
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
154
- process(spec, argv, name: name, usage: usage)
155
- [shellopts.to_a, shellopts.args]
156
- 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
157
70
 
158
- # Process command line, set current shellopts object, and return a [hash,
159
- # argv] tuple. Automatically includes the ShellOpts module if called from the
160
- # main Ruby object (ie. your executable)
161
- def self.as_hash(
162
- spec, argv,
163
- name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
164
- key_type: ::ShellOpts.default_key_type,
165
- aliases: {})
166
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
167
- process(spec, argv, name: name, usage: usage)
168
- [shellopts.to_h(key_type: key_type, aliases: aliases), shellopts.args]
169
- 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
170
75
 
171
- # Process command line, set current shellopts object, and return a [struct,
172
- # argv] tuple. Automatically includes the ShellOpts module if called from the
173
- # main Ruby object (ie. your executable)
174
- def self.as_struct(
175
- spec, argv,
176
- name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
177
- aliases: {})
178
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
179
- process(spec, argv, name: name, usage: usage)
180
- [shellopts.to_struct(aliases: aliases), shellopts.args]
181
- 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
182
85
 
183
- # Process command line, set current shellopts object, and then iterate
184
- # options and commands as an array. Returns an enumerator to the array
185
- # representation of the current shellopts object if not given a block
186
- # argument. Automatically includes the ShellOpts module if called from the
187
- # main Ruby object (ie. your executable)
188
- def self.each(spec = nil, argv = nil, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage, &block)
189
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
190
- process(spec, argv, name: name, usage: usage)
191
- 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
192
96
  end
193
97
 
194
- # Print error usage and spec string and exit with status 1. This method
195
- # should be called in response to user-errors (eg. specifying an illegal
196
- # option)
197
- def self.error(*msgs, exit: true)
198
- 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]
199
101
  end
200
102
 
201
- # Print error usage and exit with status 1. This method should not be
202
- # called in response to system errors (eg. disk full)
203
- def self.fail(*msgs, exit: true)
204
- shellopts!.fail(*msgs, exit: exit)
103
+ def self.error(subject = nil, message)
104
+ $shellopts.error(subject, message)
205
105
  end
206
106
 
207
- def self.included(base)
208
- # base.equal?(Object) is only true when included in main (we hope)
209
- if !@is_included_in_main && base.equal?(Object)
210
- @is_included_in_main = true
211
- at_exit do
212
- case $!
213
- when ShellOpts::UserError
214
- ::ShellOpts.error($!.message, exit: false)
215
- exit!(1)
216
- when ShellOpts::SystemFail
217
- ::ShellOpts.fail($!.message)
218
- exit!(1)
219
- end
220
- end
221
- end
222
- super
107
+ def self.fail(message)
108
+ $shellopts.fail(message)
223
109
  end
224
110
 
225
- private
226
- # Default default key type
227
- DEFAULT_KEY_TYPE = :name
228
-
229
- # Reset state variables
230
- def self.reset()
231
- @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)
232
113
  end
233
114
 
234
- # (shorthand) Raise an InternalError if shellopts is nil. Return shellopts
235
- def self.shellopts!
236
- ::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)
237
117
  end
238
-
239
- @shellopts = nil
240
- @is_included_in_main = false
241
118
  end
242
119