cli-kit 3.3.0 → 5.0.0
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 +10 -0
- data/.github/workflows/cla.yml +22 -0
- data/.github/workflows/ruby.yml +64 -0
- data/.gitignore +2 -0
- data/.rubocop.sorbet.yml +47 -0
- data/.rubocop.yml +22 -13
- data/Gemfile +13 -3
- data/Gemfile.lock +110 -28
- data/README.md +46 -3
- data/Rakefile +28 -1
- data/bin/console +3 -3
- data/bin/onchange +30 -0
- data/bin/tapioca +29 -0
- data/bin/test_gen +4 -1
- data/bin/testunit +3 -2
- data/cli-kit.gemspec +7 -6
- data/dev.yml +35 -3
- data/examples/minimal/example.rb +5 -3
- 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 +39 -18
- 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 +30 -25
- data/lib/cli/kit/core_ext.rb +30 -0
- data/lib/cli/kit/error_handler.rb +134 -67
- data/lib/cli/kit/executor.rb +39 -20
- data/lib/cli/kit/ini.rb +32 -39
- data/lib/cli/kit/levenshtein.rb +12 -4
- data/lib/cli/kit/logger.rb +23 -3
- data/lib/cli/kit/opts.rb +301 -0
- data/lib/cli/kit/resolver.rb +10 -2
- data/lib/cli/kit/sorbet_runtime_stub.rb +156 -0
- data/lib/cli/kit/support/test_helper.rb +31 -22
- data/lib/cli/kit/support.rb +2 -0
- data/lib/cli/kit/system.rb +217 -48
- data/lib/cli/kit/util.rb +52 -107
- data/lib/cli/kit/version.rb +3 -1
- data/lib/cli/kit.rb +104 -8
- metadata +35 -22
- data/.github/probots.yml +0 -2
- data/.travis.yml +0 -14
- 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
|
|
@@ -18,22 +23,26 @@ module CLI
|
|
|
18
23
|
# `name` : the name of the config value you are looking for
|
|
19
24
|
#
|
|
20
25
|
# #### Returns
|
|
21
|
-
# `value` : the value of the config variable (
|
|
26
|
+
# `value` : the value of the config variable (nil if none)
|
|
22
27
|
#
|
|
23
28
|
# #### Example Usage
|
|
24
29
|
# `config.get('name.of.config')`
|
|
25
30
|
#
|
|
26
|
-
|
|
31
|
+
sig { params(section: String, name: String, default: T.nilable(String)).returns(T.nilable(String)) }
|
|
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
|
|
33
|
-
when
|
|
39
|
+
case get(section, name)
|
|
40
|
+
when 'true'
|
|
34
41
|
true
|
|
35
|
-
when
|
|
42
|
+
when 'false'
|
|
36
43
|
false
|
|
44
|
+
when nil
|
|
45
|
+
default
|
|
37
46
|
else
|
|
38
47
|
raise CLI::Kit::Abort, "Invalid config: #{section}.#{name} is expected to be true or false"
|
|
39
48
|
end
|
|
@@ -49,9 +58,15 @@ module CLI
|
|
|
49
58
|
# #### Example Usage
|
|
50
59
|
# `config.set('section', 'name.of.config', 'value')`
|
|
51
60
|
#
|
|
61
|
+
sig { params(section: String, name: String, value: T.nilable(T.any(String, T::Boolean))).void }
|
|
52
62
|
def set(section, name, value)
|
|
53
63
|
all_configs["[#{section}]"] ||= {}
|
|
54
|
-
|
|
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
|
|
55
70
|
write_config
|
|
56
71
|
end
|
|
57
72
|
|
|
@@ -64,6 +79,7 @@ module CLI
|
|
|
64
79
|
# #### Example Usage
|
|
65
80
|
# `config.unset('section', 'name.of.config')`
|
|
66
81
|
#
|
|
82
|
+
sig { params(section: String, name: String).void }
|
|
67
83
|
def unset(section, name)
|
|
68
84
|
set(section, name, nil)
|
|
69
85
|
end
|
|
@@ -76,24 +92,12 @@ module CLI
|
|
|
76
92
|
# #### Example Usage
|
|
77
93
|
# `config.get_section('section')`
|
|
78
94
|
#
|
|
95
|
+
sig { params(section: String).returns(T::Hash[String, String]) }
|
|
79
96
|
def get_section(section)
|
|
80
97
|
(all_configs["[#{section}]"] || {}).dup
|
|
81
98
|
end
|
|
82
99
|
|
|
83
|
-
|
|
84
|
-
# e.g. shopify corresponds to ~/src/shopify, but is expanded to /Users/name/src/shopify
|
|
85
|
-
#
|
|
86
|
-
# #### Example Usage
|
|
87
|
-
# `config.get_path('srcpath', 'shopify')`
|
|
88
|
-
#
|
|
89
|
-
# #### Returns
|
|
90
|
-
# `path` : the expanded path to the corrsponding value
|
|
91
|
-
#
|
|
92
|
-
def get_path(section, name = nil)
|
|
93
|
-
v = get(section, name)
|
|
94
|
-
false == v ? v : File.expand_path(v)
|
|
95
|
-
end
|
|
96
|
-
|
|
100
|
+
sig { returns(String) }
|
|
97
101
|
def to_s
|
|
98
102
|
ini.to_s
|
|
99
103
|
end
|
|
@@ -103,6 +107,7 @@ module CLI
|
|
|
103
107
|
# if ENV['XDG_CONFIG_HOME'] is not set, we default to ~/.config, e.g.:
|
|
104
108
|
# ~/.config/tool/config
|
|
105
109
|
#
|
|
110
|
+
sig { returns(String) }
|
|
106
111
|
def file
|
|
107
112
|
config_home = ENV.fetch(XDG_CONFIG_HOME, '~/.config')
|
|
108
113
|
File.expand_path(File.join(@tool_name, 'config'), config_home)
|
|
@@ -110,20 +115,20 @@ module CLI
|
|
|
110
115
|
|
|
111
116
|
private
|
|
112
117
|
|
|
118
|
+
sig { returns(T::Hash[String, T::Hash[String, String]]) }
|
|
113
119
|
def all_configs
|
|
114
120
|
ini.ini
|
|
115
121
|
end
|
|
116
122
|
|
|
123
|
+
sig { returns(CLI::Kit::Ini) }
|
|
117
124
|
def ini
|
|
118
|
-
@ini ||= CLI::Kit::Ini
|
|
119
|
-
.new(file, default_section: "[global]", convert_types: false)
|
|
120
|
-
.tap(&:parse)
|
|
125
|
+
@ini ||= CLI::Kit::Ini.new(file).tap(&:parse)
|
|
121
126
|
end
|
|
122
127
|
|
|
128
|
+
sig { void }
|
|
123
129
|
def write_config
|
|
124
130
|
all_configs.each do |section, sub_config|
|
|
125
|
-
all_configs
|
|
126
|
-
all_configs.delete(section) if all_configs[section].empty?
|
|
131
|
+
all_configs.delete(section) if sub_config.empty?
|
|
127
132
|
end
|
|
128
133
|
FileUtils.mkdir_p(File.dirname(file))
|
|
129
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,115 +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
|
|
11
31
|
end
|
|
12
32
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
16
43
|
end
|
|
17
44
|
end
|
|
18
45
|
|
|
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
|
|
56
|
+
end
|
|
57
|
+
end
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
67
|
+
sig { params(error: T.nilable(Exception)).void }
|
|
68
|
+
def report_exception(error)
|
|
69
|
+
if (notify_with = exception_for_submission(error))
|
|
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.
|
|
59
|
-
exit
|
|
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.
|
|
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
|
-
|
|
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(format_error_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(format_error_message(message))
|
|
95
|
-
CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
|
|
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
|
|
168
|
+
nil
|
|
96
169
|
end
|
|
97
170
|
|
|
171
|
+
sig { returns(T.class_of(ExceptionReporter)) }
|
|
98
172
|
def exception_reporter
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
else
|
|
173
|
+
case @exception_reporter_or_proc
|
|
174
|
+
when Proc
|
|
102
175
|
@exception_reporter_or_proc.call
|
|
176
|
+
else
|
|
177
|
+
@exception_reporter_or_proc
|
|
103
178
|
end
|
|
104
179
|
end
|
|
105
|
-
|
|
106
|
-
def format_error_message(msg)
|
|
107
|
-
CLI::UI.fmt("{{red:#{msg}}}")
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def print_error_message(e)
|
|
111
|
-
$stderr.puts(format_error_message(e.message))
|
|
112
|
-
end
|
|
113
180
|
end
|
|
114
181
|
end
|
|
115
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,61 +7,77 @@ 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|
|
|
22
|
+
command.call(args, command_name)
|
|
23
|
+
rescue => e
|
|
16
24
|
begin
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# Outputting to stderr is best-effort. Avoid raising another error when outputting debug info so that
|
|
24
|
-
# we can detect and log the original error, which may even be the source of this error.
|
|
25
|
-
nil
|
|
26
|
-
end
|
|
27
|
-
raise e
|
|
25
|
+
$stderr.puts "This command ran with ID: #{id}"
|
|
26
|
+
$stderr.puts 'Please include this information in any issues/report along with relevant logs'
|
|
27
|
+
rescue SystemCallError
|
|
28
|
+
# Outputting to stderr is best-effort. Avoid raising another error when outputting debug info so that
|
|
29
|
+
# we can detect and log the original error, which may even be the source of this error.
|
|
30
|
+
nil
|
|
28
31
|
end
|
|
32
|
+
raise e
|
|
29
33
|
end
|
|
30
34
|
end
|
|
31
35
|
end
|
|
32
36
|
|
|
33
37
|
private
|
|
34
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
|
|
35
43
|
def with_logging(&block)
|
|
36
|
-
return yield unless @log_file
|
|
37
44
|
CLI::UI.log_output_to(@log_file) do
|
|
38
|
-
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|
|
|
39
46
|
block.call(id)
|
|
40
47
|
end
|
|
41
48
|
end
|
|
42
49
|
end
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
|
52
|
+
def with_traps(&block)
|
|
45
53
|
twrap('QUIT', method(:quit_handler)) do
|
|
46
|
-
twrap('INFO', method(:info_handler))
|
|
47
|
-
yield
|
|
48
|
-
end
|
|
54
|
+
twrap('INFO', method(:info_handler), &block)
|
|
49
55
|
end
|
|
50
56
|
end
|
|
51
57
|
|
|
52
|
-
|
|
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)
|
|
53
63
|
return yield unless Signal.list.key?(signal)
|
|
54
64
|
|
|
55
65
|
begin
|
|
56
|
-
|
|
66
|
+
begin
|
|
67
|
+
prev_handler = trap(signal, handler)
|
|
68
|
+
installed = true
|
|
69
|
+
rescue ArgumentError
|
|
70
|
+
# If we couldn't install a signal handler because the signal is
|
|
71
|
+
# reserved, remember not to uninstall it later.
|
|
72
|
+
installed = false
|
|
73
|
+
end
|
|
57
74
|
yield
|
|
58
75
|
ensure
|
|
59
|
-
trap(signal, prev_handler)
|
|
76
|
+
trap(signal, prev_handler) if installed
|
|
60
77
|
end
|
|
61
78
|
end
|
|
62
79
|
|
|
80
|
+
sig { params(_sig: T.untyped).void }
|
|
63
81
|
def quit_handler(_sig)
|
|
64
82
|
z = caller
|
|
65
83
|
CLI::UI.raw do
|
|
@@ -69,6 +87,7 @@ module CLI
|
|
|
69
87
|
exit(CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG)
|
|
70
88
|
end
|
|
71
89
|
|
|
90
|
+
sig { params(_sig: T.untyped).void }
|
|
72
91
|
def info_handler(_sig)
|
|
73
92
|
z = caller
|
|
74
93
|
CLI::UI.raw do
|