pedrozath-mercenary 0.3.6

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