-
# frozen_string_literal: true
-
-
1
require "zeitwerk"
-
-
1
Zeitwerk::Loader.new.then do |loader|
-
1
loader.ignore "#{__dir__}/sod/types"
-
1
loader.tag = File.basename __FILE__, ".rb"
-
1
loader.push_dir __dir__
-
1
loader.setup
-
end
-
-
# Main namespace.
-
1
module Sod
-
1
def self.loader registry = Zeitwerk::Registry
-
4
@loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
-
end
-
-
1
def self.new(...) = Shell.new(...)
-
end
-
# frozen_string_literal: true
-
-
1
require "forwardable"
-
-
1
module Sod
-
# A generic action (and DSL) from which to inherit and build custom actions from.
-
# :reek:TooManyInstanceVariables
-
1
class Action
-
1
extend Forwardable
-
-
1
def self.inherited descendant
-
82
super
-
164
descendant.class_eval { @attributes = {} }
-
end
-
-
1
def self.description text
-
14
then: 1
else: 13
@description ? fail(Error, "Description can only be defined once.") : @description = text
-
end
-
-
1
def self.ancillary(*lines)
-
10
then: 1
else: 9
@ancillary ? fail(Error, "Ancillary can only be defined once.") : @ancillary = lines
-
end
-
-
1
def self.on(aliases, **keywords)
-
81
then: 1
else: 80
fail Error, "On can only be defined once." if @attributes.any?
-
-
80
@attributes.merge! keywords, aliases: Array(aliases)
-
end
-
-
1
def self.default &block
-
28
then: 1
else: 27
@default ? fail(Error, "Default can only be defined once.") : @default = block
-
end
-
-
1
delegate [*Models::Action.members, :handle, :to_a, :to_h] => :record
-
-
1
attr_reader :record
-
-
1
def initialize context: Context::EMPTY, model: Models::Action
-
261
klass = self.class
-
-
261
@context = context
-
-
261
@record = model[
-
**klass.instance_variable_get(:@attributes),
-
description: load(:description),
-
ancillary: Array(load(:ancillary)).compact,
-
default: load_default
-
]
-
-
261
verify_aliases
-
260
verify_argument
-
end
-
-
1
def call(*)
-
2
fail NoMethodError,
-
"`#{self.class}##{__method__} #{method(__method__).parameters}` must be implemented."
-
end
-
-
1
def inspect
-
24
attributes = record.to_h.map { |key, value| "#{key}=#{value.inspect}" }
-
3
%(#<#{self.class}:#{object_id} @context=#{context.inspect} #{attributes.join ", "}>)
-
end
-
-
1
def to_proc = method(:call).to_proc
-
-
1
protected
-
-
1
attr_reader :context
-
-
1
private
-
-
1
def verify_aliases
-
261
else: 260
then: 1
fail Error, "Aliases must be defined." unless aliases
-
end
-
-
1
def verify_argument
-
260
else: 1
then: 259
return unless argument && !argument.start_with?("[") && default
-
-
1
fail Error, "Required argument can't be used with default."
-
end
-
-
1
def load attribute
-
522
klass = self.class
-
522
fallback = klass.instance_variable_get(:@attributes)[attribute]
-
-
522
klass.instance_variable_get(:"@#{attribute}") || fallback
-
end
-
-
1
def load_default
-
261
klass = self.class
-
261
fallback = klass.instance_variable_get(:@attributes)[:default].method :itself
-
-
261
(klass.instance_variable_get(:@default) || fallback).call
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "forwardable"
-
-
1
module Sod
-
# A generic command (and DSL) from which to inherit and build custom commands from.
-
# :reek:TooManyInstanceVariables
-
1
class Command
-
1
extend Forwardable
-
-
1
include Dependencies[:logger]
-
-
1
def self.inherited descendant
-
61
super
-
122
descendant.class_eval { @actions = Set.new }
-
end
-
-
1
def self.handle text
-
59
then: 1
else: 58
@handle ? fail(Error, "Handle can only be defined once.") : @handle = text
-
end
-
-
1
def self.description text
-
23
then: 1
else: 22
@description ? fail(Error, "Description can only be defined once.") : @description = text
-
end
-
-
1
def self.ancillary(*lines)
-
14
then: 1
else: 13
@ancillary ? fail(Error, "Ancillary can only be defined once.") : @ancillary = lines
-
end
-
-
1
def self.on(action, *positionals, **keywords) = @actions.add [action, positionals, keywords]
-
-
1
delegate Models::Command.members => :record
-
-
1
attr_reader :record
-
-
1
def initialize(context: Context::EMPTY, model: Models::Command, **)
-
62
super(**)
-
62
@context = context
-
62
@record = build_record model
-
-
61
verify_handle
-
end
-
-
1
def call
-
2
logger.debug { "`#{self.class}##{__method__}}` called without implementation. Skipped." }
-
end
-
-
1
def inspect
-
2
attributes = record.to_h
-
10
.map { |key, value| "#{key}=#{value.inspect}" }
-
.join ", "
-
-
2
"#<#{self.class}:#{object_id} @logger=#{logger.inspect} @context=#{context.inspect} " \
-
"#{attributes}>"
-
end
-
-
1
protected
-
-
1
attr_reader :context
-
-
1
private
-
-
1
def build_record model
-
62
klass = self.class
-
-
62
model[
-
handle: klass.instance_variable_get(:@handle),
-
description: klass.instance_variable_get(:@description),
-
ancillary: Array(klass.instance_variable_get(:@ancillary)).compact,
-
actions: Set[*build_actions],
-
operation: method(:call)
-
]
-
end
-
-
1
def build_actions
-
62
self.class.instance_variable_get(:@actions).map do |action, positionals, keywords|
-
64
action.new(*positionals, **keywords.merge!(context:))
-
end
-
end
-
-
1
def verify_handle
-
61
else: 59
then: 2
fail Error, "Invalid handle: #{handle.inspect}. Must be a string." unless handle in String
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "cogger"
-
1
require "containable"
-
1
require "optparse"
-
1
require "tone"
-
-
1
module Sod
-
# The primary container.
-
1
module Container
-
1
extend Containable
-
-
2
register(:client) { OptionParser.new nil, 40, " " }
-
2
register(:color) { Tone.new }
-
2
register(:logger) { Cogger.new id: :sod }
-
1
register :kernel, Kernel
-
1
register :io, STDOUT
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sod
-
# Provides a sharable, read-only, context for commands and actions.
-
1
class Context
-
1
EMPTY = new.freeze
-
-
1
def self.[](...) = new(...)
-
-
1
def initialize **attributes
-
16
@attributes = attributes
-
end
-
-
1
def [] override, fallback
-
80
override || public_send(fallback)
-
rescue NoMethodError
-
4
raise Error, "Invalid context. Override or fallback (#{fallback.inspect}) values are missing."
-
end
-
-
1
def to_h = attributes.dup
-
-
15
then: 6
else: 8
def method_missing(name, *) = respond_to_missing?(name) ? attributes[name] : super
-
-
1
private
-
-
1
attr_reader :attributes
-
-
1
def respond_to_missing? name, include_private = false
-
14
(attributes && attributes.key?(name)) || super
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "infusible"
-
-
1
module Sod
-
1
Dependencies = Infusible[Container]
-
end
-
# frozen_string_literal: true
-
-
1
module Sod
-
# The namespaced root of all errors for this gem.
-
1
class Error < StandardError
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sod
-
1
module Graph
-
# Loads and decorates option parsers within graph.
-
1
class Loader
-
1
include Dependencies[:client]
-
-
1
using Refines::OptionParser
-
-
1
def initialize(graph, **)
-
36
super(**)
-
36
@graph = graph
-
36
@registry = {}
-
end
-
-
1
def call
-
36
registry.clear
-
36
load graph
-
84
graph.children.each { |child| visit child, child.handle }
-
36
registry
-
end
-
-
1
private
-
-
1
attr_reader :graph, :registry
-
-
1
def visit command, key = ""
-
74
load command, key
-
100
command.children.each { |child| visit child, "#{key} #{child.handle}".strip }
-
end
-
-
1
def load node, key = ""
-
110
parser = client.replicate
-
256
node.actions.each { |action| parser.on(*action.to_a, action.to_proc) }
-
110
registry[key] = [parser, node]
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/array"
-
-
1
module Sod
-
# :reek:DataClump
-
1
module Graph
-
# A generic graph node (and DSL) from which to build multiple lineages with.
-
1
Node = Struct.new :handle, :description, :ancillary, :actions, :operation, :children do
-
1
using Refinements::Array
-
-
1
def initialize(**)
-
220
super
-
220
self[:actions] = Set.new actions
-
220
self[:children] = Set.new children
-
220
self[:ancillary] = Array ancillary
-
220
@depth = 0
-
220
@lineage = []
-
end
-
-
1
def get_action *lineage
-
15
handle = lineage.pop
-
39
get_actions(*lineage).find { |action| action.handle.include? handle }
-
end
-
-
# :reek:DataClump
-
1
def get_actions *lineage, node: self
-
22
then: 17
else: 5
lineage.empty? ? node.actions : get(lineage, node, __method__)
-
end
-
-
417
then: 273
else: 143
def get_child(*lineage, node: self) = lineage.empty? ? node : get(lineage, node, __method__)
-
-
1
def on(object, *, **, &)
-
267
then: 171
else: 96
lineage.clear if depth.zero?
-
-
267
process(object, *, **)
-
264
visit(&)
-
264
self
-
end
-
-
21
then: 2
else: 18
def call = (operation.call if operation)
-
-
1
private
-
-
1
attr_reader :lineage
-
-
1
attr_accessor :depth
-
-
1
def process(object, *, **)
-
267
then: 178
else: 89
ancestry = object.is_a?(Class) ? object.ancestors : []
-
-
267
then: 44
if ancestry.include? Command
-
44
else: 223
add_child(*lineage, self.class[**object.new(*, **).record.to_h])
-
223
then: 89
elsif object.is_a? String
-
89
else: 134
add_inline_command(object, *, **)
-
134
then: 133
elsif ancestry.include? Action
-
133
add_action(*lineage, object.new(*, **))
-
else: 1
else
-
1
fail Error, "Invalid command or action. Unable to add: #{object.inspect}."
-
end
-
end
-
-
1
def add_inline_command handle, *positionals
-
89
description, *ancillary = positionals
-
-
89
else: 88
then: 1
fail Error, <<~CONTENT unless handle && description
-
Unable to add command. Invalid handle or description (both are required):
-
- Handle: #{handle.inspect}
-
- Description: #{description.inspect}
-
CONTENT
-
-
88
add_child(*lineage, self.class[handle:, description:, ancillary: ancillary.compact])
-
end
-
-
1
def add_child *lineage
-
132
node = lineage.pop
-
132
handle = node.handle
-
132
tracked_lineage = self.lineage
-
-
132
add lineage[...depth], node, :children
-
132
tracked_lineage.replace_at depth, handle
-
end
-
-
1
def add_action(*lineage) = add lineage, lineage.pop, :actions
-
-
1
def add lineage, node, message
-
528
get_child(*lineage).then { |child| child.public_send(message).add node }
-
end
-
-
1
def get lineage, node, message
-
148
handle = lineage.shift
-
382
node = node.children.find { |child| child.handle == handle }
-
-
148
else: 143
then: 5
fail Error, "Unable to find command or action: #{handle.inspect}." unless node
-
-
143
public_send(message, *lineage, node:)
-
end
-
-
1
def visit &block
-
264
increment
-
264
then: 64
else: 200
instance_eval(&block) if block
-
264
decrement
-
end
-
-
1
def increment = self.depth += 1
-
-
1
def decrement = self.depth -= 1
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sod
-
1
module Graph
-
# Runs the appropriate parser for given command line arguments.
-
1
class Runner
-
1
include Dependencies[:client, :logger]
-
-
1
using Refines::OptionParser
-
-
1
HELP_PATTERN = /
-
\A # Start of string.
-
-h # Short alias.
-
| # Or.
-
--help # Long alias.
-
\Z # End of string.
-
/x
-
-
# rubocop:todo Metrics/ParameterLists
-
1
def initialize(graph, help_pattern: HELP_PATTERN, loader: Loader, **)
-
28
super(**)
-
28
@graph = graph
-
28
@registry = loader.new(graph).call
-
28
@help_pattern = help_pattern
-
28
@lineage = +""
-
end
-
# rubocop:enable Metrics/ParameterLists
-
-
# :reek:DuplicateMethodCall
-
# :reek:TooManyStatements
-
1
def call arguments = ARGV
-
24
lineage.clear
-
24
visit arguments.dup
-
rescue OptionParser::ParseError => error
-
3
log_error error.message
-
rescue Sod::Error => error
-
2
log_error error.message
-
2
help
-
end
-
-
1
private
-
-
1
attr_reader :graph, :registry, :help_pattern, :lineage
-
-
# :reek:TooManyStatements
-
1
def visit arguments
-
78
then: 10
if arguments.empty? || arguments.any? { |argument| argument.match? help_pattern }
-
10
usage(*arguments)
-
else: 24
else
-
24
parser, node = registry.fetch lineage, client
-
24
alter_callback_for parser
-
-
24
parser.order! arguments, command: node do |command|
-
10
lineage.concat(" ", command).tap(&:strip!)
-
10
visit arguments
-
end
-
end
-
end
-
-
# :reek:FeatureEnvy
-
1
def alter_callback_for parser
-
24
parser.define_singleton_method :callback! do |function, max_arity, value|
-
11
then: 5
else: 6
return function.call if function.arity == -1 && !value
-
-
6
super(function, max_arity, value)
-
end
-
end
-
-
1
def usage(*arguments)
-
10
commands = arguments.grep_v help_pattern
-
10
then: 8
else: 2
commands = lineage.split if commands.empty?
-
10
help(*commands)
-
end
-
-
1
def help(*commands)
-
24
then: 11
else: 1
graph.get_action("help").then { |action| action.call(*commands) if action }
-
end
-
-
6
def log_error(message) = logger.error { message.capitalize }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/array"
-
-
1
module Sod
-
1
module Models
-
# Defines all attributes of an action.
-
1
Action = Data.define(
-
:aliases,
-
:argument,
-
:type,
-
:allow,
-
:default,
-
:description,
-
:ancillary
-
) do
-
1
using Refinements::Array
-
-
1
def initialize aliases: nil,
-
argument: nil,
-
type: nil,
-
allow: nil,
-
default: nil,
-
description: nil,
-
ancillary: nil
-
287
super
-
end
-
-
1
def handle = [Array(aliases).join(", "), argument].tap(&:compact!).join " "
-
-
1
def to_a = [*handles, type, allow, description, *ancillary].tap(&:compress!)
-
-
1
private
-
-
197
def handles = Array(aliases).map { |item| [item, argument].tap(&:compact!).join " " }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sod
-
1
module Models
-
# Defines all attributes of a command.
-
1
Command = Data.define :handle, :description, :ancillary, :actions, :operation do
-
1
def initialize handle:, description:, actions:, operation:, ancillary: []
-
62
super
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/pathname"
-
-
1
module Sod
-
1
module Prefabs
-
1
module Actions
-
1
module Config
-
# Creates project configuration.
-
1
class Create < Action
-
1
include Dependencies[:kernel, :logger]
-
-
1
using Refinements::Pathname
-
-
1
description "Create default configuration."
-
-
1
ancillary "Prompts for local or global path."
-
-
1
on %w[-c --create]
-
-
1
def initialize(path = nil, xdg_config: nil, **)
-
11
super(**)
-
11
@xdg_config = context[xdg_config, :xdg_config]
-
10
@path = Pathname context[path, :defaults_path]
-
end
-
-
1
def call(*)
-
10
ARGV.clear
-
10
valid_defaults? && choose
-
end
-
-
1
private
-
-
1
attr_reader :path, :xdg_config
-
-
1
def valid_defaults?
-
10
then: 9
else: 1
return true if path.exist?
-
-
1
logger.abort "Default configuration doesn't exist: #{path.to_s.inspect}."
-
1
false
-
end
-
-
1
def choose
-
9
kernel.print "Would you like to create (g)lobal, (l)ocal, or (n)o configuration? " \
-
"(g/l/n)? "
-
9
response = kernel.gets.chomp
-
-
9
when: 3
case response
-
3
when: 3
when "g" then create xdg_config.global
-
3
else: 3
when "l" then create xdg_config.local
-
3
else quit
-
end
-
end
-
-
# :reek:TooManyStatements
-
1
def create xdg_path
-
6
path_info = xdg_path.to_s.inspect
-
-
8
then: 2
else: 4
return logger.warn { "Skipped. Configuration exists: #{path_info}." } if xdg_path.exist?
-
-
4
path.copy xdg_path.make_ancestors
-
8
logger.info { "Created: #{path_info}." }
-
end
-
-
1
def quit
-
6
logger.info { "Creation canceled." }
-
3
kernel.exit
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/pathname"
-
1
require "refinements/string"
-
-
1
module Sod
-
1
module Prefabs
-
1
module Actions
-
1
module Config
-
# Deletes project configuration.
-
1
class Delete < Action
-
1
include Dependencies[:kernel, :logger]
-
-
1
using Refinements::Pathname
-
1
using Refinements::String
-
-
1
description "Delete project configuration."
-
-
1
ancillary "Prompts for confirmation."
-
-
1
on %w[-d --delete]
-
-
# :reek:ControlParameter
-
1
def initialize(path = nil, **)
-
6
super(**)
-
6
@path = Pathname(path || context.xdg_config.active)
-
end
-
-
1
def call(*)
-
5
ARGV.clear
-
-
5
then: 4
else: 1
return confirm if path.exist?
-
-
2
logger.warn { "Skipped. Configuration doesn't exist: #{path_info}." }
-
end
-
-
1
private
-
-
1
attr_reader :path
-
-
1
def confirm
-
4
kernel.print "Are you sure you want to delete #{path_info} (y/n)? "
-
-
4
then: 2
if kernel.gets.chomp.truthy?
-
2
path.delete
-
2
info "Deleted: #{path_info}."
-
else: 2
else
-
2
info "Skipped: #{path_info}."
-
end
-
end
-
-
1
def path_info = path.to_s.inspect
-
-
5
def info(message) = logger.info { message }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/pathname"
-
-
1
module Sod
-
1
module Prefabs
-
1
module Actions
-
1
module Config
-
# Edits project configuration.
-
1
class Edit < Action
-
1
include Dependencies[:kernel, :logger]
-
-
1
using Refinements::Pathname
-
-
1
description "Edit project configuration."
-
-
1
on %w[-e --edit]
-
-
# :reek:ControlParameter
-
1
def initialize(path = nil, **)
-
17
super(**)
-
17
@path = Pathname(path || context.xdg_config.active)
-
end
-
-
1
def call(*)
-
5
else: 4
then: 1
return unless exist?
-
-
8
logger.info { "Editing: #{path.to_s.inspect}." }
-
4
kernel.system "$EDITOR #{path}"
-
end
-
-
1
private
-
-
1
attr_reader :path
-
-
1
def exist?
-
5
then: 4
else: 1
return true if path.exist?
-
-
1
logger.abort "Configuration doesn't exist: #{path.to_s.inspect}."
-
1
false
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/pathname"
-
-
1
module Sod
-
1
module Prefabs
-
1
module Actions
-
1
module Config
-
# Displays project configuration.
-
1
class View < Action
-
1
include Dependencies[:logger, :io]
-
-
1
using Refinements::Pathname
-
-
1
description "View project configuration."
-
-
1
on %w[-v --view]
-
-
# :reek:ControlParameter
-
1
def initialize(path = nil, **)
-
6
super(**)
-
6
@path = Pathname(path || context.xdg_config.active)
-
end
-
-
1
def call(*)
-
5
else: 4
then: 1
return unless exist?
-
-
8
logger.info { "Viewing (#{path.to_s.inspect}):" }
-
4
io.puts path.read
-
end
-
-
1
private
-
-
1
attr_reader :path
-
-
1
def exist?
-
5
then: 4
else: 1
return true if path.exist?
-
-
1
logger.abort "Configuration doesn't exist: #{path.to_s.inspect}."
-
1
false
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sod
-
1
module Prefabs
-
1
module Actions
-
# Displays help (usage) information.
-
1
class DryRun < Action
-
1
description "Simulate execution without making changes."
-
-
1
on %w[-n --dry_run]
-
-
1
def initialize(settings: Struct.new(:dry_run).new(dry_run: false), **)
-
1
super(**)
-
1
@settings = settings
-
end
-
-
1
def call = settings.dry_run = true
-
-
1
private
-
-
1
attr_reader :settings
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sod
-
1
module Prefabs
-
1
module Actions
-
# Displays help (usage) information.
-
1
class Help < Action
-
1
include Dependencies[:io]
-
-
1
description "Show this message."
-
-
1
on %w[-h --help], argument: "[COMMAND]"
-
-
1
def initialize(graph, presenter: Presenters::Node, **)
-
27
super(**)
-
27
@graph = graph
-
27
@presenter = presenter
-
end
-
-
1
def call *lineage
-
16
then: 7
if lineage.empty?
-
7
io.puts presenter.new(graph).to_s
-
else: 9
else
-
9
io.puts presenter.new(graph.get_child(*lineage)).to_s
-
end
-
end
-
-
1
private
-
-
1
attr_reader :graph, :presenter
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sod
-
1
module Prefabs
-
1
module Actions
-
# Provides a generic version action for use in upstream applications.
-
1
class Version < Action
-
1
include Dependencies[:io]
-
-
1
description "Show version."
-
-
1
on %w[-v --version]
-
-
1
def initialize(label = nil, **)
-
56
super(**)
-
56
@label = context[label, :version_label]
-
end
-
-
1
def call(*) = io.puts label
-
-
1
private
-
-
1
attr_reader :label
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sod
-
1
module Prefabs
-
1
module Commands
-
# Provides a generic configuration command for use in upstream applications.
-
1
class Config < Sod::Command
-
1
handle "config"
-
-
1
description "Manage configuration."
-
-
1
ancillary "Path is dynamic per current directory."
-
-
1
on Prefabs::Actions::Config::Create
-
1
on Prefabs::Actions::Config::Edit
-
1
on Prefabs::Actions::Config::View
-
1
on Prefabs::Actions::Config::Delete
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "forwardable"
-
1
require "refinements/array"
-
-
1
module Sod
-
1
module Presenters
-
# Aids in rendering an action for display.
-
1
class Action
-
1
include Dependencies[:color]
-
-
1
extend Forwardable
-
-
1
using Refinements::Array
-
-
1
delegate [*Models::Action.members, :handle] => :record
-
-
1
def initialize(record, **)
-
42
super(**)
-
42
@record = record
-
end
-
-
1
def colored_handle = [color_aliases, argument].tap(&:compact!).join(" ")
-
-
1
def colored_documentation = [*ancillary, color_allows, color_default].tap(&:compact!)
-
-
1
private
-
-
1
attr_reader :record
-
-
1
def color_aliases
-
61
Array(record.aliases).map { |value| color[value, :cyan] }
-
.join ", "
-
end
-
-
1
def color_allows
-
26
else: 10
then: 16
return unless allow
-
-
30
values = Array(allow).map { |value| color[value, :green] }
-
.to_sentence "or"
-
10
"Use: #{values}."
-
end
-
-
1
def color_default
-
26
cast = default.to_s
-
-
26
then: 15
else: 11
return if cast.empty?
-
-
11
then: 1
else: 10
value = cast == "false" ? color[default, :red] : color[default, :green]
-
11
"Default: #{value}."
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "forwardable"
-
1
require "refinements/array"
-
1
require "refinements/string"
-
-
1
module Sod
-
1
module Presenters
-
# Aids in rendering a node for display.
-
# :reek:TooManyInstanceVariables
-
1
class Node
-
1
include Dependencies[:color]
-
-
1
extend Forwardable
-
-
1
using Refinements::Array
-
1
using Refinements::String
-
-
1
delegate %i[handle description ancillary operation children] => :node
-
-
1
attr_reader :actions
-
-
# rubocop:todo Metrics/ParameterLists
-
1
def initialize(node, indent: 2, gap: 5, action_presenter: Presenters::Action, **)
-
24
super(**)
-
24
@node = node
-
24
@indent = indent
-
24
@gap = gap
-
55
@actions = node.actions.map { |action| action_presenter.new action.record }
-
24
@all = actions + children.to_a
-
end
-
# rubocop:enable Metrics/ParameterLists
-
-
1
def to_s
-
20
[banner, body, "", *usage, "", *colored_actions, "", *colored_commands].tap(&:compact!)
-
.join("\n")
-
.strip
-
end
-
-
1
private
-
-
1
attr_reader :node, :indent, :gap, :all
-
-
1
def banner = color[description, :bold]
-
-
21
then: 19
else: 1
def body = ancillary.empty? ? nil : ancillary.join("\n").prepend("\n")
-
-
1
def usage
-
20
else: 4
then: 16
actions = " #{colored_handle} [OPTIONS]" unless all.empty?
-
20
else: 8
then: 12
commands = " #{colored_handle} COMMAND [OPTIONS]" unless children.empty?
-
-
20
add_section "USAGE", [actions, commands].tap(&:compact!)
-
end
-
-
1
def colored_handle = color[handle, :cyan]
-
-
1
def colored_actions
-
20
then: 5
else: 15
return if actions.empty?
-
-
15
collection = actions.each_with_object [] do |action, content|
-
23
content.append " #{action.colored_handle}#{description_padding action}" \
-
"#{action.description}"
-
23
add_ancillary action, :colored_documentation, content
-
end
-
-
15
add_section "OPTIONS", collection
-
end
-
-
1
def colored_commands
-
20
then: 8
else: 12
return if children.empty?
-
-
12
collection = children.each_with_object [] do |command, content|
-
21
content.append " #{color[command.handle, :cyan]}#{description_padding command}" \
-
"#{command.description}"
-
21
add_ancillary command, :ancillary, content
-
end
-
-
12
add_section "COMMANDS", collection
-
end
-
-
45
def description_padding(item) = " " * ((max_handle_size - item.handle.size) + gap)
-
-
1
def max_handle_size = all.map(&:handle).maximum :size
-
-
1
def add_ancillary target, message, content
-
44
target.public_send(message).each do |line|
-
19
content.append line.indent (max_handle_size + gap + indent), pad: " "
-
end
-
end
-
-
# :reek:FeatureEnvy
-
1
def add_section text, collection
-
47
then: 4
else: 43
collection.empty? ? collection : collection.prepend(color[text, :bold, :underline])
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "optparse"
-
-
1
module Sod
-
1
module Refines
-
# Provides additional enhancements to the option parser primitive.
-
1
module OptionParser
-
1
refine ::OptionParser do
-
1
def order!(argument = default_argv, into: nil, command: nil, **, &)
-
26
super(argument, into:, **, &)
-
20
then: 19
else: 1
command.call if command
-
end
-
-
1
def replicate
-
112
self.class.new banner, summary_width, summary_indent do |instance|
-
112
instance.set_program_name program_name
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "cogger"
-
-
1
module Sod
-
# The Command Line Interface (CLI).
-
1
class Shell
-
1
attr_reader :name, :banner
-
-
# rubocop:todo Metrics/ParameterLists
-
1
def initialize name = Cogger::Program.call,
-
banner: nil,
-
node: Graph::Node,
-
runner: Graph::Runner,
-
&block
-
6
@name = name.to_s
-
6
@banner = banner
-
6
graph = node[handle: name, description: banner]
-
6
then: 2
else: 4
graph.instance_eval(&block) if block
-
6
@runner = runner.new graph
-
end
-
# rubocop:enable Metrics/ParameterLists
-
-
1
def call arguments = ARGV, process: Process
-
2
process.setproctitle name
-
2
runner.call arguments
-
end
-
-
1
private
-
-
1
attr_reader :runner
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "optparse"
-
1
require "pathname"
-
-
2
OptionParser.accept(Pathname) { |value| Pathname value }