simple-cli 0.3.0 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|