shellopts 0.9.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f97bd1dadffdc1a8e5eab5635bdb186ddb925bafc2fe60b478c84d3b9590fb63
4
+ data.tar.gz: 9909d8069a937593e7a583d6fd8db1989f7eb8e321d9c655e2d0f376caeffd24
5
+ SHA512:
6
+ metadata.gz: 161c50d8cb2dc9abaa91164c3898f57942a6d51fcaadcb83bfd52cb96115548772be5d244d7e620f71751b523fdd5db0ab8a8a8bc76624be40a94d0341ac53c3
7
+ data.tar.gz: abaa5c58dfc5aaa98a553b4cb582c738a4bcd3649e5681c034a32b550bb583d106e4b0240cb0c5225dc0c8f58b3a32af2bd314d5894209b39b72e26446c6f4e3
data/.gitignore ADDED
@@ -0,0 +1,28 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /doc/
5
+ /pkg/
6
+ /spec/reports/
7
+ /tmp/
8
+
9
+ # rspec failure tracking
10
+ .rspec_status
11
+
12
+ # simplecov
13
+ /coverage/
14
+
15
+ # Ignore Gemfile.lock. See https://stackoverflow.com/questions/4151495/should-gemfile-lock-be-included-in-gitignore
16
+ /Gemfile.lock
17
+
18
+ # Ignore vim files
19
+ .*.swp
20
+
21
+ # Ignore t.* files
22
+ t
23
+ t.*
24
+ tt
25
+ tt.*
26
+ s
27
+ s.*
28
+
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.5.1
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.1
5
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in shellopts.gemspec
6
+ gemspec
data/README.md ADDED
@@ -0,0 +1,352 @@
1
+ # Shellopts
2
+
3
+ `ShellOpts` is a simple command line parsing libray that covers most modern use
4
+ cases incl. sub-commands. Options and commands are specified using a
5
+ getopt(1)-like string that is interpreted by the library to process the command
6
+ line
7
+
8
+ ## Usage
9
+
10
+ The following program accepts `-a` and `--all` that are aliases
11
+ for the same option, `--count` that may be given an integer argument but
12
+ defaults to 42, `--file` that has a mandatory argument, and `-v` and
13
+ `--verbose` that can be repeated to increase the verbosity level
14
+
15
+ ```ruby
16
+ require 'shellopts'
17
+
18
+ # Define options
19
+ USAGE = "a,all count=#? file= +v,verbose -- FILE..."
20
+
21
+ # Define default values
22
+ all = false
23
+ count = nil
24
+ file = nil
25
+ verbosity_level = 0
26
+
27
+ # Process command line and return remaining non-option arguments
28
+ args = ShellOpts.process(USAGE, ARGV) do |opt, arg|
29
+ case opt
30
+ when '-a', '--all'; all = true
31
+ when '--count'; count = arg || 42
32
+ when '--file'; file = arg # never nil
33
+ when '-v, '--verbose'; verbosity_level += 1
34
+ else
35
+ fail "Internal Error: Unmatched option: '#{opt}'"
36
+ end
37
+ end
38
+
39
+ # Process remaining arguments
40
+ args.each { |arg| ... }
41
+ ```
42
+
43
+ Note that the `else` clause catches legal but unhandled options; it is not an
44
+ user error. It typically happens because of a missing or misspelled option name
45
+ in the `when` clauses
46
+
47
+ If there is an error in the command line options, the program will exit with
48
+ status 1 and print an error message and a short usage description on standard
49
+ error
50
+
51
+ ## Processing
52
+
53
+ `ShellOpts.process` compiles a usage definition string into a grammar and use that to
54
+ parse the command line. If given a block, the block is called with a name/value
55
+ pair for each option or command and return a list of the remaining non-option
56
+ arguments
57
+
58
+ ```ruby
59
+ args = ShellOpts.process(USAGE, ARGV) do |opt, arg|
60
+ case opt
61
+ when ...
62
+ end
63
+ end
64
+ ```
65
+
66
+ This calls the block for each option in the same order as on the command line
67
+ and return the remaining non-option args. It also sets up the `ShellOpts.error` and
68
+ `ShellOpts.fail` methods. Please note that you need to call `ShellOpts.reset`
69
+ if you want to process another command line
70
+
71
+ If `ShellOpts.process` is called without a block it returns a
72
+ `ShellOpts::ShellOpts` object. It can be used to process more than one command
73
+ line at a time and to inspect the grammar and AST
74
+
75
+ ```ruby
76
+ shellopts = ShellOpts.process(USAGE, ARGV) # Returns a ShellOpts::ShellOpts object
77
+ shellopts.each { |opt, val| ... } # Access options
78
+ args = shellopts.args # Access remaining arguments
79
+ shellopts.error "Something went wrong" # Emit an error message and exit
80
+ ```
81
+
82
+ ## Usage string
83
+
84
+ A usage string, typically named `USAGE`, is a list of option and command
85
+ definitions separated by whitespace. It can span multiple lines. A double
86
+ dash (`--`) marks the end of the definition, anything after that is not
87
+ interpreted but copied verbatim in error messages
88
+
89
+ The general [syntax](https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form) is
90
+
91
+ ```EBNF
92
+ options { command options } [ "--" anything ]
93
+ ```
94
+
95
+ ## Options
96
+
97
+ An option is defined by a list of comma-separated names optionally prefixed by a
98
+ `+` and/or followed by a `=` and a set of flags. The syntax is
99
+
100
+ ```EBNF
101
+ [ "+" ] name-list [ "=" [ "#" | "$" ] [ label ] [ "?" ] ]
102
+ ```
103
+
104
+ #### Repeated options
105
+
106
+ Options are unique by default and the user will get an error if an option is
107
+ used more than once. You can tell the parser to allow several instances of the
108
+ same option by prefixing the option names with a `+`. A typical use case is to
109
+ let the user repeat a 'verbose' option to increase verbosity: `+v,verbose`
110
+ allows `-vvv` or `--verbose --verbose --verbose`. `ShellOpts::process` yields
111
+ an entry for each usage so should handle repeated options like this
112
+
113
+ ```ruby
114
+ verbosity_level = 0
115
+
116
+ args = ShellOpts.process(USAGE, ARGV) do |opt, arg|
117
+ case opt
118
+ when '-v', '--verbose'; verbosity_level += 1
119
+ # other options
120
+ end
121
+ end
122
+ ```
123
+
124
+ #### Option names
125
+
126
+ Option names are a comma-separated list of names. Names can consist of one or more
127
+ ASCII letters (a-zA-Z), digits, underscores ('\_') and dashes ('-'). A name
128
+ can't start with a dash, though
129
+
130
+ Names that are one character long are considered 'short options' and are
131
+ prefixed with a single dash on the command line (eg. '-a'). Names with two or
132
+ more characters are 'long options' and are used with two dashes (eg. '--all').
133
+ Note that short and long names handles arguments differently
134
+
135
+ Examples:
136
+ ```
137
+ a # -a
138
+ all # --all
139
+ a,all # -a or --all
140
+ r,R,recursive # -r, -R, or --recursive
141
+ ```
142
+
143
+ #### Option argument
144
+
145
+ An option that takes an an argument is declared with a `=` after the name list.
146
+ By default the type of an option is a `String` but a integer argument can be
147
+ specified by the `#` flag and a float argument by the `$` flag.
148
+
149
+ You can label a option value that will be used in help texts and error
150
+ messages. A usage string like `file=FILE` will be displayed as `--file=FILE`
151
+ and `file=FILE?` like `--file[=FILE]`. If no label is given, `INT` will be used
152
+ for integer arguments, `FLOAT` for floating point, and else `ARG`
153
+
154
+ Arguments are mandatory by default but can be made optional by suffixing a `?`
155
+
156
+ ## Commands
157
+
158
+ Sub-commands (like `git clone`) are defined by a name (or a dot-separated list
159
+ of names) followed by an exclamation mark. All options following a command are
160
+ local to that command. It is not possible to 'reset' this behaviour so global
161
+ options should always come before the first command. Nested commands are
162
+ specified using a dot-separated "path" to the nested sub-command
163
+
164
+ Examples
165
+ ```
166
+ g,global clone! t,template=
167
+ g,global clone! t,template= clone.list! v,verbose
168
+ ```
169
+
170
+ The last example could be called like `program -g clone list -v`. You may split
171
+ the usage string to improve readability:
172
+
173
+ ```
174
+ g,global
175
+ clone! t,template=
176
+ clone.list! v,verbose
177
+ ```
178
+
179
+ #### Command processing
180
+
181
+ Commands are treated like options but with a value that is an array of options (and
182
+ sub-commands) to the command:
183
+
184
+ ```ruby
185
+ USAGE = "a cmd! b c"
186
+
187
+ args = ShellOpts.process(USAGE, ARGV) { |opt,val|
188
+ case opt
189
+ when '-a'; # Handle -a
190
+ when 'cmd'
191
+ opt.each { |opt, val|
192
+ case opt
193
+ when '-b'; # Handle -b
194
+ when '-c'; # Handle -c
195
+ end
196
+ }
197
+ end
198
+ }
199
+ ```
200
+
201
+ ## Parsing
202
+
203
+ Parsing of the command line follows the UNIX traditions for short and long
204
+ options. Short options are one letter long and prefixed by a `-`. Short options
205
+ can be grouped so that `-abc` is the same as `-a -b -c`. Long options are
206
+ prefixed with a `--` and can't be grouped
207
+
208
+ Mandatory arguments to short options can be separated by a whitespace (`-f
209
+ /path/to/file`) but optional arguments needs to come immediately after the
210
+ option: `-f/path/to/file`. Long options also allow a space separator for
211
+ mandatory arguments but use `=` to separate the option from optional arguments:
212
+ `--file=/path/to/file`
213
+
214
+ Examples
215
+ ```
216
+ f= # -farg or -f arg
217
+ f=? # -farg
218
+
219
+ file= # --file=arg or --file arg
220
+ file=? # --file=arg
221
+ ```
222
+
223
+ #### Error handling
224
+
225
+ If the command line is invalid, it's a user error and the program exits with
226
+ status 1 and prints an error message on STDERR
227
+
228
+ If there is an error in the usage string, ShellOpts raises a
229
+ `ShellOpts::CompileError`. Note that this exception signals an error by the
230
+ application developer and shouldn't be catched. If there is an internal error
231
+ in the library, a ShellOpts::InternalError is raised and you should look for a
232
+ newer version of `ShellOpts` or file a bug-report
233
+
234
+ All ShellOpt exceptions derive from ShellOpt::Error
235
+
236
+ #### Error handling methods
237
+
238
+ ShellOpts provides two methods that can be used by the application to
239
+ generate error messages in the style of ShellOpts: `ShellOpts.error` and
240
+ `ShellOpts.fail`. Both write an error message on STDERR and terminates the
241
+ program with status 1.
242
+
243
+ `error` is intended to respond to user errors (like giving a file name that
244
+ doesn't exist) and prints a short usage summary to remind the user:
245
+
246
+ ```
247
+ <PROGRAM>: <MESSAGE>
248
+ Usage: <PROGRAM> <USAGE>
249
+ ```
250
+ The usage string is a prettyfied version of the usage definition given to
251
+ ShellOpts
252
+
253
+ `fail` is used to report that something is wrong with the assumptions about the
254
+ system (eg. disk full) and omits the usage summary
255
+ ```
256
+ <PROGRAM>: <MESSAGE>
257
+ ```
258
+
259
+ The methods are defined as instance methods on `ShellOpts::ShellOpts` and as
260
+ class methods on `ShellOpts`. The class methods stores program name and usage
261
+ string in global variables that are reset by `ShellOpts.reset`
262
+
263
+ ## Example
264
+
265
+ The rm(1) command could be implemented like this
266
+ ```ruby
267
+
268
+ require 'shellopts'
269
+
270
+ # Define options
271
+ USAGE = %{
272
+ f,force i I interactive=WHEN? r,R,recusive d,dir
273
+ one-file-system no-preserve-root preserve-root
274
+ v,verbose help version
275
+ }
276
+
277
+ # Define defaults
278
+ force = false
279
+ prompt = false
280
+ prompt_once = false
281
+ interactive = false
282
+ interactive_when = nil
283
+ recursive = false
284
+ remove_empty_dirs = false
285
+ one_file_system = false
286
+ preserve_root = true
287
+ verbose = false
288
+
289
+ # Process command line
290
+ args = ShellOpts.process(USAGE, ARGV) { |opt, val|
291
+ case opt
292
+ when '-f', '--force'; force = true
293
+ when '-i'; prompt = true
294
+ when '-I'; prompt_once = true
295
+ when '--interactive'; interactive = true; interactive_when = val
296
+ when '-r', '-R', '--recursive'; recursive = true
297
+ when '-d', '--dir'; remove_empty_dirs = true
298
+ when '--one-file-system'; one_file_system = true
299
+ when '--preserve-root'; preserve_root = true
300
+ when '--no-preserve-root'; preserve_root = false
301
+ when '--verbose'; verbose = true
302
+ when '--help'; print_help; exit
303
+ when '--version'; puts VERSION; exit
304
+ end
305
+ end
306
+
307
+ # Remaining arguments are files or directories
308
+ files = args
309
+ ```
310
+
311
+
312
+ ## See also
313
+
314
+ * [Command Line Options: How To Parse In Bash Using “getopt”](http://www.bahmanm.com/blogs/command-line-options-how-to-parse-in-bash-using-getopt)
315
+
316
+ ## Installation
317
+
318
+ To install in your gem repository:
319
+
320
+ ```
321
+ $ gem install shellopts
322
+ ```
323
+
324
+ To add it as a dependency for an executable add this line to your application's
325
+ `Gemfile`. Use exact version match as ShellOpts is still in development:
326
+
327
+ ```ruby
328
+ gem 'shellopts', 'x.y.z'
329
+ ```
330
+
331
+ If you're developing a library, you should add the dependency to the `*.gemfile` instead:
332
+
333
+ ```ruby
334
+ spec.add_dependency 'shellopts', 'x.y.z'
335
+ ```
336
+
337
+ ## Development
338
+
339
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
340
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
341
+ prompt that will allow you to experiment.
342
+
343
+ To install this gem onto your local machine, run `bundle exec rake install`. To
344
+ release a new version, update the version number in `version.rb`, and then run
345
+ `bundle exec rake release`, which will create a git tag for the version, push
346
+ git commits and tags, and push the `.gem` file to
347
+ [rubygems.org](https://rubygems.org).
348
+
349
+ ## Contributing
350
+
351
+ Bug reports and pull requests are welcome on GitHub at
352
+ https://github.com/[USERNAME]/shellopts.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/TODO ADDED
@@ -0,0 +1,97 @@
1
+
2
+ TODO
3
+ o Add a option flag for solitary options (--help)
4
+ o Make a #to_yaml
5
+ o Make an official dump method for debug
6
+ o Make a note that all options are processed at once and not as-you-go
7
+ o Test that arguments with spaces work
8
+ o Long version usage strings
9
+ o Doc: Example of processing of sub-commands and sub-sub-commands
10
+
11
+ + More tests
12
+ + More doc
13
+ + Implement value-name-before-flags rule
14
+ + Kill option array values
15
+ + Kill key forms
16
+ + Rename Option#opt to Option#name
17
+ + Have all Ast objects to be on [key, name, value] form
18
+ + Change #=>i, $=>f and introduce b (boolean)
19
+ + Unshift program name to usage definition string before compiling
20
+ + Rename to UsageCompiler and ArgvParser
21
+ + Make usage-string handle commands
22
+ + Change !cmd to cmd!
23
+ + Clean-up terminology: Option-name is used for names with and without the prefixed dashes
24
+ + Rename Option#has_argument? and #optional? to something else
25
+ + Fix location reporting of compiler errors
26
+ + Allow '--' in usage so that everything after can be used as USAGE in error messages
27
+ + Handle pretty-printing of usage string in handling of ParserError
28
+ + Compiler.new.compile(usage), Parser.new(compiled_program).parse(argv)
29
+ + Check for duplicate option in the parser
30
+ + Handle CompilerError
31
+ + Use nil value as the name of the top 'command'
32
+ + Refactor compilation to avoid having the Command objects throw CompilerErrors
33
+ + Change to 'parser.parse' / 'parser.parse3'
34
+ + Use first long option as symbolic key
35
+ + Use full option names everywhere (eg. '--all' instead of 'all')
36
+
37
+ - Revert change from '#' -> 'i'
38
+ - Guard against reserved 'object_id' name in OpenStruct
39
+ - Default value ('=' -> ':')
40
+ Default values are better handled in the calling program
41
+
42
+ ? More specialized exceptions: "MissingArgumentError" etc.
43
+
44
+ LATER
45
+ o Allow '-a' and '--aa' in usage
46
+ o Allow single-line comments
47
+ o Allow multi-line comments
48
+ o Regex as option value spec
49
+ o "FILE", "DIR", "NEWFILE", "NEWDIR" as keyword in option value spec
50
+ RFILE, RDIR
51
+ WFILE, WDIR
52
+ EFILE, EDIR
53
+ o Octal and hexadecimal integers
54
+ o Escape of separator in lists
55
+ o Handle output of subcommand usage like "cmd1 cmd1.cmd2 cmd2"
56
+ o Command-specific arguments: clone! o,opt ++ ARG1 ARG2...
57
+
58
+ ON TO_H BRANCH
59
+ ShellOpts.process(usage, argv) { |opt,val| ... } => args
60
+ ShellOpts.process(usage, argv) { |key,opt,val| ... } => args
61
+
62
+ opts = ShellOpts.new(usage, argv, defaults = {})
63
+ opts = ShellOpts.new(usage, argv, defaults = OpenStruct.new)
64
+
65
+ opts.args
66
+ opts.to_a
67
+ opts.to_h
68
+ opts.to_openstruct
69
+
70
+ opts.each { |opt,val| ... }
71
+ opts.each { |key,opt,val| ... }
72
+
73
+ LONG FORMAT
74
+
75
+ PROGRAM = File.basename(ARGV.first)
76
+ USAGE = "-a -f FILE -lvh FILE..."
77
+ DESCR = %(
78
+ Short description
79
+
80
+ Longer description
81
+ )
82
+ OPTIONS = %(
83
+ -a,--all
84
+ Process all files
85
+
86
+ -f, --file=FILE
87
+ Process file
88
+
89
+ !command
90
+ This is a command
91
+
92
+ --this-is-a-command-option
93
+ Options for commands are nested
94
+
95
+ ...
96
+ )
97
+
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "shellopts"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/mkdoc ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/bash
2
+
3
+ LINK='<link rel="stylesheet" type="text/css" href="stylesheet.css">'
4
+
5
+ {
6
+ echo $LINK
7
+ pandoc README.md
8
+ } >index.html
9
+
10
+ rdoc lib
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/ext/array.rb ADDED
@@ -0,0 +1,9 @@
1
+
2
+ module XArray
3
+ refine Array do
4
+ # Find and return first duplicate. Return nil if not found
5
+ def find_dup
6
+ detect { |e| rindex(e) != index(e) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ module ShellOpts
2
+ module Ast
3
+ class Command < Node
4
+ # Array of options (Ast::Option). Initially empty but filled out by the
5
+ # parser
6
+ attr_reader :options
7
+
8
+ # Optional sub-command (Ast::Command). Initially nil but assigned by the
9
+ # parser
10
+ attr_accessor :command
11
+
12
+ def initialize(grammar, name)
13
+ super(grammar, name)
14
+ @options = []
15
+ @command = nil
16
+ end
17
+
18
+ # Array of option or command tuples
19
+ def values
20
+ (options + (Array(command || []))).map { |node| node.to_tuple }
21
+ end
22
+
23
+ # :nocov:
24
+ def dump(&block)
25
+ super {
26
+ yield if block_given?
27
+ puts "options:"
28
+ indent { options.each { |opt| opt.dump } }
29
+ print "command:"
30
+ if command
31
+ puts
32
+ indent { command.dump }
33
+ else
34
+ puts "nil"
35
+ end
36
+ }
37
+ end
38
+ # :nocov:
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ module ShellOpts
2
+ module Ast
3
+ class Node
4
+ # The associated Grammar::Node object
5
+ attr_reader :grammar
6
+
7
+ # Key of node. Shorthand for grammar.key
8
+ def key() @grammar.key end
9
+
10
+ # Name of node (either program, command, or option name)
11
+ attr_reader :name
12
+
13
+ # Initialize an +Ast::Node+ object. +grammar+ is the corresponding
14
+ # grammar object (+Grammar::Node+) and +name+ is the name of the option
15
+ # or sub-command
16
+ def initialize(grammar, name)
17
+ @grammar, @name = grammar, name
18
+ end
19
+
20
+ # Return a name/value pair
21
+ def to_tuple
22
+ [name, values]
23
+ end
24
+
25
+ # Return either a value (option value), an array of values (command), or
26
+ # nil (option without a value). Should be defined in sub-classes
27
+ def values() raise end
28
+
29
+ # :nocov:
30
+ def dump(&block)
31
+ puts key.inspect
32
+ indent { yield } if block_given?
33
+ end
34
+ # :nocov:
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ module ShellOpts
2
+ module Ast
3
+ class Option < Node
4
+ # Optional value. Can be a String, Integer, or Float
5
+ attr_reader :value
6
+
7
+ def initialize(grammar, name, value)
8
+ super(grammar, name)
9
+ @value = value
10
+ end
11
+
12
+ def values() value end
13
+
14
+ # :nocov:
15
+ def dump
16
+ super { puts "values: #{values.inspect}" }
17
+ end
18
+ # :nocov:
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ module ShellOpts
2
+ module Ast
3
+ class Program < Command
4
+ # Command line arguments. Initially nil but assigned by the parser. This array
5
+ # is the same as the argument array returned by Ast.parse
6
+ attr_accessor :arguments
7
+
8
+ def initialize(grammar)
9
+ super(grammar, grammar.name)
10
+ @arguments = nil
11
+ end
12
+ end
13
+ end
14
+ end