cli-kit 4.0.0 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/cla.yml +22 -0
- data/.github/workflows/ruby.yml +34 -2
- data/.gitignore +2 -0
- data/.rubocop.sorbet.yml +47 -0
- data/.rubocop.yml +16 -1
- data/Gemfile +10 -1
- data/Gemfile.lock +94 -18
- data/README.md +46 -3
- data/Rakefile +1 -0
- data/bin/onchange +30 -0
- data/bin/tapioca +29 -0
- data/bin/testunit +1 -0
- data/cli-kit.gemspec +2 -2
- data/dev.yml +35 -3
- data/examples/minimal/example.rb +3 -1
- 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 +28 -7
- 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/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 +245 -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 +69 -17
- 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 +19 -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/resolver.rb +8 -0
- data/lib/cli/kit/sorbet_runtime_stub.rb +156 -0
- data/lib/cli/kit/support/test_helper.rb +23 -14
- data/lib/cli/kit/support.rb +2 -0
- data/lib/cli/kit/system.rb +188 -54
- data/lib/cli/kit/util.rb +48 -103
- data/lib/cli/kit/version.rb +3 -1
- data/lib/cli/kit.rb +103 -7
- metadata +22 -10
- 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,64 +1,110 @@
|
|
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
110
|
resolve_global_command(name) || \
|
@@ -66,24 +112,30 @@ module CLI
|
|
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 nil unless klass
|
118
|
+
return nil 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
128
|
return nil 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: strong
|
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,30 @@ 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).params(signal: String, handler: Method,
|
60
|
+
block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T))
|
61
|
+
end
|
62
|
+
def twrap(signal, handler, &block)
|
49
63
|
return yield unless Signal.list.key?(signal)
|
50
64
|
|
51
65
|
begin
|
@@ -63,6 +77,7 @@ module CLI
|
|
63
77
|
end
|
64
78
|
end
|
65
79
|
|
80
|
+
sig { params(_sig: T.untyped).void }
|
66
81
|
def quit_handler(_sig)
|
67
82
|
z = caller
|
68
83
|
CLI::UI.raw do
|
@@ -72,6 +87,7 @@ module CLI
|
|
72
87
|
exit(CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG)
|
73
88
|
end
|
74
89
|
|
90
|
+
sig { params(_sig: T.untyped).void }
|
75
91
|
def info_handler(_sig)
|
76
92
|
z = caller
|
77
93
|
CLI::UI.raw do
|