command_mapper-gen 0.1.0.pre1

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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +27 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +1 -0
  5. data/.yardopts +1 -0
  6. data/ChangeLog.md +20 -0
  7. data/Gemfile +17 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.md +145 -0
  10. data/Rakefile +15 -0
  11. data/bin/command_mapper-gen +7 -0
  12. data/commnad_mapper-gen.gemspec +61 -0
  13. data/examples/grep.rb +63 -0
  14. data/gemspec.yml +26 -0
  15. data/lib/command_mapper/gen/arg.rb +43 -0
  16. data/lib/command_mapper/gen/argument.rb +53 -0
  17. data/lib/command_mapper/gen/cli.rb +233 -0
  18. data/lib/command_mapper/gen/command.rb +202 -0
  19. data/lib/command_mapper/gen/exceptions.rb +9 -0
  20. data/lib/command_mapper/gen/option.rb +66 -0
  21. data/lib/command_mapper/gen/option_value.rb +23 -0
  22. data/lib/command_mapper/gen/parsers/common.rb +49 -0
  23. data/lib/command_mapper/gen/parsers/help.rb +351 -0
  24. data/lib/command_mapper/gen/parsers/man.rb +80 -0
  25. data/lib/command_mapper/gen/parsers/options.rb +127 -0
  26. data/lib/command_mapper/gen/parsers/usage.rb +141 -0
  27. data/lib/command_mapper/gen/parsers.rb +2 -0
  28. data/lib/command_mapper/gen/task.rb +90 -0
  29. data/lib/command_mapper/gen/types/enum.rb +30 -0
  30. data/lib/command_mapper/gen/types/key_value.rb +34 -0
  31. data/lib/command_mapper/gen/types/list.rb +34 -0
  32. data/lib/command_mapper/gen/types/map.rb +36 -0
  33. data/lib/command_mapper/gen/types/num.rb +18 -0
  34. data/lib/command_mapper/gen/types/str.rb +48 -0
  35. data/lib/command_mapper/gen/types.rb +6 -0
  36. data/lib/command_mapper/gen/version.rb +6 -0
  37. data/lib/command_mapper/gen.rb +2 -0
  38. data/spec/argument_spec.rb +92 -0
  39. data/spec/cli_spec.rb +269 -0
  40. data/spec/command_spec.rb +316 -0
  41. data/spec/option_spec.rb +85 -0
  42. data/spec/option_value_spec.rb +20 -0
  43. data/spec/parsers/common_spec.rb +616 -0
  44. data/spec/parsers/help_spec.rb +612 -0
  45. data/spec/parsers/man_spec.rb +158 -0
  46. data/spec/parsers/options_spec.rb +802 -0
  47. data/spec/parsers/usage_spec.rb +1175 -0
  48. data/spec/spec_helper.rb +6 -0
  49. data/spec/task_spec.rb +69 -0
  50. data/spec/types/enum_spec.rb +45 -0
  51. data/spec/types/key_value_spec.rb +36 -0
  52. data/spec/types/list_spec.rb +36 -0
  53. data/spec/types/map_spec.rb +48 -0
  54. data/spec/types/str_spec.rb +70 -0
  55. metadata +133 -0
@@ -0,0 +1,233 @@
1
+ require 'command_mapper/gen/parsers'
2
+ require 'command_mapper/gen/command'
3
+ require 'command_mapper/gen/version'
4
+
5
+ require 'optparse'
6
+
7
+ module CommandMapper
8
+ module Gen
9
+ class CLI
10
+
11
+ PROGRAM_NAME = "command_mapper-gen"
12
+
13
+ PARSERS = {
14
+ 'help' => Parsers::Help,
15
+ 'man' => Parsers::Man
16
+ }
17
+
18
+ BUG_REPORT_URL = "https://github.com/postmodern/command_mapper-gen/issues/new"
19
+
20
+ # The output file or `nil` for stdout.
21
+ #
22
+ # @return [File, nil]
23
+ attr_reader :output
24
+
25
+ # The parsers to run.
26
+ #
27
+ # @return [Array<Parsers::Help, Parsers::Man>]
28
+ attr_reader :parsers
29
+
30
+ # Specifies whether debug output should be printed.
31
+ #
32
+ # @return [Boolean, nil]
33
+ attr_reader :debug
34
+
35
+ # The parsed command.
36
+ #
37
+ # @return [Command]
38
+ attr_reader :command
39
+
40
+ # The command's option parser.
41
+ #
42
+ # @return [OptionParser]
43
+ attr_reader :option_parser
44
+
45
+ #
46
+ # Initializes the command.
47
+ #
48
+ def initialize
49
+ @output = nil
50
+ @parsers = PARSERS.values
51
+ @debug = false
52
+ @command = nil
53
+
54
+ @option_parser = option_parser
55
+ end
56
+
57
+ #
58
+ # Initializes and runs the command.
59
+ #
60
+ # @param [Array<String>] argv
61
+ # Command-line arguments.
62
+ #
63
+ # @return [Integer]
64
+ # The exit status of the command.
65
+ #
66
+ def self.run(argv=ARGV)
67
+ new().run(argv)
68
+ rescue Interrupt
69
+ # https://tldp.org/LDP/abs/html/exitcodes.html
70
+ return 130
71
+ rescue Errno::EPIPE
72
+ # STDOUT pipe broken
73
+ return 0
74
+ end
75
+
76
+ #
77
+ # Runs the command.
78
+ #
79
+ # @param [Array<String>] argv
80
+ # Command-line arguments.
81
+ #
82
+ # @return [Integer]
83
+ # The exit status of the command.
84
+ #
85
+ def run(argv=ARGV)
86
+ argv = begin
87
+ @option_parser.parse(argv)
88
+ rescue OptionParser::ParseError => error
89
+ print_error(error.message)
90
+ return -1
91
+ end
92
+
93
+ if argv.empty?
94
+ print_error "expects a COMMAND_NAME"
95
+ return -1
96
+ end
97
+
98
+ begin
99
+ @command = Command.new(argv.first)
100
+
101
+ @parsers.each do |parser|
102
+ parse_command = ->(command) {
103
+ parser.run(command) do |line,parse_error|
104
+ print_parser_error(command,line,parse_error)
105
+ end
106
+
107
+ command.subcommands.each_value do |subcommand|
108
+ parse_command.call(subcommand)
109
+ end
110
+ }
111
+
112
+ parse_command.call(@command)
113
+ end
114
+ rescue Error => error
115
+ print_error(error.message)
116
+ return -1
117
+ end
118
+
119
+ if (@command.options.empty? &&
120
+ @command.arguments.empty? &&
121
+ @command.subcommands.empty?)
122
+ print_error "no options or arguments detected"
123
+ return -2
124
+ end
125
+
126
+ if @output then @command.save(@output)
127
+ else puts command.to_ruby
128
+ end
129
+
130
+ return 0
131
+ rescue => error
132
+ print_backtrace(error)
133
+ return -1
134
+ end
135
+
136
+ #
137
+ # The option parser.
138
+ #
139
+ # @return [OptionParser]
140
+ #
141
+ def option_parser
142
+ OptionParser.new do |opts|
143
+ opts.banner = "usage: #{PROGRAM_NAME} [options] COMMAND_NAME"
144
+
145
+ opts.separator ""
146
+ opts.separator "Options:"
147
+
148
+ opts.on('-o','--output FILE','Saves the output to FILE') do |file|
149
+ @output = file
150
+ end
151
+
152
+ opts.on('-p','--parser=PARSER', PARSERS, 'Selects which parser to use (help or man)') do |parser|
153
+ @parsers = [parser]
154
+ end
155
+
156
+ opts.on('-d','--debug','Enables debugging output') do
157
+ @debug = true
158
+ end
159
+
160
+ opts.on('-V','--version','Print the version') do
161
+ puts "#{PROGRAM_NAME} #{VERSION}"
162
+ exit
163
+ end
164
+
165
+ opts.on('-h','--help','Print the help output') do
166
+ puts opts
167
+ exit
168
+ end
169
+
170
+ opts.separator ""
171
+ opts.separator "Examples:"
172
+ opts.separator " #{PROGRAM_NAME} grep"
173
+ opts.separator ""
174
+ end
175
+ end
176
+
177
+ #
178
+ # Prints an error message to stderr.
179
+ #
180
+ # @param [String] error
181
+ # The error message.
182
+ #
183
+ def print_error(error)
184
+ $stderr.puts "#{PROGRAM_NAME}: #{error}"
185
+ end
186
+
187
+ #
188
+ # Prints a parsing error to stderr.
189
+ #
190
+ # @param [Command] command
191
+ # The command that was being populated.
192
+ #
193
+ # @param [String] string
194
+ # The text that could not be parsed.
195
+ #
196
+ # @param [Parslet::ParseError] error
197
+ # The parsing error.
198
+ #
199
+ def print_parser_error(command,string,error)
200
+ $stderr.puts "Failed to parse line in `#{command.command_string} --help`:"
201
+ $stderr.puts ""
202
+ $stderr.puts " #{string}"
203
+ $stderr.puts
204
+
205
+ if @debug
206
+ error.parse_failure_cause.ascii_tree.each_line do |backtrace_line|
207
+ $stderr.puts " #{backtrace_line}"
208
+ end
209
+ else
210
+ $stderr.puts error.message
211
+ end
212
+
213
+ $stderr.puts ""
214
+ end
215
+
216
+ #
217
+ # Prints a backtrace to stderr.
218
+ #
219
+ # @param [Exception] exception
220
+ # The exception.
221
+ #
222
+ def print_backtrace(exception)
223
+ $stderr.puts "Oops! Looks like you've found a bug!"
224
+ $stderr.puts "Please report the following to: #{BUG_REPORT_URL}"
225
+ $stderr.puts
226
+ $stderr.puts "```"
227
+ $stderr.puts "#{exception.full_message}"
228
+ $stderr.puts "```"
229
+ end
230
+
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,202 @@
1
+ require 'command_mapper/gen/option'
2
+ require 'command_mapper/gen/argument'
3
+ require 'command_mapper/gen/types'
4
+
5
+ module CommandMapper
6
+ module Gen
7
+ #
8
+ # Represents a mock `CommandMapper::Command` class that will be populated
9
+ # by the {Parsers} and written out to a file.
10
+ #
11
+ # @api private
12
+ #
13
+ class Command
14
+
15
+ # The command's name.
16
+ #
17
+ # @return [String]
18
+ attr_accessor :command_name
19
+
20
+ # The parent command of this sub-command.
21
+ #
22
+ # @return [Command, nil]
23
+ attr_reader :parent_command
24
+
25
+ # @return [Hash{String => Option}]
26
+ attr_reader :options
27
+
28
+ # @return [Hash{Symbol => Argument}]
29
+ attr_reader :arguments
30
+
31
+ # @return [Hash{String => Command}]
32
+ attr_reader :subcommands
33
+
34
+ #
35
+ # Initializes the parsed command.
36
+ #
37
+ # @param [String] command_name
38
+ # The command name or path to the command.
39
+ #
40
+ def initialize(command_name,parent_command=nil)
41
+ @command_name = command_name
42
+ @parent_command = parent_command
43
+
44
+ @options = {}
45
+ @arguments = {}
46
+ @subcommands = {}
47
+ end
48
+
49
+ #
50
+ # The command string to run the command.
51
+ #
52
+ # @return [String]
53
+ #
54
+ def command_string
55
+ if @parent_command
56
+ "#{@parent_command.command_string} #{@command_name}"
57
+ else
58
+ @command_name
59
+ end
60
+ end
61
+
62
+ #
63
+ # The man-page name for the command.
64
+ #
65
+ # @return [String]
66
+ #
67
+ def man_page
68
+ if @parent_command
69
+ "#{@parent_command.man_page}-#{@command_name}"
70
+ else
71
+ @command_name
72
+ end
73
+ end
74
+
75
+ #
76
+ # Defines an option for the command.
77
+ #
78
+ # @param [String] flag
79
+ #
80
+ # @param [Hash{Symbol => Object}] kwargs
81
+ #
82
+ def option(flag,**kwargs)
83
+ @options[flag] = Option.new(flag,**kwargs)
84
+ end
85
+
86
+ #
87
+ # Defines an argument for the command.
88
+ #
89
+ # @param [Symbol] name
90
+ #
91
+ # @param [Hash{Symbol => Object}] kwargs
92
+ #
93
+ def argument(name,**kwargs)
94
+ @arguments[name] = Argument.new(name,**kwargs)
95
+ end
96
+
97
+ #
98
+ # Defines a new sub-command.
99
+ #
100
+ # @param [String] name
101
+ # The subcommand name.
102
+ #
103
+ # @return [Command]
104
+ # The newly defined subcommand.
105
+ #
106
+ def subcommand(name)
107
+ @subcommands[name] = Command.new(name,self)
108
+ end
109
+
110
+ #
111
+ # The CamelCase class name derived from the {#command_name}.
112
+ #
113
+ # @return [String, nil]
114
+ # The class name or `nil` if {#command_name} is also `nil`.
115
+ #
116
+ def class_name
117
+ @command_name.split(/[_-]+/).map(&:capitalize).join
118
+ end
119
+
120
+ #
121
+ # Converts the parsed command to Ruby source code.
122
+ #
123
+ # @return [String]
124
+ # The generated ruby source code for the command.
125
+ #
126
+ def to_ruby
127
+ lines = []
128
+
129
+ if @parent_command.nil?
130
+ lines << "require 'command_mapper/command'"
131
+ lines << ""
132
+ lines << "#"
133
+ lines << "# Represents the `#{@command_name}` command"
134
+ lines << "#"
135
+
136
+ lines << "class #{class_name} < CommandMapper::Command"
137
+ lines << ""
138
+ lines << " command #{@command_name.inspect} do"
139
+
140
+ indent = " "
141
+ else
142
+ lines << "subcommand #{@command_name.inspect} do"
143
+
144
+ indent = " "
145
+ end
146
+
147
+ unless @options.empty?
148
+ @options.each_value do |option|
149
+ lines << "#{indent}#{option.to_ruby}"
150
+ end
151
+ end
152
+
153
+ if (!@options.empty? && !@arguments.empty?)
154
+ lines << ''
155
+ end
156
+
157
+ unless @arguments.empty?
158
+ @arguments.each_value do |argument|
159
+ lines << "#{indent}#{argument.to_ruby}"
160
+ end
161
+ end
162
+
163
+ unless @subcommands.empty?
164
+ if (!@options.empty? || !@arguments.empty?)
165
+ lines << ''
166
+ end
167
+
168
+ @subcommands.each_value.each_with_index do |subcommand,index|
169
+ lines << '' if index > 0
170
+
171
+ subcommand.to_ruby.each_line do |line|
172
+ if line == $/
173
+ lines << ''
174
+ else
175
+ lines << "#{indent}#{line.chomp}"
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ if @parent_command.nil?
182
+ lines << " end"
183
+ lines << ''
184
+ end
185
+
186
+ lines << "end"
187
+
188
+ return lines.join($/) + $/
189
+ end
190
+
191
+ #
192
+ # Saves the parsed command to the given file path.
193
+ #
194
+ # @param [String] path
195
+ #
196
+ def save(path)
197
+ File.write(path,to_ruby)
198
+ end
199
+
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,9 @@
1
+ module CommandMapper
2
+ module Gen
3
+ class Error < RuntimeError
4
+ end
5
+
6
+ class CommandNotInstalled < Error
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,66 @@
1
+ require 'command_mapper/gen/option_value'
2
+
3
+ module CommandMapper
4
+ module Gen
5
+ #
6
+ # Represents a mock `CommandMapper::Option` class.
7
+ #
8
+ class Option
9
+
10
+ # The option flag for the option.
11
+ #
12
+ # @return [String]
13
+ attr_reader :flag
14
+
15
+ # @return [Boolean, :equals, nil]
16
+ attr_reader :equals
17
+
18
+ # @return [Boolean, nil]
19
+ attr_reader :repeats
20
+
21
+ # @return [OptionValue, nil]
22
+ attr_reader :value
23
+
24
+ #
25
+ # Initializes the parsed argument.
26
+ #
27
+ # @param [String] flag
28
+ # The option flag.
29
+ #
30
+ # @param [Boolean, :optional, nil] equals
31
+ #
32
+ # @param [Boolean, nil] repeats
33
+ #
34
+ # @param [Hash{Symbol => Object}, nil] value
35
+ #
36
+ def initialize(flag, equals: nil, repeats: nil, value: nil)
37
+ @flag = flag
38
+ @equals = equals
39
+ @value = OptionValue.new(**value) if value
40
+ @repeats = repeats
41
+ end
42
+
43
+ #
44
+ # Converts the parsed option to Ruby source code.
45
+ #
46
+ # @return [String]
47
+ #
48
+ def to_ruby
49
+ ruby = "option #{@flag.inspect}"
50
+ fixme = nil
51
+
52
+ if @flag =~ /^-[a-zA-Z0-9]/ && @flag.length <= 3
53
+ ruby << ", name: "
54
+ fixme = "name"
55
+ end
56
+
57
+ ruby << ", equals: #{@equals.inspect}" unless @equals.nil?
58
+ ruby << ", repeats: #{@repeats.inspect}" unless @repeats.nil?
59
+ ruby << ", value: #{@value.to_ruby}" if @value
60
+ ruby << "\t# FIXME: #{fixme}" if fixme
61
+ ruby
62
+ end
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,23 @@
1
+ require 'command_mapper/gen/arg'
2
+ require 'command_mapper/gen/types/str'
3
+
4
+ module CommandMapper
5
+ module Gen
6
+ class OptionValue < Arg
7
+
8
+ #
9
+ # Converts the parsed option to Ruby source code.
10
+ #
11
+ # @return [String]
12
+ #
13
+ def to_ruby
14
+ ruby = super()
15
+
16
+ if ruby.empty? then "true"
17
+ else "{#{ruby}}"
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,49 @@
1
+ require 'parslet'
2
+
3
+ module CommandMapper
4
+ module Gen
5
+ module Parsers
6
+ class Common < Parslet::Parser
7
+
8
+ rule(:space) { match[' '] }
9
+ rule(:spaces) { space.repeat(1) }
10
+ rule(:space?) { space.maybe }
11
+
12
+ rule(:ellipsis) { str('...') }
13
+ rule(:ellipsis?) { (space? >> ellipsis.as(:repeats)).maybe }
14
+
15
+ rule(:lowercase_name) do
16
+ match['a-z'] >> match['a-z0-9'].repeat(0) >> (
17
+ match['_-'] >> match['a-z0-9'].repeat(1)
18
+ ).repeat(0)
19
+ end
20
+
21
+ rule(:uppercase_name) do
22
+ match['A-Z'] >> match['A-Z0-9'].repeat(0) >> (
23
+ match['_-'] >> match['A-Z0-9'].repeat(1)
24
+ ).repeat(0)
25
+ end
26
+
27
+ rule(:camelcase_name) do
28
+ match['a-z'] >> match['a-z0-9'].repeat(0) >> (
29
+ match['A-Z'] >> match['a-z0-9'].repeat(1)
30
+ ).repeat(1)
31
+ end
32
+
33
+ rule(:capitalized_name) do
34
+ match['A-Z'] >> match['a-z0-9'].repeat(1) >> (
35
+ match['_-'] >> match['a-z0-9'].repeat(1)
36
+ ).repeat(0)
37
+ end
38
+
39
+ rule(:short_flag) { str('-') >> match['a-zA-Z0-9#'] }
40
+ rule(:long_flag) do
41
+ str('--') >> match['a-zA-Z'] >> match['a-zA-Z0-9'].repeat(1) >> (
42
+ match['_-'] >> match['a-zA-Z0-9'].repeat(1)
43
+ ).repeat(0)
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end