shellopts 1.0.0 → 2.0.0.pre.7
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 +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
|