simple-cli 0.3.0 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/Gemfile +2 -0
- data/VERSION +1 -0
- data/lib/simple/cli.rb +98 -12
- data/lib/simple/cli/adapter.rb +11 -3
- data/lib/simple/cli/default_options.rb +73 -0
- data/lib/simple/cli/helper.rb +9 -0
- data/lib/simple/cli/helper/help.rb +60 -0
- data/lib/simple/cli/helper/help_on_command.rb +55 -0
- data/lib/simple/cli/helper/short_help.rb +27 -0
- data/lib/simple/cli/helpers.rb +4 -2
- data/lib/simple/cli/logger.rb +2 -0
- data/lib/simple/cli/logger/colored_logger.rb +2 -1
- data/lib/simple/cli/on_exception.rb +23 -0
- data/lib/simple/cli/runner.rb +27 -199
- data/simple-cli.gemspec +2 -2
- data/spec/spec_helper.rb +0 -1
- metadata +23 -5
- data/lib/simple/cli/pp.rb +0 -12
- data/lib/simple/cli/runner/command_help.rb +0 -120
- data/lib/simple/cli/runner/module_ex.rb +0 -69
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 43c838ad9921bef80717cd3a61c33d68e794adb84799b7e764cc8f23e8fc30db
|
4
|
+
data.tar.gz: a3266ec697b770e68add5bbe8511db6253aa9453e4bde36cf86cddecc5577bff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 198ffc917415a4221f1b2de93ff32f42e55dfb480b0bd8f51b2d191277ac3206c801d0cc14ada5bc6c18e2aad38447ddf93a1b303f0254e9f28e2821907f654b
|
7
|
+
data.tar.gz: cc2f55d0a681d4376eeff4b76f27f40199e3248b53736fe8f1061307328764477e212cff2e429a01c1dc28b7f214e0eb19f8839acc30e5e65d0d8196115c4c2d
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.3
|
data/lib/simple/cli.rb
CHANGED
@@ -1,29 +1,115 @@
|
|
1
|
+
# rubocop:disable Metrics/AbcSize
|
2
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
3
|
+
# rubocop:disable Metrics/MethodLength
|
4
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
5
|
+
|
1
6
|
module Simple; end
|
2
7
|
module Simple::CLI; end
|
3
8
|
|
4
|
-
require_relative "cli/
|
5
|
-
|
6
|
-
require_relative "cli/helpers"
|
9
|
+
require_relative "cli/default_options"
|
7
10
|
require_relative "cli/runner"
|
11
|
+
require_relative "cli/helper"
|
8
12
|
require_relative "cli/adapter"
|
9
13
|
require_relative "cli/logger"
|
14
|
+
require_relative "cli/on_exception"
|
15
|
+
require_relative "cli/helpers"
|
16
|
+
|
17
|
+
require "simple/service"
|
10
18
|
|
11
19
|
module Simple::CLI
|
12
20
|
extend ::Simple::CLI::Logger
|
13
21
|
|
22
|
+
# It is not strictly necessary to include this module into another module
|
23
|
+
# (the "target module") to be able to run the target module via the command
|
24
|
+
# line. It is sufficient to just include ::Simple::Service, which turns
|
25
|
+
# the target into a service module, and then
|
26
|
+
#
|
27
|
+
# However, just including Simple::CLI gives you access to the Simple::CLI::Helpers
|
28
|
+
# module as well.
|
14
29
|
def self.included(base)
|
15
|
-
base.
|
30
|
+
base.include(::Simple::Service)
|
16
31
|
base.include(::Simple::CLI::Helpers)
|
17
32
|
end
|
18
33
|
|
19
|
-
#
|
20
|
-
# name, which is derived from the command passed in via the command line,
|
21
|
-
# and parsed arguments.
|
34
|
+
# Runs the service with the current command line arguments.
|
22
35
|
#
|
23
|
-
# The
|
24
|
-
#
|
25
|
-
#
|
26
|
-
def run!(
|
27
|
-
|
36
|
+
# The +service+ argument must match a simple-service service module. The CLI
|
37
|
+
# application's subcommands and their arguments are derived from the actions
|
38
|
+
# provided by the service module.
|
39
|
+
def self.run!(service, args: nil)
|
40
|
+
::Simple::Service.verify_service!(service)
|
41
|
+
|
42
|
+
# prepare arguments: we always duplicate the args array, to make guarantee
|
43
|
+
# we don't interfere with the caller's view of the world.
|
44
|
+
args ||= ARGV
|
45
|
+
args = args.dup
|
46
|
+
|
47
|
+
logger.level = ::Logger::DEBUG
|
48
|
+
|
49
|
+
# Extract default options. This returns the command to run, the verbosity
|
50
|
+
# setting, and the help flag.
|
51
|
+
options = DefaultOptions.new(args)
|
52
|
+
|
53
|
+
# Set logger verbosity. This happens before anything else - this way
|
54
|
+
# any further step which raises an exception will have the correct log
|
55
|
+
# level applied during exception handling.
|
56
|
+
logger.level = options.log_level
|
57
|
+
|
58
|
+
# Validate the command. If this command is invalid this will print a short
|
59
|
+
# help message.
|
60
|
+
if options.command
|
61
|
+
unless H.action_for_command(service, options.command)
|
62
|
+
logger.error "Invalid command '#{options.command}'."
|
63
|
+
Helper.short_help!(service)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Run help if requested.
|
68
|
+
if options.help?
|
69
|
+
if options.command
|
70
|
+
Helper.help_on_command! service, options.command, verbose: options.verbose?
|
71
|
+
else
|
72
|
+
Helper.help! service, verbose: options.verbose?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Run help if command is missing..
|
77
|
+
unless options.command
|
78
|
+
Helper.short_help! service
|
79
|
+
end
|
80
|
+
|
81
|
+
# Run service.
|
82
|
+
Runner.run! service, options.command, *args, verbose: options.verbose?
|
83
|
+
rescue ::Simple::Service::ArgumentError
|
84
|
+
Helper.help_on_command! service, command, verbose: false
|
85
|
+
rescue StandardError => e
|
86
|
+
on_exception(e)
|
87
|
+
exit 3
|
88
|
+
end
|
89
|
+
|
90
|
+
module H
|
91
|
+
def self.action_for_command(service, command)
|
92
|
+
actions = ::Simple::Service.actions(service)
|
93
|
+
|
94
|
+
action_name = H.command_to_action(command)
|
95
|
+
return nil unless actions.key?(action_name)
|
96
|
+
actions[action_name]
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.action_to_command(action_name)
|
100
|
+
raise "action_name must by a Symbol" unless action_name.is_a?(Symbol)
|
101
|
+
|
102
|
+
action_name.to_s.tr("_", ":")
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.command_to_action(command)
|
106
|
+
raise "command must by a String" unless command.is_a?(String)
|
107
|
+
|
108
|
+
command.tr(":", "_").to_sym
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.binary_name
|
112
|
+
$0.gsub(/.*\//, "")
|
113
|
+
end
|
28
114
|
end
|
29
115
|
end
|
data/lib/simple/cli/adapter.rb
CHANGED
@@ -1,17 +1,25 @@
|
|
1
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
2
|
+
# rubocop:disable Metrics/MethodLength
|
3
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
4
|
+
|
1
5
|
module Simple::CLI::Adapter
|
2
6
|
# Run a Simple::CLI application
|
3
7
|
#
|
4
8
|
# This is usually called with as either
|
5
9
|
#
|
6
10
|
# - Application::CLI.run!: runs the Application's CLI with subcommand support.
|
7
|
-
#
|
11
|
+
#
|
8
12
|
# or
|
9
13
|
#
|
10
14
|
# - Application::CLI.run!("main"): runs the Application's CLI without subcommand support.
|
11
15
|
#
|
12
|
-
def run!(
|
16
|
+
def run!(*argv)
|
17
|
+
if argv.length == 1 && argv != ARGV
|
18
|
+
main_command = *argv
|
19
|
+
end
|
20
|
+
|
13
21
|
runner = Simple::CLI::Runner.new(self)
|
14
|
-
|
22
|
+
|
15
23
|
if main_command && (ARGV.include?("--help") || ARGV.include?("-h"))
|
16
24
|
runner.help(main_command)
|
17
25
|
elsif main_command
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "logger"
|
2
|
+
|
3
|
+
module Simple::CLI
|
4
|
+
# A DefaultOptions object holds values for default options.
|
5
|
+
class DefaultOptions
|
6
|
+
# extract default CLI options and the "help" command. Returns a DefaultOptions object
|
7
|
+
def extract!(args:)
|
8
|
+
new args
|
9
|
+
end
|
10
|
+
|
11
|
+
# verbosity (one of ::Logger::WARN, ::Logger::INFO, ::Logger::DEBUG)
|
12
|
+
attr_reader :log_level
|
13
|
+
|
14
|
+
# returns true if we run in verbose mode.
|
15
|
+
def verbose?
|
16
|
+
log_level == ::Logger::DEBUG
|
17
|
+
end
|
18
|
+
|
19
|
+
# command
|
20
|
+
attr_reader :command
|
21
|
+
|
22
|
+
# The help flag. Is set when
|
23
|
+
#
|
24
|
+
# - running the "help" command
|
25
|
+
# - when a "-h" or "--help" CLI flag was given.
|
26
|
+
def help?
|
27
|
+
@help
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
LOG_LEVEL_FLAGS = {
|
33
|
+
"--verbose" => ::Logger::DEBUG,
|
34
|
+
"-v" => ::Logger::DEBUG,
|
35
|
+
"--quiet" => ::Logger::WARN,
|
36
|
+
"-q" => ::Logger::WARN,
|
37
|
+
default: ::Logger::INFO
|
38
|
+
}
|
39
|
+
|
40
|
+
HELP_FLAGS = {
|
41
|
+
"--help" => true,
|
42
|
+
"-h" => true,
|
43
|
+
default: false
|
44
|
+
}
|
45
|
+
|
46
|
+
def initialize(args)
|
47
|
+
@args = args
|
48
|
+
|
49
|
+
@log_level = extract_w_lookup!(LOG_LEVEL_FLAGS) # get -v/--verbose and -q7--quiet flags
|
50
|
+
@command = extract_command! # extract the command
|
51
|
+
if @command == "help"
|
52
|
+
@help = true
|
53
|
+
@command = extract_command!
|
54
|
+
else
|
55
|
+
@help = extract_w_lookup!(HELP_FLAGS) # extract --help flag
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def extract_w_lookup!(hsh)
|
60
|
+
value = hsh[:default]
|
61
|
+
@args.reject! do |str|
|
62
|
+
next unless hsh.key?(str)
|
63
|
+
value = hsh[str]
|
64
|
+
end
|
65
|
+
value
|
66
|
+
end
|
67
|
+
|
68
|
+
def extract_command!
|
69
|
+
return nil if /^-/ =~ @args.first
|
70
|
+
@args.shift
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Simple::CLI
|
2
|
+
module Helper
|
3
|
+
def help!(service, verbose:)
|
4
|
+
STDERR.puts <<~MSG
|
5
|
+
#{H.binary_name} <command> [ options... ]
|
6
|
+
|
7
|
+
Commands:
|
8
|
+
|
9
|
+
#{format_usages usages(service, verbose: verbose), prefix: " "}
|
10
|
+
|
11
|
+
Default options and commands include:
|
12
|
+
|
13
|
+
#{format_usages default_usages(service, verbose: verbose), prefix: " "}
|
14
|
+
|
15
|
+
MSG
|
16
|
+
|
17
|
+
exit 2
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def usages(service, verbose:)
|
23
|
+
actions = ::Simple::Service.actions(service).values
|
24
|
+
actions = actions.select(&:short_description) unless verbose
|
25
|
+
actions = actions.sort_by(&:name)
|
26
|
+
|
27
|
+
actions.map do |action|
|
28
|
+
[ action_usage(action), action.short_description ]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def default_usages(service, verbose:)
|
33
|
+
_ = service
|
34
|
+
_ = verbose
|
35
|
+
|
36
|
+
[
|
37
|
+
[ "#{H.binary_name} help [ <command> ]", "print help for all or a specific command" ],
|
38
|
+
[ "#{H.binary_name} help -v", "show help for internal commands as well"],
|
39
|
+
[ "#{H.binary_name} [ --verbose | -v ]", "run on DEBUG log level"],
|
40
|
+
[ "#{H.binary_name} [ --quiet | -q ]", "run on WARN log level"],
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
def format_usages(ary, prefix:)
|
45
|
+
# each entry is an Array of one or two entries. The first entry is a command usage
|
46
|
+
# string, the second entry is the command's short_description.
|
47
|
+
max_cmd_length = ary.inject(45) do |max, (cmd, _description)|
|
48
|
+
cmd.length > max ? cmd.length : max
|
49
|
+
end
|
50
|
+
|
51
|
+
ary.map do |cmd, description|
|
52
|
+
if description
|
53
|
+
format("#{prefix}%-#{max_cmd_length}s # %s", cmd, description)
|
54
|
+
else
|
55
|
+
format("#{prefix}%-#{max_cmd_length}s", cmd)
|
56
|
+
end
|
57
|
+
end.join("\n")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# rubocop:disable Metrics/AbcSize
|
2
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
3
|
+
# rubocop:disable Metrics/MethodLength
|
4
|
+
|
5
|
+
module Simple::CLI
|
6
|
+
module Helper
|
7
|
+
def help_on_command!(service, command, verbose:)
|
8
|
+
action = H.action_for_command(service, command)
|
9
|
+
|
10
|
+
parts = [
|
11
|
+
action.short_description,
|
12
|
+
action_usage(action),
|
13
|
+
action.full_description,
|
14
|
+
].compact
|
15
|
+
|
16
|
+
STDERR.puts <<~MSG
|
17
|
+
#{parts.join("\n\n")}
|
18
|
+
|
19
|
+
MSG
|
20
|
+
|
21
|
+
exit 2
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Used in command_help.rb and in help.rb
|
27
|
+
def action_usage(action)
|
28
|
+
args = action.parameters.reject(&:keyword?).map do |param|
|
29
|
+
case param.kind
|
30
|
+
when :req then "<#{param.name}>"
|
31
|
+
when :opt then "[ <#{param.name}> ]"
|
32
|
+
when :rest then "[ <#{param.name}> .. ]"
|
33
|
+
end
|
34
|
+
end.compact
|
35
|
+
|
36
|
+
options = action.parameters.select(&:keyword?).map do |param|
|
37
|
+
if param.required?
|
38
|
+
"--#{name}=<#{name}>"
|
39
|
+
else
|
40
|
+
case param.default_value
|
41
|
+
when false then "[ --#{param.name} ]"
|
42
|
+
when true then "[ --no-#{param.name} ]"
|
43
|
+
when nil then "[ --#{param.name}=<#{param.name}> ]"
|
44
|
+
else "[ --#{param.name}=#{param.default_value} ]"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
help = "#{H.binary_name} #{H.action_to_command(action.name)}"
|
50
|
+
help << " #{options.join(" ")}" unless options.empty?
|
51
|
+
help << " #{args.join(" ")}" unless args.empty?
|
52
|
+
help
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Simple::CLI
|
2
|
+
module Helper
|
3
|
+
def short_help!(service)
|
4
|
+
# We check if we have only a few number of actions. In that case we just show the full help instead.
|
5
|
+
actions = ::Simple::Service.actions(service).values
|
6
|
+
actions, hidden_actions = actions.partition(&:short_description)
|
7
|
+
|
8
|
+
STDERR.puts <<~MSG
|
9
|
+
#{H.binary_name} <command> [ options... ]
|
10
|
+
|
11
|
+
MSG
|
12
|
+
|
13
|
+
subcommands = actions.map { |action| "'" + H.action_to_command(action.name) + "'" }
|
14
|
+
msg = "Subcommands include #{subcommands.sort.join(", ")}"
|
15
|
+
msg += " (and an additional #{hidden_actions.count} internal commands)"
|
16
|
+
|
17
|
+
STDERR.puts <<~MSG
|
18
|
+
#{msg}. Default options and commands include:
|
19
|
+
|
20
|
+
#{format_usages default_usages(service, verbose: false), prefix: " "}
|
21
|
+
|
22
|
+
MSG
|
23
|
+
|
24
|
+
exit 2
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/simple/cli/helpers.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require "open3"
|
2
2
|
|
3
|
-
#
|
4
|
-
# mostly to help with integrating external commands:
|
3
|
+
# This module defines various helpers that might be useful for many CLI applications.
|
5
4
|
#
|
6
5
|
# - sys
|
7
6
|
# - sys!
|
@@ -9,6 +8,9 @@ require "open3"
|
|
9
8
|
# - die!
|
10
9
|
#
|
11
10
|
module Simple::CLI::Helpers
|
11
|
+
# The methods here must be private, lest they not show up as subcommands.
|
12
|
+
private
|
13
|
+
|
12
14
|
def die!(msg)
|
13
15
|
STDERR.puts msg
|
14
16
|
exit 1
|
data/lib/simple/cli/logger.rb
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
# rubocop:disable Metrics/AbcSize
|
2
|
+
# rubocop:disable Metrics/MethodLength
|
3
|
+
|
4
|
+
module Simple::CLI
|
5
|
+
def self.on_exception(e)
|
6
|
+
msg = e.message
|
7
|
+
msg += " (#{e.class.name})" unless $!.class.name == "RuntimeError"
|
8
|
+
|
9
|
+
logger.error msg
|
10
|
+
|
11
|
+
raise(e) if Simple::CLI.logger.level == ::Logger::DEBUG
|
12
|
+
|
13
|
+
logger.info do
|
14
|
+
backtrace = e.backtrace.reject { |l| l =~ /simple-cli/ }
|
15
|
+
"called from\n " + backtrace[0, 10].join("\n ")
|
16
|
+
end
|
17
|
+
|
18
|
+
verbosity_hint = "(Backtraces are currently silenced. Run with --verbose to see backtraces.)"
|
19
|
+
logger.warn verbosity_hint
|
20
|
+
|
21
|
+
exit 2
|
22
|
+
end
|
23
|
+
end
|
data/lib/simple/cli/runner.rb
CHANGED
@@ -1,213 +1,41 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
# rubocop:disable Metrics/MethodLength
|
5
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
1
|
+
module Simple::CLI
|
2
|
+
module Runner
|
3
|
+
extend self
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
require_relative "runner/command_help"
|
5
|
+
def run!(service, command, *args, verbose:)
|
6
|
+
_ = verbose
|
11
7
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
new(app).run(*args)
|
17
|
-
end
|
18
|
-
|
19
|
-
def initialize(app)
|
20
|
-
@app = app
|
21
|
-
end
|
22
|
-
|
23
|
-
def extract_default_flags!(args)
|
24
|
-
args.reject! do |arg|
|
25
|
-
case arg
|
26
|
-
when "--verbose", "-v" then logger.level = Logger::DEBUG
|
27
|
-
when "--quiet", "-q" then logger.level = Logger::WARN
|
8
|
+
action_name = H.command_to_action(command)
|
9
|
+
Simple::Service.with_context do
|
10
|
+
flags = extract_flags!(args)
|
11
|
+
::Simple::Service.invoke(service, action_name, *args, **flags)
|
28
12
|
end
|
29
13
|
end
|
30
|
-
end
|
31
14
|
|
32
|
-
|
15
|
+
private
|
33
16
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
17
|
+
# extract options from the array. Note: simple-cli is the correct place
|
18
|
+
# for this, and not simple-service, because it deals with a conversion
|
19
|
+
# which is strictly related to command line applications, and simple-service
|
20
|
+
# doesn't have any knowledge of CLI applications.
|
21
|
+
#
|
22
|
+
# This returns a hash of flag values, as determined by a "--flagname[=<value>]"
|
23
|
+
# command line options, and removes all such options from the arg array.
|
24
|
+
def extract_flags!(args)
|
25
|
+
flags = {}
|
41
26
|
|
42
|
-
|
43
|
-
|
27
|
+
args.reject! do |arg|
|
28
|
+
next false unless arg =~ /^--(no-)?([^=]+)(=(.+))?/
|
44
29
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
if command == :help
|
50
|
-
do_help!(*args)
|
51
|
-
elsif commands.include?(command)
|
52
|
-
self.subcommand = command
|
53
|
-
@instance.run! command, *args_with_options(args)
|
54
|
-
else
|
55
|
-
help!
|
56
|
-
end
|
57
|
-
rescue StandardError => e
|
58
|
-
on_exception(e)
|
59
|
-
end
|
60
|
-
|
61
|
-
def has_subcommands?
|
62
|
-
commands.length > 1
|
63
|
-
end
|
64
|
-
|
65
|
-
def do_help!(subcommand = nil)
|
66
|
-
if !subcommand
|
67
|
-
help!
|
68
|
-
else
|
69
|
-
help_subcommand!(subcommand)
|
70
|
-
end
|
71
|
-
end
|
30
|
+
flag_name = $2.tr("-", "_")
|
31
|
+
flag_name = "no_#{flag_name}" if $4 && $1
|
32
|
+
value = $4 || ($1 ? false : true)
|
72
33
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
puts <<~MSG
|
77
|
-
#{help_for_command(subcommand)}
|
78
|
-
|
79
|
-
#{edoc.full}
|
80
|
-
MSG
|
81
|
-
|
82
|
-
unless has_subcommands?
|
83
|
-
|
84
|
-
STDERR.puts <<~MSG
|
85
|
-
|
86
|
-
Default options include:
|
87
|
-
|
88
|
-
#{binary_name} [ --help | -h ] ... print this help
|
89
|
-
#{binary_name} [ --verbose | -v ] ... run on DEBUG log level
|
90
|
-
#{binary_name} [ --quiet | -q ] ... run on WARN log level
|
91
|
-
MSG
|
92
|
-
end
|
93
|
-
exit 1
|
94
|
-
end
|
95
|
-
|
96
|
-
def logger
|
97
|
-
Simple::CLI.logger
|
98
|
-
end
|
99
|
-
|
100
|
-
def on_exception(e)
|
101
|
-
raise(e) if Simple::CLI.logger.level == Logger::DEBUG
|
102
|
-
|
103
|
-
verbosity_hint = "Backtraces are currently silenced. Run with --verbose to see backtraces."
|
104
|
-
|
105
|
-
case e
|
106
|
-
when ArgumentError
|
107
|
-
logger.error e.message
|
108
|
-
logger.warn verbosity_hint
|
109
|
-
if subcommand
|
110
|
-
help_subcommand! subcommand
|
111
|
-
else
|
112
|
-
help!
|
34
|
+
flags[flag_name.to_sym] = value
|
35
|
+
true
|
113
36
|
end
|
114
|
-
else
|
115
|
-
msg = e.message
|
116
|
-
msg += " (#{e.class.name})" unless $!.class.name == "RuntimeError"
|
117
|
-
logger.error msg
|
118
|
-
logger.warn verbosity_hint
|
119
|
-
exit 2
|
120
|
-
end
|
121
|
-
end
|
122
37
|
|
123
|
-
|
124
|
-
r = []
|
125
|
-
options = {}
|
126
|
-
while (arg = args.shift)
|
127
|
-
case arg
|
128
|
-
when /^--(.*)=(.*)/ then options[$1.to_sym] = $2
|
129
|
-
when /^--no-(.*)/ then options[$1.to_sym] = false
|
130
|
-
when /^--(.*)/ then options[$1.to_sym] = true
|
131
|
-
else r << arg
|
132
|
-
end
|
38
|
+
flags
|
133
39
|
end
|
134
|
-
|
135
|
-
r << options unless options.empty?
|
136
|
-
r
|
137
|
-
end
|
138
|
-
|
139
|
-
def command_to_string(sym)
|
140
|
-
sym.to_s.tr("_", ":")
|
141
|
-
end
|
142
|
-
|
143
|
-
def string_to_command(s)
|
144
|
-
s.to_s.tr(":", "_").to_sym
|
145
|
-
end
|
146
|
-
|
147
|
-
def commands
|
148
|
-
@app.public_instance_methods(false).grep(/^[_a-zA-Z0-9]+$/)
|
149
|
-
end
|
150
|
-
|
151
|
-
def help_for_command(sym)
|
152
|
-
cmd = string_to_command(sym)
|
153
|
-
CommandHelp.new(@app, cmd).interface(binary_name, cmd, include_subcommand: has_subcommands?)
|
154
|
-
end
|
155
|
-
|
156
|
-
def binary_name
|
157
|
-
$0.gsub(/.*\//, "")
|
158
|
-
end
|
159
|
-
|
160
|
-
def help!
|
161
|
-
# collect help information on individual comments; when not on DEBUG
|
162
|
-
# level skipping the commands that don't jave a command help.
|
163
|
-
command_helps = commands.inject({}) do |hsh, sym|
|
164
|
-
edoc = CommandHelp.new(@app, sym)
|
165
|
-
next hsh if !edoc.head && logger.level != ::Logger::DEBUG
|
166
|
-
|
167
|
-
hsh.update sym => help_for_command(sym)
|
168
|
-
end
|
169
|
-
|
170
|
-
# build a lambda which prints a help line with nice formatting
|
171
|
-
max_length = command_helps.values.map(&:length).max
|
172
|
-
print_help_line = lambda do |cmd, description|
|
173
|
-
if description
|
174
|
-
STDERR.puts format(" %-#{max_length}s # %s", cmd, description)
|
175
|
-
else
|
176
|
-
STDERR.puts format(" %-#{max_length}s", cmd)
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
# print help for commands
|
181
|
-
STDERR.puts "Usage:\n\n"
|
182
|
-
|
183
|
-
command_helps.keys.sort.each do |sym|
|
184
|
-
command_help = command_helps[sym]
|
185
|
-
edoc = CommandHelp.new(@app, sym)
|
186
|
-
print_help_line.call command_help, edoc.head
|
187
|
-
end
|
188
|
-
|
189
|
-
# print help for default commands
|
190
|
-
|
191
|
-
STDERR.puts <<~DOC
|
192
|
-
|
193
|
-
Default options include:
|
194
|
-
|
195
|
-
DOC
|
196
|
-
|
197
|
-
print_help_line.call "#{binary_name} [ --verbose | -v ]", "run on DEBUG log level"
|
198
|
-
print_help_line.call "#{binary_name} [ --quiet | -q ]", "run on WARN log level"
|
199
|
-
|
200
|
-
STDERR.puts <<~DOC
|
201
|
-
|
202
|
-
Other commands:
|
203
|
-
|
204
|
-
DOC
|
205
|
-
|
206
|
-
print_help_line.call "#{binary_name} help [ subcommand ]", "print help on a specific subcommand"
|
207
|
-
print_help_line.call "#{binary_name} help -v", "show help for internal commands as well"
|
208
|
-
|
209
|
-
STDERR.puts "\n"
|
210
|
-
|
211
|
-
exit 1
|
212
40
|
end
|
213
41
|
end
|
data/simple-cli.gemspec
CHANGED
@@ -5,11 +5,10 @@
|
|
5
5
|
|
6
6
|
lib = File.expand_path('../lib', __FILE__)
|
7
7
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
8
|
-
require 'simple/cli/version'
|
9
8
|
|
10
9
|
Gem::Specification.new do |gem|
|
11
10
|
gem.name = "simple-cli"
|
12
|
-
gem.version =
|
11
|
+
gem.version = File.read("VERSION")
|
13
12
|
|
14
13
|
gem.authors = [ "radiospiel", "mediapeers GmbH" ]
|
15
14
|
gem.email = "eno@radiospiel.org"
|
@@ -28,6 +27,7 @@ Gem::Specification.new do |gem|
|
|
28
27
|
gem.required_ruby_version = '~> 2.3'
|
29
28
|
|
30
29
|
# optional gems (required by some of the parts)
|
30
|
+
gem.add_dependency "simple-service", "~> 0.1.2"
|
31
31
|
|
32
32
|
# development gems
|
33
33
|
gem.add_development_dependency 'rake', '~> 12'
|
data/spec/spec_helper.rb
CHANGED
@@ -16,6 +16,5 @@ RSpec.configure do |config|
|
|
16
16
|
config.run_all_when_everything_filtered = true
|
17
17
|
config.filter_run focus: (ENV["CI"] != "true")
|
18
18
|
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
19
|
-
config.include FactoryGirl::Syntax::Methods
|
20
19
|
config.order = "random"
|
21
20
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- radiospiel
|
@@ -9,8 +9,22 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2019-11-
|
12
|
+
date: 2019-11-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: simple-service
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 0.1.2
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 0.1.2
|
14
28
|
- !ruby/object:Gem::Dependency
|
15
29
|
name: rake
|
16
30
|
requirement: !ruby/object:Gem::Requirement
|
@@ -77,18 +91,22 @@ files:
|
|
77
91
|
- ".rubocop.yml"
|
78
92
|
- Gemfile
|
79
93
|
- Rakefile
|
94
|
+
- VERSION
|
80
95
|
- bin/rake
|
81
96
|
- lib/simple-cli.rb
|
82
97
|
- lib/simple/cli.rb
|
83
98
|
- lib/simple/cli/adapter.rb
|
99
|
+
- lib/simple/cli/default_options.rb
|
100
|
+
- lib/simple/cli/helper.rb
|
101
|
+
- lib/simple/cli/helper/help.rb
|
102
|
+
- lib/simple/cli/helper/help_on_command.rb
|
103
|
+
- lib/simple/cli/helper/short_help.rb
|
84
104
|
- lib/simple/cli/helpers.rb
|
85
105
|
- lib/simple/cli/logger.rb
|
86
106
|
- lib/simple/cli/logger/adapter.rb
|
87
107
|
- lib/simple/cli/logger/colored_logger.rb
|
88
|
-
- lib/simple/cli/
|
108
|
+
- lib/simple/cli/on_exception.rb
|
89
109
|
- lib/simple/cli/runner.rb
|
90
|
-
- lib/simple/cli/runner/command_help.rb
|
91
|
-
- lib/simple/cli/runner/module_ex.rb
|
92
110
|
- lib/simple/cli/version.rb
|
93
111
|
- log/.gitkeep
|
94
112
|
- simple-cli.gemspec
|
data/lib/simple/cli/pp.rb
DELETED
@@ -1,120 +0,0 @@
|
|
1
|
-
# rubocop:disable Metrics/AbcSize
|
2
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
3
|
-
# rubocop:disable Metrics/MethodLength
|
4
|
-
|
5
|
-
require_relative "./module_ex"
|
6
|
-
|
7
|
-
class Simple::CLI::Runner::CommandHelp
|
8
|
-
def self.option_names(app, subcommand)
|
9
|
-
new(app, subcommand).option_names
|
10
|
-
rescue NameError
|
11
|
-
[]
|
12
|
-
end
|
13
|
-
|
14
|
-
def initialize(mod, method_id)
|
15
|
-
raise(ArgumentError, "#{method_id.inspect} should be a Symbol") unless method_id.is_a?(Symbol)
|
16
|
-
|
17
|
-
@method_id = method_id
|
18
|
-
@method = mod.instance_method(@method_id)
|
19
|
-
@method_parameters_ex = mod.method_parameters_ex(@method_id)
|
20
|
-
end
|
21
|
-
|
22
|
-
# First line of the help as read from the method comments.
|
23
|
-
def head
|
24
|
-
comments.first
|
25
|
-
end
|
26
|
-
|
27
|
-
# Full help as read from the method comments
|
28
|
-
def full
|
29
|
-
comments.join("\n") if comments.first
|
30
|
-
end
|
31
|
-
|
32
|
-
def option_names
|
33
|
-
option_names = @method_parameters_ex.map do |mode, name, _|
|
34
|
-
case mode
|
35
|
-
when :key then name
|
36
|
-
when :keyreq then name
|
37
|
-
end
|
38
|
-
end.compact
|
39
|
-
|
40
|
-
option_names.map do |name|
|
41
|
-
["--#{name}", "--#{name}="]
|
42
|
-
end.flatten
|
43
|
-
end
|
44
|
-
|
45
|
-
# A help string constructed from the commands method signature.
|
46
|
-
def interface(binary_name, command_name, include_subcommand: false)
|
47
|
-
args = @method_parameters_ex.map do |mode, name|
|
48
|
-
case mode
|
49
|
-
when :req then "<#{name}>"
|
50
|
-
when :opt then "[ <#{name}> ]"
|
51
|
-
when :rest then "[ <#{name}> .. ]"
|
52
|
-
end
|
53
|
-
end.compact
|
54
|
-
|
55
|
-
options = @method_parameters_ex.map do |mode, name, default_value|
|
56
|
-
case mode
|
57
|
-
when :key then
|
58
|
-
case default_value
|
59
|
-
when false then "[ --#{name} ]"
|
60
|
-
when true then "[ --no-#{name} ]"
|
61
|
-
when nil then "[ --#{name}=<#{name}> ]"
|
62
|
-
else "[ --#{name}=#{default_value} ]"
|
63
|
-
end
|
64
|
-
when :keyreq then
|
65
|
-
"--#{name}=<#{name}>"
|
66
|
-
end
|
67
|
-
end.compact
|
68
|
-
|
69
|
-
help = "#{binary_name}"
|
70
|
-
help << " #{command_to_string(command_name)}" if include_subcommand
|
71
|
-
help << " #{options.join(' ')}" unless options.empty?
|
72
|
-
help << " #{args.join(' ')}" unless args.empty?
|
73
|
-
help
|
74
|
-
end
|
75
|
-
|
76
|
-
private
|
77
|
-
|
78
|
-
def command_to_string(s)
|
79
|
-
s.to_s.tr("_", ":")
|
80
|
-
end
|
81
|
-
|
82
|
-
def comments
|
83
|
-
@comments ||= begin
|
84
|
-
file, line = @method.source_location
|
85
|
-
extract_comments(from: parsed_source(file), before_line: line)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
# reads the source \a file and turns each non-comment into :code and each comment
|
90
|
-
# into a string without the leading comment markup.
|
91
|
-
def parsed_source(file)
|
92
|
-
File.readlines(file).map do |line|
|
93
|
-
case line
|
94
|
-
when /^\s*# ?(.*)$/ then $1
|
95
|
-
when /^\s*end/ then :end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def extract_comments(from:, before_line:)
|
101
|
-
parsed_source = from
|
102
|
-
|
103
|
-
# go down from before_line until we see a line which is either a comment
|
104
|
-
# or an :end. Note that the line at before_line-1 should be the first
|
105
|
-
# line of the method definition in question.
|
106
|
-
last_line = before_line - 1
|
107
|
-
last_line -= 1 while last_line >= 0 && !parsed_source[last_line]
|
108
|
-
|
109
|
-
first_line = last_line
|
110
|
-
first_line -= 1 while first_line >= 0 && parsed_source[first_line]
|
111
|
-
first_line += 1
|
112
|
-
|
113
|
-
comments = parsed_source[first_line..last_line]
|
114
|
-
if comments.include?(:end)
|
115
|
-
[]
|
116
|
-
else
|
117
|
-
parsed_source[first_line..last_line]
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
@@ -1,69 +0,0 @@
|
|
1
|
-
# rubocop:disable Metrics/MethodLength
|
2
|
-
# rubocop:disable Metrics/AbcSize
|
3
|
-
|
4
|
-
class Module
|
5
|
-
#
|
6
|
-
# returns an array with entries like the following:
|
7
|
-
#
|
8
|
-
# [ :key, name, default_value ]
|
9
|
-
# [ :keyreq, name [, nil ] ]
|
10
|
-
# [ :req, name [, nil ] ]
|
11
|
-
# [ :opt, name [, nil ] ]
|
12
|
-
# [ :rest, name [, nil ] ]
|
13
|
-
#
|
14
|
-
def method_parameters_ex(method_id)
|
15
|
-
method = instance_method(method_id)
|
16
|
-
parameters = method.parameters
|
17
|
-
|
18
|
-
# method parameters with a :key mode are optional keyword arguments. We only
|
19
|
-
# support defaults for those - if there are none we abort here already.
|
20
|
-
keys = parameters.map { |mode, name| name if mode == :key }.compact
|
21
|
-
return parameters if keys.empty?
|
22
|
-
|
23
|
-
# We are now doing a fake call to the method, with a minimal viable set of
|
24
|
-
# arguments, to let the ruby runtime fill in default values for arguments.
|
25
|
-
# We do not, however, let the call complete. Instead we use a TracePoint to
|
26
|
-
# abort as soon as the method is called, and use the its binding to determine
|
27
|
-
# the default values.
|
28
|
-
|
29
|
-
fake_recipient = Object.new.extend(self)
|
30
|
-
fake_call_args = minimal_arguments(method)
|
31
|
-
|
32
|
-
trace_point = TracePoint.trace(:call) do |tp|
|
33
|
-
throw :received_fake_call, tp.binding if tp.defined_class == self && tp.method_id == method_id
|
34
|
-
end
|
35
|
-
|
36
|
-
bnd = catch(:received_fake_call) do
|
37
|
-
fake_recipient.send(method_id, *fake_call_args)
|
38
|
-
end
|
39
|
-
|
40
|
-
trace_point.disable
|
41
|
-
|
42
|
-
# extract default values from the received binding, and merge with the
|
43
|
-
# parameters array.
|
44
|
-
default_values = keys.each_with_object({}) do |key_parameter, hsh|
|
45
|
-
hsh[key_parameter] = bnd.local_variable_get(key_parameter)
|
46
|
-
end
|
47
|
-
|
48
|
-
parameters.map do |mode, name|
|
49
|
-
[mode, name, default_values[name]]
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
private
|
54
|
-
|
55
|
-
# returns a minimal Array of arguments, which is suitable for a call to the method
|
56
|
-
def minimal_arguments(method)
|
57
|
-
# Build an arguments array with holds all required parameters. The actual
|
58
|
-
# values for these arguments doesn't matter at all.
|
59
|
-
args = method.parameters.select { |mode, _name| mode == :req }
|
60
|
-
|
61
|
-
# Add a hash with all required keyword arguments
|
62
|
-
required_keyword_args = method.parameters.each_with_object({}) do |(mode, name), hsh|
|
63
|
-
hsh[name] = :anything if mode == :keyreq
|
64
|
-
end
|
65
|
-
args << required_keyword_args if required_keyword_args
|
66
|
-
|
67
|
-
args
|
68
|
-
end
|
69
|
-
end
|