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.
@@ -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
+
@@ -16,7 +16,7 @@ module ShellOpts
16
16
  end
17
17
 
18
18
  private
19
- # Parse a command line
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
- parse_command(program)
30
+ parse_subcommand(program)
31
31
  program.arguments = @argv
32
32
  program
33
33
  end
34
34
 
35
35
  private
36
- def parse_command(command)
37
- @seen_options = {} # Every new command resets the seen options
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(command)
44
- elsif cmd = command.grammar.commands[arg]
43
+ parse_option(subcommand)
44
+ elsif cmd = subcommand.grammar.subcommands[arg]
45
45
  @argv.shift
46
- command.command = Ast::Command.new(cmd, arg)
47
- parse_command(command.command)
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(command)
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 = command.grammar.options[name] or raise Error, "Unknown option '#{name}'"
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
- command.options << Ast::Option.new(option, name, arg)
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
+
@@ -1,3 +1,3 @@
1
1
  module Shellopts
2
- VERSION = "1.0.0"
2
+ VERSION = "2.0.0.pre.7"
3
3
  end
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: 1.0.0
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-07-20 00:00:00.000000000 Z
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/utils.rb
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: '0'
143
+ version: 1.3.1
140
144
  requirements: []
141
145
  rubygems_version: 3.0.8
142
146
  signing_key:
@@ -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