Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
+18
View File
@@ -0,0 +1,18 @@
*.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
+2
View File
@@ -0,0 +1,2 @@
--color
--format progress
+10
View File
@@ -0,0 +1,10 @@
inherit_from: .rubocop_todo.yml
require: rubocop-jekyll
inherit_gem:
rubocop-jekyll: .rubocop.yml
AllCops:
TargetRubyVersion: 2.4
Exclude:
- vendor/**/*
@@ -0,0 +1,79 @@
# 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'
+15
View File
@@ -0,0 +1,15 @@
language: 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
+6
View File
@@ -0,0 +1,6 @@
# frozen_string_literal: true
source "https://rubygems.org"
# Specify your gem's dependencies in mercenary.gemspec
gemspec
+127
View File
@@ -0,0 +1,127 @@
## 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!
+22
View File
@@ -0,0 +1,22 @@
Copyright (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.
+249
View File
@@ -0,0 +1,249 @@
# 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 <subcommand> [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 <SUBCOMMAND> [OPTIONS]"`
When a parent command exists, say `supercommand`, with syntax set as
`supercommand <SUBCOMMAND> [OPTIONS]`, the syntax of the command in question
will be `supercommand command_name <SUBCOMMAND> [OPTIONS]` with both
`<SUBCOMMAND>` 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
+3
View File
@@ -0,0 +1,3 @@
# frozen_string_literal: true
require "bundler/gem_tasks"
+45
View File
@@ -0,0 +1,45 @@
#!/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 <subcommand>"
p.command(:some_subcommand) do |c|
c.version "1.4.2"
c.syntax "some_subcommand <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 <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
+35
View File
@@ -0,0 +1,35 @@
#!/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
+20
View File
@@ -0,0 +1,20 @@
#!/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 <subcommand>"
p.action do |_, _|
raise ArgumentError, "YOU DID SOMETHING TERRIBLE YOU BUFFOON"
end
end
+23
View File
@@ -0,0 +1,23 @@
# 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
@@ -0,0 +1,281 @@
# 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
"<Command name=#{identity}>"
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
@@ -0,0 +1,89 @@
# 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
@@ -0,0 +1,103 @@
# 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
@@ -0,0 +1,55 @@
# 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
@@ -0,0 +1,5 @@
# frozen_string_literal: true
module Mercenary
VERSION = "0.4.0"
end
@@ -0,0 +1,28 @@
# 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
+7
View File
@@ -0,0 +1,7 @@
#! /bin/sh
set -e
echo "Time to get set up."
bundle install
echo "Boom."
+7
View File
@@ -0,0 +1,7 @@
#! /bin/sh
set -ex
bundle exec rspec
script/fmt
script/examples
+3
View File
@@ -0,0 +1,3 @@
#! /bin/bash
irb -r./lib/mercenary.rb
+18
View File
@@ -0,0 +1,18 @@
#! /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
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
echo "Rubocop $(bundle exec rubocop --version)"
bundle exec rubocop -S -D -E $@
@@ -0,0 +1,98 @@
# 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
@@ -0,0 +1,84 @@
# 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
@@ -0,0 +1,45 @@
# 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
@@ -0,0 +1,19 @@
# 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
@@ -0,0 +1,16 @@
# 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