shellopts 0.9.1

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