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 +7 -0
- data/.gitignore +28 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/README.md +352 -0
- data/Rakefile +6 -0
- data/TODO +97 -0
- data/bin/console +14 -0
- data/bin/mkdoc +10 -0
- data/bin/setup +8 -0
- data/lib/ext/array.rb +9 -0
- data/lib/shellopts/ast/command.rb +41 -0
- data/lib/shellopts/ast/node.rb +37 -0
- data/lib/shellopts/ast/option.rb +21 -0
- data/lib/shellopts/ast/program.rb +14 -0
- data/lib/shellopts/compiler.rb +130 -0
- data/lib/shellopts/grammar/command.rb +64 -0
- data/lib/shellopts/grammar/node.rb +25 -0
- data/lib/shellopts/grammar/option.rb +55 -0
- data/lib/shellopts/grammar/program.rb +65 -0
- data/lib/shellopts/parser.rb +106 -0
- data/lib/shellopts/version.rb +3 -0
- data/lib/shellopts.rb +195 -0
- data/shellopts.gemspec +40 -0
- metadata +144 -0
@@ -0,0 +1,130 @@
|
|
1
|
+
require "ext/array.rb"
|
2
|
+
|
3
|
+
require 'shellopts/grammar/node.rb'
|
4
|
+
require 'shellopts/grammar/option.rb'
|
5
|
+
require 'shellopts/grammar/command.rb'
|
6
|
+
require 'shellopts/grammar/program.rb'
|
7
|
+
|
8
|
+
module ShellOpts
|
9
|
+
module Grammar
|
10
|
+
# Compiles an option definition string and returns a Grammar::Program
|
11
|
+
# object. program_name is the name of the program and source is the
|
12
|
+
# option definition string
|
13
|
+
def self.compile(program_name, source)
|
14
|
+
program_name.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{program_name.class}"
|
15
|
+
source.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{source.class}"
|
16
|
+
Compiler.new(program_name, source).call
|
17
|
+
end
|
18
|
+
|
19
|
+
# Service object for compiling an option definition string. Returns a
|
20
|
+
# Grammar::Program object
|
21
|
+
#
|
22
|
+
# Compiler implements a recursive descend algorithm to compile the option
|
23
|
+
# string. The algorithm uses state variables and is embedded in a
|
24
|
+
# Grammar::Compiler service object
|
25
|
+
class Compiler
|
26
|
+
class Error < RuntimeError; end
|
27
|
+
|
28
|
+
# Initialize a Compiler object. source is the option definition string
|
29
|
+
def initialize(program_name, source)
|
30
|
+
@program_name, @tokens = program_name, source.split(/\s+/)
|
31
|
+
|
32
|
+
# @commands_by_path is an hash from command-path to Command or Program
|
33
|
+
# object. The top level Program object has nil as its path.
|
34
|
+
# @commands_by_path is used to check for uniqueness of commands and to
|
35
|
+
# link sub-commands to their parents
|
36
|
+
@commands_by_path = {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def call
|
40
|
+
compile_program
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
using XArray # For Array#find_dup
|
45
|
+
|
46
|
+
# Returns the current token
|
47
|
+
def curr_token() @tokens.first end
|
48
|
+
|
49
|
+
# Returns the current token and advance to the next token
|
50
|
+
def next_token() @tokens.shift end
|
51
|
+
|
52
|
+
def error(msg) # Just a shorthand. Unrelated to ShellOpts.error
|
53
|
+
raise Compiler::Error.new(msg)
|
54
|
+
end
|
55
|
+
|
56
|
+
def compile_program
|
57
|
+
program = @commands_by_path[nil] = Grammar::Program.new(@program_name, compile_options)
|
58
|
+
while curr_token && curr_token != "--"
|
59
|
+
compile_command
|
60
|
+
end
|
61
|
+
program.args.concat(@tokens[1..-1]) if curr_token
|
62
|
+
program
|
63
|
+
end
|
64
|
+
|
65
|
+
def compile_command
|
66
|
+
path = curr_token[0..-2]
|
67
|
+
ident_list = compile_ident_list(path, ".")
|
68
|
+
parent_path = ident_list.size > 1 ? ident_list[0..-2].join(".") : nil
|
69
|
+
name = ident_list[-1]
|
70
|
+
|
71
|
+
parent = @commands_by_path[parent_path] or
|
72
|
+
error "No such command: #{parent_path.inspect}"
|
73
|
+
!@commands_by_path.key?(path) or error "Duplicate command: #{path.inspect}"
|
74
|
+
next_token
|
75
|
+
@commands_by_path[path] = Grammar::Command.new(parent, name, compile_options)
|
76
|
+
end
|
77
|
+
|
78
|
+
def compile_options
|
79
|
+
option_list = []
|
80
|
+
while curr_token && curr_token != "--" && !curr_token.end_with?("!")
|
81
|
+
option_list << compile_option
|
82
|
+
end
|
83
|
+
dup = option_list.map(&:names).flatten.find_dup and
|
84
|
+
error "Duplicate option name: #{dup.inspect}"
|
85
|
+
option_list
|
86
|
+
end
|
87
|
+
|
88
|
+
def compile_option
|
89
|
+
# Match string and build flags
|
90
|
+
flags = []
|
91
|
+
curr_token =~ /^(\+)?(.+?)(?:(=)(\$|\#)?(.*?)(\?)?)?$/
|
92
|
+
flags << :repeated if $1 == "+"
|
93
|
+
names = $2
|
94
|
+
flags << :argument if $3 == "="
|
95
|
+
flags << :integer if $4 == "#"
|
96
|
+
flags << :float if $4 == "$"
|
97
|
+
label = $5 == "" ? nil : $5
|
98
|
+
flags << :optional if $6 == "?"
|
99
|
+
|
100
|
+
# Build names
|
101
|
+
short_names = []
|
102
|
+
long_names = []
|
103
|
+
ident_list = compile_ident_list(names, ",")
|
104
|
+
(dup = ident_list.find_dup).nil? or
|
105
|
+
error "Duplicate identifier #{dup.inspect} in #{curr_token.inspect}"
|
106
|
+
ident_list.each { |ident|
|
107
|
+
if ident.size == 1
|
108
|
+
short_names << "-#{ident}"
|
109
|
+
else
|
110
|
+
long_names << "--#{ident}"
|
111
|
+
end
|
112
|
+
}
|
113
|
+
|
114
|
+
next_token
|
115
|
+
Grammar::Option.new(short_names, long_names, flags, label)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Compile list of option names or a command path
|
119
|
+
def compile_ident_list(ident_list_str, sep)
|
120
|
+
ident_list_str.split(sep, -1).map { |str|
|
121
|
+
!str.empty? or error "Empty identifier in #{curr_token.inspect}"
|
122
|
+
!str.start_with?("-") or error "Identifier can't start with '-' in #{curr_token.inspect}"
|
123
|
+
str !~ /([^\w\d#{sep}-])/ or
|
124
|
+
error "Illegal character #{$1.inspect} in #{curr_token.inspect}"
|
125
|
+
str
|
126
|
+
}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ShellOpts
|
2
|
+
module Grammar
|
3
|
+
# A command. Commands are organized hierarchically with a Program object as
|
4
|
+
# the root node
|
5
|
+
#
|
6
|
+
# Sets Node#key to the name of the command incl. the exclamation point
|
7
|
+
class Command < Node
|
8
|
+
# Parent command. Nil if this is the top level command (the program)
|
9
|
+
attr_reader :parent
|
10
|
+
|
11
|
+
# Name of command (String). Name doesn't include the exclamation point ('!')
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
# Hash from option names (both short and long names) to option. This
|
15
|
+
# means an option can occur more than once as the hash value
|
16
|
+
attr_reader :options
|
17
|
+
|
18
|
+
# Sub-commands of this command. Is a hash from sub-command name to command object
|
19
|
+
attr_reader :commands
|
20
|
+
|
21
|
+
# List of options in declaration order
|
22
|
+
# order
|
23
|
+
attr_reader :option_list
|
24
|
+
|
25
|
+
# List of commands in declaration order
|
26
|
+
attr_reader :command_list
|
27
|
+
|
28
|
+
# Initialize a Command object. parent is the parent Command object or nil
|
29
|
+
# if this is the root object. name is the name of the command (without
|
30
|
+
# the exclamation mark), and option_list a list of Option objects
|
31
|
+
def initialize(parent, name, option_list)
|
32
|
+
super("#{name}!".to_sym)
|
33
|
+
@name = name
|
34
|
+
parent.attach(self) if parent
|
35
|
+
@option_list = option_list
|
36
|
+
@options = @option_list.flat_map { |opt| opt.names.map { |name| [name, opt] } }.to_h
|
37
|
+
@commands = {}
|
38
|
+
@command_list = []
|
39
|
+
end
|
40
|
+
|
41
|
+
# :nocov:
|
42
|
+
def dump(&block)
|
43
|
+
puts "#{key.inspect}"
|
44
|
+
indent {
|
45
|
+
puts "parent: #{parent&.key.inspect}"
|
46
|
+
puts "name: #{name.inspect}"
|
47
|
+
yield if block_given?
|
48
|
+
puts "options:"
|
49
|
+
indent { option_list.each { |opt| opt.dump } }
|
50
|
+
puts "commands: "
|
51
|
+
indent { command_list.each { |cmd| cmd.dump } }
|
52
|
+
}
|
53
|
+
end
|
54
|
+
# :nocov:
|
55
|
+
|
56
|
+
protected
|
57
|
+
def attach(command)
|
58
|
+
command.instance_variable_set(:@parent, self)
|
59
|
+
@commands[command.name] = command
|
60
|
+
@command_list << command
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ShellOpts
|
2
|
+
module Grammar
|
3
|
+
# Root class for Grammar objects
|
4
|
+
#
|
5
|
+
# Node objects are created by ShellOpts::Grammar.compile that returns a
|
6
|
+
# Program object that in turn contains other node objects in a hierarchical
|
7
|
+
# structure that reflects the grammar of the program. Only
|
8
|
+
# ShellOpts::Grammar.compile should create node objects
|
9
|
+
class Node
|
10
|
+
# Key (Symbol) of node. Unique within the enclosing command
|
11
|
+
attr_reader :key
|
12
|
+
|
13
|
+
def initialize(key)
|
14
|
+
@key = key
|
15
|
+
end
|
16
|
+
|
17
|
+
# :nocov:
|
18
|
+
def dump(&block)
|
19
|
+
puts key.inspect
|
20
|
+
indent { yield } if block_given?
|
21
|
+
end
|
22
|
+
# :nocov:
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module ShellOpts
|
2
|
+
module Grammar
|
3
|
+
# Models an Option
|
4
|
+
#
|
5
|
+
# Sets Node#key to the first long option name if present or else the first short option
|
6
|
+
class Option < Node
|
7
|
+
# List of short names (incl. '-')
|
8
|
+
attr_reader :short_names
|
9
|
+
|
10
|
+
# List of long names (incl. '--')
|
11
|
+
attr_reader :long_names
|
12
|
+
|
13
|
+
# List of flags (Symbol)
|
14
|
+
def flags() @flags.keys end
|
15
|
+
|
16
|
+
# Informal name of argument (eg. 'FILE'). nil if not present
|
17
|
+
attr_reader :label
|
18
|
+
|
19
|
+
# Initialize an option. Short and long names are arrays of the short/long
|
20
|
+
# option names (incl. the '-'/'--' prefix). It is assumed that at least
|
21
|
+
# one name is given. Flags is a list of symbolic flags. Allowed flags are
|
22
|
+
# :repeated, :argument, :optional, :integer, and :float. Note that
|
23
|
+
# there's no :string flag, it's status is inferred. label is the optional
|
24
|
+
# informal name of the option argument (eg. 'FILE') or nil if not present
|
25
|
+
def initialize(short_names, long_names, flags, label = nil)
|
26
|
+
super((long_names.first || short_names.first).sub(/^-+/, "").to_sym)
|
27
|
+
@short_names, @long_names = short_names, long_names
|
28
|
+
@flags = flags.map { |flag| [flag, true] }.to_h
|
29
|
+
@label = label
|
30
|
+
end
|
31
|
+
|
32
|
+
# Array of option names with short names first and then the long names
|
33
|
+
def names() @short_names + @long_names end
|
34
|
+
|
35
|
+
# Flag query methods. Returns true if the flag is present and otherwise nil
|
36
|
+
def repeated?() @flags[:repeated] || false end
|
37
|
+
def argument?() @flags[:argument] || false end
|
38
|
+
def optional?() argument? && @flags[:optional] || false end
|
39
|
+
def string?() argument? && !integer? && !float? end
|
40
|
+
def integer?() argument? && @flags[:integer] || false end
|
41
|
+
def float?() argument? && @flags[:float] || false end
|
42
|
+
|
43
|
+
# :nocov:
|
44
|
+
def dump
|
45
|
+
super {
|
46
|
+
puts "short_names: #{short_names.inspect}"
|
47
|
+
puts "long_names: #{long_names.inspect}"
|
48
|
+
puts "flags: #{flags.inspect}"
|
49
|
+
puts "label: #{label.inspect}"
|
50
|
+
}
|
51
|
+
end
|
52
|
+
# :nocov:
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module ShellOpts
|
2
|
+
module Grammar
|
3
|
+
# Program is the root object of the grammar
|
4
|
+
class Program < Command
|
5
|
+
# Array of non-option litteral arguments (ie. what comes after the double dash ('+--+') in
|
6
|
+
# the usage definition). Initially empty but filled out during compilation
|
7
|
+
attr_reader :args
|
8
|
+
|
9
|
+
# Initialize a top-level Program object
|
10
|
+
def initialize(name, option_list)
|
11
|
+
super(nil, name, option_list)
|
12
|
+
@args = []
|
13
|
+
end
|
14
|
+
|
15
|
+
# Usage string to be used in error messages. The string is kept short by
|
16
|
+
# only listing the shortest option (if there is more than one)
|
17
|
+
def usage
|
18
|
+
(
|
19
|
+
render_options(option_list) +
|
20
|
+
commands.values.map { |cmd| render_command(cmd) } +
|
21
|
+
args
|
22
|
+
).flatten.join(" ")
|
23
|
+
end
|
24
|
+
|
25
|
+
# :nocov:
|
26
|
+
def dump(&block)
|
27
|
+
super {
|
28
|
+
puts "args: #{args.inspect}"
|
29
|
+
puts "usage: #{usage.inspect}"
|
30
|
+
}
|
31
|
+
end
|
32
|
+
# :nocov:
|
33
|
+
|
34
|
+
private
|
35
|
+
def render_command(command)
|
36
|
+
[command.name] + render_options(command.option_list) +
|
37
|
+
command.commands.values.map { |cmd| render_command(cmd) }.flatten
|
38
|
+
end
|
39
|
+
|
40
|
+
def render_options(options)
|
41
|
+
options.map { |opt|
|
42
|
+
s = opt.names.first
|
43
|
+
if opt.argument?
|
44
|
+
arg_string =
|
45
|
+
if opt.label
|
46
|
+
opt.label
|
47
|
+
elsif opt.integer?
|
48
|
+
"INT"
|
49
|
+
elsif opt.float?
|
50
|
+
"FLOAT"
|
51
|
+
else
|
52
|
+
"ARG"
|
53
|
+
end
|
54
|
+
if opt.optional?
|
55
|
+
s += "[=#{arg_string}]"
|
56
|
+
else
|
57
|
+
s += "=#{arg_string}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
s
|
61
|
+
}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
|
2
|
+
require 'shellopts/ast/node.rb'
|
3
|
+
require 'shellopts/ast/option.rb'
|
4
|
+
require 'shellopts/ast/command.rb'
|
5
|
+
require 'shellopts/ast/program.rb'
|
6
|
+
|
7
|
+
module ShellOpts
|
8
|
+
module Ast
|
9
|
+
# Parse ARGV according to grammar. Returns a Ast::Program object
|
10
|
+
def self.parse(grammar, argv)
|
11
|
+
grammar.is_a?(Grammar::Program) or
|
12
|
+
raise InternalError, "Expected Grammar::Program object, got #{grammar.class}"
|
13
|
+
argv.is_a?(Array) or
|
14
|
+
raise InternalError, "Expected Array object, got #{argv.class}"
|
15
|
+
Parser.new(grammar, argv).call
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
# Parse a command line
|
20
|
+
class Parser
|
21
|
+
class Error < RuntimeError; end
|
22
|
+
|
23
|
+
def initialize(grammar, argv)
|
24
|
+
@grammar, @argv = grammar, argv.dup
|
25
|
+
@seen_options = {} # Used to keep track of repeated options
|
26
|
+
end
|
27
|
+
|
28
|
+
def call
|
29
|
+
program = Ast::Program.new(@grammar)
|
30
|
+
parse_command(program)
|
31
|
+
program.arguments = @argv
|
32
|
+
program
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def parse_command(command)
|
37
|
+
@seen_options = {} # Every new command resets the seen options
|
38
|
+
while arg = @argv.first
|
39
|
+
if arg == "--"
|
40
|
+
@argv.shift
|
41
|
+
break
|
42
|
+
elsif arg.start_with?("-")
|
43
|
+
parse_option(command)
|
44
|
+
elsif cmd = command.grammar.commands[arg]
|
45
|
+
@argv.shift
|
46
|
+
command.command = Ast::Command.new(cmd, arg)
|
47
|
+
parse_command(command.command)
|
48
|
+
break
|
49
|
+
else
|
50
|
+
break
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_option(command)
|
56
|
+
# Split into name and argument
|
57
|
+
case @argv.first
|
58
|
+
when /^(--.+?)(?:=(.*))?$/
|
59
|
+
name, arg, short = $1, $2, false
|
60
|
+
when /^(-.)(.+)?$/
|
61
|
+
name, arg, short = $1, $2, true
|
62
|
+
end
|
63
|
+
@argv.shift
|
64
|
+
|
65
|
+
option = command.grammar.options[name] or raise Error, "Unknown option '#{name}'"
|
66
|
+
!@seen_options.key?(option.key) || option.repeated? or raise Error, "Duplicate option '#{name}'"
|
67
|
+
@seen_options[option.key] = true
|
68
|
+
|
69
|
+
# Parse (optional) argument
|
70
|
+
if option.argument?
|
71
|
+
if arg.nil? && !option.optional?
|
72
|
+
if !@argv.empty?
|
73
|
+
arg = @argv.shift
|
74
|
+
else
|
75
|
+
raise Error, "Missing argument for option '#{name}'"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
arg &&= parse_arg(option, name, arg)
|
79
|
+
elsif arg && short
|
80
|
+
@argv.unshift("-#{arg}")
|
81
|
+
arg = nil
|
82
|
+
elsif !arg.nil?
|
83
|
+
raise Error, "No argument allowed for option '#{name}'"
|
84
|
+
end
|
85
|
+
|
86
|
+
command.options << Ast::Option.new(option, name, arg)
|
87
|
+
end
|
88
|
+
|
89
|
+
def parse_arg(option, name, arg)
|
90
|
+
if option.string?
|
91
|
+
arg
|
92
|
+
elsif arg == ""
|
93
|
+
nil
|
94
|
+
elsif option.integer?
|
95
|
+
arg =~ /^-?\d+$/ or raise Error, "Illegal integer in '#{name}' argument: '#{arg}'"
|
96
|
+
arg.to_i
|
97
|
+
else # option.float?
|
98
|
+
# https://stackoverflow.com/a/21891705/2130986
|
99
|
+
arg =~ /^[+-]?(?:0|[1-9]\d*)(?:\.(?:\d*[1-9]|0))?$/ or
|
100
|
+
raise Error, "Illegal float in '#{name}' argument: '#{arg}'"
|
101
|
+
arg.to_f
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/shellopts.rb
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
require "shellopts/version"
|
2
|
+
|
3
|
+
require 'shellopts/compiler.rb'
|
4
|
+
require 'shellopts/parser.rb'
|
5
|
+
|
6
|
+
# ShellOpts is a library for parsing command line options and sub-commands. The
|
7
|
+
# library API consists of the methods {ShellOpts.process}, {ShellOpts.error},
|
8
|
+
# and {ShellOpts.fail} and the result class {ShellOpts::ShellOpts}
|
9
|
+
#
|
10
|
+
module ShellOpts
|
11
|
+
# Process command line options and arguments. #process takes a usage string
|
12
|
+
# defining the options and the array of command line arguments to be parsed
|
13
|
+
# as arguments
|
14
|
+
#
|
15
|
+
# If called with a block, the block is called with name and value of each
|
16
|
+
# option or command and #process returns a list of remaining command line
|
17
|
+
# arguments. If called without a block a ShellOpts::ShellOpts object is
|
18
|
+
# returned
|
19
|
+
#
|
20
|
+
# The value of an option is its argument, the value of a command is an array
|
21
|
+
# of name/value pairs of options and subcommands. Option values are converted
|
22
|
+
# to the target type (String, Integer, Float) if specified
|
23
|
+
#
|
24
|
+
# Example
|
25
|
+
#
|
26
|
+
# # Define options
|
27
|
+
# USAGE = 'a,all g,global +v,verbose h,help save! snapshot f,file=FILE h,help'
|
28
|
+
#
|
29
|
+
# # Define defaults
|
30
|
+
# all = false
|
31
|
+
# global = false
|
32
|
+
# verbose = 0
|
33
|
+
# save = false
|
34
|
+
# snapshot = false
|
35
|
+
# file = nil
|
36
|
+
#
|
37
|
+
# # Process options
|
38
|
+
# argv = ShellOpts.process(USAGE, ARGV) do |name, value|
|
39
|
+
# case name
|
40
|
+
# when '-a', '--all'; all = true
|
41
|
+
# when '-g', '--global'; global = value
|
42
|
+
# when '-v', '--verbose'; verbose += 1
|
43
|
+
# when '-h', '--help'; print_help(); exit(0)
|
44
|
+
# when 'save'
|
45
|
+
# save = true
|
46
|
+
# value.each do |name, value|
|
47
|
+
# case name
|
48
|
+
# when '--snapshot'; snapshot = true
|
49
|
+
# when '-f', '--file'; file = value
|
50
|
+
# when '-h', '--help'; print_save_help(); exit(0)
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
# else
|
54
|
+
# raise "Not a user error. The developer forgot or misspelled an option"
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# # Process remaining arguments
|
59
|
+
# argv.each { |arg| ... }
|
60
|
+
#
|
61
|
+
# If an error is encountered while compiling the usage string, a
|
62
|
+
# +ShellOpts::Compiler+ exception is raised. If the error happens while
|
63
|
+
# parsing the command line arguments, the program prints an error message and
|
64
|
+
# exits with status 1. Failed assertions raise a +ShellOpts::InternalError+
|
65
|
+
# exception
|
66
|
+
#
|
67
|
+
# Note that you can't process more than one command line at a time because
|
68
|
+
# #process saves a hidden {ShellOpts::ShellOpts} class variable used by the
|
69
|
+
# class methods #error and #fail. Call #reset to clear the global object if
|
70
|
+
# you really need to parse more than one command line. Alternatively you can
|
71
|
+
# create +ShellOpts::ShellOpts+ objects yourself and use the object methods
|
72
|
+
# #error and #fail instead:
|
73
|
+
#
|
74
|
+
# shellopts = ShellOpts::ShellOpts.new(USAGE, ARGS)
|
75
|
+
# shellopts.each { |name, value| ... }
|
76
|
+
# shellopts.args.each { |arg| ... }
|
77
|
+
# shellopts.error("Something went wrong")
|
78
|
+
#
|
79
|
+
def self.process(usage, argv, program_name: File.basename($0), &block)
|
80
|
+
if !block_given?
|
81
|
+
ShellOpts.new(usage, argv, program_name: program_name)
|
82
|
+
else
|
83
|
+
@shellopts.nil? or raise InternalError, "ShellOpts class variable already initialized"
|
84
|
+
@shellopts = ShellOpts.new(usage, argv, program_name: program_name)
|
85
|
+
@shellopts.each(&block)
|
86
|
+
@shellopts.args
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Reset the hidden +ShellOpts::ShellOpts+ class variable so that you can process
|
91
|
+
# another command line
|
92
|
+
def self.reset()
|
93
|
+
@shellopts = nil
|
94
|
+
end
|
95
|
+
|
96
|
+
# Print error message and usage string and exit with status 1. Can only be
|
97
|
+
# called after #process. Forwards to {::ShellOpts#error}
|
98
|
+
def self.error(*msgs)
|
99
|
+
@shellopts&.error(*msgs) or raise InternalError, "ShellOpts class variable not initialized"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Print error message and exit with status 1. Forwards to {::ShellOpts#fail}
|
103
|
+
def self.fail(*msgs)
|
104
|
+
@shellopts&.fail(*msgs) or raise InternalError, "ShellOpts class variable not initialized"
|
105
|
+
end
|
106
|
+
|
107
|
+
# The compilation object
|
108
|
+
class ShellOpts
|
109
|
+
# Name of program
|
110
|
+
attr_reader :program_name
|
111
|
+
|
112
|
+
# Usage string. Shorthand for +grammar.usage+
|
113
|
+
def usage() @grammar.usage end
|
114
|
+
|
115
|
+
# The grammar compiled from the usage string. If #ast is defined, it's
|
116
|
+
# equal to ast.grammar
|
117
|
+
attr_reader :grammar
|
118
|
+
|
119
|
+
# The AST resulting from parsing the command line arguments
|
120
|
+
attr_reader :ast
|
121
|
+
|
122
|
+
# List of remaining non-option command line arguments. Shorthand for ast.arguments
|
123
|
+
def args() @ast.arguments end
|
124
|
+
|
125
|
+
# Compile a usage string into a grammar and use that to parse command line
|
126
|
+
# arguments
|
127
|
+
#
|
128
|
+
# +usage+ is the usage string, and +argv+ the command line (typically the
|
129
|
+
# global ARGV array). +program_name+ is the name of the program and is
|
130
|
+
# used in error messages. It defaults to the basename of the program
|
131
|
+
#
|
132
|
+
# Errors in the usage string raise a CompilerError exception. Errors in the
|
133
|
+
# argv arguments terminates the program with an error message
|
134
|
+
def initialize(usage, argv, program_name: File.basename($0))
|
135
|
+
@program_name = program_name
|
136
|
+
begin
|
137
|
+
@grammar = Grammar.compile(program_name, usage)
|
138
|
+
@ast = Ast.parse(@grammar, argv)
|
139
|
+
rescue Grammar::Compiler::Error => ex
|
140
|
+
raise CompilerError.new(5, ex.message)
|
141
|
+
rescue Ast::Parser::Error => ex
|
142
|
+
error(ex.message)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Unroll the AST into a nested array
|
147
|
+
def to_a
|
148
|
+
@ast.values
|
149
|
+
end
|
150
|
+
|
151
|
+
# Iterate the result as name/value pairs. See {ShellOpts.process} for a
|
152
|
+
# detailed description
|
153
|
+
def each(&block)
|
154
|
+
if block_given?
|
155
|
+
to_a.each { |*args| yield(*args) }
|
156
|
+
else
|
157
|
+
to_a # FIXME: Iterator
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Print error message and usage string and exit with status 1. This method
|
162
|
+
# should be called in response to user-errors (eg. specifying an illegal
|
163
|
+
# option)
|
164
|
+
def error(*msgs)
|
165
|
+
$stderr.puts "#{program_name}: #{msgs.join}"
|
166
|
+
$stderr.puts "Usage: #{program_name} #{usage}"
|
167
|
+
exit 1
|
168
|
+
end
|
169
|
+
|
170
|
+
# Print error message and exit with status 1. This method should not be
|
171
|
+
# called in response to user-errors but system errors (like disk full)
|
172
|
+
def fail(*msgs)
|
173
|
+
$stderr.puts "#{program_name}: #{msgs.join}"
|
174
|
+
exit 1
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Base class for ShellOpts exceptions
|
179
|
+
class Error < RuntimeError; end
|
180
|
+
|
181
|
+
# Raised when an error is detected in the usage string
|
182
|
+
class CompilerError < Error
|
183
|
+
def initialize(start, message)
|
184
|
+
super(message)
|
185
|
+
set_backtrace(caller(start))
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Raised when an internal error is detected
|
190
|
+
class InternalError < Error; end
|
191
|
+
|
192
|
+
private
|
193
|
+
@shellopts = nil
|
194
|
+
end
|
195
|
+
|
data/shellopts.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "shellopts/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "shellopts"
|
8
|
+
spec.version = Shellopts::VERSION
|
9
|
+
spec.authors = ["Claus Rasmussen"]
|
10
|
+
spec.email = ["claus.l.rasmussen@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Parse command line options and arguments}
|
13
|
+
spec.description = %q{ShellOpts is a simple command line parsing libray
|
14
|
+
that covers most modern use cases incl. sub-commands.
|
15
|
+
Options and commands are specified using a
|
16
|
+
getopt(1)-like string that is interpreted by the
|
17
|
+
library to process the command line}
|
18
|
+
spec.homepage = "http://github.com/clrgit/shellopts"
|
19
|
+
|
20
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
21
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
22
|
+
if spec.respond_to?(:metadata)
|
23
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
24
|
+
else
|
25
|
+
raise "RubyGems 2.0+ is required to protect against public gem pushes"
|
26
|
+
end
|
27
|
+
|
28
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
29
|
+
f.match(%r{^(test|spec|features)/})
|
30
|
+
end
|
31
|
+
spec.bindir = "exe"
|
32
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
33
|
+
spec.require_paths = ["lib"]
|
34
|
+
|
35
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
36
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
37
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
38
|
+
spec.add_development_dependency "indented_io"
|
39
|
+
spec.add_development_dependency "simplecov"
|
40
|
+
end
|