mercenary-0.4.0/0000755000004100000410000000000013624570164013540 5ustar www-datawww-datamercenary-0.4.0/.travis.yml0000644000004100000410000000033613624570164015653 0ustar www-datawww-datalanguage: ruby cache: bundler rvm: - 2.4 - 2.6 before_script: bundle update script: script/cibuild notifications: email: recipients: - mercenary@jekyllrb.com on_success: change on_failure: change mercenary-0.4.0/.rspec0000644000004100000410000000003213624570164014650 0ustar www-datawww-data--color --format progress mercenary-0.4.0/README.md0000644000004100000410000001606013624570164015022 0ustar www-datawww-data# Mercenary Lightweight and flexible library for writing command-line apps in Ruby. [![Build Status](https://secure.travis-ci.org/jekyll/mercenary.png)](https://travis-ci.org/jekyll/mercenary) ## Installation Add this line to your application's Gemfile: gem 'mercenary' And then execute: $ bundle Or install it yourself as: $ gem install mercenary **Note: Mercenary may not work with Ruby < 1.9.3.** ## Usage Creating programs and commands with Mercenary is easy: ```ruby Mercenary.program(:jekyll) do |p| p.version Jekyll::VERSION p.description 'Jekyll is a blog-aware, static site generator in Ruby' p.syntax "jekyll [options]" p.command(:new) do |c| c.syntax "new PATH" # do not include the program name or super commands c.description "Creates a new Jekyll site scaffold in PATH" c.option 'blank', '--blank', 'Initialize the new site without any content.' c.action do |args, options| Jekyll::Commands::New.process(args, blank: options['blank']) end end p.command(:build) do |c| c.syntax "build [options]" c.description "Builds your Jekyll site" c.option 'safe', '--safe', 'Run in safe mode' c.option 'source', '--source DIR', 'From where to collect the source files' c.option 'destination', '--dest DIR', 'To where the compiled files should be written' c.action do |_, options| Jekyll::Commands::Build.process(options) end end # Bring in command bundled in external gem begin require "jekyll-import" JekyllImport.init_with_program(p) rescue LoadError end p.default_command(:build) end ``` All commands have the following default options: - `-h/--help` - show a help message - `-v/--version` - show the program version - `-t/--trace` - show the full backtrace when an error occurs ## API ### `Mercenary` #### `.program` Creates and executes a program. Accepts two arguments: - `name` - program name as a Symbol - `block` - the specification for the program, passed the program instance as an argument. Example is above, under the heading [Usage](#usage). ### `Program` `Program` is a subclass of `Command`, so it has all of the methods documented below as well as those for `Command`. #### `#config` Fetches the program configuration hash. ### `Command` #### `#new` Create a new command. Accepts one argument: - `name` - the name of your command, as a symbol #### `#version` Sets or gets the version of the command. Accepts an optional argument: - `version` - (optional) the version to set for the command. If present, this becomes the new version for the command and persists. #### `#syntax` Sets or gets the syntax of the command. Built on parent syntaxes if a parent exists. Accepts one optional argument: - `syntax` - (optional) the syntax to set for the command. Will inherit from the parent commands or program. Usually in the form of `"command_name [OPTIONS]"` When a parent command exists, say `supercommand`, with syntax set as `supercommand [OPTIONS]`, the syntax of the command in question will be `supercommand command_name [OPTIONS]` with both `` and `[OPTIONS]` stripped out. Any text between `<` and `>` or between `[` and `]` will be stripped from parent command syntaxes. The purpose of this chaining is to reduce redundancy. #### `#description` Sets or gets the description of the command. Accepts one optional argument: - `desc` - (optional) the description to set for the command. If provided, will override any previous description set for the command. #### `#default_command` Sets or gets the default subcommand of the command to execute in the event no subcommand is passed during execution. Accepts one optional argument: - `command_name` - (optional) the `Symbol` name of the subcommand to be executed. Raises an `ArgumentError` if the subcommand doesn't exist. Overwrites previously-set default commands. #### `#option` Adds a new option to the command. Accepts many arguments: - `config_key` - the configuration key that the value of this option maps to. - `*options` - all the options, globbed, to be passed to `OptionParser`, namely the switches and the option description. Usually in the format `"-s", "--switch", "Sets the 'switch' flag"`. Valid option calls: ```ruby cmd.option 'config_key', '-c', 'Sets the "config" flag' cmd.option 'config_key', '--config', 'Sets the "config" flag' cmd.option 'config_key', '-c', '--config', 'Sets the "config" flag.' cmd.option 'config_key', '-c FILE', '--config FILE', 'The config file.' cmd.option 'config_key', '-c FILE1[,FILE2[,FILE3...]]', '--config FILE1[,FILE2[,FILE3...]]', Array, 'The config files.' ``` Notice that you can specify either a short switch, a long switch, or both. If you want to accept an argument, you have to specify it in the switch strings. The class of the argument defaults to `String`, but you can optionally set a different class to create, e.g. `Array`, if you are expecting a particular class in your code from this option's value. The description is also optional, but it's highly recommended to include a description. #### `#alias` Specifies an alias for this command such that the alias may be used in place of the command during execution. Accepts one argument: - `cmd_name` - the alias name for this command as a `Symbol` Example: ```ruby cmd.alias(:my_alias) # Now `cmd` is now also executable via "my_alias" ``` #### `#action` Specifies a block to be executed in the event the command is specified at runtime. The block is given two arguments: - `args` - the non-switch arguments given from the command-line - `options` - the options hash built via the switches passed **Note that actions are additive**, meaning any new call to `#action` will result in another action to be executed at runtime. Actions will be executed in the order they are specified in. Example: ```ruby cmd.action do |args, options| # do something! end ``` #### `#logger` Access the logger for this command. Useful for outputting information to STDOUT. Accepts one optional argument: - `level` - (optional) the severity threshold at which to begin logging. Uses Ruby's built-in [`Logger`](http://www.ruby-doc.org/stdlib-2.1.0/libdoc/logger/rdoc/Logger.html) levels. Log level defaults to `Logger::INFO`. Examples: ```ruby cmd.logger(Logger::DEBUG) cmd.logger.debug "My debug message." cmd.logger.info "My informative message." cmd.logger.warn "ACHTUNG!!" cmd.logger.error "Something terrible has happened." cmd.logger.fatal "I can't continue doing what I'm doing." ``` #### `#command` Creates a new subcommand for the current command. Accepts two arguments: - `cmd_name` - the command name, as a Symbol - `block` - the specification of the subcommand in a block Example: ```ruby my_command.command(:my_subcommand) do |subcmd| subcmd.description 'My subcommand' subcmd.syntax 'my_subcommand [OPTIONS]' # ... end ``` ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request mercenary-0.4.0/spec/0000755000004100000410000000000013624570164014472 5ustar www-datawww-datamercenary-0.4.0/spec/option_spec.rb0000644000004100000410000000436213624570164017346 0ustar www-datawww-data# frozen_string_literal: true require "spec_helper" describe(Mercenary::Option) do let(:config_key) { "largo" } let(:description) { "This is a description" } let(:switches) { ["-l", "--largo"] } let(:option) { described_class.new(config_key, [switches, description].flatten.reject(&:nil?)) } it "knows its config key" do expect(option.config_key).to eql(config_key) end it "knows its description" do expect(option.description).to eql(description) end it "knows its switches" do expect(option.switches).to eql(switches) end it "knows how to present itself" do expect(option.to_s).to eql(" -l, --largo #{description}") end it "has an OptionParser representation" do expect(option.for_option_parser).to eql([switches, description].flatten) end it "compares itself with other options well" do new_option = described_class.new(config_key, ["-l", "--largo", description]) expect(option.eql?(new_option)).to be(true) expect(option.hash.eql?(new_option.hash)).to be(true) end it "has a custom #hash" do expect(option.hash.to_s).to match(%r!\d+!) end context "with just the long switch" do let(:switches) { ["--largo"] } it "adds an empty string in place of the short switch" do expect(option.switches).to eql(["", "--largo"]) end it "sets its description properly" do expect(option.description).to eql(description) end it "knows how to present the switch" do expect(option.formatted_switches).to eql(" --largo ") end end context "with just the short switch" do let(:switches) { ["-l"] } it "adds an empty string in place of the long switch" do expect(option.switches).to eql(["-l", ""]) end it "sets its description properly" do expect(option.description).to eql(description) end it "knows how to present the switch" do expect(option.formatted_switches).to eql(" -l ") end end context "without a description" do let(:description) { nil } it "knows there is no description" do expect(option.description).to be(nil) end it "knows both inputs are switches" do expect(option.switches).to eql(switches) end end end mercenary-0.4.0/spec/program_spec.rb0000644000004100000410000000065613624570164017507 0ustar www-datawww-data# frozen_string_literal: true require "spec_helper" describe(Mercenary::Program) do context "a basic program" do let(:program) { Mercenary::Program.new(:my_name) } it "can be created with just a name" do expect(program.name).to eql(:my_name) end it "can set its version" do version = Mercenary::VERSION program.version version expect(program.version).to eq(version) end end end mercenary-0.4.0/spec/presenter_spec.rb0000644000004100000410000000407013624570164020041 0ustar www-datawww-data# frozen_string_literal: true require "spec_helper" describe(Mercenary::Presenter) do let(:supercommand) { Mercenary::Command.new(:script_name) } let(:command) { Mercenary::Command.new(:subcommand, supercommand) } let(:presenter) { described_class.new(command) } before(:each) do supercommand.option "version", "-v", "--version", "Show version" supercommand.option "help", "-h", "--help", "Help!" command.version "1.4.2" command.description "Do all the things." command.option "help", "-h", "--help", "Help!" command.option "one", "-1", "--one", "The first option" command.option "two", "-2", "--two", "The second option" command.alias :cmd supercommand.commands[command.name] = command end it "knows how to present the command" do expect(presenter.command_presentation).to eql("script_name subcommand 1.4.2 -- Do all the things.\n\nUsage:\n\n script_name subcommand\n\nOptions:\n -1, --one The first option\n -2, --two The second option\n -v, --version Show version\n -h, --help Help!") end it "knows how to present the subcommands, without duplicates for aliases" do expect(described_class.new(supercommand).subcommands_presentation).to eql(" subcommand, cmd Do all the things.") end it "knows how to present the usage" do expect(presenter.usage_presentation).to eql(" script_name subcommand") end it "knows how to present the options" do expect(presenter.options_presentation).to eql(" -1, --one The first option\n -2, --two The second option\n -v, --version Show version\n -h, --help Help!") end it "allows you to say print_* instead of *_presentation" do expect(presenter.print_usage).to eql(presenter.usage_presentation) expect(presenter.print_subcommands).to eql(presenter.subcommands_presentation) expect(presenter.print_options).to eql(presenter.options_presentation) expect(presenter.print_command).to eql(presenter.command_presentation) end end mercenary-0.4.0/spec/command_spec.rb0000644000004100000410000000537013624570164017454 0ustar www-datawww-data# frozen_string_literal: true require "spec_helper" describe(Mercenary::Command) do context "a basic command" do let(:command) { Mercenary::Command.new(:my_name) } let(:parent) { Mercenary::Command.new(:my_parent) } let(:with_sub) do c = Mercenary::Command.new(:i_have_subcommand) add_sub.call(c) c end let(:command_with_parent) do Mercenary::Command.new( :i_have_parent, parent ) end let(:add_sub) do proc do |c| c.command(:sub_command) { |p| } end end it "can be created with just a name" do expect(command.name).to eql(:my_name) end it "can hold a parent command" do expect(command_with_parent.parent).to eql(parent) end it "can create subcommands" do expect(add_sub.call(command)).to be_a(Mercenary::Command) expect(add_sub.call(command).parent).to eq(command) end it "can set its version" do version = "1.4.2" command.version version expect(command.version).to eq(version) end it "can set its syntax" do syntax_string = "my_name [options]" cmd = described_class.new(:my_name) cmd.syntax syntax_string expect(cmd.syntax).to eq(syntax_string) end it "can set its description" do desc = "run all the things" command.description desc expect(command.description).to eq(desc) end it "can set its options" do name = "show_drafts" opts = ["--drafts", "Render posts in the _drafts folder"] option = Mercenary::Option.new(name, opts) command.option name, *opts expect(command.options).to eql([option]) expect(command.map.values).to include(name) end it "knows its full name" do expect(command_with_parent.full_name).to eql("my_parent i_have_parent") end it "knows its identity" do command_with_parent.version "1.8.7" expect(command_with_parent.identity).to eql("my_parent i_have_parent 1.8.7") end it "raises an ArgumentError if I specify a default_command that isn't there" do c = command # some weird NameError with the block below? expect { c.default_command(:nope) }.to raise_error(ArgumentError) end it "sets the default_command" do expect(with_sub.default_command(:sub_command).name).to eq(:sub_command) end context "with an alias" do before(:each) do command_with_parent.alias(:an_alias) end it "shows the alias in the summary" do expect(command_with_parent.summarize).to eql(" i_have_parent, an_alias ") end it "its names_and_aliases method reports both the name and alias" do expect(command_with_parent.names_and_aliases).to eql("i_have_parent, an_alias") end end end end mercenary-0.4.0/spec/spec_helper.rb0000644000004100000410000000077213624570164017316 0ustar www-datawww-data# frozen_string_literal: true lib = File.expand_path("../lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "mercenary" RSpec.configure do |config| config.run_all_when_everything_filtered = true config.filter_run :focus # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = "random" end mercenary-0.4.0/.rubocop.yml0000644000004100000410000000024513624570164016013 0ustar www-datawww-datainherit_from: .rubocop_todo.yml require: rubocop-jekyll inherit_gem: rubocop-jekyll: .rubocop.yml AllCops: TargetRubyVersion: 2.4 Exclude: - vendor/**/* mercenary-0.4.0/.gitignore0000644000004100000410000000025013624570164015525 0ustar www-datawww-data*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp vendor/bundle mercenary-0.4.0/mercenary.gemspec0000644000004100000410000000222213624570164017070 0ustar www-datawww-data# frozen_string_literal: true lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "mercenary/version" Gem::Specification.new do |spec| spec.name = "mercenary" spec.version = Mercenary::VERSION spec.authors = ["Tom Preston-Werner", "Parker Moore"] spec.email = ["tom@mojombo.com", "parkrmoore@gmail.com"] spec.description = "Lightweight and flexible library for writing command-line apps in Ruby." spec.summary = "Lightweight and flexible library for writing command-line apps in Ruby." spec.homepage = "https://github.com/jekyll/mercenary" spec.license = "MIT" spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) spec.executables = spec.files.grep(%r!^bin/!) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r!^(test|spec|features)/!) spec.require_paths = ["lib"] spec.required_ruby_version = ">= 2.4.0" spec.add_development_dependency "bundler" spec.add_development_dependency "rake" spec.add_development_dependency "rspec", "~> 3.0" spec.add_development_dependency "rubocop-jekyll", "~> 0.10.0" end mercenary-0.4.0/script/0000755000004100000410000000000013624570164015044 5ustar www-datawww-datamercenary-0.4.0/script/console0000755000004100000410000000004713624570164016435 0ustar www-datawww-data#! /bin/bash irb -r./lib/mercenary.rb mercenary-0.4.0/script/examples0000755000004100000410000000061613624570164016613 0ustar www-datawww-data#! /bin/bash set -e function run () { echo "+ ruby ./examples/$@" ruby -e "puts '=' * 79" ruby ./examples/$@ ruby -e "puts '=' * 79" } run logging.rb run logging.rb -v run help_dialogue.rb -h run help_dialogue.rb some_subcommand -h run help_dialogue.rb another_subcommand -h run help_dialogue.rb some_subcommand yet_another_sub -h run help_dialogue.rb some_subcommand yet_another_sub -b mercenary-0.4.0/script/cibuild0000755000004100000410000000010213624570164016376 0ustar www-datawww-data#! /bin/sh set -ex bundle exec rspec script/fmt script/examples mercenary-0.4.0/script/bootstrap0000755000004100000410000000011313624570164017002 0ustar www-datawww-data#! /bin/sh set -e echo "Time to get set up." bundle install echo "Boom." mercenary-0.4.0/script/fmt0000755000004100000410000000013513624570164015557 0ustar www-datawww-data#!/bin/bash echo "Rubocop $(bundle exec rubocop --version)" bundle exec rubocop -S -D -E $@ mercenary-0.4.0/examples/0000755000004100000410000000000013624570164015356 5ustar www-datawww-datamercenary-0.4.0/examples/trace.rb0000755000004100000410000000075113624570164017007 0ustar www-datawww-data#!/usr/bin/env ruby # frozen_string_literal: true $LOAD_PATH.unshift File.join(__dir__, "..", "lib") require "mercenary" # This example sets the logging mode of mercenary to # debug. Logging messages from "p.logger.debug" will # be output to STDOUT. Mercenary.program(:trace) do |p| p.version "2.0.1" p.description "An example of traces in Mercenary" p.syntax "trace " p.action do |_, _| raise ArgumentError, "YOU DID SOMETHING TERRIBLE YOU BUFFOON" end end mercenary-0.4.0/examples/logging.rb0000755000004100000410000000222413624570164017334 0ustar www-datawww-data#!/usr/bin/env ruby # frozen_string_literal: true $LOAD_PATH.unshift File.join(__dir__, "..", "lib") require "mercenary" # This example sets the logging mode of mercenary to # debug. Logging messages from "p.logger.debug" will # be output to STDOUT. Mercenary.program(:logger_output) do |p| p.version "5.2.6" p.description "An example of turning on logging for Mercenary." p.syntax "logger_output" p.logger.info "The default log level is INFO. So this will output." p.logger.debug "Since DEBUG is below INFO, this will not output." p.logger(Logger::DEBUG) p.logger.debug "Logger level now set to DEBUG. So everything will output." p.logger.debug "Example of DEBUG level message." p.logger.info "Example of INFO level message." p.logger.warn "Example of WARN level message." p.logger.error "Example of ERROR level message." p.logger.fatal "Example of FATAL level message." p.logger.unknown "Example of UNKNOWN level message." p.action do |_args, _options| p.logger(Logger::INFO) p.logger.debug "Logger level back to INFO. This line will not output." p.logger.info "This INFO message will output." end end mercenary-0.4.0/examples/help_dialogue.rb0000755000004100000410000000252313624570164020511 0ustar www-datawww-data#!/usr/bin/env ruby # frozen_string_literal: true $LOAD_PATH.unshift File.join(__dir__, "..", "lib") require "mercenary" # This example sets the logging mode of mercenary to # debug. Logging messages from "p.logger.debug" will # be output to STDOUT. Mercenary.program(:help_dialogue) do |p| p.version "2.0.1" p.description "An example of the help dialogue in Mercenary" p.syntax "help_dialogue " p.command(:some_subcommand) do |c| c.version "1.4.2" c.syntax "some_subcommand [options]" c.description "Some subcommand to do something" c.option "an_option", "-o", "--option", "Some option" c.alias(:blah) c.command(:yet_another_sub) do |f| f.syntax "yet_another_sub [options]" f.description "Do amazing things" f.option "blah", "-b", "--blah", "Trigger blah flag" f.option "heh", "-H ARG", "--heh ARG", "Give a heh" f.action do |args, options| print "Args: " p args print "Opts: " p options end end end p.command(:another_subcommand) do |c| c.syntax "another_subcommand [options]" c.description "Another subcommand to do something different." c.option "an_option", "-O", "--option", "Some option" c.option "another_options", "--pluginzzz", "Set where the plugins should be found from" end end mercenary-0.4.0/.rubocop_todo.yml0000644000004100000410000000400513624570164017036 0ustar www-datawww-data# This configuration was generated by # `rubocop --auto-gen-config` # on 2020-01-18 15:09:24 +0100 using RuboCop version 0.71.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 2 Jekyll/NoPAllowed: Exclude: - 'examples/help_dialogue.rb' # Offense count: 2 Jekyll/NoPutsAllowed: Exclude: - 'lib/mercenary/command.rb' # Offense count: 4 # Configuration parameters: AllowSafeAssignment. Lint/AssignmentInCondition: Exclude: - 'lib/mercenary/command.rb' - 'lib/mercenary/option.rb' - 'lib/mercenary/presenter.rb' # Offense count: 2 Lint/DuplicateMethods: Exclude: - 'lib/mercenary/command.rb' # Offense count: 1 # Configuration parameters: AllowComments. Lint/HandleExceptions: Exclude: - 'lib/mercenary/option.rb' # Offense count: 1 Lint/UselessAssignment: Exclude: - 'lib/mercenary/presenter.rb' # Offense count: 1 Metrics/AbcSize: Max: 25 # Offense count: 1 # Configuration parameters: CountComments, ExcludedMethods. # ExcludedMethods: refine Metrics/BlockLength: Max: 28 # Offense count: 6 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https Metrics/LineLength: Max: 319 # Offense count: 1 # Configuration parameters: CountComments, ExcludedMethods. Metrics/MethodLength: Max: 24 # Offense count: 1 # Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist, MethodDefinitionMacros. # NamePrefix: is_, has_, have_ # NamePrefixBlacklist: is_, has_, have_ # NameWhitelist: is_a? # MethodDefinitionMacros: define_method, define_singleton_method Naming/PredicateName: Exclude: - 'spec/**/*' - 'lib/mercenary/command.rb' # Offense count: 1 Style/MissingRespondToMissing: Exclude: - 'lib/mercenary/presenter.rb' mercenary-0.4.0/Rakefile0000644000004100000410000000007313624570164015205 0ustar www-datawww-data# frozen_string_literal: true require "bundler/gem_tasks" mercenary-0.4.0/lib/0000755000004100000410000000000013624570164014306 5ustar www-datawww-datamercenary-0.4.0/lib/mercenary.rb0000644000004100000410000000120413624570164016615 0ustar www-datawww-data# frozen_string_literal: true require File.expand_path("mercenary/version", __dir__) require "optparse" require "logger" module Mercenary autoload :Command, File.expand_path("mercenary/command", __dir__) autoload :Option, File.expand_path("mercenary/option", __dir__) autoload :Presenter, File.expand_path("mercenary/presenter", __dir__) autoload :Program, File.expand_path("mercenary/program", __dir__) # Public: Instantiate a new program and execute. # # name - the name of your program # # Returns nothing. def self.program(name) program = Program.new(name) yield program program.go(ARGV) end end mercenary-0.4.0/lib/mercenary/0000755000004100000410000000000013624570164016273 5ustar www-datawww-datamercenary-0.4.0/lib/mercenary/version.rb0000644000004100000410000000011013624570164020275 0ustar www-datawww-data# frozen_string_literal: true module Mercenary VERSION = "0.4.0" end mercenary-0.4.0/lib/mercenary/option.rb0000644000004100000410000000512213624570164020130 0ustar www-datawww-data# frozen_string_literal: true module Mercenary class Option attr_reader :config_key, :description, :short, :long, :return_type # Public: Create a new Option # # config_key - the key in the config hash to which the value of this option # will map # info - an array containing first the switches, then an optional # return type (e.g. Array), then a description of the option # # Returns nothing def initialize(config_key, info) @config_key = config_key while arg = info.shift begin @return_type = Object.const_get(arg.to_s) next rescue NameError end if arg.start_with?("-") if arg.start_with?("--") @long = arg else @short = arg end next end @description = arg end end # Public: Fetch the array containing the info OptionParser is interested in # # Returns the array which OptionParser#on wants def for_option_parser [short, long, return_type, description].flatten.reject { |o| o.to_s.empty? } end # Public: Build a string representation of this option including the # switches and description # # Returns a string representation of this option def to_s "#{formatted_switches} #{description}" end # Public: Build a beautifully-formatted string representation of the switches # # Returns a formatted string representation of the switches def formatted_switches [ switches.first.rjust(10), switches.last.ljust(13), ].join(", ").gsub(%r! , !, " ").gsub(%r!, !, " ") end # Public: Hash based on the hash value of instance variables # # Returns a Fixnum which is unique to this Option based on the instance variables def hash instance_variables.map do |var| instance_variable_get(var).hash end.reduce(:^) end # Public: Check equivalence of two Options based on equivalence of their # instance variables # # Returns true if all the instance variables are equal, false otherwise def eql?(other) return false unless self.class.eql?(other.class) instance_variables.map do |var| instance_variable_get(var).eql?(other.instance_variable_get(var)) end.all? end # Public: Fetch an array of switches, including the short and long versions # # Returns an array of two strings. An empty string represents no switch in # that position. def switches [short, long].map(&:to_s) end end end mercenary-0.4.0/lib/mercenary/command.rb0000644000004100000410000001757213624570164020252 0ustar www-datawww-data# frozen_string_literal: false module Mercenary class Command attr_reader :name attr_reader :description attr_reader :syntax attr_accessor :options attr_accessor :commands attr_accessor :actions attr_reader :map attr_accessor :parent attr_reader :trace attr_reader :aliases # Public: Creates a new Command # # name - the name of the command # parent - (optional) the instancce of Mercenary::Command which you wish to # be the parent of this command # # Returns nothing def initialize(name, parent = nil) @name = name @options = [] @commands = {} @actions = [] @map = {} @parent = parent @trace = false @aliases = [] end # Public: Sets or gets the command version # # version - the command version (optional) # # Returns the version and sets it if an argument is non-nil def version(version = nil) @version = version if version @version end # Public: Sets or gets the syntax string # # syntax - the string which describes this command's usage syntax (optional) # # Returns the syntax string and sets it if an argument is present def syntax(syntax = nil) @syntax = syntax if syntax syntax_list = [] syntax_list << parent.syntax.to_s.gsub(%r!<[\w\s-]+>!, "").gsub(%r!\[[\w\s-]+\]!, "").strip if parent syntax_list << (@syntax || name.to_s) syntax_list.join(" ") end # Public: Sets or gets the command description # # description - the description of what the command does (optional) # # Returns the description and sets it if an argument is present def description(desc = nil) @description = desc if desc @description end # Public: Sets the default command # # command_name - the command name to be executed in the event no args are # present # # Returns the default command if there is one, `nil` otherwise def default_command(command_name = nil) if command_name if commands.key?(command_name) @default_command = commands[command_name] if command_name @default_command else raise ArgumentError, "'#{command_name}' couldn't be found in this command's list of commands." end else @default_command end end # Public: Adds an option switch # # sym - the variable key which is used to identify the value of the switch # at runtime in the options hash # # Returns nothing def option(sym, *options) new_option = Option.new(sym, options) @options << new_option @map[new_option] = sym end # Public: Adds a subcommand # # cmd_name - the name of the command # block - a block accepting the new instance of Mercenary::Command to be # modified (optional) # # Returns nothing def command(cmd_name) cmd = Command.new(cmd_name, self) yield cmd @commands[cmd_name] = cmd end # Public: Add an alias for this command's name to be attached to the parent # # cmd_name - the name of the alias # # Returns nothing def alias(cmd_name) logger.debug "adding alias to parent for self: '#{cmd_name}'" aliases << cmd_name @parent.commands[cmd_name] = self end # Public: Add an action Proc to be executed at runtime # # block - the Proc to be executed at runtime # # Returns nothing def action(&block) @actions << block end # Public: Fetch a Logger (stdlib) # # level - the logger level (a Logger constant, see docs for more info) # # Returns the instance of Logger def logger(level = nil) unless @logger @logger = Logger.new(STDOUT) @logger.level = level || Logger::INFO @logger.formatter = proc do |severity, _datetime, _progname, msg| "#{identity} | " << "#{severity.downcase.capitalize}:".ljust(7) << " #{msg}\n" end end @logger.level = level unless level.nil? @logger end # Public: Run the command # # argv - an array of string args # opts - the instance of OptionParser # config - the output config hash # # Returns the command to be executed def go(argv, opts, config) opts.banner = "Usage: #{syntax}" process_options(opts, config) add_default_options(opts) if argv[0] && cmd = commands[argv[0].to_sym] logger.debug "Found subcommand '#{cmd.name}'" argv.shift cmd.go(argv, opts, config) else logger.debug "No additional command found, time to exec" self end end # Public: Add this command's options to OptionParser and set a default # action of setting the value of the option to the inputted hash # # opts - instance of OptionParser # config - the Hash in which the option values should be placed # # Returns nothing def process_options(opts, config) options.each do |option| opts.on(*option.for_option_parser) do |x| config[map[option]] = x end end end # Public: Add version and help options to the command # # opts - instance of OptionParser # # Returns nothing def add_default_options(opts) option "show_help", "-h", "--help", "Show this message" option "show_version", "-v", "--version", "Print the name and version" option "show_backtrace", "-t", "--trace", "Show the full backtrace when an error occurs" opts.on("-v", "--version", "Print the version") do puts "#{name} #{version}" exit(0) end opts.on("-t", "--trace", "Show full backtrace if an error occurs") do @trace = true end opts.on_tail("-h", "--help", "Show this message") do puts self exit end end # Public: Execute all actions given the inputted args and options # # argv - (optional) command-line args (sans opts) # config - (optional) the Hash configuration of string key to value # # Returns nothing def execute(argv = [], config = {}) if actions.empty? && !default_command.nil? default_command.execute else actions.each { |a| a.call(argv, config) } end end # Public: Check if this command has a subcommand # # sub_command - the name of the subcommand # # Returns true if this command is the parent of a command of name # 'sub_command' and false otherwise def has_command?(sub_command) commands.key?(sub_command) end # Public: Identify this command # # Returns a string which identifies this command def ident "" end # Public: Get the full identity (name & version) of this command # # Returns a string containing the name and version if it exists def identity "#{full_name} #{version}".strip end # Public: Get the name of the current command plus that of # its parent commands # # Returns the full name of the command def full_name the_name = [] the_name << parent.full_name if parent&.full_name the_name << name the_name.join(" ") end # Public: Return all the names and aliases for this command. # # Returns a comma-separated String list of the name followed by its aliases def names_and_aliases ([name.to_s] + aliases).compact.join(", ") end # Public: Build a string containing a summary of the command # # Returns a one-line summary of the command. def summarize " #{names_and_aliases.ljust(20)} #{description}" end # Public: Build a string containing the command name, options and any subcommands # # Returns the string identifying this command, its options and its subcommands def to_s Presenter.new(self).print_command end end end mercenary-0.4.0/lib/mercenary/program.rb0000644000004100000410000000225613624570164020274 0ustar www-datawww-data# frozen_string_literal: true module Mercenary class Program < Command attr_reader :optparse attr_reader :config # Public: Creates a new Program # # name - the name of the program # # Returns nothing def initialize(name) @config = {} super(name) end # Public: Run the program # # argv - an array of string args (usually ARGV) # # Returns nothing def go(argv) logger.debug("Using args passed in: #{argv.inspect}") cmd = nil @optparse = OptionParser.new do |opts| cmd = super(argv, opts, @config) end begin @optparse.parse!(argv) rescue OptionParser::InvalidOption => e logger.error "Whoops, we can't understand your command." logger.error e.message.to_s logger.error "Run your command again with the --help switch to see available options." abort end logger.debug("Parsed config: #{@config.inspect}") begin cmd.execute(argv, @config) rescue StandardError => e if cmd.trace raise e else logger.error e.message abort end end end end end mercenary-0.4.0/lib/mercenary/presenter.rb0000644000004100000410000000570613624570164020637 0ustar www-datawww-data# frozen_string_literal: true module Mercenary class Presenter attr_accessor :command # Public: Make a new Presenter # # command - a Mercenary::Command to present # # Returns nothing def initialize(command) @command = command end # Public: Builds a string representation of the command usage # # Returns the string representation of the command usage def usage_presentation " #{command.syntax}" end # Public: Builds a string representation of the options # # Returns the string representation of the options def options_presentation return nil unless command_options_presentation || parent_command_options_presentation [command_options_presentation, parent_command_options_presentation].compact.join("\n") end def command_options_presentation return nil if command.options.empty? options = command.options options -= command.parent.options if command.parent options.map(&:to_s).join("\n") end # Public: Builds a string representation of the options for parent # commands # # Returns the string representation of the options for parent commands def parent_command_options_presentation return nil unless command.parent Presenter.new(command.parent).options_presentation end # Public: Builds a string representation of the subcommands # # Returns the string representation of the subcommands def subcommands_presentation return nil if command.commands.empty? command.commands.values.uniq.map(&:summarize).join("\n") end # Public: Builds the command header, including the command identity and description # # Returns the command header as a String def command_header header = command.identity.to_s header << " -- #{command.description}" if command.description header end # Public: Builds a string representation of the whole command # # Returns the string representation of the whole command def command_presentation msg = [] msg << command_header msg << "Usage:" msg << usage_presentation if opts = options_presentation msg << "Options:\n#{opts}" end if subcommands = subcommands_presentation msg << "Subcommands:\n#{subcommands_presentation}" end msg.join("\n\n") end # Public: Turn a print_* into a *_presentation or freak out # # meth - the method being called # args - an array of arguments passed to the missing method # block - the block passed to the missing method # # Returns the value of whatever function is called def method_missing(meth, *args, &block) if meth.to_s =~ %r!^print_(.+)$! send("#{Regexp.last_match(1).downcase}_presentation") else # You *must* call super if you don't handle the method, # otherwise you'll mess up Ruby's method lookup. super end end end end mercenary-0.4.0/History.markdown0000644000004100000410000000507113624570164016750 0ustar www-datawww-data## 0.4.0 / 2020-01-18 ### Major Enhancements * Drop Ruby 2.3 support ### Minor Enhancements * Remove parent command's flags from subcommand usage (#44) ### Development Fixes * Adopt Jekyll's rubocop config for consistency ### Documentation * fixes the readme (#52) ## 0.3.6 / 2016-04-07 ### Bug Fixes * Presenter: Options should include those from parent command (#42) ## 0.3.5 / 2014-11-12 ### Bug Fixes * Capture `OptionsParser::InvalidOption` and show a nice error message (#38) * Absolute paths for requires and autoloads (#39) ### Development Fixes * Bump to RSpec 3 (#40) ## 0.3.4 / 2014-07-11 ### Bug Fixes * Use option object as key in the command's `@map` hash (#35) ## 0.3.3 / 2014-05-07 ### Bug Fixes * The `--version` flag should not exit with code 1, but instead code 0. (#33) ## 0.3.2 / 2014-03-18 ### Bug Fixes * Remove duplicate commands from help output; show aliases w/command names (#29) ## 0.3.1 / 2014-02-21 ### Minor Enhancements * Add `-t/--trace` to list of options in help message (#19) ### Bug Fixes * `Mercenary::Option` now accepts return values in the form of Class constants (#22) ## 0.3.0 / 2014-02-20 ### Major Enhancements * Officially drop 1.8.7 support (#14) * Allow Commands to set their own versions (#17) * Show subcommands, options and usage in help and attach to all commands (#18) * Add `-t, --trace` to allow full exception backtrace to print, otherwise print just the error message (#19) ### Minor Enhancements * Logging state is maintained throughout process (#12) * Tidy up Command#logger output (#21) ### Development Fixes * Added specs for `Program` (#13) ## 0.2.1 / 2013-12-25 ### Bug Fixes * Added missing comma to fix '-v' and '--version' options (#9) ## 0.2.0 / 2013-11-30 ### Major Enhancements * Add `Command#default_command` to specify a default command if none is given by the user at runtime (#7) ### Minor Enhancements * Add `Command#execute` to execute the actions of a command (#6) ### Development Fixes * Add standard GitHub bootstrap and cibuild scripts to `script/` (#2) ## 0.1.0 / 2013-11-08 ### Major Enhancements * It works! ### Minor Enhancements * Add a logger to `Command` * Add `--version` switch to all programs ### Bug Fixes * Fix `Command#syntax` and `Command#description`'s handing of setting vs getting * Fix load path problem in `lib/mercenary.rb` ### Development Fixes * Add TomDoc to everything * Add a couple starter specs * Add TravisCI badge * Add Travis configuration ## 0.0.1 / 2013-11-06 * Birthday! mercenary-0.4.0/Gemfile0000644000004100000410000000017513624570164015036 0ustar www-datawww-data# frozen_string_literal: true source "https://rubygems.org" # Specify your gem's dependencies in mercenary.gemspec gemspec mercenary-0.4.0/LICENSE.txt0000644000004100000410000000212413624570164015362 0ustar www-datawww-dataCopyright (c) 2013-present Parker Moore and the mercenary contributors MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.