shellopts 1.0.0 → 2.0.0.pre.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/README.md +10 -0
- data/TODO +23 -1
- data/lib/shellopts.rb +209 -199
- data/lib/shellopts/args.rb +48 -0
- data/lib/shellopts/ast/command.rb +6 -6
- data/lib/shellopts/ast/node.rb +1 -1
- data/lib/shellopts/compiler.rb +25 -27
- data/lib/shellopts/generator.rb +15 -0
- data/lib/shellopts/grammar/command.rb +35 -18
- data/lib/shellopts/grammar/node.rb +11 -3
- data/lib/shellopts/grammar/option.rb +12 -1
- data/lib/shellopts/grammar/program.rb +4 -4
- data/lib/shellopts/idr.rb +236 -0
- data/lib/shellopts/option_struct.rb +148 -0
- data/lib/shellopts/parser.rb +11 -11
- data/lib/shellopts/shellopts.rb +116 -0
- data/lib/shellopts/version.rb +1 -1
- metadata +9 -5
- data/lib/shellopts/utils.rb +0 -16
@@ -0,0 +1,148 @@
|
|
1
|
+
|
2
|
+
require 'shellopts/shellopts.rb'
|
3
|
+
require 'shellopts/idr'
|
4
|
+
|
5
|
+
module ShellOpts
|
6
|
+
# FIXME: Outdated
|
7
|
+
#
|
8
|
+
# Struct representation of options. Usually created by ShellOpts::to_struct
|
9
|
+
#
|
10
|
+
# OptionStruct objects give easy access to configuration option values but
|
11
|
+
# meta data are more circuitously accessed through class methods with an
|
12
|
+
# explicit instance argument
|
13
|
+
#
|
14
|
+
# Option values are accessed through a member methods named after the key of
|
15
|
+
# the option. Repeated options have an Array value with one element (possibly
|
16
|
+
# nil) for each use of the option. A query method with a '?' suffixed to the
|
17
|
+
# name returns true or false depending on whether the option was used or not
|
18
|
+
#
|
19
|
+
# option - Value of option. Either an object or an Array if the option can
|
20
|
+
# be repeated
|
21
|
+
# option? - True iff option was given
|
22
|
+
#
|
23
|
+
# Command methods return a nested OptionStruct object while the special
|
24
|
+
# #command method returns the key of actual command (if any). Use
|
25
|
+
# +strukt.send(strukt.command)+ to get the subcommand of a OptionStruct. It
|
26
|
+
# is possible to rename #command method to avoid name collisions
|
27
|
+
#
|
28
|
+
# name! - Command. An OptionStruct or nil if not given on the command line
|
29
|
+
# subcommand - Key of command. Can be renamed
|
30
|
+
#
|
31
|
+
# ---------------------------------
|
32
|
+
# name! - Command. An OptionStruct or nil if not given on the command line
|
33
|
+
#
|
34
|
+
# key! - Key of command
|
35
|
+
# value! - Value of command (a subcommand). Can be renamed
|
36
|
+
#
|
37
|
+
# Note: There is no command query method because option and command names
|
38
|
+
# live in seperate namespaces and could cause colllisions. Check +name!+ for
|
39
|
+
# nil to detect if a command was given
|
40
|
+
#
|
41
|
+
# Meta data are extracted through class methods to avoid polluting the object
|
42
|
+
# namespace. OptionStruct use an OptionsHash object internally and
|
43
|
+
# implements a subset of its meta methods by forwarding to it. The
|
44
|
+
# OptionsHash object can be accessed through the #options_hash method
|
45
|
+
#
|
46
|
+
# Note that #command is defined as both an instance method and a class
|
47
|
+
# method. Use the class method to make the code work with all OptionStruct
|
48
|
+
# objects even if #command has been renamed
|
49
|
+
#
|
50
|
+
# +ShellOpts+ is derived from +BascicObject+ that reserves some words for
|
51
|
+
# internal use (+__id__+, +__send__+, +instance_eval+, +instance_exec+,
|
52
|
+
# +method_missing+, +singleton_method_added+, +singleton_method_removed+,
|
53
|
+
# +singleton_method_undefined+). ShellOpts also define two reserved words of
|
54
|
+
# its own (+__options_hash__+ and +__command__+). ShellOpts raise an
|
55
|
+
# ShellOpts::ConversionError if an option collides with one of the
|
56
|
+
# reserved words or with the special #command method
|
57
|
+
#
|
58
|
+
class OptionStruct < BasicObject
|
59
|
+
# Create a OptionStruct object recursively from an Idr::Command object
|
60
|
+
def self.new(idr, key_type, aliases = {})
|
61
|
+
# Shorthands
|
62
|
+
ast = idr.instance_variable_get("@ast")
|
63
|
+
grammar = ast.grammar
|
64
|
+
|
65
|
+
# Get key map
|
66
|
+
keys = idr.send(:map_keys, key_type, aliases, RESERVED_WORDS)
|
67
|
+
|
68
|
+
# Allocate OptionStruct instance
|
69
|
+
instance = allocate
|
70
|
+
|
71
|
+
# Set reference to Idr object. Is currently unused
|
72
|
+
set_variable(instance, "@__idr__", idr)
|
73
|
+
|
74
|
+
# Generate general option accessor methods
|
75
|
+
grammar.option_list.each { |option|
|
76
|
+
key = keys[option.key]
|
77
|
+
instance.instance_eval("def #{key}() @#{key} end")
|
78
|
+
instance.instance_eval("def #{key}?() false end")
|
79
|
+
}
|
80
|
+
|
81
|
+
# Generate accessor method for present options
|
82
|
+
idr.option_list.each { |option|
|
83
|
+
key = keys[option.key]
|
84
|
+
set_variable(instance, "@#{key}", idr[option.key])
|
85
|
+
instance.instance_eval("def #{key}?() true end")
|
86
|
+
}
|
87
|
+
|
88
|
+
# Generate general #subcommand methods
|
89
|
+
if !idr.subcommand
|
90
|
+
instance.instance_eval("def subcommand() nil end")
|
91
|
+
instance.instance_eval("def subcommand?() false end")
|
92
|
+
instance.instance_eval %(
|
93
|
+
def subcommand!(*msgs)
|
94
|
+
$stderr.puts "in subcommand!"
|
95
|
+
::Kernel.raise ::ShellOpts::UserError, (msgs.empty? ? 'No command' : msgs.join)
|
96
|
+
end
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Generate individual subcommand methods
|
101
|
+
grammar.subcommand_list.each { |subcommand|
|
102
|
+
key = keys[subcommand.key]
|
103
|
+
if subcommand.key == idr.subcommand&.key
|
104
|
+
struct = OptionStruct.new(idr.subcommand, key_type, aliases[idr.subcommand.key] || {})
|
105
|
+
set_variable(instance, "@subcommand", struct)
|
106
|
+
instance.instance_eval("def #{key}() @subcommand end")
|
107
|
+
instance.instance_eval("def subcommand() :#{key} end")
|
108
|
+
instance.instance_eval("def subcommand?() true end")
|
109
|
+
instance.instance_eval("def subcommand!(*msgs) :#{key} end")
|
110
|
+
else
|
111
|
+
instance.instance_eval("def #{key}() nil end")
|
112
|
+
end
|
113
|
+
}
|
114
|
+
|
115
|
+
instance
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
# Return class of object. #class is not defined for BasicObjects so this
|
120
|
+
# method provides an alternative way of getting the class
|
121
|
+
def self.class_of(object)
|
122
|
+
# https://stackoverflow.com/a/18621313/2130986
|
123
|
+
::Kernel.instance_method(:class).bind(object).call
|
124
|
+
end
|
125
|
+
|
126
|
+
# Class method implementation of ObjectStruct#instance_variable_set that is
|
127
|
+
# not defined in a BasicObject
|
128
|
+
def self.set_variable(this, var, value)
|
129
|
+
# https://stackoverflow.com/a/18621313/2130986
|
130
|
+
::Kernel.instance_method(:instance_variable_set).bind(this).call(var, value)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Class method implementation of ObjectStruct#instance_variable_get that is
|
134
|
+
# not defined in a BasicObject
|
135
|
+
def self.get_variable(this, var)
|
136
|
+
# https://stackoverflow.com/a/18621313/2130986
|
137
|
+
::Kernel.instance_method(:instance_variable_get).bind(this).call(var)
|
138
|
+
end
|
139
|
+
|
140
|
+
BASIC_OBJECT_RESERVED_WORDS = %w(
|
141
|
+
__id__ __send__ instance_eval instance_exec method_missing
|
142
|
+
singleton_method_added singleton_method_removed
|
143
|
+
singleton_method_undefined).map(&:to_sym)
|
144
|
+
OPTIONS_STRUCT_RESERVED_WORDS = %w(__idr__ subcommand).map(&:to_sym)
|
145
|
+
RESERVED_WORDS = BASIC_OBJECT_RESERVED_WORDS + OPTIONS_STRUCT_RESERVED_WORDS
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
data/lib/shellopts/parser.rb
CHANGED
@@ -16,7 +16,7 @@ module ShellOpts
|
|
16
16
|
end
|
17
17
|
|
18
18
|
private
|
19
|
-
# Parse a
|
19
|
+
# Parse a subcommand line
|
20
20
|
class Parser
|
21
21
|
class Error < RuntimeError; end
|
22
22
|
|
@@ -27,24 +27,24 @@ module ShellOpts
|
|
27
27
|
|
28
28
|
def call
|
29
29
|
program = Ast::Program.new(@grammar)
|
30
|
-
|
30
|
+
parse_subcommand(program)
|
31
31
|
program.arguments = @argv
|
32
32
|
program
|
33
33
|
end
|
34
34
|
|
35
35
|
private
|
36
|
-
def
|
37
|
-
@seen_options = {} # Every new
|
36
|
+
def parse_subcommand(subcommand)
|
37
|
+
@seen_options = {} # Every new subcommand resets the seen options
|
38
38
|
while arg = @argv.first
|
39
39
|
if arg == "--"
|
40
40
|
@argv.shift
|
41
41
|
break
|
42
42
|
elsif arg.start_with?("-")
|
43
|
-
parse_option(
|
44
|
-
elsif cmd =
|
43
|
+
parse_option(subcommand)
|
44
|
+
elsif cmd = subcommand.grammar.subcommands[arg]
|
45
45
|
@argv.shift
|
46
|
-
|
47
|
-
|
46
|
+
subcommand.subcommand = Ast::Command.new(cmd, arg)
|
47
|
+
parse_subcommand(subcommand.subcommand)
|
48
48
|
break
|
49
49
|
else
|
50
50
|
break
|
@@ -52,7 +52,7 @@ module ShellOpts
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
-
def parse_option(
|
55
|
+
def parse_option(subcommand)
|
56
56
|
# Split into name and argument
|
57
57
|
case @argv.first
|
58
58
|
when /^(--.+?)(?:=(.*))?$/
|
@@ -62,7 +62,7 @@ module ShellOpts
|
|
62
62
|
end
|
63
63
|
@argv.shift
|
64
64
|
|
65
|
-
option =
|
65
|
+
option = subcommand.grammar.options[name] or raise Error, "Unknown option '#{name}'"
|
66
66
|
!@seen_options.key?(option.key) || option.repeated? or raise Error, "Duplicate option '#{name}'"
|
67
67
|
@seen_options[option.key] = true
|
68
68
|
|
@@ -83,7 +83,7 @@ module ShellOpts
|
|
83
83
|
raise Error, "No argument allowed for option '#{name}'"
|
84
84
|
end
|
85
85
|
|
86
|
-
|
86
|
+
subcommand.options << Ast::Option.new(option, name, arg)
|
87
87
|
end
|
88
88
|
|
89
89
|
def parse_arg(option, name, arg)
|
@@ -0,0 +1,116 @@
|
|
1
|
+
|
2
|
+
require "shellopts"
|
3
|
+
|
4
|
+
require "shellopts/args.rb"
|
5
|
+
|
6
|
+
# TODO
|
7
|
+
#
|
8
|
+
# PROCESSING
|
9
|
+
# 1. Compile spec string and yield a grammar
|
10
|
+
# 2. Parse the options using the grammar and yield an AST
|
11
|
+
# 3. Construct the Program model from the AST
|
12
|
+
# 4. Apply defaults to the model
|
13
|
+
# 6. Run validations on the model
|
14
|
+
# 5. Create representation from the model
|
15
|
+
#
|
16
|
+
|
17
|
+
module ShellOpts
|
18
|
+
# The command line processing object
|
19
|
+
class ShellOpts
|
20
|
+
# Name of program
|
21
|
+
attr_accessor :name
|
22
|
+
|
23
|
+
# Usage string. If #usage is nil, the auto-generated default is used
|
24
|
+
def usage() @usage || @grammar.usage end
|
25
|
+
def usage=(usage) @usage = usage end
|
26
|
+
|
27
|
+
# Specification of the command
|
28
|
+
attr_reader :spec
|
29
|
+
|
30
|
+
# Original argv argument
|
31
|
+
attr_reader :argv
|
32
|
+
|
33
|
+
# The grammar compiled from the spec string
|
34
|
+
attr_reader :grammar
|
35
|
+
|
36
|
+
# The AST parsed from the command line arguments
|
37
|
+
attr_reader :ast
|
38
|
+
|
39
|
+
# The IDR generated from the Ast
|
40
|
+
attr_reader :idr
|
41
|
+
|
42
|
+
# Compile a spec string into a grammar and use that to parse command line
|
43
|
+
# arguments
|
44
|
+
#
|
45
|
+
# +spec+ is the spec string, and +argv+ the command line (typically the
|
46
|
+
# global ARGV array). +name+ is the name of the program and defaults to the
|
47
|
+
# basename of the program
|
48
|
+
#
|
49
|
+
# Syntax errors in the spec string are caused by the developer and raise a
|
50
|
+
# +ShellOpts::CompilerError+ exception. Errors in the +argv+ arguments are
|
51
|
+
# caused by the user and terminates the program with an error message and a
|
52
|
+
# short description of its spec
|
53
|
+
#
|
54
|
+
# TODO: Change to (name, spec, argv, usage: nil) because
|
55
|
+
# ShellOpts::ShellOpts isn't a magician like the ShellOpts module
|
56
|
+
def initialize(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
|
57
|
+
@name = name
|
58
|
+
@spec = spec
|
59
|
+
@usage = usage
|
60
|
+
@argv = argv
|
61
|
+
begin
|
62
|
+
@grammar = Grammar.compile(@name, @spec)
|
63
|
+
@ast = Ast.parse(@grammar, @argv)
|
64
|
+
@idr = Idr.generate(self)
|
65
|
+
rescue Grammar::Compiler::Error => ex
|
66
|
+
raise CompilerError.new(5, ex.message)
|
67
|
+
rescue Ast::Parser::Error => ex
|
68
|
+
raise UserError.new(ex.message)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Return an array representation of options and commands in the same order
|
73
|
+
# as on the command line. Each option or command is represented by a [name,
|
74
|
+
# value] pair. The value of an option is be nil if the option didn't have
|
75
|
+
# an argument and else either a String, Integer, or Float. The value of a
|
76
|
+
# command is an array of its options and commands
|
77
|
+
def to_a() idr.to_a end
|
78
|
+
|
79
|
+
# Return a hash representation of the options. See {ShellOpts::OptionsHash}
|
80
|
+
def to_h(key_type: ::ShellOpts.default_key_type, aliases: {})
|
81
|
+
@idr.to_h(key_type: :key_type, aliases: aliases)
|
82
|
+
end
|
83
|
+
|
84
|
+
# TODO
|
85
|
+
# Return OptionHash object
|
86
|
+
# def to_hash(...)
|
87
|
+
|
88
|
+
# Return a struct representation of the options. See {ShellOpts::OptionStruct}
|
89
|
+
def to_struct(key_type: ::ShellOpts.default_key_type, aliases: {})
|
90
|
+
@idr.to_struct(key_type: key_type, aliases: aliases)
|
91
|
+
end
|
92
|
+
|
93
|
+
# List of remaining non-option command line arguments. Returns a Argv object
|
94
|
+
def args() Args.new(self, ast&.arguments) end
|
95
|
+
|
96
|
+
# Iterate options and commands as name/value pairs. Same as +to_a.each+
|
97
|
+
def each(&block) to_a.each(&block) end
|
98
|
+
|
99
|
+
# Print error messages and spec string and exit with status 1. This method
|
100
|
+
# should be called in response to user-errors (eg. specifying an illegal
|
101
|
+
# option)
|
102
|
+
def error(*msgs, exit: true)
|
103
|
+
msg = "#{name}: #{msgs.join}\n" + (@usage ? usage : "Usage: #{name} #{usage}")
|
104
|
+
$stderr.puts msg.rstrip
|
105
|
+
exit(1) if exit
|
106
|
+
end
|
107
|
+
|
108
|
+
# Print error message and exit with status 1. This method should called in
|
109
|
+
# response to system errors (like disk full)
|
110
|
+
def fail(*msgs, exit: true)
|
111
|
+
$stderr.puts "#{name}: #{msgs.join}"
|
112
|
+
exit(1) if exit
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
data/lib/shellopts/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shellopts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0.pre.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Claus Rasmussen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-10-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -106,17 +106,21 @@ files:
|
|
106
106
|
- doc/stylesheet.css
|
107
107
|
- lib/ext/array.rb
|
108
108
|
- lib/shellopts.rb
|
109
|
+
- lib/shellopts/args.rb
|
109
110
|
- lib/shellopts/ast/command.rb
|
110
111
|
- lib/shellopts/ast/node.rb
|
111
112
|
- lib/shellopts/ast/option.rb
|
112
113
|
- lib/shellopts/ast/program.rb
|
113
114
|
- lib/shellopts/compiler.rb
|
115
|
+
- lib/shellopts/generator.rb
|
114
116
|
- lib/shellopts/grammar/command.rb
|
115
117
|
- lib/shellopts/grammar/node.rb
|
116
118
|
- lib/shellopts/grammar/option.rb
|
117
119
|
- lib/shellopts/grammar/program.rb
|
120
|
+
- lib/shellopts/idr.rb
|
121
|
+
- lib/shellopts/option_struct.rb
|
118
122
|
- lib/shellopts/parser.rb
|
119
|
-
- lib/shellopts/
|
123
|
+
- lib/shellopts/shellopts.rb
|
120
124
|
- lib/shellopts/version.rb
|
121
125
|
- shellopts.gemspec
|
122
126
|
homepage: http://github.com/clrgit/shellopts
|
@@ -134,9 +138,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
134
138
|
version: '0'
|
135
139
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
140
|
requirements:
|
137
|
-
- - "
|
141
|
+
- - ">"
|
138
142
|
- !ruby/object:Gem::Version
|
139
|
-
version:
|
143
|
+
version: 1.3.1
|
140
144
|
requirements: []
|
141
145
|
rubygems_version: 3.0.8
|
142
146
|
signing_key:
|
data/lib/shellopts/utils.rb
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
|
2
|
-
module ShellOpts
|
3
|
-
# Use `include ShellOpts::Utils` to include ShellOpts utility methods in the
|
4
|
-
# global namespace
|
5
|
-
module Utils
|
6
|
-
# Forwards to `ShellOpts.error`
|
7
|
-
def error(*msgs)
|
8
|
-
::ShellOpts.error(*msgs)
|
9
|
-
end
|
10
|
-
|
11
|
-
# Forwards to `ShellOpts.fail`
|
12
|
-
def fail(*msgs)
|
13
|
-
::ShellOpts.fail(*msgs)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|