command_mapper-gen 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
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