pedrozath-mercenary 0.3.6

Sign up to get free protection for your applications and to get access to all the features.
data/examples/trace.rb ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.join(__dir__, "..", "lib")
5
+
6
+ require "mercenary"
7
+
8
+ # This example sets the logging mode of mercenary to
9
+ # debug. Logging messages from "p.logger.debug" will
10
+ # be output to STDOUT.
11
+
12
+ Mercenary.program(:trace) do |p|
13
+ p.version "2.0.1"
14
+ p.description "An example of traces in Mercenary"
15
+ p.syntax "trace <subcommand>"
16
+
17
+ p.action do |_, _|
18
+ raise ArgumentError, "YOU DID SOMETHING TERRIBLE YOU BUFFOON"
19
+ end
20
+ end
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: false
2
+
3
+ module Mercenary
4
+ class Command
5
+ attr_reader :name
6
+ attr_reader :description
7
+ attr_reader :syntax
8
+ attr_accessor :options
9
+ attr_accessor :commands
10
+ attr_accessor :actions
11
+ attr_reader :map
12
+ attr_accessor :parent
13
+ attr_reader :trace
14
+ attr_reader :aliases
15
+
16
+ # Public: Creates a new Command
17
+ #
18
+ # name - the name of the command
19
+ # parent - (optional) the instancce of Mercenary::Command which you wish to
20
+ # be the parent of this command
21
+ #
22
+ # Returns nothing
23
+ def initialize(name, parent = nil)
24
+ @name = name
25
+ @options = []
26
+ @commands = {}
27
+ @actions = []
28
+ @map = {}
29
+ @parent = parent
30
+ @trace = false
31
+ @aliases = []
32
+ end
33
+
34
+ # Public: Sets or gets the command version
35
+ #
36
+ # version - the command version (optional)
37
+ #
38
+ # Returns the version and sets it if an argument is non-nil
39
+ def version(version = nil)
40
+ @version = version if version
41
+ @version
42
+ end
43
+
44
+ # Public: Sets or gets the syntax string
45
+ #
46
+ # syntax - the string which describes this command's usage syntax (optional)
47
+ #
48
+ # Returns the syntax string and sets it if an argument is present
49
+ def syntax(syntax = nil)
50
+ @syntax = syntax if syntax
51
+ syntax_list = []
52
+ if parent
53
+ syntax_list << parent.syntax.to_s.gsub(%r!<[\w\s-]+>!, "").gsub(%r!\[[\w\s-]+\]!, "").strip
54
+ end
55
+ syntax_list << (@syntax || name.to_s)
56
+ syntax_list.join(" ")
57
+ end
58
+
59
+ # Public: Sets or gets the command description
60
+ #
61
+ # description - the description of what the command does (optional)
62
+ #
63
+ # Returns the description and sets it if an argument is present
64
+ def description(desc = nil)
65
+ @description = desc if desc
66
+ @description
67
+ end
68
+
69
+ # Public: Sets the default command
70
+ #
71
+ # command_name - the command name to be executed in the event no args are
72
+ # present
73
+ #
74
+ # Returns the default command if there is one, `nil` otherwise
75
+ def default_command(command_name = nil)
76
+ if command_name
77
+ if commands.key?(command_name)
78
+ @default_command = commands[command_name] if command_name
79
+ @default_command
80
+ else
81
+ raise ArgumentError, "'#{command_name}' couldn't be found in this command's list of commands."
82
+ end
83
+ else
84
+ @default_command
85
+ end
86
+ end
87
+
88
+ # Public: Adds an option switch
89
+ #
90
+ # sym - the variable key which is used to identify the value of the switch
91
+ # at runtime in the options hash
92
+ #
93
+ # Returns nothing
94
+ def option(sym, *options)
95
+ new_option = Option.new(sym, options)
96
+ @options << new_option
97
+ @map[new_option] = sym
98
+ end
99
+
100
+ # Public: Adds a subcommand
101
+ #
102
+ # cmd_name - the name of the command
103
+ # block - a block accepting the new instance of Mercenary::Command to be
104
+ # modified (optional)
105
+ #
106
+ # Returns nothing
107
+ def command(cmd_name)
108
+ cmd = Command.new(cmd_name, self)
109
+ yield cmd
110
+ @commands[cmd_name] = cmd
111
+ end
112
+
113
+ # Public: Add an alias for this command's name to be attached to the parent
114
+ #
115
+ # cmd_name - the name of the alias
116
+ #
117
+ # Returns nothing
118
+ def alias(cmd_name)
119
+ logger.debug "adding alias to parent for self: '#{cmd_name}'"
120
+ aliases << cmd_name
121
+ @parent.commands[cmd_name] = self
122
+ end
123
+
124
+ # Public: Add an action Proc to be executed at runtime
125
+ #
126
+ # block - the Proc to be executed at runtime
127
+ #
128
+ # Returns nothing
129
+ def action(&block)
130
+ @actions << block
131
+ end
132
+
133
+ # Public: Fetch a Logger (stdlib)
134
+ #
135
+ # level - the logger level (a Logger constant, see docs for more info)
136
+ #
137
+ # Returns the instance of Logger
138
+
139
+ def logger(level = nil)
140
+ unless @logger
141
+ @logger = Logger.new(STDOUT)
142
+ @logger.level = level || Logger::INFO
143
+ @logger.formatter = proc do |severity, _datetime, _progname, msg|
144
+ "#{identity} | " << "#{severity.downcase.capitalize}:".ljust(7) << " #{msg}\n"
145
+ end
146
+ end
147
+
148
+ @logger.level = level unless level.nil?
149
+ @logger
150
+ end
151
+
152
+ # Public: Run the command
153
+ #
154
+ # argv - an array of string args
155
+ # opts - the instance of OptionParser
156
+ # config - the output config hash
157
+ #
158
+ # Returns the command to be executed
159
+ def go(argv, opts, config)
160
+ opts.banner = "Usage: #{syntax}"
161
+ process_options(opts, config)
162
+ add_default_options(opts)
163
+
164
+ if argv[0] && cmd = commands[argv[0].to_sym]
165
+ logger.debug "Found subcommand '#{cmd.name}'"
166
+ argv.shift
167
+ cmd.go(argv, opts, config)
168
+ else
169
+ logger.debug "No additional command found, time to exec"
170
+ self
171
+ end
172
+ end
173
+
174
+ # Public: Add this command's options to OptionParser and set a default
175
+ # action of setting the value of the option to the inputted hash
176
+ #
177
+ # opts - instance of OptionParser
178
+ # config - the Hash in which the option values should be placed
179
+ #
180
+ # Returns nothing
181
+ def process_options(opts, config)
182
+ options.each do |option|
183
+ opts.on(*option.for_option_parser) do |x|
184
+ config[map[option]] = x
185
+ end
186
+ end
187
+ end
188
+
189
+ # Public: Add version and help options to the command
190
+ #
191
+ # opts - instance of OptionParser
192
+ #
193
+ # Returns nothing
194
+ def add_default_options(opts)
195
+ option "show_help", "-h", "--help", "Show this message"
196
+ option "show_version", "-v", "--version", "Print the name and version"
197
+ option "show_backtrace", "-t", "--trace", "Show the full backtrace when an error occurs"
198
+ opts.on("-v", "--version", "Print the version") do
199
+ puts "#{name} #{version}"
200
+ exit(0)
201
+ end
202
+
203
+ opts.on("-t", "--trace", "Show full backtrace if an error occurs") do
204
+ @trace = true
205
+ end
206
+
207
+ opts.on_tail("-h", "--help", "Show this message") do
208
+ puts self
209
+ exit
210
+ end
211
+ end
212
+
213
+ # Public: Execute all actions given the inputted args and options
214
+ #
215
+ # argv - (optional) command-line args (sans opts)
216
+ # config - (optional) the Hash configuration of string key to value
217
+ #
218
+ # Returns nothing
219
+ def execute(argv = [], config = {})
220
+ if actions.empty? && !default_command.nil?
221
+ default_command.execute
222
+ else
223
+ actions.each { |a| a.call(argv, config) }
224
+ end
225
+ end
226
+
227
+ # Public: Check if this command has a subcommand
228
+ #
229
+ # sub_command - the name of the subcommand
230
+ #
231
+ # Returns true if this command is the parent of a command of name
232
+ # 'sub_command' and false otherwise
233
+ def has_command?(sub_command)
234
+ commands.keys.include?(sub_command)
235
+ end
236
+
237
+ # Public: Identify this command
238
+ #
239
+ # Returns a string which identifies this command
240
+ def ident
241
+ "<Command name=#{identity}>"
242
+ end
243
+
244
+ # Public: Get the full identity (name & version) of this command
245
+ #
246
+ # Returns a string containing the name and version if it exists
247
+ def identity
248
+ "#{full_name} #{version if version}".strip
249
+ end
250
+
251
+ # Public: Get the name of the current command plus that of
252
+ # its parent commands
253
+ #
254
+ # Returns the full name of the command
255
+ def full_name
256
+ the_name = []
257
+ the_name << parent.full_name if parent && parent.full_name
258
+ the_name << name
259
+ the_name.join(" ")
260
+ end
261
+
262
+ # Public: Return all the names and aliases for this command.
263
+ #
264
+ # Returns a comma-separated String list of the name followed by its aliases
265
+ def names_and_aliases
266
+ ([name.to_s] + aliases).compact.join(", ")
267
+ end
268
+
269
+ # Public: Build a string containing a summary of the command
270
+ #
271
+ # Returns a one-line summary of the command.
272
+ def summarize
273
+ " #{names_and_aliases.ljust(20)} #{description}"
274
+ end
275
+
276
+ # Public: Build a string containing the command name, options and any subcommands
277
+ #
278
+ # Returns the string identifying this command, its options and its subcommands
279
+ def to_s
280
+ Presenter.new(self).print_command
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mercenary
4
+ class Option
5
+ attr_reader :config_key, :description, :short, :long, :return_type
6
+
7
+ # Public: Create a new Option
8
+ #
9
+ # config_key - the key in the config hash to which the value of this option
10
+ # will map
11
+ # info - an array containing first the switches, then an optional
12
+ # return type (e.g. Array), then a description of the option
13
+ #
14
+ # Returns nothing
15
+ def initialize(config_key, info)
16
+ @config_key = config_key
17
+ while arg = info.shift
18
+ begin
19
+ @return_type = Object.const_get(arg.to_s)
20
+ next
21
+ rescue NameError
22
+ end
23
+ if arg.start_with?("-")
24
+ if arg.start_with?("--")
25
+ @long = arg
26
+ else
27
+ @short = arg
28
+ end
29
+ next
30
+ end
31
+ @description = arg
32
+ end
33
+ end
34
+
35
+ # Public: Fetch the array containing the info OptionParser is interested in
36
+ #
37
+ # Returns the array which OptionParser#on wants
38
+ def for_option_parser
39
+ [short, long, return_type, description].flatten.reject { |o| o.to_s.empty? }
40
+ end
41
+
42
+ # Public: Build a string representation of this option including the
43
+ # switches and description
44
+ #
45
+ # Returns a string representation of this option
46
+ def to_s
47
+ "#{formatted_switches} #{description}"
48
+ end
49
+
50
+ # Public: Build a beautifully-formatted string representation of the switches
51
+ #
52
+ # Returns a formatted string representation of the switches
53
+ def formatted_switches
54
+ [
55
+ switches.first.rjust(10),
56
+ switches.last.ljust(13),
57
+ ].join(", ").gsub(%r! , !, " ").gsub(%r!, !, " ")
58
+ end
59
+
60
+ # Public: Hash based on the hash value of instance variables
61
+ #
62
+ # Returns a Fixnum which is unique to this Option based on the instance variables
63
+ def hash
64
+ instance_variables.map do |var|
65
+ instance_variable_get(var).hash
66
+ end.reduce(:^)
67
+ end
68
+
69
+ # Public: Check equivalence of two Options based on equivalence of their
70
+ # instance variables
71
+ #
72
+ # Returns true if all the instance variables are equal, false otherwise
73
+ def eql?(other)
74
+ return false unless self.class.eql?(other.class)
75
+ instance_variables.map do |var|
76
+ instance_variable_get(var).eql?(other.instance_variable_get(var))
77
+ end.all?
78
+ end
79
+
80
+ # Public: Fetch an array of switches, including the short and long versions
81
+ #
82
+ # Returns an array of two strings. An empty string represents no switch in
83
+ # that position.
84
+ def switches
85
+ [short, long].map(&:to_s)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mercenary
4
+ class Presenter
5
+ attr_accessor :command
6
+
7
+ # Public: Make a new Presenter
8
+ #
9
+ # command - a Mercenary::Command to present
10
+ #
11
+ # Returns nothing
12
+ def initialize(command)
13
+ @command = command
14
+ end
15
+
16
+ # Public: Builds a string representation of the command usage
17
+ #
18
+ # Returns the string representation of the command usage
19
+ def usage_presentation
20
+ " #{command.syntax}"
21
+ end
22
+
23
+ # Public: Builds a string representation of the options
24
+ #
25
+ # Returns the string representation of the options
26
+ def options_presentation
27
+ return nil unless command_options_presentation || parent_command_options_presentation
28
+ [command_options_presentation, parent_command_options_presentation].compact.join("\n")
29
+ end
30
+
31
+ def command_options_presentation
32
+ return nil if command.options.empty?
33
+ command.options.map(&:to_s).join("\n")
34
+ end
35
+
36
+ # Public: Builds a string representation of the options for parent
37
+ # commands
38
+ #
39
+ # Returns the string representation of the options for parent commands
40
+ def parent_command_options_presentation
41
+ return nil unless command.parent
42
+ Presenter.new(command.parent).options_presentation
43
+ end
44
+
45
+ # Public: Builds a string representation of the subcommands
46
+ #
47
+ # Returns the string representation of the subcommands
48
+ def subcommands_presentation
49
+ return nil if command.commands.empty?
50
+ command.commands.values.uniq.map(&:summarize).join("\n")
51
+ end
52
+
53
+ # Public: Builds the command header, including the command identity and description
54
+ #
55
+ # Returns the command header as a String
56
+ def command_header
57
+ header = command.identity.to_s
58
+ header << " -- #{command.description}" if command.description
59
+ header
60
+ end
61
+
62
+ # Public: Builds a string representation of the whole command
63
+ #
64
+ # Returns the string representation of the whole command
65
+ def command_presentation
66
+ msg = []
67
+ msg << command_header
68
+ msg << "Usage:"
69
+ msg << usage_presentation
70
+
71
+ if opts = options_presentation
72
+ msg << "Options:\n#{opts}"
73
+ end
74
+ if subcommands = subcommands_presentation
75
+ msg << "Subcommands:\n#{subcommands_presentation}"
76
+ end
77
+ msg.join("\n\n")
78
+ end
79
+
80
+ # Public: Turn a print_* into a *_presentation or freak out
81
+ #
82
+ # meth - the method being called
83
+ # args - an array of arguments passed to the missing method
84
+ # block - the block passed to the missing method
85
+ #
86
+ # Returns the value of whatever function is called
87
+ def method_missing(meth, *args, &block)
88
+ if meth.to_s =~ %r!^print_(.+)$!
89
+ send("#{Regexp.last_match(1).downcase}_presentation")
90
+ else
91
+ # You *must* call super if you don't handle the method,
92
+ # otherwise you'll mess up Ruby's method lookup.
93
+ super
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mercenary
4
+ class Program < Command
5
+ attr_reader :optparse
6
+ attr_reader :config
7
+
8
+ # Public: Creates a new Program
9
+ #
10
+ # name - the name of the program
11
+ #
12
+ # Returns nothing
13
+ def initialize(name)
14
+ @config = {}
15
+ super(name)
16
+ end
17
+
18
+ # Public: Run the program
19
+ #
20
+ # argv - an array of string args (usually ARGV)
21
+ #
22
+ # Returns nothing
23
+ def go(argv)
24
+ if argv.empty?
25
+ default_command.execute
26
+ abort
27
+ end
28
+
29
+ logger.debug("Using args passed in: #{argv.inspect}")
30
+
31
+ cmd = nil
32
+
33
+ @optparse = OptionParser.new do |opts|
34
+ cmd = super(argv, opts, @config)
35
+ end
36
+
37
+ if cmd.actions.compact.empty?
38
+ logger.error 'Invalid command.'
39
+ abort
40
+ end
41
+
42
+ begin
43
+ @optparse.parse!(argv)
44
+ rescue OptionParser::InvalidOption => e
45
+ logger.error "Whoops, we can't understand your command."
46
+ logger.error e.message.to_s
47
+ logger.error "Run your command again with the --help switch to see available options."
48
+ abort
49
+ end
50
+
51
+ logger.debug("Parsed config: #{@config.inspect}")
52
+
53
+ begin
54
+ cmd.execute(argv, @config)
55
+ rescue => e
56
+ if cmd.trace
57
+ raise e
58
+ else
59
+ logger.error e.message
60
+ abort
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mercenary
4
+ VERSION = "0.3.6".freeze
5
+ end
data/lib/mercenary.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("mercenary/version", __dir__)
4
+ require "optparse"
5
+ require "logger"
6
+
7
+ module Mercenary
8
+ autoload :Command, File.expand_path("mercenary/command", __dir__)
9
+ autoload :Option, File.expand_path("mercenary/option", __dir__)
10
+ autoload :Presenter, File.expand_path("mercenary/presenter", __dir__)
11
+ autoload :Program, File.expand_path("mercenary/program", __dir__)
12
+
13
+ # Public: Instantiate a new program and execute.
14
+ #
15
+ # name - the name of your program
16
+ #
17
+ # Returns nothing.
18
+ def self.program(name)
19
+ program = Program.new(name)
20
+ yield program
21
+ program.go(ARGV)
22
+ end
23
+ end
data/mercenary.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "mercenary/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "pedrozath-mercenary"
9
+ spec.version = Mercenary::VERSION
10
+ spec.authors = ["Tom Preston-Werner", "Parker Moore"]
11
+ spec.email = ["tom@mojombo.com", "parkrmoore@gmail.com"]
12
+ spec.description = "Lightweight and flexible library for writing command-line apps in Ruby."
13
+ spec.summary = "Lightweight and flexible library for writing command-line apps in Ruby."
14
+ spec.homepage = "https://github.com/jekyll/mercenary"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
18
+ spec.executables = spec.files.grep(%r!^bin/!) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r!^(test|spec|features)/!)
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "rspec", "~> 3.0"
25
+ spec.add_development_dependency "rubocop", "~> 0.51"
26
+ end
data/script/bootstrap ADDED
@@ -0,0 +1,7 @@
1
+ #! /bin/sh
2
+
3
+ set -e
4
+
5
+ echo "Time to get set up."
6
+ bundle install
7
+ echo "Boom."
data/script/cibuild ADDED
@@ -0,0 +1,7 @@
1
+ #! /bin/sh
2
+
3
+ set -ex
4
+
5
+ bundle exec rspec
6
+ script/fmt
7
+ script/examples
data/script/console ADDED
@@ -0,0 +1,3 @@
1
+ #! /bin/bash
2
+
3
+ irb -r./lib/mercenary.rb
data/script/examples ADDED
@@ -0,0 +1,18 @@
1
+ #! /bin/bash
2
+
3
+ set -e
4
+
5
+ function run () {
6
+ echo "+ ruby ./examples/$@"
7
+ ruby -e "puts '=' * 79"
8
+ ruby ./examples/$@
9
+ ruby -e "puts '=' * 79"
10
+ }
11
+
12
+ run logging.rb
13
+ run logging.rb -v
14
+ run help_dialogue.rb -h
15
+ run help_dialogue.rb some_subcommand -h
16
+ run help_dialogue.rb another_subcommand -h
17
+ run help_dialogue.rb some_subcommand yet_another_sub -h
18
+ run help_dialogue.rb some_subcommand yet_another_sub -b
data/script/fmt ADDED
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+
3
+ echo "Rubocop $(bundle exec rubocop --version)"
4
+ bundle exec rubocop -S -D -E $@