cli-kit 4.0.0 → 5.0.1
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/.github/dependabot.yml +3 -0
- data/.github/workflows/cla.yml +22 -0
- data/.github/workflows/ruby.yml +16 -2
- data/.gitignore +2 -0
- data/.rubocop.sorbet.yml +47 -0
- data/.rubocop.yml +32 -1
- data/.ruby-version +1 -0
- data/Gemfile +10 -1
- data/Gemfile.lock +102 -29
- data/README.md +46 -3
- data/Rakefile +1 -0
- data/bin/onchange +30 -0
- data/bin/tapioca +28 -0
- data/bin/testunit +1 -0
- data/cli-kit.gemspec +9 -4
- data/dev.yml +38 -3
- data/examples/minimal/example.rb +11 -6
- data/examples/single-file/example.rb +25 -35
- data/gen/lib/gen/commands/help.rb +8 -10
- data/gen/lib/gen/commands/new.rb +23 -9
- data/gen/lib/gen/commands.rb +21 -9
- data/gen/lib/gen/entry_point.rb +12 -3
- data/gen/lib/gen/generator.rb +32 -11
- data/gen/lib/gen/help.rb +63 -0
- data/gen/lib/gen.rb +18 -23
- data/gen/template/bin/update-deps +2 -2
- data/gen/template/dev-gems.yml +1 -1
- data/gen/template/dev-vendor.yml +1 -1
- data/gen/template/lib/__app__/commands.rb +1 -4
- data/gen/template/lib/__app__.rb +8 -17
- data/gen/template/test/example_test.rb +1 -1
- data/lib/cli/kit/args/definition.rb +344 -0
- data/lib/cli/kit/args/evaluation.rb +234 -0
- data/lib/cli/kit/args/parser/node.rb +132 -0
- data/lib/cli/kit/args/parser.rb +129 -0
- data/lib/cli/kit/args/tokenizer.rb +133 -0
- data/lib/cli/kit/args.rb +16 -0
- data/lib/cli/kit/base_command.rb +17 -32
- data/lib/cli/kit/command_help.rb +271 -0
- data/lib/cli/kit/command_registry.rb +72 -20
- data/lib/cli/kit/config.rb +25 -22
- data/lib/cli/kit/core_ext.rb +30 -0
- data/lib/cli/kit/error_handler.rb +131 -70
- data/lib/cli/kit/executor.rb +20 -3
- data/lib/cli/kit/ini.rb +31 -38
- data/lib/cli/kit/levenshtein.rb +12 -4
- data/lib/cli/kit/logger.rb +16 -2
- data/lib/cli/kit/opts.rb +301 -0
- data/lib/cli/kit/parse_args.rb +55 -0
- data/lib/cli/kit/resolver.rb +8 -0
- data/lib/cli/kit/sorbet_runtime_stub.rb +154 -0
- data/lib/cli/kit/support/test_helper.rb +27 -16
- data/lib/cli/kit/support.rb +2 -0
- data/lib/cli/kit/system.rb +194 -57
- data/lib/cli/kit/util.rb +48 -103
- data/lib/cli/kit/version.rb +3 -1
- data/lib/cli/kit.rb +104 -7
- metadata +30 -14
- data/.github/probots.yml +0 -2
- data/lib/cli/kit/autocall.rb +0 -21
- data/lib/cli/kit/ruby_backports/enumerable.rb +0 -6
@@ -1,89 +1,141 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'cli/kit'
|
2
4
|
|
3
5
|
module CLI
|
4
6
|
module Kit
|
5
7
|
class CommandRegistry
|
6
|
-
|
8
|
+
extend T::Sig
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
10
|
+
CommandOrProc = T.type_alias do
|
11
|
+
T.any(T.class_of(CLI::Kit::BaseCommand), T.proc.returns(T.class_of(CLI::Kit::BaseCommand)))
|
12
|
+
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
end
|
14
|
+
sig { returns(T::Hash[String, CommandOrProc]) }
|
15
|
+
attr_reader :commands
|
16
16
|
|
17
|
-
|
18
|
-
|
17
|
+
sig { returns(T::Hash[String, String]) }
|
18
|
+
attr_reader :aliases
|
19
|
+
|
20
|
+
module ContextualResolver
|
21
|
+
extend T::Sig
|
22
|
+
extend T::Helpers
|
23
|
+
interface!
|
24
|
+
|
25
|
+
sig { abstract.returns(T::Array[String]) }
|
26
|
+
def command_names; end
|
27
|
+
|
28
|
+
sig { abstract.returns(T::Hash[String, String]) }
|
29
|
+
def aliases; end
|
30
|
+
|
31
|
+
sig { abstract.params(_name: String).returns(T.class_of(CLI::Kit::BaseCommand)) }
|
32
|
+
def command_class(_name); end
|
33
|
+
end
|
34
|
+
|
35
|
+
module NullContextualResolver
|
36
|
+
extend T::Sig
|
37
|
+
extend ContextualResolver
|
38
|
+
|
39
|
+
class << self
|
40
|
+
extend T::Sig
|
41
|
+
|
42
|
+
sig { override.returns(T::Array[String]) }
|
43
|
+
def command_names
|
44
|
+
[]
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { override.returns(T::Hash[String, String]) }
|
48
|
+
def aliases
|
49
|
+
{}
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { override.params(_name: String).returns(T.class_of(CLI::Kit::BaseCommand)) }
|
53
|
+
def command_class(_name)
|
54
|
+
raise(CLI::Kit::Abort, 'Cannot be called on the NullContextualResolver since command_names is empty')
|
55
|
+
end
|
19
56
|
end
|
20
57
|
end
|
21
58
|
|
22
|
-
|
59
|
+
sig { params(default: String, contextual_resolver: ContextualResolver).void }
|
60
|
+
def initialize(default:, contextual_resolver: NullContextualResolver)
|
23
61
|
@commands = {}
|
24
62
|
@aliases = {}
|
25
63
|
@default = default
|
26
|
-
@contextual_resolver = contextual_resolver
|
64
|
+
@contextual_resolver = contextual_resolver
|
27
65
|
end
|
28
66
|
|
67
|
+
sig { returns(T::Hash[String, T.class_of(CLI::Kit::BaseCommand)]) }
|
29
68
|
def resolved_commands
|
30
69
|
@commands.each_with_object({}) do |(k, v), a|
|
31
70
|
a[k] = resolve_class(v)
|
32
71
|
end
|
33
72
|
end
|
34
73
|
|
74
|
+
sig { params(const: CommandOrProc, name: String).void }
|
35
75
|
def add(const, name)
|
36
76
|
commands[name] = const
|
37
77
|
end
|
38
78
|
|
79
|
+
sig { params(name: T.nilable(String)).returns([T.nilable(T.class_of(CLI::Kit::BaseCommand)), String]) }
|
39
80
|
def lookup_command(name)
|
40
81
|
name = @default if name.to_s.empty?
|
41
|
-
resolve_command(name)
|
82
|
+
resolve_command(T.must(name))
|
42
83
|
end
|
43
84
|
|
85
|
+
sig { params(from: String, to: String).void }
|
44
86
|
def add_alias(from, to)
|
45
87
|
aliases[from] = to unless aliases[from]
|
46
88
|
end
|
47
89
|
|
90
|
+
sig { returns(T::Array[String]) }
|
48
91
|
def command_names
|
49
92
|
@contextual_resolver.command_names + commands.keys
|
50
93
|
end
|
51
94
|
|
95
|
+
sig { params(name: String).returns(T::Boolean) }
|
52
96
|
def exist?(name)
|
53
97
|
!resolve_command(name).first.nil?
|
54
98
|
end
|
55
99
|
|
56
100
|
private
|
57
101
|
|
102
|
+
sig { params(name: String).returns(String) }
|
58
103
|
def resolve_alias(name)
|
59
104
|
aliases[name] || @contextual_resolver.aliases.fetch(name, name)
|
60
105
|
end
|
61
106
|
|
107
|
+
sig { params(name: String).returns([T.nilable(T.class_of(CLI::Kit::BaseCommand)), String]) }
|
62
108
|
def resolve_command(name)
|
63
109
|
name = resolve_alias(name)
|
64
|
-
resolve_global_command(name) ||
|
65
|
-
resolve_contextual_command(name) ||
|
110
|
+
resolve_global_command(name) ||
|
111
|
+
resolve_contextual_command(name) ||
|
66
112
|
[nil, name]
|
67
113
|
end
|
68
114
|
|
115
|
+
sig { params(name: String).returns(T.nilable([T.class_of(CLI::Kit::BaseCommand), String])) }
|
69
116
|
def resolve_global_command(name)
|
70
117
|
klass = resolve_class(commands.fetch(name, nil))
|
71
|
-
return
|
118
|
+
return unless klass
|
119
|
+
|
72
120
|
[klass, name]
|
73
121
|
rescue NameError
|
74
122
|
nil
|
75
123
|
end
|
76
124
|
|
125
|
+
sig { params(name: String).returns(T.nilable([T.class_of(CLI::Kit::BaseCommand), String])) }
|
77
126
|
def resolve_contextual_command(name)
|
78
127
|
found = @contextual_resolver.command_names.include?(name)
|
79
|
-
return
|
128
|
+
return unless found
|
129
|
+
|
80
130
|
[@contextual_resolver.command_class(name), name]
|
81
131
|
end
|
82
132
|
|
133
|
+
sig { params(class_or_proc: T.nilable(CommandOrProc)).returns(T.nilable(T.class_of(CLI::Kit::BaseCommand))) }
|
83
134
|
def resolve_class(class_or_proc)
|
84
|
-
|
85
|
-
|
86
|
-
|
135
|
+
case class_or_proc
|
136
|
+
when nil
|
137
|
+
nil
|
138
|
+
when Proc
|
87
139
|
class_or_proc.call
|
88
140
|
else
|
89
141
|
class_or_proc
|
data/lib/cli/kit/config.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'cli/kit'
|
2
4
|
require 'fileutils'
|
3
5
|
|
4
6
|
module CLI
|
5
7
|
module Kit
|
6
8
|
class Config
|
9
|
+
extend T::Sig
|
10
|
+
|
7
11
|
XDG_CONFIG_HOME = 'XDG_CONFIG_HOME'
|
8
12
|
|
13
|
+
sig { params(tool_name: String).void }
|
9
14
|
def initialize(tool_name:)
|
10
15
|
@tool_name = tool_name
|
11
16
|
end
|
@@ -23,18 +28,20 @@ module CLI
|
|
23
28
|
# #### Example Usage
|
24
29
|
# `config.get('name.of.config')`
|
25
30
|
#
|
31
|
+
sig { params(section: String, name: String, default: T.nilable(String)).returns(T.nilable(String)) }
|
26
32
|
def get(section, name, default: nil)
|
27
33
|
all_configs.dig("[#{section}]", name) || default
|
28
34
|
end
|
29
35
|
|
30
36
|
# Coalesce and enforce the value of a config to a boolean
|
37
|
+
sig { params(section: String, name: String, default: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) }
|
31
38
|
def get_bool(section, name, default: false)
|
32
|
-
case get(section, name
|
39
|
+
case get(section, name)
|
33
40
|
when 'true'
|
34
41
|
true
|
35
42
|
when 'false'
|
36
43
|
false
|
37
|
-
when
|
44
|
+
when nil
|
38
45
|
default
|
39
46
|
else
|
40
47
|
raise CLI::Kit::Abort, "Invalid config: #{section}.#{name} is expected to be true or false"
|
@@ -51,9 +58,15 @@ module CLI
|
|
51
58
|
# #### Example Usage
|
52
59
|
# `config.set('section', 'name.of.config', 'value')`
|
53
60
|
#
|
61
|
+
sig { params(section: String, name: String, value: T.nilable(T.any(String, T::Boolean))).void }
|
54
62
|
def set(section, name, value)
|
55
63
|
all_configs["[#{section}]"] ||= {}
|
56
|
-
|
64
|
+
case value
|
65
|
+
when nil
|
66
|
+
T.must(all_configs["[#{section}]"]).delete(name)
|
67
|
+
else
|
68
|
+
T.must(all_configs["[#{section}]"])[name] = value.to_s
|
69
|
+
end
|
57
70
|
write_config
|
58
71
|
end
|
59
72
|
|
@@ -66,6 +79,7 @@ module CLI
|
|
66
79
|
# #### Example Usage
|
67
80
|
# `config.unset('section', 'name.of.config')`
|
68
81
|
#
|
82
|
+
sig { params(section: String, name: String).void }
|
69
83
|
def unset(section, name)
|
70
84
|
set(section, name, nil)
|
71
85
|
end
|
@@ -78,24 +92,12 @@ module CLI
|
|
78
92
|
# #### Example Usage
|
79
93
|
# `config.get_section('section')`
|
80
94
|
#
|
95
|
+
sig { params(section: String).returns(T::Hash[String, String]) }
|
81
96
|
def get_section(section)
|
82
97
|
(all_configs["[#{section}]"] || {}).dup
|
83
98
|
end
|
84
99
|
|
85
|
-
|
86
|
-
# e.g. shopify corresponds to ~/src/shopify, but is expanded to /Users/name/src/shopify
|
87
|
-
#
|
88
|
-
# #### Example Usage
|
89
|
-
# `config.get_path('srcpath', 'shopify')`
|
90
|
-
#
|
91
|
-
# #### Returns
|
92
|
-
# `path` : the expanded path to the corrsponding value
|
93
|
-
#
|
94
|
-
def get_path(section, name = nil)
|
95
|
-
v = get(section, name)
|
96
|
-
false == v ? v : File.expand_path(v)
|
97
|
-
end
|
98
|
-
|
100
|
+
sig { returns(String) }
|
99
101
|
def to_s
|
100
102
|
ini.to_s
|
101
103
|
end
|
@@ -105,6 +107,7 @@ module CLI
|
|
105
107
|
# if ENV['XDG_CONFIG_HOME'] is not set, we default to ~/.config, e.g.:
|
106
108
|
# ~/.config/tool/config
|
107
109
|
#
|
110
|
+
sig { returns(String) }
|
108
111
|
def file
|
109
112
|
config_home = ENV.fetch(XDG_CONFIG_HOME, '~/.config')
|
110
113
|
File.expand_path(File.join(@tool_name, 'config'), config_home)
|
@@ -112,20 +115,20 @@ module CLI
|
|
112
115
|
|
113
116
|
private
|
114
117
|
|
118
|
+
sig { returns(T::Hash[String, T::Hash[String, String]]) }
|
115
119
|
def all_configs
|
116
120
|
ini.ini
|
117
121
|
end
|
118
122
|
|
123
|
+
sig { returns(CLI::Kit::Ini) }
|
119
124
|
def ini
|
120
|
-
@ini ||= CLI::Kit::Ini
|
121
|
-
.new(file, default_section: '[global]', convert_types: false)
|
122
|
-
.tap(&:parse)
|
125
|
+
@ini ||= CLI::Kit::Ini.new(file).tap(&:parse)
|
123
126
|
end
|
124
127
|
|
128
|
+
sig { void }
|
125
129
|
def write_config
|
126
130
|
all_configs.each do |section, sub_config|
|
127
|
-
all_configs
|
128
|
-
all_configs.delete(section) if all_configs[section].empty?
|
131
|
+
all_configs.delete(section) if sub_config.empty?
|
129
132
|
end
|
130
133
|
FileUtils.mkdir_p(File.dirname(file))
|
131
134
|
File.write(file, to_s)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class Exception
|
5
|
+
extend(T::Sig)
|
6
|
+
|
7
|
+
# You'd think instance variables @bug and @silent would work here. They
|
8
|
+
# don't. I'm not sure why. If you, the reader, want to take some time to
|
9
|
+
# figure it out, go ahead and refactor to that.
|
10
|
+
|
11
|
+
sig { returns(T::Boolean) }
|
12
|
+
def bug?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
sig { returns(T::Boolean) }
|
17
|
+
def silent?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
sig { params(bug: T::Boolean).void }
|
22
|
+
def bug!(bug = true)
|
23
|
+
singleton_class.define_method(:bug?) { bug }
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { params(silent: T::Boolean).void }
|
27
|
+
def silent!(silent = true)
|
28
|
+
singleton_class.define_method(:silent?) { silent }
|
29
|
+
end
|
30
|
+
end
|
@@ -1,121 +1,182 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'cli/kit'
|
2
4
|
require 'English'
|
3
5
|
|
4
6
|
module CLI
|
5
7
|
module Kit
|
6
8
|
class ErrorHandler
|
7
|
-
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
ExceptionReporterOrProc = T.type_alias do
|
12
|
+
T.any(T.class_of(ExceptionReporter), T.proc.returns(T.class_of(ExceptionReporter)))
|
13
|
+
end
|
14
|
+
|
15
|
+
sig { params(override_exception_handler: T.proc.params(arg0: Exception).returns(Integer)).void }
|
16
|
+
attr_writer :override_exception_handler
|
17
|
+
|
18
|
+
sig do
|
19
|
+
params(
|
20
|
+
log_file: T.nilable(String),
|
21
|
+
exception_reporter: ExceptionReporterOrProc,
|
22
|
+
tool_name: T.nilable(String),
|
23
|
+
dev_mode: T::Boolean,
|
24
|
+
).void
|
25
|
+
end
|
26
|
+
def initialize(log_file: nil, exception_reporter: NullExceptionReporter, tool_name: nil, dev_mode: false)
|
8
27
|
@log_file = log_file
|
9
|
-
@exception_reporter_or_proc = exception_reporter
|
28
|
+
@exception_reporter_or_proc = exception_reporter
|
10
29
|
@tool_name = tool_name
|
30
|
+
@dev_mode = dev_mode
|
31
|
+
end
|
32
|
+
|
33
|
+
class ExceptionReporter
|
34
|
+
extend T::Sig
|
35
|
+
extend T::Helpers
|
36
|
+
abstract!
|
37
|
+
|
38
|
+
class << self
|
39
|
+
extend T::Sig
|
40
|
+
|
41
|
+
sig { abstract.params(exception: T.nilable(Exception), logs: T.nilable(String)).void }
|
42
|
+
def report(exception, logs = nil); end
|
43
|
+
end
|
11
44
|
end
|
12
45
|
|
13
|
-
|
14
|
-
|
15
|
-
|
46
|
+
class NullExceptionReporter < ExceptionReporter
|
47
|
+
extend T::Sig
|
48
|
+
|
49
|
+
class << self
|
50
|
+
extend T::Sig
|
51
|
+
|
52
|
+
sig { override.params(_exception: T.nilable(Exception), _logs: T.nilable(String)).void }
|
53
|
+
def report(_exception, _logs = nil)
|
54
|
+
nil
|
55
|
+
end
|
16
56
|
end
|
17
57
|
end
|
18
58
|
|
59
|
+
sig { params(block: T.proc.void).returns(Integer) }
|
19
60
|
def call(&block)
|
20
|
-
|
21
|
-
|
61
|
+
# @at_exit_exception is set if handle_abort decides to submit an error.
|
62
|
+
# $ERROR_INFO is set if we terminate because of a signal.
|
63
|
+
at_exit { report_exception(@at_exit_exception || $ERROR_INFO) }
|
64
|
+
triage_all_exceptions(&block)
|
22
65
|
end
|
23
66
|
|
24
|
-
|
67
|
+
sig { params(error: T.nilable(Exception)).void }
|
68
|
+
def report_exception(error)
|
25
69
|
if (notify_with = exception_for_submission(error))
|
26
|
-
logs =
|
27
|
-
|
28
|
-
|
29
|
-
|
70
|
+
logs = nil
|
71
|
+
if @log_file
|
72
|
+
logs = begin
|
73
|
+
File.read(@log_file)
|
74
|
+
rescue => e
|
75
|
+
"(#{e.class}: #{e.message})"
|
76
|
+
end
|
30
77
|
end
|
31
78
|
exception_reporter.report(notify_with, logs)
|
32
79
|
end
|
33
80
|
end
|
34
81
|
|
35
|
-
|
36
|
-
|
82
|
+
SIGNALS_THAT_ARENT_BUGS = [
|
83
|
+
'SIGTERM', 'SIGHUP', 'SIGINT',
|
84
|
+
].freeze
|
37
85
|
|
38
86
|
private
|
39
87
|
|
88
|
+
# Run the program, handling any errors that occur.
|
89
|
+
#
|
90
|
+
# Errors are printed to stderr unless they're #silent?, and are reported
|
91
|
+
# to bugsnag (by setting @at_exit_exeption for our at_exit handler) if
|
92
|
+
# they're #bug?
|
93
|
+
#
|
94
|
+
# Returns an exit status for the program.
|
95
|
+
sig { params(block: T.proc.void).returns(Integer) }
|
96
|
+
def triage_all_exceptions(&block)
|
97
|
+
begin
|
98
|
+
block.call
|
99
|
+
CLI::Kit::EXIT_SUCCESS
|
100
|
+
rescue Interrupt => e # Ctrl-C
|
101
|
+
# transform message, prevent bugsnag
|
102
|
+
exc = e.exception('Interrupt')
|
103
|
+
CLI::Kit.raise(exc, bug: false)
|
104
|
+
rescue Errno::ENOSPC => e
|
105
|
+
# transform message, prevent bugsnag
|
106
|
+
message = if @tool_name
|
107
|
+
"Your disk is full - {{command:#{@tool_name}}} requires free space to operate"
|
108
|
+
else
|
109
|
+
'Your disk is full - free space is required to operate'
|
110
|
+
end
|
111
|
+
exc = e.exception(message)
|
112
|
+
CLI::Kit.raise(exc, bug: false)
|
113
|
+
end
|
114
|
+
# If SystemExit was raised, e.g. `exit()`, then
|
115
|
+
# return whatever status is attached to the exception
|
116
|
+
# object. The special exit statuses have already been
|
117
|
+
# handled below.
|
118
|
+
rescue SystemExit => e
|
119
|
+
e.status
|
120
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
121
|
+
@at_exit_exception = e if e.bug?
|
122
|
+
|
123
|
+
if (eh = @override_exception_handler)
|
124
|
+
return eh.call(e)
|
125
|
+
end
|
126
|
+
|
127
|
+
raise(e) if @dev_mode && e.bug?
|
128
|
+
|
129
|
+
stderr_puts(e.message) unless e.silent?
|
130
|
+
e.bug? ? CLI::Kit::EXIT_BUG : CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
|
131
|
+
end
|
132
|
+
|
133
|
+
sig { params(error: T.nilable(Exception)).returns(T.nilable(Exception)) }
|
40
134
|
def exception_for_submission(error)
|
135
|
+
# happens on normal non-error termination
|
136
|
+
return(nil) if error.nil?
|
137
|
+
|
138
|
+
return(nil) unless error.bug?
|
139
|
+
|
41
140
|
case error
|
42
|
-
when nil # normal, non-error termination
|
43
|
-
nil
|
44
|
-
when Interrupt # ctrl-c
|
45
|
-
nil
|
46
|
-
when CLI::Kit::Abort, CLI::Kit::AbortSilent # Not a bug
|
47
|
-
nil
|
48
141
|
when SignalException
|
49
|
-
|
50
|
-
skip.include?(error.message) ? nil : error
|
142
|
+
SIGNALS_THAT_ARENT_BUGS.include?(error.message) ? nil : error
|
51
143
|
when SystemExit # "exit N" called
|
52
144
|
case error.status
|
53
145
|
when CLI::Kit::EXIT_SUCCESS # submit nothing if it was `exit 0`
|
54
146
|
nil
|
55
147
|
when CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
|
56
|
-
# if it was `exit 30`, translate the exit code to 1, and submit
|
57
|
-
# 30 is used to signal normal failures that are not
|
58
|
-
# However, users should see it presented as 1.
|
148
|
+
# if it was `exit 30`, translate the exit code to 1, and submit
|
149
|
+
# nothing. 30 is used to signal normal failures that are not
|
150
|
+
# indicative of bugs. However, users should see it presented as 1.
|
59
151
|
exit(1)
|
60
152
|
else
|
61
|
-
#
|
62
|
-
#
|
63
|
-
|
153
|
+
# don't treat this as an exception, simply reraise.
|
154
|
+
# this is indicative of `exit` being called with a
|
155
|
+
# non-zero number, and the requested exit status
|
156
|
+
# needs to be maintained.
|
157
|
+
exit(error.status)
|
64
158
|
end
|
65
159
|
else
|
66
160
|
error
|
67
161
|
end
|
68
162
|
end
|
69
163
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
def handle_abort
|
75
|
-
yield
|
76
|
-
CLI::Kit::EXIT_SUCCESS
|
77
|
-
rescue CLI::Kit::GenericAbort => e
|
78
|
-
is_bug = e.is_a?(CLI::Kit::Bug) || e.is_a?(CLI::Kit::BugSilent)
|
79
|
-
is_silent = e.is_a?(CLI::Kit::AbortSilent) || e.is_a?(CLI::Kit::BugSilent)
|
80
|
-
|
81
|
-
print_error_message(e) unless is_silent
|
82
|
-
(@exception = e) if is_bug
|
83
|
-
|
84
|
-
CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
|
85
|
-
rescue Interrupt
|
86
|
-
stderr_puts_message('Interrupt')
|
87
|
-
CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
|
88
|
-
rescue Errno::ENOSPC
|
89
|
-
message = if @tool_name
|
90
|
-
"Your disk is full - {{command:#{@tool_name}}} requires free space to operate"
|
91
|
-
else
|
92
|
-
'Your disk is full - free space is required to operate'
|
93
|
-
end
|
94
|
-
stderr_puts_message(message)
|
95
|
-
CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
|
96
|
-
end
|
97
|
-
|
98
|
-
def stderr_puts_message(message)
|
99
|
-
$stderr.puts(format_error_message(message))
|
100
|
-
rescue Errno::EPIPE
|
164
|
+
sig { params(message: String).void }
|
165
|
+
def stderr_puts(message)
|
166
|
+
$stderr.puts(CLI::UI.fmt("{{red:#{message}}}"))
|
167
|
+
rescue Errno::EPIPE, Errno::EIO
|
101
168
|
nil
|
102
169
|
end
|
103
170
|
|
171
|
+
sig { returns(T.class_of(ExceptionReporter)) }
|
104
172
|
def exception_reporter
|
105
|
-
|
106
|
-
|
107
|
-
else
|
173
|
+
case @exception_reporter_or_proc
|
174
|
+
when Proc
|
108
175
|
@exception_reporter_or_proc.call
|
176
|
+
else
|
177
|
+
@exception_reporter_or_proc
|
109
178
|
end
|
110
179
|
end
|
111
|
-
|
112
|
-
def format_error_message(msg)
|
113
|
-
CLI::UI.fmt("{{red:#{msg}}}")
|
114
|
-
end
|
115
|
-
|
116
|
-
def print_error_message(e)
|
117
|
-
$stderr.puts(format_error_message(e.message))
|
118
|
-
end
|
119
180
|
end
|
120
181
|
end
|
121
182
|
end
|
data/lib/cli/kit/executor.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'cli/kit'
|
2
4
|
require 'English'
|
3
5
|
require 'fileutils'
|
@@ -5,11 +7,15 @@ require 'fileutils'
|
|
5
7
|
module CLI
|
6
8
|
module Kit
|
7
9
|
class Executor
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { params(log_file: String).void }
|
8
13
|
def initialize(log_file:)
|
9
14
|
FileUtils.mkpath(File.dirname(log_file))
|
10
15
|
@log_file = log_file
|
11
16
|
end
|
12
17
|
|
18
|
+
sig { params(command: T.class_of(CLI::Kit::BaseCommand), command_name: String, args: T::Array[String]).void }
|
13
19
|
def call(command, command_name, args)
|
14
20
|
with_traps do
|
15
21
|
with_logging do |id|
|
@@ -30,22 +36,31 @@ module CLI
|
|
30
36
|
|
31
37
|
private
|
32
38
|
|
39
|
+
sig do
|
40
|
+
type_parameters(:T).params(block: T.proc.params(id: String).returns(T.type_parameter(:T)))
|
41
|
+
.returns(T.type_parameter(:T))
|
42
|
+
end
|
33
43
|
def with_logging(&block)
|
34
|
-
return yield unless @log_file
|
35
44
|
CLI::UI.log_output_to(@log_file) do
|
36
|
-
CLI::UI::StdoutRouter.with_id(on_streams: [CLI::UI::StdoutRouter.duplicate_output_to]) do |id|
|
45
|
+
CLI::UI::StdoutRouter.with_id(on_streams: [CLI::UI::StdoutRouter.duplicate_output_to].compact) do |id|
|
37
46
|
block.call(id)
|
38
47
|
end
|
39
48
|
end
|
40
49
|
end
|
41
50
|
|
51
|
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
42
52
|
def with_traps(&block)
|
43
53
|
twrap('QUIT', method(:quit_handler)) do
|
44
54
|
twrap('INFO', method(:info_handler), &block)
|
45
55
|
end
|
46
56
|
end
|
47
57
|
|
48
|
-
|
58
|
+
sig do
|
59
|
+
type_parameters(:T)
|
60
|
+
.params(signal: String, handler: Method, block: T.proc.returns(T.type_parameter(:T)))
|
61
|
+
.returns(T.type_parameter(:T))
|
62
|
+
end
|
63
|
+
def twrap(signal, handler, &block)
|
49
64
|
return yield unless Signal.list.key?(signal)
|
50
65
|
|
51
66
|
begin
|
@@ -63,6 +78,7 @@ module CLI
|
|
63
78
|
end
|
64
79
|
end
|
65
80
|
|
81
|
+
sig { params(_sig: T.untyped).void }
|
66
82
|
def quit_handler(_sig)
|
67
83
|
z = caller
|
68
84
|
CLI::UI.raw do
|
@@ -72,6 +88,7 @@ module CLI
|
|
72
88
|
exit(CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG)
|
73
89
|
end
|
74
90
|
|
91
|
+
sig { params(_sig: T.untyped).void }
|
75
92
|
def info_handler(_sig)
|
76
93
|
z = caller
|
77
94
|
CLI::UI.raw do
|