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.
@@ -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