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